Merge branch 'stable-2.13'
* stable-2.13:
DynamicSet: Disable circular proxies on binder
StreamEventsApiListener: Extend module from AbstractModule
QueryProcessor: Add missing space in exception message
ProjectConfig: resolve rule's group when saving access
ProjectConfig: resolve rule's group when loading permission rules
Change-Id: I21a17c81b87a4ee450d9a92447ba38bacc02479f
diff --git a/.bazelrc b/.bazelrc
index 00acd27..a991c76 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1 +1 @@
-build --strategy=Javac=worker
+build --workspace_status_command=./tools/workspace-status.sh --strategy=Javac=worker
diff --git a/.buckconfig b/.buckconfig
index b347a96..60fd02a 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -20,8 +20,11 @@
[java]
jar_spool_mode = direct_to_jar
src_roots = java, resources, src
+ source_level = 8
+ target_level = 8
[project]
+ allow_symlinks = allow
ignore = .git, eclipse-out, bazel-gerrit, bin
parallel_parsing = true
diff --git a/.buckversion b/.buckversion
index f5fe016..af38772 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-e64a2e2ada022f81e42be750b774024469551398
+7b7817c48f30687781040b2b82ac9218d5c4eaa4
diff --git a/.gitignore b/.gitignore
index 815c5fa..c89cfb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
/.settings/org.maven.ide.eclipse.prefs
/.settings/org.eclipse.m2e.core.prefs
/.settings/org.eclipse.ltk.core.refactoring.prefs
+/.metadata
/test_site
/.idea
*.iml
diff --git a/.mailmap b/.mailmap
index 598d52d..285d8c8 100644
--- a/.mailmap
+++ b/.mailmap
@@ -3,6 +3,7 @@
Alex Blewitt <alex.blewitt@gmail.com> <alex.blewitt@gs.com>
Alex Ryazantsev <alex.ryazantsev@gmail.com> alex <alex.ryazantsev@gmail.com>
Alex Ryazantsev <alex.ryazantsev@gmail.com> alex.ryazantsev <alex.ryazantsev@gmail.com>
+Becky Siegel <beckysiegel@google.com> beckysiegel <beckysiegel@google.com>
Brad Larson <bklarson@gmail.com> <brad.larson@garmin.com>
Bruce Zu <bruce.zu@sonymobile.com> <bruce.zu@sonyericsson.com>
Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
@@ -11,6 +12,7 @@
Deniz Türkoglu <deniz@spotify.com> Deniz Turkoglu <deniz@spotify.com>
Edwin Kempin <ekempin@google.com> Edwin Kempin <edwin.kempin@gmail.com>
Edwin Kempin <ekempin@google.com> Edwin Kempin <edwin.kempin@sap.com>
+Edwin Kempin <ekempin@google.com> ekempin Edwin Kempin <ekempin@google.com>
Eryk Szymanski <eryksz@gmail.com> <eryksz@google.com>
Fredrik Luthander <fredrik.luthander@sonymobile.com> <fredrik@gandaraj.com>
Fredrik Luthander <fredrik.luthander@sonymobile.com> <fredrik.luthander@sonyericsson.com>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 828234b..fd57ff7 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -7,9 +7,9 @@
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -113,7 +113,7 @@
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.processAnnotations=enabled
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..4fa30f2
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,21 @@
+load('//tools/bzl:pkg_war.bzl', 'pkg_war')
+
+genrule(
+ name = 'gen_version',
+ stamp = 1,
+ cmd = "grep STABLE_BUILD_GERRIT_LABEL < bazel-out/volatile-status.txt | cut -d ' ' -f 2 > $@",
+ outs = ['version.txt'],
+ visibility = ['//visibility:public'],
+)
+
+genrule(
+ name = "LICENSES",
+ srcs = ["//Documentation:licenses.txt"],
+ cmd = "cp $< $@",
+ outs = ["LICENSES.txt"],
+ visibility = ['//visibility:public'],
+)
+
+pkg_war(name = 'gerrit')
+pkg_war(name = 'headless', ui = None)
+pkg_war(name = 'release', ui = 'ui_optdbg_r', context = ['//plugins:core'])
diff --git a/Documentation/BUILD b/Documentation/BUILD
new file mode 100644
index 0000000..0542b5d
--- /dev/null
+++ b/Documentation/BUILD
@@ -0,0 +1,98 @@
+load("//tools/bzl:asciidoc.bzl", "documentation_attributes")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
+load("//tools/bzl:license.bzl", "license_map")
+
+exports_files([
+ "replace_macros.py",
+])
+
+filegroup(
+ name = "prettify_files",
+ srcs = [
+ ":prettify.min.css",
+ ":prettify.min.js",
+ ],
+)
+
+genrule(
+ name = "prettify_min_css",
+ srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.css"],
+ cmd = "cp $< $@",
+ outs = ["prettify.min.css"],
+)
+
+genrule(
+ name = "prettify_min_js",
+ srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.js"],
+ cmd = "cp $< $@",
+ outs = ["prettify.min.js"],
+)
+
+filegroup(
+ name = "resources",
+ srcs = glob([
+ "images/*.jpg",
+ "images/*.png",
+ ]) + [
+ ":prettify_files",
+ "//:LICENSES.txt",
+ ],
+ visibility = ['//visibility:public'],
+)
+
+license_map(
+ name = "licenses",
+ targets = [
+ "//gerrit-pgm:pgm",
+ "//gerrit-gwtui:ui_module",
+ ],
+ opts = ["--asciidoctor"],
+ visibility = ['//visibility:public'],
+)
+
+DOC_DIR = "Documentation"
+SRCS = glob(["*.txt"]) + [":licenses.txt"]
+
+genrule(
+ name = "index",
+ cmd = "$(location //lib/asciidoctor:doc_indexer) " +
+ "-o $(OUTS) " +
+ '--prefix "%s/" ' % DOC_DIR +
+ '--in-ext ".txt" ' +
+ '--out-ext ".html" ' +
+ "$(SRCS)",
+ tools = ["//lib/asciidoctor:doc_indexer"],
+ srcs = SRCS,
+ outs = ["index.jar"],
+)
+
+# For the same srcs, we can have multiple genasciidoc_zip rules, but only one
+# genasciidoc rule. Because multiple genasciidoc rules will have conflicting
+# output files.
+genasciidoc(
+ name = "Documentation",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "html",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ directory = DOC_DIR,
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "searchfree",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ directory = DOC_DIR,
+ searchbox = False,
+ visibility = ["//visibility:public"],
+)
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 2cc8c05..41c9dc2 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
@@ -599,11 +615,10 @@
a new commit on their local system, so in practice they must also
have the `Read` access granted to upload a change.
-For an open source, public Gerrit installation, it is common to
-grant `Read` and `Push` for `+refs/for/refs/heads/*+`
-to `Registered Users` in the `All-Projects` ACL. For more
-private installations, its common to simply grant `Read` and
-`Push` for `+refs/for/refs/heads/*+` to all users of a project.
+For an open source, public Gerrit installation, it is common to grant
+`Push` for `+refs/for/refs/heads/*+` to `Registered Users` in the
+`All-Projects` ACL. For more private installations, its common to
+grant `Push` for `+refs/for/refs/heads/*+` to all users of a project.
* Force option
+
@@ -644,7 +659,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 +687,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 +698,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
@@ -863,6 +886,14 @@
can always edit or remove hashtags (even without having the `Edit Hashtags`
access right assigned).
+[[category_edit_assigned_to]]
+=== Edit Assignee
+
+This category permits users to set who is assigned to a change that is
+uploaded for review.
+
+The change owner, ref owners, and the user currently assigned to a change
+can always change the assignee.
[[example_roles]]
== Examples of typical roles in a project
@@ -997,7 +1028,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 +1098,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/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index fbe4f3f..769360d 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -20,6 +20,8 @@
Gerrit. This command will not start the indexer if it is already running or if
the active index is the latest.
+The link:cmd-show-queue.html[show-queue] command provides online index status.
+
== ACCESS
Caller must be a member of the privileged 'Administrators' group.
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index 02f1c5b..141f7e2 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -1,7 +1,7 @@
= gerrit show-queue
== NAME
-gerrit show-queue - Display the background work queues, including replication
+gerrit show-queue - Display the background work queues, including replication and indexing
== SYNOPSIS
[verse]
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 1cfb8b9..8ce7d7e 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -1,5 +1,4 @@
= gerrit stream-events
-
== NAME
gerrit stream-events - Monitor events occurring in real time
@@ -59,6 +58,21 @@
[[events]]
== EVENTS
+=== Assignee Changed
+
+Sent when the assignee of a change has been modified.
+
+type:: "assignee-changed"
+
+change:: link:json.html#change[change attribute]
+
+changer:: link:json.html#account[account attribute]
+
+oldAssignee:: Assignee before it was changed.
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
=== Change Abandoned
Sent when a change has been abandoned.
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 a956d52..1c7981a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -443,7 +443,7 @@
[[auth.gitBasicAuth]]auth.gitBasicAuth::
+
If true then Git over HTTP and HTTP/S traffic is authenticated using
-standard BasicAuth. Depending on the configured `auth.type` credentials
+standard BasicAuth. Depending on the configured `auth.type`, credentials
are validated against the randomly generated HTTP password, against LDAP
(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
+
@@ -452,8 +452,14 @@
authentication and the randomly generated HTTP password in the Gerrit
database.
+
-When `auth.type` is `LDAP`, service users that only exist in the Gerrit
-database are still authenticated by their HTTP passwords.
+When `auth.type` is `LDAP`, users should authenticate using their LDAP passwords.
+However, if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP`,
+the randomly generated HTTP password is used exclusively. In the other hand,
+if link:#auth.gitBasicAuthPolicy[`auth.gitBasicAuthPolicy`] is set to `HTTP_LDAP`,
+the password in the request is first checked against the HTTP password and, if
+it does not match, it is then validated against the LDAP password.
+Service users that only exist in the Gerrit database are authenticated by their
+HTTP passwords.
+
When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
instead of passwords in the Basic authentication header. Note that provider
@@ -463,6 +469,31 @@
+
By default this is set to false.
+[[auth.gitBasicAuthPolicy]]auth.gitBasicAuthPolicy::
++
+When `auth.type` is `LDAP` and BasicAuth (i.e., link:#auth.gitBasicAuth[`auth.gitBasicAuth`]
+is set to true), it allows using either the generated HTTP password, the LDAP
+password or both to authenticate Git over HTTP and REST API requests. The
+supported values are:
++
+*`HTTP`
++
+Only the randomly generated HTTP password is accepted when doing Git over HTTP
+and REST API requests.
++
+*`LDAP`
++
+Only the `LDAP` password is allowed when doing Git over HTTP and REST API
+requests.
++
+*`HTTP_LDAP`
++
+The password in the request is first checked against the HTTP password and, if
+it does not match, it is then validated against the `LDAP` password.
++
+By default this is set to `LDAP` when link:#auth.type[`auth.type`] is `LDAP`.
+Otherwise, the default value is `HTTP`.
+
[[auth.gitOAuthProvider]]auth.gitOAuthProvider::
+
Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
@@ -611,6 +642,7 @@
* `"adv_bases"`: default is `4096`
* `"diff"`: default is `10m` (10 MiB of memory)
* `"diff_intraline"`: default is `10m` (10 MiB of memory)
+* `"diff_summary"`: default is `10m` (10 MiB of memory)
* `"plugin_resources"`: default is 2m (2 MiB of memory)
+
@@ -626,7 +658,10 @@
grow larger than this during the day, as the size check is only
performed once every 24 hours.
+
-Default is 128 MiB per cache.
+Default is 128 MiB per cache, except:
++
+* `"diff_summary"`: default is `1g` (1 GiB of disk space)
+
+
If 0, disk storage for the cache is disabled.
@@ -698,6 +733,16 @@
cache.diff.memoryLimit to fit all files users will view in a 1 or 2
day span.
+cache `"diff_summary"`::
++
+Each item caches list of file paths which are different between two
+commits. Gerrit uses this cache to accelerate computing of the list
+of paths of changed files.
++
+Ideally, disk limit of this cache is large enough to cover all changes.
+This should significantly speed up change reindexing, especially
+full offline reindexing.
+
cache `"git_tags"`::
+
If branch or reference level READ access controls are used, this
@@ -980,6 +1025,13 @@
+
Default is true.
+[[change.showAssignee]]change.showAssignee::
++
+Allow assignee workflow. If set to false, assignees will not be visible anywhere
+in UI.
++
+Default is true.
+
[[change.submitLabel]]change.submitLabel::
+
Label name for the submit button.
@@ -2409,6 +2461,10 @@
+
A link:http://lucene.apache.org/[Lucene] index is used.
+
++
+* `ELASTICSEARCH`
++
+An link:http://www.elasticsearch.org/[Elasticsearch] index is used.
+
By default, `LUCENE`.
@@ -2533,6 +2589,43 @@
maxBufferedDocs = 500
----
+
+==== Elasticsearch configuration
+
+WARNING: ElasticSearch implementation is incomplete. Right now it is
+still using parts of Lucene index.
+
+Open and closed changes are indexed in a single index, separated
+into types 'open_changes' and 'closed_changes' respectively.
+
+The following settings are only used when the index type is
+`ELASTICSEARCH`.
+
+[[index.protocol]]index.protocol::
++
+Elasticsearch server protocol [http|https].
++
+Defaults to `http`.
+
+[[index.hostname]]index.hostname::
++
+Elasticsearch server hostname.
+
+Defaults to `localhost`.
+
+[[index.port]]index.port::
++
+Elasticsearch server port.
++
+Defauls to `9200`.
+
+[[index.name]]index.name::
++
+This setting can be used to index changes from multiple Gerrit
+instances in a single Elasticsearch cluster.
++
+Defaults to 'gerrit'.
+
[[ldap]]
=== Section ldap
@@ -3327,6 +3420,14 @@
+
By default, true, allowing notifications to be sent.
+[[sendemail.html]]sendemail.html::
++
+If false, Gerrit will only send plain-text emails.
+If true, Gerrit will send multi-part emails with an HTML and
+plain text part.
++
+By default, true, allowing HTML in the emails Gerrit sends.
+
[[sendemail.connectTimeout]]sendemail.connectTimeout::
+
The connection timeout of opening a socket connected to a
@@ -3358,7 +3459,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`
+
@@ -3384,6 +3487,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
@@ -3469,6 +3582,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/config-robot-comments.txt b/Documentation/config-robot-comments.txt
new file mode 100644
index 0000000..cf5de10
--- /dev/null
+++ b/Documentation/config-robot-comments.txt
@@ -0,0 +1,49 @@
+= Gerrit Code Review - Robot Comments
+
+Gerrit has special support for inline comments that are generated by
+automated third-party systems, so called "robot comments". For example
+robot comments can be used to represent the results of code analyzers.
+
+In contrast to regular inline comments which are free-text comments,
+robot comments are more structured and can contain additional data,
+such as a robot ID, a robot run ID and a URL, see
+link:rest-api-changes.html#robot-comment-info[RobotCommentInfo] for
+details.
+
+It is planned to visualize robot comments differently in the web UI so
+that they can be easily distinguished from human comments. Users should
+also be able to use filtering on robot comments, so that only part of
+the robot comments or no robot comments are shown. In addition it is
+planned that robot comments can contain fixes, that users can apply by
+a single click.
+
+== REST endpoints
+
+* Posting robot comments is done by the
+ link:rest-api-changes.html[Set Review] REST endpoint. The
+ link:rest-api-changes.html#review-input[input] for this REST endpoint
+ can contain robot comments in its `robot_comments` field.
+* link:rest-api-changes.html#list-robot-comments[List Robot Comments]
+* link:rest-api-changes.html#get-robot-comment[Get Robot Comment]
+
+== Storage
+
+Robot comments are stored per change in a
+`refs/changes/XX/YYYY/robot-comments` ref, where `XX/YYYY` is the
+sharded change ID.
+
+Robot comments can be dropped by deleting this ref.
+
+== Limitations
+
+* Robot comments are only supported with NoteDb, but not with ReviewDb.
+* Robot comments are not displayed in the web UI yet.
+* There is no support for draft robot comments, but robot comments are
+ always published and visible to everyone who can see the change.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index 2707e5c..cccfe5c 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -80,6 +80,13 @@
E.g. a plugin could use this to enforce a certain name scheme for
group names.
+[[assignee-validation]]
+== Assignee validation
+
+
+Plugins implementing the `AssigneeValidationListener` interface can perform
+validation of assignees before they are assigned to a change.
+
[[hashtag-validation]]
== Hashtag validation
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
new file mode 100644
index 0000000..f063b88
--- /dev/null
+++ b/Documentation/dev-bazel.txt
@@ -0,0 +1,254 @@
+= Gerrit Code Review - Building with Bazel
+
+Bazel build is experimental. Major missing parts:
+
+* PolyGerrit
+* Documentation index
+* License tracking
+* Version stamping
+* Custom plugins
+* Eclipse project generation.
+* Publishing to maven.
+* Test suites for SSH, acceptance, etc.
+* tag tests as slow, flaky, etc.
+
+Nice to have:
+
+* JGit build from local tree.
+* Global maven artifact caching.
+* local.properties proxy config.
+* coverage
+
+== Installation
+
+You need to use Java 8 and Node.js for building gerrit.
+
+You can install Bazel from the bazel.io:
+https://www.bazel.io/versions/master/docs/install.html
+
+
+[[build]]
+== Building on the Command Line
+
+=== Gerrit Development WAR File
+
+To build the Gerrit web application that includes GWT UI and PolyGerrit UI:
+
+----
+ bazel build gerrit
+----
+
+[NOTE]
+PolyGerrit UI not yet working.
+
+The output executable WAR will be placed in:
+
+----
+ bazel-bin/gerrit.war
+----
+
+
+=== Headless Mode
+
+To build Gerrit in headless mode, i.e. without the GWT Web UI:
+
+----
+ bazel build headless
+----
+
+The output executable WAR will be placed in:
+
+----
+ bazel-bin/headless/headless.war
+----
+
+=== Extension and Plugin API JAR Files
+
+To build the extension, plugin and GWT API JAR files:
+
+----
+ bazel build gerrit-plugin-api:plugin-api_deploy.jar
+ bazel build gerrit-extension-api:extension-api_deploy.jar
+----
+
+Java binaries, Java sources and Java docs are generated into corresponding
+project directories, here as example for plugin API:
+
+----
+ bazel-bin/gerrit-plugin-api/plugin-api_deploy.jar
+ bazel-bin/gerrit-extension-api/extension-api_deploy.jar
+----
+
+TODO - fix and document deployment to maven
+
+=== Plugins
+
+----
+ bazel build plugins:core
+----
+
+The output JAR files for individual plugins will be placed in:
+
+----
+ bazel-bin/plugins/<name>/<name>_deploy.jar
+----
+
+The JAR files will also be packaged in:
+
+----
+ bazel-genfiles/plugins/core.zip
+----
+
+To build a specific plugin:
+
+----
+ bazel build plugins/<name>:<name>_deploy.jar
+----
+
+The output JAR file will be be placed in:
+
+----
+ bazel-bin/plugins/<name>/<name>_deploy.jar
+----
+
+Note that when building an individual plugin, the `core.zip` package
+is not regenerated.
+
+
+[[documentation]]
+=== Documentation
+
+
+
+[[release]]
+=== Gerrit Release WAR File
+
+----
+ bazel build release
+----
+
+[[tests]]
+== Running Unit Tests
+
+----
+ bazel test --build_tests_only //...
+----
+
+Debugging tests:
+
+----
+ bazel test --test_output=streamed --test_filter=com.gerrit.TestClass.testMethod //...
+----
+
+To run a specific test group, e.g. the rest-account test group:
+
+----
+ bazel test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account
+----
+
+== Dependencies
+
+Dependency JARs are normally downloaded automatically, but Buck can inspect
+its graph and download any missing JAR files. This is useful to enable
+subsequent builds to run without network access:
+
+----
+ tools/download_all.py
+----
+
+When downloading from behind a proxy (which is common in some corporate
+environments), it might be necessary to explicitly specify the proxy that
+is then used by `curl`:
+
+----
+ export http_proxy=http://<proxy_user_id>:<proxy_password>@<proxy_server>:<proxy_port>
+----
+
+
+== Building against unpublished Maven JARs
+
+To build against unpublished Maven JARs, like gwtorm or PrologCafe, the custom
+JARs must be installed in the local Maven repository (`mvn clean install`) and
+`maven_jar()` must be updated to point to the `MAVEN_LOCAL` Maven repository for
+that artifact:
+
+[source,python]
+----
+ maven_jar(
+ name = 'gwtorm',
+ id = 'gwtorm:gwtorm:42',
+ license = 'Apache2.0',
+ repository = MAVEN_LOCAL,
+ )
+----
+
+== Building against artifacts from custom Maven repositories
+
+To build against custom Maven repositories, two modes of operations are
+supported: with rewrite in local.properties and without.
+
+Without rewrite the URL of custom Maven repository can be directly passed
+to the maven_jar() function:
+
+[source,python]
+----
+ GERRIT_FORGE = 'http://gerritforge.com/snapshot'
+
+ maven_jar(
+ name = 'gitblit',
+ id = 'com.gitblit:gitblit:1.4.0',
+ sha1 = '1b130dbf5578ace37507430a4a523f6594bf34fa',
+ license = 'Apache2.0',
+ repository = GERRIT_FORGE,
+ )
+----
+
+When the custom URL has to be rewritten, then the same logic as with Gerrit
+known Maven repository is used: Repo name must be defined that matches an entry
+in local.properties file:
+
+----
+ download.GERRIT_FORGE = http://my.company.mirror/gerrit-forge
+----
+
+And corresponding WORKSPACE excerpt:
+
+[source,python]
+----
+ GERRIT_FORGE = 'GERRIT_FORGE:'
+
+ maven_jar(
+ name = 'gitblit',
+ id = 'com.gitblit:gitblit:1.4.0',
+ sha1 = '1b130dbf5578ace37507430a4a523f6594bf34fa',
+ license = 'Apache2.0',
+ repository = GERRIT_FORGE,
+ )
+----
+
+
+[[clean-cache]]
+=== Cleaning The download cache
+
+The cache for the Gerrit Code Review project is located in
+`~/.gerritcodereview/buck-cache/locally-built-artifacts`.
+
+If you really do need to clean the cache manually, then:
+
+----
+ rm -rf ~/.gerritcodereview/buck-cache/locally-built-artifacts
+----
+
+Note that the root `buck-cache` folder should not be deleted as it also contains
+the `downloaded-artifacts` directory, which holds the artifacts that got
+downloaded (not built locally).
+
+
+== Known issues and bugs
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 315c0b0..219da7e 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -3,7 +3,7 @@
== Installation
-You need to use Java 7 and Node.js for building gerrit.
+You need to use Java 8 and Node.js for building gerrit.
There is currently no binary distribution of Buck, so it has to be manually
built and installed. Apache Ant and gcc are required. Currently only Linux
@@ -547,7 +547,7 @@
----
cat > .buckjavaargs <<EOF
- -XX:MaxPermSize=512m -Xms8000m -Xmx16000m
+ -Xms8000m -Xmx16000m
EOF
----
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 4e2b500..3260e23 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.1 \
+ -DarchetypeVersion=2.14-SNAPSHOT \
-DgroupId=com.googlesource.gerrit.plugins.testplugin \
-DartifactId=testplugin
----
@@ -1112,6 +1112,10 @@
+
Panel will be shown below the related info block.
+** `GerritUiExtensionPoint.CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS`:
++
+Panel will be shown in the history bar on the right side of the buttons.
+
** The following parameters are provided:
*** `GerritUiExtensionPoint.Key.CHANGE_INFO`:
+
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 921244f..d43c863 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -89,17 +89,15 @@
To upload artifacts to a bucket the user must authenticate with a
username and password. The username and password need to be retrieved
-from the link:https://console.developers.google.com/project/164060093628[
-Google Developers Console]:
+from the link:https://console.cloud.google.com/storage/settings?project=api-project-164060093628[
+Storage Setting in the Google Cloud Platform Console]:
-* In the menu on the left select `Storage` -> `Cloud Storage` >
-> `Storage access`
-* Select the `Interoperability` tab
-* If no keys are listed under `Interoperable storage access keys`, select "Create a new key"
-* Use the `Access Key` as username, and `Secret` as the password
+Select the `Interoperability` tab, and if no keys are listed under
+`Interoperable storage access keys`, select 'Create a new key'.
-To make the username and password known to Maven, they must be
-configured in the `~/.m2/settings.xml` file.
+Using `Access Key` as username and `Secret` as the password, add the
+configuration in the `~/.m2/settings.xml` file to make the credentials
+known to Maven:
----
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -143,10 +141,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/index.txt b/Documentation/index.txt
index f53463c..06a416d 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -45,6 +45,7 @@
. link:config-hooks.html[Hooks]
. link:config-mail.html[Mail Templates]
. link:config-cla.html[Contributor Agreements]
+. link:config-robot-comments.html[Robot Comments]
== Server Administration
. link:install.html[Installation Guide]
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 2623256..a8115db 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -26,14 +26,14 @@
----
$ java -version
- java version "1.7.0_21"
- Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
- Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)
+ openjdk version "1.8.0_72"
+ OpenJDK Runtime Environment (build 1.8.0_72-b15)
+ OpenJDK 64-Bit Server VM (build 25.72-b15, mixed mode)
----
If Java isn't installed, get it:
-* JDK, minimum version 1.7 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
+* JDK, minimum version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
[[user]]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index e3fb28d..86c9f9a 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -5,7 +5,7 @@
To run the Gerrit service, the following requirements must be met on
the host:
-* JDK, minimum version 1.7 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
+* JDK, minimum version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
You'll also need an SQL database to house the review metadata. You have the
choice of either using the embedded H2 or to host your own MySQL or PostgreSQL.
@@ -172,6 +172,50 @@
the embedded Jetty server, see
link:install-j2ee.html[J2EE installation].
+[[installation_on_windows]]
+== Installation on Windows
+
+If new site is going to be initialized with Bouncy Castle cryptography,
+ssh-keygen command must be available during the init phase. If you have
+link:https://git-for-windows.github.io/[Git for Windows] installed,
+start Command Prompt and temporary add directory with ssh-keygen to the
+PATH environment variable just before running init command:
+
+====
+ PATH=%PATH%;c:\Program Files\Git\usr\bin
+====
+
+Please note that the path in the above example must not be
+double-quoted.
+
+To run the daemon after site initialization execute:
+
+====
+ cd C:\MY\GERRIT\SITE
+ java.exe -jar bin\gerrit.war daemon --console-log
+====
+
+To stop the daemon press Ctrl+C.
+
+=== Install the daemon as Windows Service
+
+To install Gerrit as Windows Service use the
+link:http://commons.apache.org/proper/commons-daemon/procrun.html[Apache
+Commons Daemon Procrun].
+
+Sample install command:
+
+====
+ prunsrv.exe //IS//Gerrit --DisplayName="Gerrit Code Review" --Startup=auto ^
+ --Jvm="C:\Program Files\Java\jre1.8.0_65\bin\server\jvm.dll" ^
+ --Classpath=C:\MY\GERRIT\SITE\bin\gerrit.war ^
+ --LogPath=C:\MY\GERRIT\SITE\logs ^
+ --StartPath=C:\MY\GERRIT\SITE ^
+ --StartMode=jvm --StopMode=jvm ^
+ --StartClass=com.google.gerrit.launcher.GerritLauncher --StartMethod=daemonStart ^
+ --StopClass=com.google.gerrit.launcher.GerritLauncher --StopMethod=daemonStop ^
+ ++DependsOn=postgresql-x64-9.4
+====
[[customize]]
== Site Customization
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 61ea582..07a3d78 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 77ca75a..ababc16 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",
@@ -2440,8 +2441,6 @@
link:rest-api-config.html#download-info[DownloadInfo].
|`download_command` ||
The type of download command the user prefers to use.
-|`copy_self_on_email` |not set if `false`|
-Whether to CC me on comments I write.
|`date_format` ||
The format to display the date in.
Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
@@ -2450,21 +2449,21 @@
Allowed values are `HHMM_12`, `HHMM_24`.
|`relative_date_in_change_table`|not set if `false`|
Whether to show relative dates in the changes table.
+|`diff_view` ||
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
|`size_bar_in_change_table` |not set if `false`|
Whether to show the change sizes as colored bars in the change table.
|`legacycid_in_change_table` |not set if `false`|
Whether to show change number in the change table.
+|`review_category_strategy` ||
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
|`mute_common_path_prefixes` |not set if `false`|
Whether to mute common path prefixes in file names in the file table.
|`signed_off_by` |not set if `false`|
Whether to insert Signed-off-by footer in changes created with the
inline edit feature.
-|`review_category_strategy` ||
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`diff_view` ||
-The type of diff view to show.
-Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
|`my` ||
The menu items of the `MY` top menu as a list of
link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
@@ -2477,6 +2476,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]]
@@ -2498,8 +2501,6 @@
The type of download URL the user prefers to use.
|`download_command` |optional|
The type of download command the user prefers to use.
-|`copy_self_on_email` |optional|
-Whether to CC me on comments I write.
|`date_format` |optional|
The format to display the date in.
Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
@@ -2508,21 +2509,21 @@
Allowed values are `HHMM_12`, `HHMM_24`.
|`relative_date_in_change_table`|optional|
Whether to show relative dates in the changes table.
+|`diff_view` |optional|
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
|`size_bar_in_change_table` |optional|
Whether to show the change sizes as colored bars in the change table.
|`legacycid_in_change_table` |optional|
Whether to show change number in the change table.
+|`review_category_strategy` |optional|
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
|`mute_common_path_prefixes` |optional|
Whether to mute common path prefixes in file names in the file table.
|`signed_off_by` |optional|
Whether to insert Signed-off-by footer in changes created with the
inline edit feature.
-|`review_category_strategy` |optional|
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`diff_view` |optional|
-The type of diff view to show.
-Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
|`my` |optional|
The menu items of the `MY` top menu as a list of
link:rest-api-config.html#top-menu-item-info[TopMenuItemInfo] entities.
@@ -2535,6 +2536,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 d0e504c..ae02475 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -301,6 +301,12 @@
link:user-search.html#reviewedby[reviewedby:self].
--
+[[submittable]]
+--
+* `SUBMITTABLE`: include the `submittable` field in link:#change-info[ChangeInfo],
+ which can be used to tell if the change is reviewed and ready for submit.
+--
+
[[web-links]]
--
* `WEB_LINKS`: include the `web_links` field in link:#commit-info[CommitInfo],
@@ -824,6 +830,154 @@
HTTP/1.1 204 No Content
----
+[[get-assignee]]
+=== Get Assignee
+--
+'GET /changes/link:#change-id[\{change-id\}]/assignee'
+--
+
+Retrieves the account of the user assigned to a change.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
+----
+
+As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
+describing the assigned account is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+----
+
+If the change has no assignee the response is "`204 No Content`".
+
+[[get-past-assignees]]
+=== Get Past Assignees
+--
+'GET /changes/link:#change-id[\{change-id\}]/past_assignees'
+--
+
+Returns a list of every user ever assigned to a change, in the order in which
+they were first assigned.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/past_assignees HTTP/1.0
+----
+
+As a response a list of link:rest-api-accounts.html#account-info[AccountInfo]
+entities is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "_account_id": 1000051,
+ "name": "Jane Doe",
+ "email": "jane.doe@example.com",
+ "username": "janed"
+ },
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+ ]
+
+----
+
+
+[[set-assignee]]
+=== Set Assignee
+--
+'PUT /changes/link:#change-id[\{change-id\}]/assignee'
+--
+
+Sets the assignee of a change.
+
+The new assignee must be provided in the request body inside a
+link:#assignee-input[AssigneeInput] entity.
+
+.Request
+----
+ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "assignee": "jdoe"
+ }
+----
+
+As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
+describing the assigned account is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+----
+
+[[delete-assignee]]
+=== Delete Assignee
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/assignee'
+--
+
+Deletes the assignee of a change.
+
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/assignee HTTP/1.0
+----
+
+As a response an link:rest-api-accounts.html#account-info[AccountInfo] entity
+describing the account of the deleted assignee is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+----
+
+If the change had no assignee the response is "`204 No Content`".
+
[[abandon-change]]
=== Abandon Change
--
@@ -1272,8 +1426,13 @@
The listed changes use the same format as in
link:#list-changes[Query Changes] with the
link:#labels[`LABELS`], link:#detailed-labels[`DETAILED_LABELS`],
-link:#current-revision[`CURRENT_REVISION`], and
-link:#current-commit[`CURRENT_COMMIT`] options set.
+link:#current-revision[`CURRENT_REVISION`],
+link:#current-commit[`CURRENT_COMMIT`], and
+link:#submittable[`SUBMITTABLE`] options set.
+
+Standard link:#query-options[formatting options] can be specified
+with the `o` parameter, as well as the `submitted_together` specific
+option `NON_VISIBLE_CHANGES`.
.Response
----
@@ -2135,9 +2294,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 +2575,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 +2642,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 +2731,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
--
@@ -3176,6 +3416,60 @@
will suggest the browser save the patch as `commitsha1.diff.base64`,
for later processing by command line tools.
+[[submit-preview]]
+===Submit Preview
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/preview_submit'
+--
+Gets a file containing thin bundles of all modified projects if this
+change was submitted. The bundles are named `${ProjectName}.git`.
+Each thin bundle contains enough to construct the state in which a project would
+be in if this change were submitted. The base of the thin bundles are the
+current target branches, so to make use of this call in a non-racy way, first
+get the bundles and then fetch all projects contained in the bundle.
+(This assumes no non-fastforward pushes).
+
+You need to give a parameter '?format=zip' or '?format=tar' to specify the
+format for the outer container. It is always possible to use tgz, even if
+tgz is not in the list of allowed archive formats.
+
+To make good use of this call, you would roughly need code as found at:
+----
+ $ curl -Lo preview_submit_test.sh http://review.example.com:8080/tools/scripts/preview_submit_test.sh
+----
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/preview_submit?zip HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Date: Tue, 13 Sep 2016 19:13:46 GMT
+ Content-Disposition: attachment; filename="submit-preview-147.zip"
+ X-Content-Type-Options: nosniff
+ Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+ Pragma: no-cache
+ Expires: Mon, 01 Jan 1990 00:00:00 GMT
+ Content-Type: application/x-zip
+ Transfer-Encoding: chunked
+
+ [binary stuff]
+----
+
+In case of an error, the response is not a zip file but a regular json response,
+containing only the error message:
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ "Anonymous users cannot submit"
+----
+
[[get-mergeable]]
=== Get Mergeable
--
@@ -3611,6 +3905,102 @@
}
----
+[[list-comments]]
+=== List Robot Comments
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/'
+--
+
+Lists the link:config-robot-comments.html[robot comments] of a
+revision.
+
+As result a map is returned that maps the file path to a list of
+link:#robot-comment-info[RobotCommentInfo] entries. The entries in the
+map are sorted by file path.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+ {
+ "id": "TvcXrmjM",
+ "line": 23,
+ "message": "unused import",
+ "updated": "2016-02-26 15:40:43.986000000",
+ "author": {
+ "_account_id": 1000110,
+ "name": "Code Analyzer",
+ "email": "code.analyzer@example.com"
+ },
+ "robotId": "importChecker",
+ "robotRunId": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
+ },
+ {
+ "id": "TveXwFiA",
+ "line": 49,
+ "message": "wrong indention",
+ "updated": "2016-02-26 15:40:45.328000000",
+ "author": {
+ "_account_id": 1000110,
+ "name": "Code Analyzer",
+ "email": "code.analyzer@example.com"
+ },
+ "robotId": "styleChecker",
+ "robotRunId": "5c606c425dd45184484f9d0a2ffd725a7607839b"
+ }
+ ]
+ }
+----
+
+[[get-robot-comment]]
+=== Get Robot Comment
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/robotcomments/link:#comment-id[\{comment-id\}]'
+--
+
+Retrieves a link:config-robot-comments.html[robot comment] of a
+revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/robotcomments/TvcXrmjM HTTP/1.0
+----
+
+As response a link:#robot-comment-info[RobotCommentInfo] entity is
+returned that describes the robot comment.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "TvcXrmjM",
+ "line": 23,
+ "message": "unused import",
+ "updated": "2016-02-26 15:40:43.986000000",
+ "author": {
+ "_account_id": 1000110,
+ "name": "Code Analyzer",
+ "email": "code.analyzer@example.com"
+ },
+ "robotId": "importChecker",
+ "robotRunId": "76b1375aa8626ea7149792831fe2ed85e80d9e04"
+ }
+----
+
[[list-files]]
=== List Files
--
@@ -4256,6 +4646,18 @@
invocations of the REST call are required.
|===========================
+[[assignee-input]]
+=== AssigneeInput
+The `AssigneeInput` entity contains the identity of the user to be set as assignee.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`assignee` ||
+The link:rest-api-accounts.html#account-id[ID] of one account that
+should be added as assignee.
+|===========================
+
[[blame-info]]
=== BlameInfo
The `BlameInfo` entity stores the commit metadata with the row coordinates where
@@ -4340,6 +4742,9 @@
|`mergeable` |optional|
Whether the change is mergeable. +
Not set for merged changes, or if the change has not yet been tested.
+|`submittable` |optional|
+Whether the change has been approved by the project submit rules. +
+Only set if link:#submittable[requested].
|`insertions` ||
Number of inserted lines.
|`deletions` ||
@@ -4589,6 +4994,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
@@ -4974,6 +5394,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
@@ -5127,6 +5562,9 @@
|`comments` |optional|
The comments that should be added as a map that maps a file path to a
list of link:#comment-input[CommentInput] entities.
+|`robot_comments` |optional|
+The robot comments that should be added as a map that maps a file path
+to a list of link:#robot-comment-input[RobotCommentInput] entities.
|`strict_labels` |`true` if not set|
Whether all labels are required to be within the user's permitted ranges
based on access controls. +
@@ -5141,7 +5579,9 @@
Allowed values are `DELETE`, `PUBLISH`, `PUBLISH_ALL_REVISIONS` and
`KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
for a single revision. +
-If not set, the default is `DELETE`.
+Only `KEEP` is allowed when used in conjunction with `on_behalf_of`. +
+If not set, the default is `DELETE`, unless `on_behalf_of` is set, in
+which case the default is `KEEP` and any other value is disallowed.
|`notify` |optional|
Notify handling that defines to whom email notifications should be sent
after the review is stored. +
@@ -5195,6 +5635,11 @@
The Gerrit server may be configured to
link:config-gerrit.html#addreviewer.maxWithoutConfirmation[require a
confirmation] when adding a group as reviewer that has many members.
+|`notify` |optional|
+Notify handling that defines to whom email notifications should be sent
+after the reviewer is added. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
|===========================
[[revision-info]]
@@ -5253,6 +5698,31 @@
certificate was provided, it is set to an empty object.
|===========================
+[[robot-comment-info]]
+=== RobotCommentInfo
+The `RobotCommentInfo` entity contains information about a robot inline
+comment.
+
+`RobotCommentInfo` has the same fields as link:#[CommentInfo].
+In addition `RobotCommentInfo` has the following fields:
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`robot_id` ||The ID of the robot that generated this comment.
+|`robot_run_id` ||An ID of the run of the robot.
+|`url` |optional|URL to more information.
+|`properties` |optional|
+Robot specific properties as map that maps arbitrary keys to values.
+|===========================
+
+[[robot-comment-input]]
+=== RobotCommentInput
+The `RobotCommentInput` entity contains information for creating an inline
+robot comment.
+
+`RobotCommentInput` has the same fields as link:#[RobotCommentInfo].
+
[[rule-input]]
=== RuleInput
The `RuleInput` entity contains information to test a Prolog rule.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index c7c0878..7246786 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"
@@ -115,7 +123,10 @@
"gerrit": {
"all_projects": "All-Projects",
"all_users": "All-Users"
- "doc_search": true
+ "doc_search": true,
+ "web_uis": [
+ "gwt"
+ ]
},
"sshd": {},
"suggest": {
@@ -1226,6 +1237,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`.
@@ -1261,6 +1275,12 @@
is used for Git over HTTP/HTTPS]. Only set if
link:config-gerrit.html#auth.type[authentication type] is is `LDAP` or
`LDAP_BIND`.
+|`git_basic_auth_policy` |optional|
+The link:config-gerrit.html#auth.gitBasicAuthPolicy[policy] to authenticate
+Git over HTTP and REST API requests when
+link:config-gerrit.html#auth.type[authentication type] is `LDAP` and
+link:config-gerrit.html#auth.gitBasicAuth[basic authentication] is set to true.
+Can be `HTTP`, `LDAP` or `HTTP_LDAP`.
|==========================================
[[cache-info]]
@@ -1458,6 +1478,9 @@
|`report_bug_text` |optional, not set if default|
link:config-gerrit.html#gerrit.reportBugText[Display text for report
bugs link].
+|`web_uis` ||
+List of web UIs supported by the HTTP server. Possible values are `GWT`
+and `POLYGERRIT`.
|=================================
[[hit-ration-info]]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index b04898e..0b6de57 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -482,6 +482,12 @@
+
Matches changes with a +2 code review where the reviewer is jsmith.
+`label:Code-Review=+2,user=owner`::
+`label:Code-Review=+2,owner`::
++
+The special "owner" parameter corresponds to the change owner. Matches
+all changes that have a +2 vote from the change owner.
+
`label:Code-Review=+1,group=ldap/linux.workflow`::
+
Matches changes with a +1 code review where the reviewer is in the
@@ -499,7 +505,6 @@
+
Changes that are blocked from submission due to a blocking score.
-
== Magical Operators
Most of these operators exist to support features of Gerrit Code
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index ca79b93..a0803d6 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -19,11 +19,17 @@
user must authenticate via HTTP/HTTPS.
When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
-the user is authenticated using standard BasicAuth and credentials validated
-using the randomly generated HTTP password on the `HTTP Password` tab
-in the user settings page or against LDAP when configured for the Gerrit Web UI.
+the user is authenticated using standard BasicAuth. Depending on the value of
+link:#auth.gitBasicAuthPolicy[auth.gitBasicAuthPolicy], credentials are
+validated using:
-When gitBasicAuth is not configured, the user's HTTP credentials can be
+* The randomly generated HTTP password on the `HTTP Password` tab
+ in the user settings page if `gitBasicAuthPolicy` is `HTTP`.
+* The LDAP password if `gitBasicAuthPolicy` is `LDAP`
+* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
+ is `HTTP_LDAP`.
+
+When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can be
accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
Password` tab.
@@ -176,12 +182,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 +409,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.
@@ -452,6 +462,23 @@
git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%base=commit-id1,base=commit-id2
----
+[[merged]]
+=== Creating Changes for Merged Commits
+
+Normally, changes are only created for commits that have not yet
+been merged into the branch. In some cases, you may want to review a
+change that has already been merged. A new change for a merged commit
+can be created by using the '%merged' argument:
+
+----
+ git push ssh://john.doe@git.example.com:29418/kernel/common my-merged-commit:refs/for/master%merged
+----
+
+This only creates one merged change at a time, corresponding to
+exactly `my-merged-commit`. It doesn't walk all of history up to that
+point, which could be slow and create lots of unintended new changes.
+To create multiple new changes, run push multiple times.
+
== repo upload
diff --git a/ReleaseNotes/BUILD b/ReleaseNotes/BUILD
new file mode 100644
index 0000000..9bf572e
--- /dev/null
+++ b/ReleaseNotes/BUILD
@@ -0,0 +1,27 @@
+load("//tools/bzl:asciidoc.bzl", "release_notes_attributes")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
+
+
+SRCS = glob(['*.txt'])
+
+
+genasciidoc(
+ name = 'ReleaseNotes',
+ srcs = SRCS,
+ attributes = release_notes_attributes(),
+ backend = 'html5',
+ searchbox = False,
+ resources = False,
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "html",
+ srcs = SRCS,
+ attributes = release_notes_attributes(),
+ backend = 'html5',
+ searchbox = False,
+ resources = False,
+ visibility = ["//visibility:public"],
+)
diff --git a/VERSION b/VERSION
index 8f1d4f4..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.1'
+GERRIT_VERSION = '2.14-SNAPSHOT'
diff --git a/WORKSPACE b/WORKSPACE
index d465b37..17fbfea 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,3 +1,5 @@
+workspace(name="gerrit")
+
ANTLR_VERS = '3.5.2'
maven_jar(
@@ -24,24 +26,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(
@@ -62,18 +70,18 @@
sha1 = '5d9e2e895e3111622720157d0aa540066d5fce3a',
)
-GWT_VERS = '2.7.0'
+GWT_VERS = '2.8.0-rc2'
maven_jar(
name = 'user',
artifact = 'com.google.gwt:gwt-user:' + GWT_VERS,
- sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b',
+ sha1 = 'ad99b09a626c20cce2bdacf3726a51b2cd16b99e',
)
maven_jar(
name = 'dev',
artifact = 'com.google.gwt:gwt-dev:' + GWT_VERS,
- sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982',
+ sha1 = 'd70a6feb4661c07488090cb81303415e9110b15a',
)
maven_jar(
@@ -82,28 +90,68 @@
sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
)
-JGIT_VERS = '4.4.1.201607150455-r.105-g81ba2be'
+maven_jar(
+ name = 'jsinterop_annotations',
+ artifact = 'com.google.jsinterop:jsinterop-annotations:1.0.0',
+ sha1 = '23c3a3c060ffe4817e67673cc8294e154b0a4a95',
+)
+
+maven_jar(
+ name = 'ant',
+ artifact = 'ant:ant:1.6.5',
+ sha1 = '7d18faf23df1a5c3a43613952e0e8a182664564b',
+)
+
+maven_jar(
+ name = 'colt',
+ artifact = 'colt:colt:1.2.0',
+ sha1 = '0abc984f3adc760684d49e0f11ddf167ba516d4f',
+)
+
+maven_jar(
+ name = 'tapestry',
+ artifact = 'tapestry:tapestry:4.0.2',
+ sha1 = 'e855a807425d522e958cbce8697f21e9d679b1f7',
+)
+
+maven_jar(
+ name = 'w3c_css_sac',
+ artifact = 'org.w3c.css:sac:1.3',
+ sha1 = 'cdb2dcb4e22b83d6b32b93095f644c3462739e82',
+)
+
+http_jar(
+ name = "javax_validation_src",
+ url = "http://repo1.maven.org/maven2/javax/validation/validation-api/1.0.0.GA/validation-api-1.0.0.GA-sources.jar",
+ sha256 = 'a394d52a9b7fe2bb14f0718d2b3c8308ffe8f37e911956012398d55c9f9f9b54',
+)
+
+http_jar(
+ name = "jsinterop_annotations_src",
+ url = "http://central.maven.org/maven2/com/google/jsinterop/jsinterop-annotations/1.0.0/jsinterop-annotations-1.0.0-sources.jar",
+ sha256 = '80d63c117736ae2fb9837b7a39576f3f0c5bd19cd75127886550c77b4c478f87',
+)
+
+JGIT_VERS = '4.5.0.201609210915-r'
maven_jar(
name = 'jgit',
- repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS,
- sha1 = 'c07c9c66da7983095a40945c0bfab211a473c4c5',
+ sha1 = '3e3d0b73dcf4ad649f37758ea8502d92f3d299de',
)
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 = '6e36638888918d9941dddec7e2abe1f162cc74d9',
)
# 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',
- url = 'http://gerrit-maven.storage.googleapis.com/org/eclipse/jgit/org.eclipse.jgit/' +
+ sha256 = '426bf32d097a846a247d5fb1d258fcde1707dec3362b8a62c68785b953c2ae65',
+ url = 'http://repo1.maven.org/maven2/org/eclipse/jgit/org.eclipse.jgit/' +
'%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS),
)
@@ -115,46 +163,44 @@
maven_jar(
name = 'jgit_archive',
- repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS,
- sha1 = 'fc3bc40e070c54198a046fcd3a1f7cac47163961',
+ sha1 = '2db2e7666672a31fa41b7e1dadcba51df6d30954',
)
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 = 'e8fb1d81f588c3174a9730bdecdbde9faa04140a',
)
maven_jar(
name = 'gwtjsonrpc',
- artifact = 'com.google.gerrit:gwtjsonrpc:1.8',
- sha1 = 'c264bf2f543cffddceada5cdf031eea06dbd44a0',
+ artifact = 'com.google.gerrit:gwtjsonrpc:1.11',
+ sha1 = '0990e7eec9eec3a15661edcf9232acbac4aeacec',
)
http_jar(
name = 'gwtjsonrpc_src',
- sha256 = '2ef86396861a7c555c404b5a20a72dc6599b541ce2d1370a62f6470eefe7142d',
- url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtjsonrpc/1.8/gwtjsonrpc-1.8-sources.jar',
+ sha256 = 'fc503488872c022073e244015fcb6806a64b65afe546bdac2db167a3875fb418',
+ url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtjsonrpc/1.11/gwtjsonrpc-1.11-sources.jar',
)
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(
name = 'gwtorm_client',
- artifact = 'com.google.gerrit:gwtorm:1.15',
- sha1 = '26a2459f543ed78977535f92e379dc0d6cdde8bb',
+ artifact = 'com.google.gerrit:gwtorm:1.16',
+ sha1 = '3e41b6d7bb352fa0539ce23b9bce97cf8c26c3bf',
)
http_jar(
name = 'gwtorm_client_src',
- sha256 = 'e0cf9382ed8c3cd1f0884ab77dabe634a04546676c4960d8b4c4b64a20132ef6',
- url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtorm/1.15/gwtorm-1.15-sources.jar',
+ sha256 = 'd3e482c9ac1f828aa853debe6545c16503fbbde3bda94b18f652d9830b7f84b1',
+ url = 'http://repo.maven.apache.org/maven2/com/google/gerrit/gwtorm/1.16/gwtorm-1.16-sources.jar',
)
maven_jar(
@@ -165,14 +211,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(
@@ -274,6 +320,12 @@
)
maven_jar(
+ name = 'commons_lang3',
+ artifact = 'org.apache.commons:commons-lang3:3.3.2',
+ sha1 = '90a3822c38ec8c996e84c16a3477ef632cbc87a3',
+)
+
+maven_jar(
name = 'commons_dbcp',
artifact = 'commons-dbcp:commons-dbcp:1.4',
sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39',
@@ -287,8 +339,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 +379,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 +423,91 @@
sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
)
-LUCENE_VERS = '5.4.1'
+LUCENE_VERS = '5.5.2'
maven_jar(
name = 'lucene_core',
artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS,
- sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab',
+ sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb',
)
maven_jar(
name = 'lucene_analyzers_common',
artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS,
- sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e',
+ sha1 = 'f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d',
+)
+
+maven_jar(
+ name = 'lucene_codecs',
+ artifact = 'org.apache.lucene:lucene-codecs:' + LUCENE_VERS,
+ sha1 = 'e01fe463d9490bb1b4a6a168e771f7b7255a50b1',
)
maven_jar(
name = 'backward_codecs',
artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS,
- sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f',
+ sha1 = 'c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5',
)
maven_jar(
name = 'lucene_misc',
artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
- sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b',
+ sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
)
maven_jar(
name = 'lucene_queryparser',
artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS,
- sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618',
+ sha1 = '8ac921563e744463605284c6d9d2d95e1be5b87c',
+)
+
+
+maven_jar(
+ name = 'lucene_highlighter',
+ artifact = 'org.apache.lucene:lucene-highlighter:' + LUCENE_VERS,
+ sha1 = 'd127ac514e9df965ab0b57d92bbe0c68d3d145b8',
+)
+
+maven_jar(
+ name = 'lucene_join',
+ artifact = 'org.apache.lucene:lucene-join:'+ LUCENE_VERS,
+ sha1 = 'dac1b322508f3f2696ecc49a97311d34d8382054',
+)
+
+maven_jar(
+ name = 'lucene_memory',
+ artifact = 'org.apache.lucene:lucene-memory:' + LUCENE_VERS,
+ sha1 = '7409db9863d8fbc265c27793c6cc7511304182c2',
+)
+
+maven_jar(
+ name = 'lucene_misc',
+ artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
+ sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
+)
+
+maven_jar(
+ name = 'lucene_sandbox',
+ artifact = 'org.apache.lucene:lucene-sandbox:' + LUCENE_VERS,
+ sha1 = '30a91f120706ba66732d5a974b56c6971b3c8a16',
+)
+
+maven_jar(
+ name = 'lucene_spatial',
+ artifact = 'org.apache.lucene:lucene-spatial:' + LUCENE_VERS,
+ sha1 = '8ed7a9a43d78222038573dd1c295a61f3c0bb0db',
+)
+
+maven_jar(
+ name = 'lucene_suggest',
+ artifact = 'org.apache.lucene:lucene-suggest:' + LUCENE_VERS,
+ sha1 = 'e8316b37dddcf2092a54dab2ce6aad0d5ad78585',
+)
+
+maven_jar(
+ name = 'lucene_queries',
+ artifact = 'org.apache.lucene:lucene-queries:' + LUCENE_VERS,
+ sha1 = '692f1ad887cf4e006a23f45019e6de30f3312d3f',
)
maven_jar(
@@ -409,34 +516,34 @@
sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47',
)
-PROLOG_VERS = '1.4.1'
+PROLOG_VERS = '1.4.2'
maven_jar(
name = 'prolog_runtime',
repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'com.googlecode.prolog-cafe:prolog-runtime:' + PROLOG_VERS,
- sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246',
+ sha1 = '4421b4806b6e3a318680f6ab1d57569e857169c6',
)
maven_jar(
name = 'prolog_compiler',
repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'com.googlecode.prolog-cafe:prolog-compiler:' + PROLOG_VERS,
- sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41',
+ sha1 = '7e5a7ca5efe7db7f69e015cf492f8f04665244d8',
)
maven_jar(
name = 'prolog_io',
repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'com.googlecode.prolog-cafe:prolog-io:' + PROLOG_VERS,
- sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa',
+ sha1 = 'd177f6211d1013e0f31a507127f5c87a7f6941f3',
)
maven_jar(
name = 'cafeteria',
repository = 'http://gerrit-maven.storage.googleapis.com/',
artifact = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + PROLOG_VERS,
- sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641',
+ sha1 = '11f396cb2588b65e6a78070488aaa58d12bf000e',
)
maven_jar(
@@ -447,8 +554,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 +565,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',
@@ -534,8 +654,8 @@
maven_jar(
name = 'jimfs',
- artifact = 'com.google.jimfs:jimfs:1.0',
- sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97',
+ artifact = 'com.google.jimfs:jimfs:1.1',
+ sha1 = '8fbd0579dc68aba6186935cc1bee21d2f3e7ec1c',
)
maven_jar(
@@ -558,58 +678,58 @@
maven_jar(
name = 'easymock',
- artifact = 'org.easymock:easymock:3.4', # When bumping the version
- sha1 = '9fdeea183a399f25c2469497612cad131e920fa3',
+ artifact = 'org.easymock:easymock:3.1', # When bumping the version
+ sha1 = '3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e',
)
maven_jar(
- name = 'cglib_2_2',
- artifact = 'cglib:cglib-nodep:2.2.2',
- sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+ name = 'cglib_3_2',
+ artifact = 'cglib:cglib-nodep:3.2.0',
+ sha1 = 'cf1ca207c15b04ace918270b6cb3f5601160cdfd',
)
maven_jar(
name = 'objenesis',
- artifact = 'org.objenesis:objenesis:2.2',
- sha1 = '3fb533efdaa50a768c394aa4624144cf8df17845',
+ artifact = 'org.objenesis:objenesis:1.3',
+ sha1 = 'dc13ae4faca6df981fc7aeb5a522d9db446d5d50',
)
-POWERM_VERS = '1.6.4'
+POWERM_VERS = '1.6.1'
maven_jar(
name = 'powermock_module_junit4',
artifact = 'org.powermock:powermock-module-junit4:' + POWERM_VERS,
- sha1 = '8692eb1d9bb8eb1310ffe8a20c2da7ee6d1b5994',
+ sha1 = 'ea8530b2848542624f110a393513af397b37b9cf',
)
maven_jar(
name = 'powermock_module_junit4_common',
artifact = 'org.powermock:powermock-module-junit4-common:' + POWERM_VERS,
- sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81',
+ sha1 = '7222ced54dabc310895d02e45c5428ca05193cda',
)
maven_jar(
name = 'powermock_reflect',
artifact = 'org.powermock:powermock-reflect:' + POWERM_VERS,
- sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893',
+ sha1 = '97d25eda8275c11161bcddda6ef8beabd534c878',
)
maven_jar(
name = 'powermock_api_easymock',
artifact = 'org.powermock:powermock-api-easymock:' + POWERM_VERS,
- sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45',
+ sha1 = 'aa740ecf89a2f64d410b3d93ef8cd6833009ef00',
)
maven_jar(
name = 'powermock_api_support',
artifact = 'org.powermock:powermock-api-support:' + POWERM_VERS,
- sha1 = '314daafb761541293595630e10a3699ebc07881d',
+ sha1 = '592ee6d929c324109d3469501222e0c76ccf0869',
)
maven_jar(
name = 'powermock_core',
artifact = 'org.powermock:powermock-core:' + POWERM_VERS,
- sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87',
+ sha1 = '5afc1efce8d44ed76b30af939657bd598e45d962',
)
maven_jar(
@@ -624,60 +744,60 @@
sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1',
)
-JETTY_VERS = '9.2.14.v20151106'
+JETTY_VERS = '9.3.11.v20160721'
maven_jar(
name = 'jetty_servlet',
artifact = 'org.eclipse.jetty:jetty-servlet:' + JETTY_VERS,
- sha1 = '3a2cd4d8351a38c5d60e0eee010fee11d87483ef',
+ sha1 = 'd550147b85c73ea81084a4ac7915ba7f609021c5',
)
maven_jar(
name = 'jetty_security',
artifact = 'org.eclipse.jetty:jetty-security:' + JETTY_VERS,
- sha1 = '2d36974323fcb31e54745c1527b996990835db67',
+ sha1 = '1cbefc5d1196b9e1ca6f4cc36738998a6ebde8bf',
)
maven_jar(
name = 'jetty_servlets',
artifact = 'org.eclipse.jetty:jetty-servlets:' + JETTY_VERS,
- sha1 = 'a75c78a0ee544073457ca5ee9db20fdc6ed55225',
+ sha1 = 'a9f7a43977151a463aa21a9b0e882aa3d25452ef',
)
maven_jar(
name = 'jetty_server',
artifact = 'org.eclipse.jetty:jetty-server:' + JETTY_VERS,
- sha1 = '70b22c1353e884accf6300093362b25993dac0f5',
+ sha1 = 'd932e0dc1e9bd4839ae446754615163d60271a66',
)
maven_jar(
name = 'jetty_jmx',
artifact = 'org.eclipse.jetty:jetty-jmx:' + JETTY_VERS,
- sha1 = '617edc5e966b4149737811ef8b289cd94b831bab',
+ sha1 = '21a658d2f5eb87c23eef4911966625ea95f66d32',
)
maven_jar(
name = 'jetty_continuation',
artifact = 'org.eclipse.jetty:jetty-continuation:' + JETTY_VERS,
- sha1 = '8909d62fd7e28351e2da30de6fb4105539b949c0',
+ sha1 = '92a91c0dcc5f5d779a1c9f94038332be3f46c9df',
)
maven_jar(
name = 'jetty_http',
artifact = 'org.eclipse.jetty:jetty-http:' + JETTY_VERS,
- sha1 = '699ad1f2fa6fb0717e1b308a8c9e1b8c69d81ef6',
+ sha1 = 'dcfb95e5b886a981bb76467b911c5b706117f9cf',
)
maven_jar(
name = 'jetty_io',
artifact = 'org.eclipse.jetty:jetty-io:' + JETTY_VERS,
- sha1 = 'dfa4137371a3f08769820138ca1a2184dacda267',
+ sha1 = 'db5f4f481159894a4b670072a34917b5414d0c98',
)
maven_jar(
name = 'jetty_util',
artifact = 'org.eclipse.jetty:jetty-util:' + JETTY_VERS,
- sha1 = '0057e00b912ae0c35859ac81594a996007706a0b',
+ sha1 = '1812ffd5a04698051180d582c146ca807760c808',
)
maven_jar(
@@ -697,3 +817,260 @@
artifact = 'xerces:xercesImpl:2.8.1',
sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1',
)
+
+maven_jar(
+ name = 'postgresql',
+ artifact = 'postgresql:postgresql:9.1-901-1.jdbc4',
+ sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
+)
+
+CM_VERSION = '5.19.0'
+
+maven_jar(
+ name = 'codemirror_minified',
+ artifact = 'org.webjars.npm:codemirror-minified:' + CM_VERSION,
+ sha1 = '263bf4acb7c4429be3fe46908af240f9f629d51c',
+)
+
+maven_jar(
+ name = 'codemirror_original',
+ artifact = 'org.webjars.npm:codemirror:' + CM_VERSION,
+ sha1 = 'e9ab382c6be240d55f112051bba3f6c637b798ce',
+)
+
+maven_jar(
+ name = 'diff_match_patch',
+ artifact = 'org.webjars:google-diff-match-patch:20121119-1',
+ sha1 = '0cf1782dbcb8359d95070da9176059a5a9d37709',
+)
+
+maven_jar(
+ name = 'commons_io',
+ artifact = 'commons-io:commons-io:1.4',
+ sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
+)
+
+maven_jar(
+ name = "asciidoctor",
+ artifact = "org.asciidoctor:asciidoctorj:1.5.4.1",
+ sha1 = "f7ddfb2bbed2f8da3f9ad0d1a5514f04b4274a5a",
+)
+
+maven_jar(
+ name = "jruby",
+ artifact = "org.jruby:jruby-complete:9.1.5.0",
+ sha1 = "00d0003e99da3c4d830b12c099691ce910c84e39",
+)
+
+maven_jar(
+ name = 'elasticsearch',
+ artifact = 'org.elasticsearch:elasticsearch:2.4.0',
+ sha1 = 'aeb9704a76fa8654c348f38fcbb993a952a7ab07',
+)
+
+# Java REST client for Elasticsearch.
+JEST_VERSION = '2.0.3'
+
+maven_jar(
+ name = 'jest_common',
+ artifact = 'io.searchbox:jest-common:' + JEST_VERSION,
+ sha1 = 'f304c66894aaf2f6c17a886bc826f09c7a161cf9',
+)
+
+maven_jar(
+ name = 'jest',
+ artifact = 'io.searchbox:jest:' + JEST_VERSION,
+ sha1 = 'b8f9ed1423489b361804e47f640515ea9f1fa08d',
+)
+
+maven_jar(
+ name = 'compress_lzf',
+ artifact = 'com.ning:compress-lzf:1.0.2',
+ sha1 = '62896e6fca184c79cc01a14d143f3ae2b4f4b4ae',
+)
+
+maven_jar(
+ name = 'hppc',
+ artifact = 'com.carrotsearch:hppc:0.7.1',
+ sha1 = '8b5057f74ea378c0150a1860874a3ebdcb713767',
+)
+
+maven_jar(
+ name = 'jsr166e',
+ artifact = 'com.twitter:jsr166e:1.1.0',
+ sha1 = '233098147123ee5ddcd39ffc57ff648be4b7e5b2',
+)
+
+maven_jar(
+ name = 'netty',
+ artifact = 'io.netty:netty:3.10.0.Final',
+ sha1 = 'ad61cd1bba067e6634ddd3e160edf0727391ac30',
+)
+
+maven_jar(
+ name = 't_digest',
+ artifact = 'com.tdunning:t-digest:3.0',
+ sha1 = '84ccf145ac2215e6bfa63baa3101c0af41017cfc',
+)
+
+maven_jar(
+ name = 'jna',
+ artifact = 'net.java.dev.jna:jna:4.1.0',
+ sha1 = '1c12d070e602efd8021891cdd7fd18bc129372d4',
+)
+
+JACKSON_VERSION = '2.6.6'
+
+maven_jar(
+ name = 'jackson_core',
+ artifact = 'com.fasterxml.jackson.core:jackson-core:' + JACKSON_VERSION,
+ sha1 = '02eb801df67aacaf5b1deb4ac626e1964508e47b',
+)
+
+maven_jar(
+ name = 'jackson_dataformat_smile',
+ artifact = 'com.fasterxml.jackson.dataformat:jackson-dataformat-smile:' + JACKSON_VERSION,
+ sha1 = 'ccbfc948748ed2754a58c1af9e0a02b5cc1aed69',
+)
+
+maven_jar(
+ name = 'jackson_dataformat_cbor',
+ artifact = 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:' + JACKSON_VERSION,
+ sha1 = '34c7b7ff495fc6b049612bdc9db0900a68e112f8',
+)
+
+maven_jar(
+ name = 'httpasyncclient',
+ artifact = 'org.apache.httpcomponents:httpasyncclient:4.1.2',
+ sha1 = '95aa3e6fb520191a0970a73cf09f62948ee614be',
+)
+
+maven_jar(
+ name = 'httpcore_nio',
+ artifact = 'org.apache.httpcomponents:httpcore-nio:' + HTTPCOMP_VERS,
+ sha1 = 'a8c5e3c3bfea5ce23fb647c335897e415eb442e3',
+)
+
+maven_jar(
+ name = 'httpcore_niossl',
+ artifact = 'org.apache.httpcomponents:httpcore-niossl:4.0-alpha6',
+ sha1 = '9c662e7247ca8ceb1de5de629f685c9ef3e4ab58',
+)
+load("//tools/bzl:js.bzl", "npm_binary", "bower_archive")
+
+npm_binary(
+ name = "bower",
+)
+
+npm_binary(
+ name = "vulcanize",
+)
+
+npm_binary(
+ name = "crisper",
+)
+
+# bower_archive() seed components.
+bower_archive(
+ name = 'iron-autogrow-textarea',
+ package = 'polymerelements/iron-autogrow-textarea',
+ version = '1.0.12',
+ sha1 = 'b9b6874c9a2b5be435557a827ff8bd6661672ee3',
+)
+
+bower_archive(
+ name = 'es6-promise',
+ package = 'stefanpenner/es6-promise',
+ version = '3.3.0',
+ sha1 = 'a3a797bb22132f1ef75f9a2556173f81870c2e53',
+)
+
+bower_archive(
+ name = 'fetch',
+ package = 'fetch',
+ version = '1.0.0',
+ sha1 = '1b05a2bb40c73232c2909dc196de7519fe4db7a9',
+)
+
+bower_archive(
+ name = 'iron-dropdown',
+ package = 'polymerelements/iron-dropdown',
+ version = '1.4.0',
+ sha1 = '63e3d669a09edaa31c4f05afc76b53b919ef0595',
+)
+
+bower_archive(
+ name = 'iron-input',
+ package = 'polymerelements/iron-input',
+ version = '1.0.10',
+ sha1 = '9bc0c8e81de2527125383cbcf74dd9f27e7fa9ac',
+)
+
+bower_archive(
+ name = 'iron-overlay-behavior',
+ package = 'polymerelements/iron-overlay-behavior',
+ version = '1.7.6',
+ sha1 = '83181085fda59446ce74fd0d5ca30c223f38ee4a',
+)
+
+bower_archive(
+ name = 'iron-selector',
+ package = 'polymerelements/iron-selector',
+ version = '1.5.2',
+ sha1 = 'c57235dfda7fbb987c20ad0e97aac70babf1a1bf',
+)
+
+bower_archive(
+ name = 'moment',
+ package = 'moment/moment',
+ version = '2.13.0',
+ sha1 = 'fc8ce2c799bab21f6ced7aff928244f4ca8880aa',
+)
+
+bower_archive(
+ name = 'page',
+ package = 'visionmedia/page.js',
+ version = '1.7.1',
+ sha1 = '51a05428dd4f68fae1df5f12d0e2b61ba67f7757',
+)
+
+bower_archive(
+ name = 'polymer',
+ package = 'polymer/polymer',
+ version = '1.4.0',
+ sha1 = 'b84725939ead7c7bdf9917b065f68ef8dc790d06',
+)
+
+bower_archive(
+ name = 'promise-polyfill',
+ package = 'polymerlabs/promise-polyfill',
+ version = '1.0.0',
+ sha1 = 'a3b598c06cbd7f441402e666ff748326030905d6',
+)
+
+# bower test stuff
+
+bower_archive(
+ name = 'iron-test-helpers',
+ package = 'polymerelements/iron-test-helpers',
+ version = '1.2.5',
+ sha1 = '433b03b106f5ff32049b84150cd70938e18b67ac',
+)
+
+bower_archive(
+ name = 'test-fixture',
+ package = 'polymerelements/test-fixture',
+ version = '1.1.1',
+ sha1 = 'e373bd21c069163c3a754e234d52c07c77b20d3c',
+)
+
+bower_archive(
+ name = 'web-component-tester',
+ package = 'web-component-tester',
+ version = '4.2.2',
+ sha1 = '54556000c33d9ed7949aa546c1b4a1531491a5f0',
+)
+
+# Bower component transitive dependencies.
+load("//lib/js:bower_archives.bzl", "load_bower_archives")
+load_bower_archives()
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index 5f5b9ef..f62c767 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -38,7 +38,7 @@
Supports dry-run mode to only list the stale changes but not actually
abandon them.
-Requires pygerrit (https://github.com/sonyxperiadev/pygerrit).
+Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2).
"""
@@ -47,8 +47,8 @@
import re
import sys
-from pygerrit.rest import GerritRestAPI
-from pygerrit.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
+from pygerrit2.rest import GerritRestAPI
+from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
def _main():
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/contrib/git-push-review b/contrib/git-push-review
index e77785a..87eaa4c 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -46,8 +46,8 @@
help='remote name or URL to push to')
p.add_argument('-b', '--branch', default='', metavar='BRANCH',
help='remote branch name, refs/for/BRANCH')
- p.add_argument('reviewers', nargs='*', metavar='REVIEWER',
- help='reviewer names or aliases')
+ p.add_argument('args', nargs='*', metavar='REVIEWER_OR_HASHTAG',
+ help='reviewer names or aliases, or #hashtags')
p.add_argument('-t', '--topic', default='', metavar='TOPIC',
help='topic for new changes')
p.add_argument('--dry-run', action='store_true',
@@ -68,8 +68,13 @@
args.remote = args.remote or def_remote
args.branch = args.branch or def_branch
+
opts = collections.defaultdict(list)
- opts['r'].extend((get_config('reviewer.' + r) or r) for r in args.reviewers)
+ is_hashtag = lambda x: x.startswith('#')
+ opts['r'].extend(
+ (get_config('reviewer.' + r) or r)
+ for r in args.args if not is_hashtag(r))
+ opts['t'].extend(t[1:] for t in args.args if is_hashtag(t))
if args.topic:
opts['topic'].append(args.topic)
opts_str = ','.join('%s=%s' % (k, v) for k in opts for v in opts[k])
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index c35f82c..b77c41a 100644
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -182,14 +182,15 @@
def get_random_users(num_users):
- users = [(f, l) for f in FIRST_NAMES for l in LAST_NAMES][:num_users]
+ users = random.sample([(f, l) for f in FIRST_NAMES for l in LAST_NAMES],
+ num_users)
names = []
for u in users:
names.append({"firstname": u[0],
"lastname": u[1],
"name": u[0] + " " + u[1],
"username": u[0] + u[1],
- "email": u[0] + "." + u[1] + "@gmail.com",
+ "email": u[0] + "." + u[1] + "@gerritcodereview.com",
"http_password": "secret",
"groups": []})
return names
@@ -293,6 +294,7 @@
project_names = create_gerrit_projects(group_names)
for idx, u in enumerate(gerrit_users):
- create_change(u, project_names[4 * idx / len(gerrit_users)])
+ for _ in xrange(random.randint(1, 5)):
+ create_change(u, project_names[4 * idx / len(gerrit_users)])
main()
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..58c13f5 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',
@@ -58,3 +59,12 @@
],
visibility = ['//visibility:public'],
)
+
+load('//tools/bzl:javadoc.bzl', 'java_doc')
+
+java_doc(
+ name = 'acceptance-framework-javadoc',
+ title = 'Gerrit Acceptance Test Framework Documentation',
+ libs = [':lib'],
+ pkgs = ['com.google.gerrit.acceptance'],
+)
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index e8e00ea..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.1</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..c0d6f18 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
@@ -17,25 +17,30 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.initSsh;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
+import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static org.eclipse.jgit.lib.Constants.HEAD;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
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;
@@ -45,7 +50,10 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeType;
+import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -61,7 +69,9 @@
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.FileContentUtil;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.config.AllProjectsName;
@@ -70,6 +80,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;
@@ -97,12 +109,19 @@
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
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.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.TransportBundleStream;
+import org.eclipse.jgit.transport.URIish;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -114,14 +133,20 @@
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
@RunWith(ConfigSuite.class)
public abstract class AbstractDaemonTest {
@@ -217,6 +242,9 @@
@Inject
private EventRecorder.Factory eventRecorderFactory;
+ @Inject
+ private ChangeIndexCollection changeIndexes;
+
protected TestRepository<InMemoryRepository> testRepo;
protected GerritServer server;
protected TestAccount admin;
@@ -235,6 +263,9 @@
@Inject
protected ChangeNotes.Factory notesFactory;
+ @Inject
+ protected Abandon changeAbandoner;
+
@Rule
public ExpectedException exception = ExpectedException.none();
@@ -322,7 +353,8 @@
baseConfig.setString("gerrit", null, "tempSiteDir",
tempSiteDir.getRoot().getPath());
baseConfig.setInt("receive", null, "changeUpdateThreads", 4);
- if (classDesc.equals(methodDesc)) {
+ if (classDesc.equals(methodDesc) && !classDesc.sandboxed() &&
+ !methodDesc.sandboxed()) {
if (commonServer == null) {
commonServer = GerritServer.start(classDesc, baseConfig);
}
@@ -520,21 +552,26 @@
protected PushOneCommit.Result createMergeCommitChange(String ref)
throws Exception {
+ return createMergeCommitChange(ref, "foo");
+ }
+
+ protected PushOneCommit.Result createMergeCommitChange(String ref, String file)
+ throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result p1 = pushFactory.create(db, admin.getIdent(),
- testRepo, "parent 1", ImmutableMap.of("foo", "foo-1", "bar", "bar-1"))
+ testRepo, "parent 1", ImmutableMap.of(file, "foo-1", "bar", "bar-1"))
.to(ref);
// reset HEAD in order to create a sibling of the first change
testRepo.reset(initial);
PushOneCommit.Result p2 = pushFactory.create(db, admin.getIdent(),
- testRepo, "parent 2", ImmutableMap.of("foo", "foo-2", "bar", "bar-2"))
+ testRepo, "parent 2", ImmutableMap.of(file, "foo-2", "bar", "bar-2"))
.to(ref);
PushOneCommit m = pushFactory.create(db, admin.getIdent(), testRepo, "merge",
- ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+ ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
PushOneCommit.Result result = m.to(ref);
result.assertOkStatus();
@@ -666,6 +703,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 +853,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);
}
@@ -831,13 +897,7 @@
}
private static Iterable<String> changeIds(Iterable<ChangeInfo> changes) {
- return Iterables.transform(changes,
- new Function<ChangeInfo, String>() {
- @Override
- public String apply(ChangeInfo input) {
- return input.changeId;
- }
- });
+ return Iterables.transform(changes, i -> i.changeId);
}
protected void assertSubmittedTogether(String chId, String... expected)
@@ -908,7 +968,8 @@
protected RevCommit getHead(Repository repo, String name) throws Exception {
try (RevWalk rw = new RevWalk(repo)) {
- return rw.parseCommit(repo.exactRef(name).getObjectId());
+ Ref r = repo.exactRef(name);
+ return r != null ? rw.parseCommit(r.getObjectId()) : null;
}
}
@@ -940,4 +1001,141 @@
(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;
+ }
+
+ /**
+ * Fetches each bundle into a newly cloned repository, then it applies
+ * the bundle, and returns the resulting tree id.
+ */
+ protected Map<Branch.NameKey, RevTree>
+ fetchFromBundles(BinaryResult bundles) throws Exception {
+
+ assertThat(bundles.getContentType()).isEqualTo("application/x-zip");
+
+ File tempfile = File.createTempFile("test", null);
+ bundles.writeTo(new FileOutputStream(tempfile));
+
+ Map<Branch.NameKey, RevTree> ret = new HashMap<>();
+ try (ZipFile readback = new ZipFile(tempfile);) {
+ for (ZipEntry entry : ImmutableList.copyOf(
+ Iterators.forEnumeration(readback.entries()))) {
+ String bundleName = entry.getName();
+ InputStream bundleStream = readback.getInputStream(entry);
+
+ int len = bundleName.length();
+ assertThat(bundleName).endsWith(".git");
+ String repoName = bundleName.substring(0, len - 4);
+ Project.NameKey proj = new Project.NameKey(repoName);
+ TestRepository<?> localRepo = cloneProject(proj);
+
+ try (TransportBundleStream tbs = new TransportBundleStream(
+ localRepo.getRepository(), new URIish(bundleName), bundleStream);) {
+
+ FetchResult fr = tbs.fetch(NullProgressMonitor.INSTANCE,
+ Arrays.asList(new RefSpec("refs/*:refs/preview/*")));
+ for (Ref r : fr.getAdvertisedRefs()) {
+ String branchName = r.getName();
+ Branch.NameKey n = new Branch.NameKey(proj, branchName);
+
+ RevCommit c = localRepo.getRevWalk().parseCommit(r.getObjectId());
+ ret.put(n, c.getTree());
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Assert that the given branches have the given tree ids.
+ */
+ protected void assertRevTrees(Project.NameKey proj,
+ Map<Branch.NameKey, RevTree> trees) throws Exception {
+ TestRepository<?> localRepo = cloneProject(proj);
+ GitUtil.fetch(localRepo, "refs/*:refs/*");
+ Map<String, Ref> refs = localRepo.getRepository().getAllRefs();
+ Map<Branch.NameKey, RevTree> refValues = new HashMap<>();
+
+ for (Branch.NameKey b : trees.keySet()) {
+ if (!b.getParentKey().equals(proj)) {
+ continue;
+ }
+
+ Ref r = refs.get(b.get());
+ assertThat(r).isNotNull();
+ RevWalk rw = localRepo.getRevWalk();
+ RevCommit c = rw.parseCommit(r.getObjectId());
+ refValues.put(b, c.getTree());
+
+ assertThat(trees.get(b)).isEqualTo(refValues.get(b));
+ }
+ assertThat(refValues.keySet()).containsAnyIn(trees.keySet());
+ }
+
+ protected void assertDiffForNewFile(DiffInfo diff, RevCommit commit,
+ String path, String expectedContentSideB) throws Exception {
+ List<String> expectedLines = new ArrayList<>();
+ 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(commit.name());
+
+ String expectedContentType = "text/plain";
+ if (COMMIT_MSG.equals(path)) {
+ expectedContentType = FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE;
+ } else if (MERGE_LIST.equals(path)) {
+ expectedContentType = FileContentUtil.TEXT_X_GERRIT_MERGE_LIST;
+ }
+ assertThat(diff.metaB.contentType).isEqualTo(expectedContentType);
+
+ 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).containsExactlyElementsIn(expectedLines)
+ .inOrder();
+ 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-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index bce0b5a..114ef6a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.US_ASCII;
@@ -104,6 +105,7 @@
for (String n : groups) {
AccountGroup.NameKey k = new AccountGroup.NameKey(n);
AccountGroup g = groupCache.get(k);
+ checkArgument(g != null, "group not found: %s", n);
AccountGroupMember m =
new AccountGroupMember(new AccountGroupMember.Key(id, g.getId()));
db.accountGroupMembers().insert(Collections.singleton(m));
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
index 6cc8d3c..fbdfee6 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
@@ -90,16 +89,6 @@
return String.format("%s-%s-%s", type, project, ref);
}
- private static class RefEventTransformer<T extends RefEvent>
- implements Function<RefEvent, T> {
-
- @SuppressWarnings("unchecked")
- @Override
- public T apply(RefEvent e) {
- return (T) e;
- }
- }
-
private ImmutableList<RefUpdatedEvent> getRefUpdatedEvents(String project,
String refName, int expectedSize) {
String key = refEventKey(RefUpdatedEvent.TYPE, project, refName);
@@ -111,7 +100,7 @@
assertThat(recordedEvents).containsKey(key);
ImmutableList<RefUpdatedEvent> events = FluentIterable
.from(recordedEvents.get(key))
- .transform(new RefEventTransformer<RefUpdatedEvent>())
+ .transform(RefUpdatedEvent.class::cast)
.toList();
assertThat(events).hasSize(expectedSize);
return events;
@@ -128,7 +117,7 @@
assertThat(recordedEvents).containsKey(key);
ImmutableList<ChangeMergedEvent> events = FluentIterable
.from(recordedEvents.get(key))
- .transform(new RefEventTransformer<ChangeMergedEvent>())
+ .transform(ChangeMergedEvent.class::cast)
.toList();
assertThat(events).hasSize(expectedSize);
return events;
@@ -144,7 +133,7 @@
assertThat(recordedEvents).containsKey(key);
ImmutableList<ReviewerDeletedEvent> events = FluentIterable
.from(recordedEvents.get(key))
- .transform(new RefEventTransformer<ReviewerDeletedEvent>())
+ .transform(ReviewerDeletedEvent.class::cast)
.toList();
assertThat(events).hasSize(expectedSize);
return events;
@@ -217,4 +206,4 @@
public void close() {
eventListenerRegistration.remove();
}
-}
\ No newline at end of file
+}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index c4e636b..c29e8fe 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -42,10 +42,12 @@
import org.eclipse.jgit.util.FS;
import java.io.File;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
+import java.nio.file.Paths;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
@@ -61,7 +63,8 @@
return new AutoValue_GerritServer_Description(
configName,
true, // @UseLocalDisk is only valid on methods.
- !hasNoHttpd(testDesc.getTestClass()),
+ !has(NoHttpd.class, testDesc.getTestClass()),
+ has(Sandboxed.class, testDesc.getTestClass()),
null, // @GerritConfig is only valid on methods.
null); // @GerritConfigs is only valid on methods.
@@ -73,14 +76,17 @@
configName,
testDesc.getAnnotation(UseLocalDisk.class) == null,
testDesc.getAnnotation(NoHttpd.class) == null
- && !hasNoHttpd(testDesc.getTestClass()),
+ && !has(NoHttpd.class, testDesc.getTestClass()),
+ testDesc.getAnnotation(Sandboxed.class) != null ||
+ has(Sandboxed.class, testDesc.getTestClass()),
testDesc.getAnnotation(GerritConfig.class),
testDesc.getAnnotation(GerritConfigs.class));
}
- private static boolean hasNoHttpd(Class<?> clazz) {
+ private static boolean has(
+ Class<? extends Annotation> annotation, Class<?> clazz) {
for (; clazz != null; clazz = clazz.getSuperclass()) {
- if (clazz.getAnnotation(NoHttpd.class) != null) {
+ if (clazz.getAnnotation(annotation) != null) {
return true;
}
}
@@ -90,6 +96,7 @@
@Nullable abstract String configName();
abstract boolean memory();
abstract boolean httpd();
+ abstract boolean sandboxed();
@Nullable abstract GerritConfig config();
@Nullable abstract GerritConfigs configs();
@@ -123,7 +130,7 @@
throw new RuntimeException(e);
}
}
- });
+ }, Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir")));
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
final File site;
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..e5182df 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
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance;
import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
import org.apache.http.HttpHost;
import org.apache.http.client.fluent.Executor;
@@ -24,20 +25,27 @@
import java.net.URI;
public class HttpSession {
-
+ protected TestAccount account;
protected final String url;
private final Executor executor;
- public HttpSession(GerritServer server, TestAccount account) {
+ public HttpSession(GerritServer server, @Nullable TestAccount account) {
this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
URI uri = URI.create(url);
- this.executor = Executor
- .newInstance()
- .auth(new HttpHost(uri.getHost(), uri.getPort()),
+ this.executor = Executor.newInstance();
+ this.account = account;
+ if (account != null) {
+ executor.auth(
+ new HttpHost(uri.getHost(), uri.getPort()),
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/InMemoryTestingDatabaseModule.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
index f53202f..0783688 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InMemoryTestingDatabaseModule.java
@@ -27,6 +27,8 @@
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.config.TrackingFootersProvider;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
@@ -89,6 +91,7 @@
bind(Key.get(schemaFactory, ReviewDbFactory.class))
.to(InMemoryDatabase.class);
bind(InMemoryDatabase.class).in(SINGLETON);
+ bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
listener().to(CreateDatabase.class);
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..689b2d0 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
@@ -18,6 +18,7 @@
import com.google.common.base.Preconditions;
import com.google.common.net.HttpHeaders;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.server.OutputFormat;
@@ -32,7 +33,7 @@
public class RestSession extends HttpSession {
- public RestSession(GerritServer server, TestAccount account) {
+ public RestSession(GerritServer server, @Nullable TestAccount account) {
super(server, account);
}
@@ -45,9 +46,9 @@
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);
+ Request get = Request.Get(getUrl(endPoint));
if (header != null) {
get.addHeader(header);
}
@@ -55,7 +56,7 @@
}
public RestResponse head(String endPoint) throws IOException {
- return execute(Request.Head(url + "/a" + endPoint));
+ return execute(Request.Head(getUrl(endPoint)));
}
public RestResponse put(String endPoint) throws IOException {
@@ -73,7 +74,7 @@
public RestResponse putWithHeader(String endPoint, Header header,
Object content) throws IOException {
- Request put = Request.Put(url + "/a" + endPoint);
+ Request put = Request.Put(getUrl(endPoint));
if (header != null) {
put.addHeader(header);
}
@@ -88,7 +89,7 @@
public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
Preconditions.checkNotNull(stream);
- Request put = Request.Put(url + "/a" + endPoint);
+ Request put = Request.Put(getUrl(endPoint));
put.addHeader(new BasicHeader("Content-Type", stream.getContentType()));
put.body(new BufferedHttpEntity(
new InputStreamEntity(
@@ -102,7 +103,15 @@
}
public RestResponse post(String endPoint, Object content) throws IOException {
- Request post = Request.Post(url + "/a" + endPoint);
+ return postWithHeader(endPoint, content, null);
+ }
+
+ public RestResponse postWithHeader(String endPoint, Object content,
+ Header header) throws IOException {
+ Request post = Request.Post(getUrl(endPoint));
+ if (header != null) {
+ post.addHeader(header);
+ }
if (content != null) {
post.addHeader(new BasicHeader("Content-Type", "application/json"));
post.body(new StringEntity(
@@ -113,6 +122,10 @@
}
public RestResponse delete(String endPoint) throws IOException {
- return execute(Request.Delete(url + "/a" + endPoint));
+ return execute(Request.Delete(getUrl(endPoint)));
+ }
+
+ private String getUrl(String endPoint) {
+ return url + (account != null ? "/a" : "") + endPoint;
}
}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.java
new file mode 100644
index 0000000..11446e0
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/Sandboxed.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.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface Sandboxed {
+}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 7f08b6f..c1a815a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -14,7 +14,6 @@
package com.google.gerrit.acceptance;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.mail.Address;
@@ -29,13 +28,7 @@
public class TestAccount {
public static FluentIterable<Account.Id> ids(
Iterable<TestAccount> accounts) {
- return FluentIterable.from(accounts)
- .transform(new Function<TestAccount, Account.Id>() {
- @Override
- public Account.Id apply(TestAccount in) {
- return in.id;
- }
- });
+ return FluentIterable.from(accounts).transform(a -> a.id);
}
public static FluentIterable<Account.Id> ids(TestAccount... accounts) {
@@ -43,13 +36,7 @@
}
public static FluentIterable<String> names(Iterable<TestAccount> accounts) {
- return FluentIterable.from(accounts)
- .transform(new Function<TestAccount, String>() {
- @Override
- public String apply(TestAccount in) {
- return in.fullName;
- }
- });
+ return FluentIterable.from(accounts).transform(a -> a.fullName);
}
public static FluentIterable<String> names(TestAccount... accounts) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
new file mode 100644
index 0000000..3ff7112
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SandboxTest.java
@@ -0,0 +1,38 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import org.junit.After;
+import org.junit.Test;
+
+@Sandboxed
+public class SandboxTest extends AbstractDaemonTest {
+ @After
+ public void addUser() throws Exception {
+ gApi.accounts().create("sandboxuser");
+ }
+
+ @Test
+ public void testUserNotPresent1() throws Exception {
+ assertThat(gApi.accounts().query("sandboxuser").get()).isEmpty();
+ }
+
+ @Test
+ public void testUserNotPresent2() throws Exception {
+ assertThat(gApi.accounts().query("sandboxuser").get()).isEmpty();
+ }
+}
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..01009aa 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,8 +29,8 @@
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;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -199,6 +199,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();
@@ -722,13 +752,7 @@
Map<String, GpgKeyInfo> keyMap = gApi.accounts().self().listGpgKeys();
assertThat(keyMap.keySet())
.named("keys returned by listGpgKeys()")
- .containsExactlyElementsIn(
- expected.transform(new Function<TestKey, String>() {
- @Override
- public String apply(TestKey in) {
- return in.getKeyIdString();
- }
- }));
+ .containsExactlyElementsIn(expected.transform(TestKey::getKeyIdString));
for (TestKey key : expected) {
assertKeyEquals(key, gApi.accounts().self().gpgKey(
@@ -740,23 +764,13 @@
// Check raw external IDs.
Account.Id currAccountId = atrScope.get().getUser().getAccountId();
- assertThat(
- GpgKeys.getGpgExtIds(db, currAccountId)
- .transform(new Function<AccountExternalId, String>() {
- @Override
- public String apply(AccountExternalId in) {
- return in.getSchemeRest();
- }
- }))
+ Iterable<String> expectedFps = expected.transform(
+ k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
+ Iterable<String> actualFps = GpgKeys.getGpgExtIds(db, currAccountId)
+ .transform(AccountExternalId::getSchemeRest);
+ assertThat(actualFps)
.named("external IDs in database")
- .containsExactlyElementsIn(
- expected.transform(new Function<TestKey, String>() {
- @Override
- public String apply(TestKey in) {
- return BaseEncoding.base16().encode(
- in.getPublicKey().getFingerprint());
- }
- }));
+ .containsExactlyElementsIn(expectedFps);
// Check raw stored keys.
for (TestKey key : expected) {
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..1a43784 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,8 @@
i.dateFormat = DateFormat.US;
i.timeFormat = TimeFormat.HHMM_24;
i.emailStrategy = EmailStrategy.DISABLED;
+ i.defaultBaseForMerges = DefaultBase.AUTO_MERGE;
+ i.highlightAssigneeInChangeTable ^= true;
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..3dd1ff3 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
@@ -30,7 +30,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;
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 +41,12 @@
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.TimeUtil;
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;
@@ -68,9 +72,12 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.git.ProjectConfig;
@@ -78,7 +85,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;
@@ -96,6 +102,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
@@ -184,6 +191,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();
@@ -350,37 +415,6 @@
}
@Test
- public void voteOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType codeReviewType = Util.codeReview();
- String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
- String heads = "refs/heads/*";
- AccountGroup.UUID owner =
- SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
- Util.allow(cfg, forCodeReviewAs, -1, 1, owner, heads);
- saveProjectConfig(project, cfg);
-
- PushOneCommit.Result r = createChange();
- RevisionApi revision = gApi.changes()
- .id(r.getChangeId())
- .current();
-
- ReviewInput in = ReviewInput.recommend();
- in.onBehalfOf = user.id.toString();
- revision.review(in);
-
- ChangeInfo c = gApi.changes()
- .id(r.getChangeId())
- .get();
-
- LabelInfo codeReview = c.labels.get("Code-Review");
- assertThat(codeReview.all).hasSize(1);
- ApprovalInfo approval = codeReview.all.get(0);
- assertThat(approval._accountId).isEqualTo(user.id.get());
- assertThat(approval.value).isEqualTo(1);
- }
-
- @Test
public void rebaseUpToDateChange() throws Exception {
PushOneCommit.Result r = createChange();
exception.expect(ResourceConflictException.class);
@@ -538,6 +572,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 +754,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();
@@ -619,6 +812,26 @@
}
@Test
+ public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ PushOneCommit.Result r = createChange();
+
+ // insert dummy approval in ReviewDb
+ PatchSetApproval psa =
+ new PatchSetApproval(new PatchSetApproval.Key(r.getPatchSetId(),
+ user.id, new LabelId("Code-Review")), (short) 0, TimeUtil.nowTs());
+ db.patchSetApprovals().insert(Collections.singleton(psa));
+
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ gApi.changes()
+ .id(r.getChangeId())
+ .addReviewer(in);
+ }
+
+ @Test
public void addSelfAsReviewer() throws Exception {
TestTimeUtil.resetWithClockStep(1, SECONDS);
PushOneCommit.Result r = createChange();
@@ -755,11 +968,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 +1005,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 +1039,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 +1126,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 +1439,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();
@@ -1673,12 +1932,7 @@
private static Iterable<Account.Id> getReviewers(
Collection<AccountInfo> r) {
- return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {
- @Override
- public Account.Id apply(AccountInfo account) {
- return new Account.Id(account._accountId);
- }
- });
+ return Iterables.transform(r, a -> new Account.Id(a._accountId));
}
private ChangeResource parseResource(PushOneCommit.Result r)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/MergeListIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/MergeListIT.java
new file mode 100644
index 0000000..481df31
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -0,0 +1,209 @@
+// 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.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.dircache.InvalidPathException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Set;
+
+@NoHttpd
+public class MergeListIT extends AbstractDaemonTest {
+
+ private String changeId;
+ private RevCommit merge;
+ private RevCommit parent1;
+ private RevCommit grandParent1;
+ private RevCommit parent2;
+ private RevCommit grandParent2;
+
+ @Inject
+ private ChangeEditModifier modifier;
+
+ @Inject
+ private ChangeEditUtil editUtil;
+
+ @Before
+ public void setup() 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");
+ grandParent1 = gp1.getCommit();
+
+ 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");
+ parent1 = p1.getCommit();
+
+ // 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 2",
+ ImmutableMap.of("foo", "foo-2.1", "bar", "bar-2.1"))
+ .to("refs/for/master");
+ grandParent2 = gp2.getCommit();
+
+ 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");
+ parent2 = p2.getCommit();
+
+ 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();
+ merge = result.getCommit();
+ changeId = result.getChangeId();
+ }
+
+ @Test
+ public void getMergeList() throws Exception {
+ List<CommitInfo> mergeList = current(changeId).getMergeList().get();
+ assertThat(mergeList).hasSize(2);
+ assertThat(mergeList.get(0).commit).isEqualTo(parent2.name());
+ assertThat(mergeList.get(1).commit).isEqualTo(grandParent2.name());
+
+ mergeList = current(changeId).getMergeList()
+ .withUninterestingParent(2).get();
+ assertThat(mergeList).hasSize(2);
+ assertThat(mergeList.get(0).commit).isEqualTo(parent1.name());
+ assertThat(mergeList.get(1).commit).isEqualTo(grandParent1.name());
+ }
+
+ @Test
+ public void getMergeListContent() throws Exception {
+ BinaryResult bin = current(changeId).file(MERGE_LIST).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String content = new String(os.toByteArray(), UTF_8);
+ assertThat(content).isEqualTo(
+ getMergeListContent(parent2, grandParent2));
+ }
+
+ @Test
+ public void getFileList() throws Exception {
+ assertThat(getFiles(changeId)).contains(MERGE_LIST);
+ assertThat(getFiles(changeId, 1)).contains(MERGE_LIST);
+ assertThat(getFiles(changeId, 2)).contains(MERGE_LIST);
+
+ assertThat(getFiles(createChange().getChangeId()))
+ .doesNotContain(MERGE_LIST);
+ }
+
+ @Test
+ public void getDiffForMergeList() throws Exception {
+ DiffInfo diff = getMergeListDiff(changeId);
+ assertDiffForNewFile(diff, merge, MERGE_LIST,
+ getMergeListContent(parent2, grandParent2));
+
+ diff = getMergeListDiff(changeId, 1);
+ assertDiffForNewFile(diff, merge, MERGE_LIST,
+ getMergeListContent(parent2, grandParent2));
+
+ diff = getMergeListDiff(changeId, 2);
+ assertDiffForNewFile(diff, merge, MERGE_LIST,
+ getMergeListContent(parent1, grandParent1));
+ }
+
+ @Test
+ public void editMergeList() throws Exception {
+ ChangeData cd = getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
+ modifier.createEdit(cd.change(), cd.currentPatchSet());
+
+ exception.expect(InvalidPathException.class);
+ exception.expectMessage("Invalid path: " + MERGE_LIST);
+ modifier.modifyFile(editUtil.byChange(cd.change()).get(), MERGE_LIST,
+ RawInputUtil.create("new content"));
+ }
+
+ @Test
+ public void deleteMergeList() throws Exception {
+ ChangeData cd = getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
+ modifier.createEdit(cd.change(), cd.currentPatchSet());
+
+ exception.expect(InvalidChangeOperationException.class);
+ exception.expectMessage("no changes were made");
+ modifier.deleteFile(editUtil.byChange(cd.change()).get(), MERGE_LIST);
+ }
+
+ private String getMergeListContent(RevCommit... commits) {
+ StringBuilder mergeList = new StringBuilder("Merge List:\n\n");
+ for (RevCommit c : commits) {
+ mergeList.append("* ")
+ .append(c.abbreviate(8).name())
+ .append(" ")
+ .append(c.getShortMessage())
+ .append("\n");
+ }
+ return mergeList.toString();
+ }
+
+ private Set<String> getFiles(String changeId) throws Exception {
+ return current(changeId).files().keySet();
+ }
+
+ private Set<String> getFiles(String changeId, int parent) throws Exception {
+ return current(changeId).files(parent).keySet();
+ }
+
+ private DiffInfo getMergeListDiff(String changeId) throws Exception {
+ return current(changeId).file(MERGE_LIST).diff();
+ }
+
+ private DiffInfo getMergeListDiff(String changeId, int parent)
+ throws Exception {
+ return current(changeId).file(MERGE_LIST).diff(parent);
+ }
+
+ private RevisionApi current(String changeId) throws Exception {
+ return gApi.changes()
+ .id(changeId)
+ .current();
+ }
+}
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/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 3f8c1bc..78c0cda 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -18,14 +18,12 @@
import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -401,12 +399,8 @@
public void testListAllGroups() throws Exception {
List<String> expectedGroups = FluentIterable
.from(groupCache.all())
- .transform(new Function<AccountGroup, String>() {
- @Override
- public String apply(AccountGroup group) {
- return group.getName();
- }
- }).toSortedList(Ordering.natural());
+ .transform(AccountGroup::getName)
+ .toSortedList(Ordering.natural());
assertThat(expectedGroups.size()).isAtLeast(2);
assertThat(gApi.groups().list().getAsMap().keySet())
.containsExactlyElementsIn(expectedGroups).inOrder();
@@ -518,14 +512,7 @@
private void assertMembers(Iterable<AccountInfo> members,
String... expectedNames) {
- Iterable<String> memberNames = Iterables.transform(members,
- new Function<AccountInfo, String>() {
- @Override
- public String apply(@Nullable AccountInfo info) {
- return info.name;
- }
- });
- assertThat(memberNames)
+ assertThat(Iterables.transform(members, i -> i.name))
.containsExactlyElementsIn(Arrays.asList(expectedNames)).inOrder();
}
@@ -540,15 +527,7 @@
private static void assertIncludes(
Iterable<GroupInfo> includes, String... expectedNames) {
- Iterable<String> includeNames = Iterables.transform(
- includes,
- new Function<GroupInfo, String>() {
- @Override
- public String apply(@Nullable GroupInfo info) {
- return info.name;
- }
- });
- assertThat(includeNames)
+ assertThat(Iterables.transform(includes, i -> i.name))
.containsExactlyElementsIn(Arrays.asList(expectedNames)).inOrder();
}
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..7f5c6bc 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,18 +19,17 @@
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.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
+import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.junit.Assert.fail;
-import com.google.common.base.Predicate;
+import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -38,7 +37,6 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.SubmitType;
@@ -46,29 +44,26 @@
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BinaryResult;
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;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.Util;
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.junit.Before;
+import org.eclipse.jgit.revwalk.RevCommit;
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;
@@ -82,13 +77,6 @@
@Inject
private GetRevisionActions getRevisionActions;
- private TestAccount admin2;
-
- @Before
- public void setUp() throws Exception {
- admin2 = accounts.admin2();
- }
-
@Test
public void reviewTriplet() throws Exception {
PushOneCommit.Result r = createChange();
@@ -138,70 +126,6 @@
.isEqualTo(ChangeStatus.MERGED);
}
- private void allowSubmitOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(cfg,
- Permission.SUBMIT_AS,
- SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
- "refs/heads/*");
- saveProjectConfig(project, cfg);
- }
-
- @Test
- public void submitOnBehalfOf() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
- String changeId = project.get() + "~master~" + r.getChangeId();
- gApi.changes()
- .id(changeId)
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email;
- gApi.changes()
- .id(changeId)
- .current()
- .submit(in);
- assertThat(gApi.changes().id(changeId).get().status)
- .isEqualTo(ChangeStatus.MERGED);
- }
-
- @Test
- public void submitOnBehalfOfInvalidUser() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
- String changeId = project.get() + "~master~" + r.getChangeId();
- gApi.changes()
- .id(changeId)
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = "doesnotexist";
- exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("Account Not Found: doesnotexist");
- gApi.changes()
- .id(changeId)
- .current()
- .submit(in);
- }
-
- @Test
- public void submitOnBehalfOfNotPermitted() throws Exception {
- PushOneCommit.Result r = createChange();
- gApi.changes()
- .id(project.get() + "~master~" + r.getChangeId())
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email;
- exception.expect(AuthException.class);
- exception.expectMessage("submit on behalf of not permitted");
- gApi.changes()
- .id(project.get() + "~master~" + r.getChangeId())
- .current()
- .submit(in);
- }
-
@Test
public void deleteDraft() throws Exception {
PushOneCommit.Result r = createDraft();
@@ -511,17 +435,15 @@
@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(), f -> f.matches(FILE_NAME + '|' + COMMIT_MSG)))
+ .isTrue();
}
@Test
@@ -534,7 +456,7 @@
.revision(r.getCommit().name())
.files()
.keySet()
- ).containsExactly(Patch.COMMIT_MSG, "foo", "bar");
+ ).containsExactly(COMMIT_MSG, MERGE_LIST, "foo", "bar");
// list files against parent 1
assertThat(gApi.changes()
@@ -542,7 +464,7 @@
.revision(r.getCommit().name())
.files(1)
.keySet()
- ).containsExactly(Patch.COMMIT_MSG, "bar");
+ ).containsExactly(COMMIT_MSG, MERGE_LIST, "bar");
// list files against parent 2
assertThat(gApi.changes()
@@ -550,19 +472,30 @@
.revision(r.getCommit().name())
.files(2)
.keySet()
- ).containsExactly(Patch.COMMIT_MSG, "foo");
+ ).containsExactly(COMMIT_MSG, MERGE_LIST, "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 +543,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 +748,62 @@
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> headers = new ArrayList<>();
+ if (path.equals(COMMIT_MSG)) {
+ RevCommit c = pushResult.getCommit();
+
+ RevCommit parentCommit = c.getParents()[0];
+ String parentCommitId = testRepo.getRevWalk().getObjectReader()
+ .abbreviate(parentCommit.getId(), 8).name();
+ headers.add("Parent: " + parentCommitId + " ("
+ + parentCommit.getShortMessage() + ")");
+
+ SimpleDateFormat dtfmt =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
+ PersonIdent author = c.getAuthorIdent();
+ dtfmt.setTimeZone(author.getTimeZone());
+ headers.add("Author: " + author.getName() + " <"
+ + author.getEmailAddress() + ">");
+ headers.add("AuthorDate: "
+ + dtfmt.format(Long.valueOf(author.getWhen().getTime())));
+
+ PersonIdent committer = c.getCommitterIdent();
+ dtfmt.setTimeZone(committer.getTimeZone());
+ headers.add("Commit: " + committer.getName() + " <"
+ + committer.getEmailAddress() + ">");
+ headers.add("CommitDate: "
+ + dtfmt.format(Long.valueOf(committer.getWhen().getTime())));
+ headers.add("");
+ }
+
+ if (!headers.isEmpty()) {
+ String header = Joiner.on("\n").join(headers);
+ expectedContentSideB = header + "\n" + expectedContentSideB;
+ }
+
+ assertDiffForNewFile(diff, pushResult.getCommit(), path,
+ expectedContentSideB);
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
new file mode 100644
index 0000000..7aa5876
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -0,0 +1,164 @@
+// 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.revision;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RobotCommentsIT extends AbstractDaemonTest {
+ @Test
+ public void comments() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ PushOneCommit.Result r = createChange();
+ RobotCommentInput in = createRobotCommentInput();
+ ReviewInput reviewInput = new ReviewInput();
+ Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
+ robotComments.put(in.path, Collections.singletonList(in));
+ reviewInput.robotComments = robotComments;
+ reviewInput.message = "comment test";
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(reviewInput);
+
+ Map<String, List<RobotCommentInfo>> out = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .robotComments();
+ assertThat(out).hasSize(1);
+ RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
+ assertRobotComment(comment, in, false);
+
+ List<RobotCommentInfo> list = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .robotCommentsAsList();
+ assertThat(list).hasSize(1);
+
+ RobotCommentInfo comment2 = list.get(0);
+ assertRobotComment(comment2, in);
+
+ RobotCommentInfo comment3 = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .robotComment(comment.id)
+ .get();
+ assertRobotComment(comment3, in);
+ }
+
+ @Test
+ public void noOptionalFields() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ PushOneCommit.Result r = createChange();
+ RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
+ ReviewInput reviewInput = new ReviewInput();
+ Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
+ robotComments.put(in.path, Collections.singletonList(in));
+ reviewInput.robotComments = robotComments;
+ reviewInput.message = "comment test";
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(reviewInput);
+
+ Map<String, List<RobotCommentInfo>> out = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .robotComments();
+ assertThat(out).hasSize(1);
+ RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
+ assertRobotComment(comment, in, false);
+ }
+
+ @Test
+ public void robotCommentsNotSupported() throws Exception {
+ assume().that(notesMigration.enabled()).isFalse();
+
+ PushOneCommit.Result r = createChange();
+ RobotCommentInput in = createRobotCommentInput();
+ ReviewInput reviewInput = new ReviewInput();
+ Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
+ robotComments.put(FILE_NAME, Collections.singletonList(in));
+ reviewInput.robotComments = robotComments;
+ reviewInput.message = "comment test";
+
+ exception.expect(MethodNotAllowedException.class);
+ exception.expectMessage("robot comments not supported");
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(reviewInput);
+ }
+
+ private RobotCommentInput createRobotCommentInputWithMandatoryFields() {
+ RobotCommentInput in = new RobotCommentInput();
+ in.robotId = "happyRobot";
+ in.robotRunId = "1";
+ in.line = 1;
+ in.message = "nit: trailing whitespace";
+ in.path = FILE_NAME;
+ return in;
+ }
+
+ private RobotCommentInput createRobotCommentInput() {
+ RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
+ in.url = "http://www.happy-robot.com";
+ in.properties = new HashMap<>();
+ in.properties.put("key1", "value1");
+ in.properties.put("key2", "value2");
+ return in;
+ }
+
+ private void assertRobotComment(RobotCommentInfo c,
+ RobotCommentInput expected) {
+ assertRobotComment(c, expected, true);
+ }
+
+ private void assertRobotComment(RobotCommentInfo c,
+ RobotCommentInput expected, boolean expectPath) {
+ assertThat(c.robotId).isEqualTo(expected.robotId);
+ assertThat(c.robotRunId).isEqualTo(expected.robotRunId);
+ assertThat(c.url).isEqualTo(expected.url);
+ assertThat(c.properties).isEqualTo(expected.properties);
+ assertThat(c.line).isEqualTo(expected.line);
+ assertThat(c.message).isEqualTo(expected.message);
+
+ assertThat(c.author.email).isEqualTo(admin.email);
+
+ if (expectPath) {
+ assertThat(c.path).isEqualTo(expected.path);
+ } else {
+ assertThat(c.path).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 e47d570..c611f2c 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;
@@ -172,7 +175,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,
@@ -202,6 +205,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(
@@ -365,7 +386,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,
@@ -408,7 +429,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.",
@@ -711,7 +732,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);
@@ -738,7 +759,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..6e2e8b1 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
@@ -19,6 +19,7 @@
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
@@ -33,6 +34,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;
@@ -40,6 +42,7 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.EditInfo;
@@ -61,6 +64,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -68,6 +72,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -129,6 +134,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 +202,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 +824,179 @@
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();
+ }
+
+ @Test
+ public void createChangeForMergedCommit() throws Exception {
+ String master = "refs/heads/master";
+ grant(Permission.PUSH, project, master, true);
+
+ // Update master with a direct push.
+ RevCommit c1 = testRepo.commit()
+ .message("Non-change 1")
+ .create();
+ RevCommit c2 = testRepo.parseBody(
+ testRepo.commit()
+ .parent(c1)
+ .message("Non-change 2")
+ .insertChangeId()
+ .create());
+ String changeId = Iterables.getOnlyElement(c2.getFooterLines(CHANGE_ID));
+
+ testRepo.reset(c2);
+ assertPushOk(pushHead(testRepo, master, false, true), master);
+
+ String q = "commit:" + c1.name()
+ + " OR commit:" + c2.name()
+ + " OR change:" + changeId;
+ assertThat(gApi.changes().query(q).get()).isEmpty();
+
+ // Push c2 as a merged change.
+ String r = "refs/for/master%merged";
+ assertPushOk(pushHead(testRepo, r, false), r);
+
+ EnumSet<ListChangesOption> opts =
+ EnumSet.of(ListChangesOption.CURRENT_REVISION);
+ ChangeInfo info = gApi.changes().id(changeId).get(opts);
+ assertThat(info.currentRevision).isEqualTo(c2.name());
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+
+ // Only c2 was created as a change.
+ String q1 = "commit: " + c1.name();
+ assertThat(gApi.changes().query(q1).get()).isEmpty();
+
+ // Push c1 as a merged change.
+ testRepo.reset(c1);
+ assertPushOk(pushHead(testRepo, r, false), r);
+ List<ChangeInfo> infos =
+ gApi.changes().query(q1).withOptions(opts).get();
+ assertThat(infos).hasSize(1);
+ info = infos.get(0);
+ assertThat(info.currentRevision).isEqualTo(c1.name());
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
+ public void mergedOptionFailsWhenCommitIsNotMerged() throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master%merged");
+ r.assertErrorStatus("not merged into branch");
+ }
+
+ @Test
+ public void mergedOptionFailsWhenCommitIsMergedOnOtherBranch()
+ throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ TestRepository<?> tr = new TestRepository<>(repo);
+ tr.branch("refs/heads/branch")
+ .commit()
+ .message("Initial commit on branch")
+ .create();
+ }
+
+ pushTo("refs/for/master%merged")
+ .assertErrorStatus("not merged into branch");
+ }
+
+ @Test
+ public void mergedOptionFailsWhenChangeExists() throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ testRepo.reset(r.getCommit());
+ String ref = "refs/for/master%merged";
+ PushResult pr = pushHead(testRepo, ref, false);
+ RemoteRefUpdate rru = pr.getRemoteUpdate(ref);
+ assertThat(rru.getStatus())
+ .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+ assertThat(rru.getMessage()).contains("no new changes");
+ }
+
+ @Test
+ public void mergedOptionWithNewCommitWithSameChangeIdFails()
+ throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ RevCommit c2 = testRepo.amend(r.getCommit())
+ .message("New subject")
+ .insertChangeId(r.getChangeId().substring(1))
+ .create();
+ testRepo.reset(c2);
+
+ String ref = "refs/for/master%merged";
+ PushResult pr = pushHead(testRepo, ref, false);
+ RemoteRefUpdate rru = pr.getRemoteUpdate(ref);
+ assertThat(rru.getStatus())
+ .isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+ assertThat(rru.getMessage()).contains("not merged into branch");
+ }
+
+ @Test
+ public void mergedOptionWithExistingChangeInsertsPatchSet()
+ throws Exception {
+ String master = "refs/heads/master";
+ grant(Permission.PUSH, project, master, true);
+
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ ObjectId c1 = r.getCommit().copy();
+
+ // Create a PS2 commit directly on master in the server's repo. This
+ // simulates the client amending locally and pushing directly to the branch,
+ // expecting the change to be auto-closed, but the change metadata update
+ // fails.
+ ObjectId c2;
+ try (Repository repo = repoManager.openRepository(project)) {
+ TestRepository<?> tr = new TestRepository<>(repo);
+ RevCommit commit2 = tr.amend(c1)
+ .message("New subject")
+ .insertChangeId(r.getChangeId().substring(1))
+ .create();
+ c2 = commit2.copy();
+ tr.update(master, c2);
+ }
+
+ testRepo.git().fetch()
+ .setRefSpecs(new RefSpec("refs/heads/master")).call();
+ testRepo.reset(c2);
+
+ String ref = "refs/for/master%merged";
+ assertPushOk(pushHead(testRepo, ref, false), ref);
+
+ EnumSet<ListChangesOption> opts =
+ EnumSet.of(ListChangesOption.ALL_REVISIONS);
+ ChangeInfo info = gApi.changes().id(r.getChangeId()).get(opts);
+ assertThat(info.currentRevision).isEqualTo(c2.name());
+ assertThat(info.revisions.keySet())
+ .containsExactly(c1.name(), c2.name());
+ // TODO(dborowitz): Fix ReceiveCommits to also auto-close the change.
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+ }
+
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/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 28f7ff8..dfeac35 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -29,6 +29,7 @@
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTree;
@@ -75,8 +76,9 @@
}
protected TestRepository<?> createProjectWithPush(String name,
- @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
- Project.NameKey project = createProject(name, parent, submitType);
+ @Nullable Project.NameKey parent, boolean createEmptyCommit,
+ SubmitType submitType) throws Exception {
+ Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType);
grant(Permission.PUSH, project, "refs/heads/*");
grant(Permission.SUBMIT, project, "refs/for/refs/heads/*");
return cloneProject(project);
@@ -84,12 +86,17 @@
protected TestRepository<?> createProjectWithPush(String name,
@Nullable Project.NameKey parent) throws Exception {
- return createProjectWithPush(name, parent, getSubmitType());
+ return createProjectWithPush(name, parent, true, getSubmitType());
+ }
+
+ protected TestRepository<?> createProjectWithPush(String name,
+ boolean createEmptyCommit) throws Exception {
+ return createProjectWithPush(name, null, createEmptyCommit, getSubmitType());
}
protected TestRepository<?> createProjectWithPush(String name)
throws Exception {
- return createProjectWithPush(name, null);
+ return createProjectWithPush(name, null, true, getSubmitType());
}
private static AtomicInteger contentCounter = new AtomicInteger(0);
@@ -305,8 +312,13 @@
String submodule) throws Exception {
submodule = name(submodule);
- ObjectId commitId = repo.git().fetch().setRemote("origin").call()
- .getAdvertisedRef("refs/heads/" + branch).getObjectId();
+ Ref branchTip = repo.git().fetch().setRemote("origin").call()
+ .getAdvertisedRef("refs/heads/" + branch);
+ if (branchTip == null) {
+ return false;
+ }
+
+ ObjectId commitId = branchTip.getObjectId();
RevWalk rw = repo.getRevWalk();
RevCommit c = rw.parseCommit(commitId);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
index f6796a5..42ece25 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -6,6 +6,7 @@
deps = [
':submodule_util',
':push_for_review',
+ '//gerrit-extension-api:api',
],
labels = ['git'],
)
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/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 0ff3af5..43f19b9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -21,15 +21,22 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.testutil.ConfigSuite;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
+import java.util.Map;
+
@NoHttpd
public class SubmoduleSubscriptionsWholeTopicMergeIT
extends AbstractSubmoduleSubscription {
@@ -101,22 +108,50 @@
gApi.changes().id(id2).current().review(ReviewInput.approve());
gApi.changes().id(id3).current().review(ReviewInput.approve());
+ BinaryResult request = gApi.changes().id(id1).current().submitPreview();
+ Map<Branch.NameKey, RevTree> preview =
+ fetchFromBundles(request);
+
gApi.changes().id(id1).current().submit();
ObjectId subRepoId = subRepo.git().fetch().setRemote("origin").call()
.getAdvertisedRef("refs/heads/master").getObjectId();
expectToHaveSubmoduleState(superRepo, "master",
"subscribed-to-project", subRepoId);
+
+ // As the submodules have changed commits, the superproject tree will be
+ // different, so we cannot directly compare the trees here, so make
+ // assumptions only about the changed branches:
+ Project.NameKey p1 = new Project.NameKey(name("super-project"));
+ Project.NameKey p2 = new Project.NameKey(name("subscribed-to-project"));
+ assertThat(preview).containsKey(
+ new Branch.NameKey(p1, "refs/heads/master"));
+ assertThat(preview).containsKey(
+ new Branch.NameKey(p2, "refs/heads/master"));
+
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ // each change is updated and the respective target branch is updated:
+ assertThat(preview).hasSize(5);
+ } else if (getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
+ // Either the first is used first as is, then the second and third need
+ // rebasing, or those two stay as is and the first is rebased.
+ // add in 2 master branches, expect 3 or 4:
+ assertThat(preview.size()).isAnyOf(3, 4);
+ } else {
+ assertThat(preview).hasSize(2);
+ }
}
@Test
- public void testSubscriptionUpdateIncludingChangeInSuperproject() throws Exception {
+ public void testSubscriptionUpdateIncludingChangeInSuperproject()
+ throws Exception {
TestRepository<?> superRepo = createProjectWithPush("super-project");
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
- allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
- "super-project", "refs/heads/master");
+ allowMatchingSubmoduleSubscription("subscribed-to-project",
+ "refs/heads/master", "super-project", "refs/heads/master");
- createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+ createSubmoduleSubscription(superRepo, "master",
+ "subscribed-to-project", "master");
ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
.message("some change")
@@ -223,6 +258,56 @@
}
@Test
+ public void testDoNotUseFastForward() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project", false);
+ TestRepository<?> sub = createProjectWithPush("sub", false);
+
+ allowMatchingSubmoduleSubscription("sub", "refs/heads/master",
+ "super-project", "refs/heads/master");
+
+ createSubmoduleSubscription(superRepo, "master", "sub", "master");
+
+ ObjectId subId =
+ pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+
+ ObjectId superId =
+ pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
+
+ String subChangeId = getChangeId(sub, subId).get();
+ approve(subChangeId);
+ approve(getChangeId(superRepo, superId).get());
+
+ gApi.changes().id(subChangeId).current().submit();
+
+ expectToHaveSubmoduleState(superRepo, "master", "sub", sub, "master");
+ RevCommit superHead = getRemoteHead(name("super-project"), "master");
+ assertThat(superHead.getShortMessage()).contains("some message");
+ assertThat(superHead.getId()).isNotEqualTo(superId);
+ }
+
+ @Test
+ public void testUseFastForwardWhenNoSubmodule() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project", false);
+ TestRepository<?> sub = createProjectWithPush("sub", false);
+
+ ObjectId subId =
+ pushChangeTo(sub, "refs/for/master", "some message", "same-topic");
+
+ ObjectId superId =
+ pushChangeTo(superRepo, "refs/for/master", "some message", "same-topic");
+
+ String subChangeId = getChangeId(sub, subId).get();
+ approve(subChangeId);
+ approve(getChangeId(superRepo, superId).get());
+
+ gApi.changes().id(subChangeId).current().submit();
+
+ RevCommit superHead = getRemoteHead(name("super-project"), "master");
+ assertThat(superHead.getShortMessage()).isEqualTo("some message");
+ assertThat(superHead.getId()).isEqualTo(superId);
+ }
+
+ @Test
public void testDifferentPaths() throws Exception {
TestRepository<?> superRepo = createProjectWithPush("super-project");
TestRepository<?> sub = createProjectWithPush("sub");
@@ -310,7 +395,8 @@
createSubmoduleSubscription(midRepo, "master", "bottom-project", "master");
ObjectId bottomHead =
- pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+ pushChangeTo(bottomRepo, "refs/for/master",
+ "some message", "same-topic");
ObjectId topHead =
pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
@@ -322,8 +408,10 @@
gApi.changes().id(id1).current().submit();
- expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
- expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
+ expectToHaveSubmoduleState(midRepo, "master", "bottom-project",
+ bottomRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master", "mid-project",
+ midRepo, "master");
}
@Test
@@ -346,7 +434,8 @@
pushSubmoduleConfig(topRepo, "master", config);
ObjectId bottomHead =
- pushChangeTo(bottomRepo, "refs/for/master", "some message", "same-topic");
+ pushChangeTo(bottomRepo, "refs/for/master",
+ "some message", "same-topic");
ObjectId topHead =
pushChangeTo(topRepo, "refs/for/master", "some message", "same-topic");
@@ -358,13 +447,16 @@
gApi.changes().id(id1).current().submit();
- expectToHaveSubmoduleState(midRepo, "master", "bottom-project", bottomRepo, "master");
- expectToHaveSubmoduleState(topRepo, "master", "mid-project", midRepo, "master");
- expectToHaveSubmoduleState(topRepo, "master", "bottom-project", bottomRepo, "master");
+ expectToHaveSubmoduleState(midRepo, "master",
+ "bottom-project", bottomRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master",
+ "mid-project", midRepo, "master");
+ expectToHaveSubmoduleState(topRepo, "master",
+ "bottom-project", bottomRepo, "master");
}
- @Test
- public void testBranchCircularSubscription() throws Exception {
+
+ private String prepareBranchCircularSubscription() throws Exception {
TestRepository<?> topRepo = createProjectWithPush("top-project");
TestRepository<?> midRepo = createProjectWithPush("mid-project");
TestRepository<?> bottomRepo = createProjectWithPush("bottom-project");
@@ -385,15 +477,23 @@
String changeId = getChangeId(bottomRepo, bottomMasterHead).get();
approve(changeId);
-
exception.expectMessage("Branch level circular subscriptions detected");
exception.expectMessage("top-project,refs/heads/master");
exception.expectMessage("mid-project,refs/heads/master");
exception.expectMessage("bottom-project,refs/heads/master");
- gApi.changes().id(changeId).current().submit();
+ return changeId;
+ }
- assertThat(hasSubmodule(midRepo, "master", "bottom-project")).isFalse();
- assertThat(hasSubmodule(topRepo, "master", "mid-project")).isFalse();
+ @Test
+ public void testBranchCircularSubscription() throws Exception {
+ String changeId = prepareBranchCircularSubscription();
+ gApi.changes().id(changeId).current().submit();
+ }
+
+ @Test
+ public void testBranchCircularSubscriptionPreview() throws Exception {
+ String changeId = prepareBranchCircularSubscription();
+ gApi.changes().id(changeId).current().submitPreview();
}
@Test
@@ -401,8 +501,8 @@
TestRepository<?> superRepo = createProjectWithPush("super-project");
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
- allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
- "super-project", "refs/heads/master");
+ allowMatchingSubmoduleSubscription("subscribed-to-project",
+ "refs/heads/master", "super-project", "refs/heads/master");
allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev",
"subscribed-to-project", "refs/heads/dev");
@@ -433,4 +533,56 @@
.isFalse();
assertThat(hasSubmodule(subRepo, "dev", "super-project")).isFalse();
}
+
+ @Test
+ public void testProjectNoSubscriptionWholeTopic() throws Exception {
+ TestRepository<?> repoA = createProjectWithPush("project-a");
+ TestRepository<?> repoB = createProjectWithPush("project-b");
+ // bootstrap the dev branch
+ ObjectId a0 = pushChangeTo(repoA, "dev");
+
+ // bootstrap the dev branch
+ ObjectId b0 = pushChangeTo(repoB, "dev");
+
+ // create a change for master branch in repo a
+ ObjectId aHead =
+ pushChangeTo(repoA, "refs/for/master", "master.txt", "content master A",
+ "some message in a master.txt", "same-topic");
+
+ // create a change for master branch in repo b
+ ObjectId bHead =
+ pushChangeTo(repoB, "refs/for/master", "master.txt", "content master B",
+ "some message in b master.txt", "same-topic");
+
+ // create a change for dev branch in repo a
+ repoA.reset(a0);
+ ObjectId aDevHead =
+ pushChangeTo(repoA, "refs/for/dev", "dev.txt", "content dev A",
+ "some message in a dev.txt", "same-topic");
+
+ // create a change for dev branch in repo b
+ repoB.reset(b0);
+ ObjectId bDevHead =
+ pushChangeTo(repoB, "refs/for/dev", "dev.txt", "content dev B",
+ "some message in b dev.txt", "same-topic");
+
+ approve(getChangeId(repoA, aHead).get());
+ approve(getChangeId(repoB, bHead).get());
+ approve(getChangeId(repoA, aDevHead).get());
+ approve(getChangeId(repoB, bDevHead).get());
+
+ gApi.changes().id(getChangeId(repoA, aDevHead).get()).current().submit();
+ assertThat(
+ getRemoteHead(name("project-a"), "refs/heads/master").getShortMessage())
+ .contains("some message in a master.txt");
+ assertThat(
+ getRemoteHead(name("project-a"), "refs/heads/dev").getShortMessage())
+ .contains("some message in a dev.txt");
+ assertThat(
+ getRemoteHead(name("project-b"), "refs/heads/master").getShortMessage())
+ .contains("some message in b master.txt");
+ assertThat(
+ getRemoteHead(name("project-b"), "refs/heads/dev").getShortMessage())
+ .contains("some message in b dev.txt");
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
index ff167ac..3522991 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK
@@ -3,6 +3,5 @@
acceptance_tests(
group = 'pgm',
srcs = glob(['*IT.java']),
- source_under_test = ['//gerrit-pgm:pgm'],
labels = ['pgm'],
)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
index 806acd2..56cecb8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
@@ -3,6 +3,5 @@
acceptance_tests(
group = 'pgm',
srcs = glob(['*IT.java']),
- source_under_test = ['//gerrit-pgm:pgm'],
labels = ['pgm'],
)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 91ee332..787902e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -36,13 +35,7 @@
List<AccountInfo> actual) {
Iterable<Account.Id> expectedIds = TestAccount.ids(expected);
Iterable<Account.Id> actualIds = Iterables.transform(
- actual,
- new Function<AccountInfo, Account.Id>() {
- @Override
- public Account.Id apply(AccountInfo in) {
- return new Account.Id(in._accountId);
- }
- });
+ actual, a -> new Account.Id(a._accountId));
assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder();
for (int i = 0; i < expected.size(); i++) {
AccountAssert.assertAccountInfo(expected.get(i), actual.get(i));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index ce82270..116a3ac 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -26,7 +26,6 @@
import static com.google.gerrit.common.data.GlobalCapability.RUN_AS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
@@ -40,13 +39,9 @@
@Test
public void testCapabilitiesUser() throws Exception {
- Iterable<String> all = Iterables.filter(GlobalCapability.getAllNames(),
- new Predicate<String>() {
- @Override
- public boolean apply(String in) {
- return !ADMINISTRATE_SERVER.equals(in) && !PRIORITY.equals(in);
- }
- });
+ Iterable<String> all = Iterables.filter(
+ GlobalCapability.getAllNames(),
+ c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c));
allowGlobalCapabilities(REGISTERED_USERS, all);
try {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
new file mode 100644
index 0000000..c1f4237
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -0,0 +1,656 @@
+// 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.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ImpersonationIT extends AbstractDaemonTest {
+ @Inject
+ private AccountControl.Factory accountControlFactory;
+
+ @Inject
+ private ApprovalsUtil approvalsUtil;
+
+ @Inject
+ private ChangeMessagesUtil cmUtil;
+
+ @Inject
+ private CommentsUtil commentsUtil;
+
+ private RestSession anonRestSession;
+ private TestAccount admin2;
+ private GroupInfo newGroup;
+
+ @Before
+ public void setUp() throws Exception {
+ anonRestSession = new RestSession(server, null);
+ admin2 = accounts.admin2();
+ GroupInput gi = new GroupInput();
+ gi.name = name("New-Group");
+ gi.members = ImmutableList.of(user.id.toString());
+ newGroup = gApi.groups().create(gi).get();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ removeRunAs();
+ }
+
+ @Test
+ public void voteOnBehalfOf() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(user.id);
+ assertThat(m.getRealAuthor()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void voteOnBehalfOfRequiresLabel() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+
+ exception.expect(AuthException.class);
+ exception.expectMessage(
+ "label required to post review on behalf of \"" + in.onBehalfOf + '"');
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfInvalidLabel() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.strictLabels = true;
+ in.label("Not-A-Label", 5);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage(
+ "label \"Not-A-Label\" is not a configured label");
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfInvalidLabelIgnoredWithoutStrictLabels()
+ throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.strictLabels = false;
+ in.label("Code-Review", 1);
+ in.label("Not-A-Label", 5);
+
+ revision.review(in);
+
+ assertThat(gApi.changes().id(r.getChangeId()).get().labels)
+ .doesNotContainKey("Not-A-Label");
+ }
+
+ @Test
+ public void voteOnBehalfOfLabelNotPermitted() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ LabelType verified = Util.verified();
+ cfg.getLabelSections().put(verified.getName(), verified);
+ saveProjectConfig(project, cfg);
+
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Verified", 1);
+
+ exception.expect(AuthException.class);
+ exception.expectMessage(
+ "not permitted to modify label \"Verified\" on behalf of \""
+ + in.onBehalfOf + '"');
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfWithComment() throws Exception {
+ testVoteOnBehalfOfWithComment();
+ }
+
+ @GerritConfig(name = "notedb.writeJson", value = "true")
+ @Test
+ public void voteOnBehalfOfWithCommentWritingJson() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ testVoteOnBehalfOfWithComment();
+ }
+
+ private void testVoteOnBehalfOfWithComment() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ CommentInput ci = new CommentInput();
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "message";
+ in.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+
+ ChangeData cd = r.getChange();
+ Comment c = Iterables.getOnlyElement(
+ commentsUtil.publishedByChange(db, cd.notes()));
+ assertThat(c.message).isEqualTo(ci.message);
+ assertThat(c.author.getId()).isEqualTo(user.id);
+ assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+ }
+
+ @GerritConfig(name = "notedb.writeJson", value = "true")
+ @Test
+ public void voteOnBehalfOfWithRobotComment() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ RobotCommentInput ci = new RobotCommentInput();
+ ci.robotId = "my-robot";
+ ci.robotRunId = "abcd1234";
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "message";
+ in.robotComments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ ChangeData cd = r.getChange();
+ RobotComment c = Iterables.getOnlyElement(
+ commentsUtil.robotCommentsByChange(cd.notes()));
+ assertThat(c.message).isEqualTo(ci.message);
+ assertThat(c.robotId).isEqualTo(ci.robotId);
+ assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
+ assertThat(c.author.getId()).isEqualTo(user.id);
+ assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void voteOnBehalfOfCannotModifyDrafts() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ setApiUser(user);
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "message";
+ gApi.changes().id(r.getChangeId()).current().createDraft(di);
+
+ setApiUser(admin);
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ in.drafts = DraftHandling.PUBLISH;
+
+ exception.expect(AuthException.class);
+ exception.expectMessage("not allowed to modify other user's drafts");
+ gApi.changes().id(r.getChangeId()).current().review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfMissingUser() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = "doesnotexist";
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: doesnotexist");
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfFailsWhenUserCannotSeeDestinationRef()
+ throws Exception {
+ blockRead(newGroup);
+
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage(
+ "on_behalf_of account " + user.id + " cannot see destination ref");
+ revision.review(in);
+ }
+
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ @Test
+ public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ setApiUser(accounts.user2());
+ assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: " + in.onBehalfOf);
+ revision.review(in);
+ }
+
+ @Test
+ public void submitOnBehalfOf() throws Exception {
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = admin2.email;
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+
+ ChangeData cd = r.getChange();
+ assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+ PatchSetApproval submitter = approvalsUtil.getSubmitter(
+ db, cd.notes(), cd.change().currentPatchSetId());
+ assertThat(submitter.getAccountId()).isEqualTo(admin2.id);
+ assertThat(submitter.getRealAccountId()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void submitOnBehalfOfInvalidUser() throws Exception {
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = "doesnotexist";
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: doesnotexist");
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void submitOnBehalfOfNotPermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = admin2.email;
+ exception.expect(AuthException.class);
+ exception.expectMessage("submit on behalf of not permitted");
+ gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void submitOnBehalfOfFailsWhenUserCannotSeeDestinationRef()
+ throws Exception {
+ blockRead(newGroup);
+
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = user.email;
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage(
+ "on_behalf_of account " + user.id + " cannot see destination ref");
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ @Test
+ public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
+ allowSubmitOnBehalfOf();
+ setApiUser(accounts.user2());
+ assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = user.email;
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: " + in.onBehalfOf);
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void runAsValidUser() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id));
+ res.assertOK();
+ AccountInfo account =
+ newGson().fromJson(res.getEntityContent(), AccountInfo.class);
+ assertThat(account._accountId).isEqualTo(user.id.get());
+ }
+
+ @GerritConfig(name = "auth.enableRunAs", value = "false")
+ @Test
+ public void runAsDisabledByConfig() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("X-Gerrit-RunAs disabled by auth.enableRunAs = false");
+ }
+
+ @Test
+ public void runAsNotPermitted() throws Exception {
+ RestResponse res =
+ adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("not permitted to use X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void runAsNeverPermittedForAnonymousUsers() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ anonRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("not permitted to use X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void runAsInvalidUser() throws Exception {
+ allowRunAs();
+ RestResponse res = adminRestSession.getWithHeader(
+ "/changes/", runAsHeader("doesnotexist"));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("no account matches X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void voteUsingRunAsAvoidsRestrictionsOfOnBehalfOf() throws Exception {
+ allowRunAs();
+ PushOneCommit.Result r = createChange();
+
+ setApiUser(user);
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "inline comment";
+ gApi.changes().id(r.getChangeId()).current().createDraft(di);
+ setApiUser(admin);
+
+ // Things that aren't allowed with on_behalf_of:
+ // - no labels.
+ // - publish other user's drafts.
+ ReviewInput in = new ReviewInput();
+ in.message = "message";
+ in.drafts = DraftHandling.PUBLISH;
+ RestResponse res = adminRestSession.postWithHeader(
+ "/changes/" + r.getChangeId() + "/revisions/current/review", in,
+ runAsHeader(user.id));
+ res.assertOK();
+
+ ChangeMessageInfo m = Iterables.getLast(
+ gApi.changes().id(r.getChangeId()).get().messages);
+ assertThat(m.message).endsWith(in.message);
+ assertThat(m.author._accountId).isEqualTo(user.id.get());
+
+ CommentInfo c = Iterables.getOnlyElement(
+ gApi.changes().id(r.getChangeId()).comments().get(di.path));
+ assertThat(c.author._accountId).isEqualTo(user.id.get());
+ assertThat(c.message).isEqualTo(di.message);
+
+ setApiUser(user);
+ assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
+ }
+
+ @Test
+ public void runAsWithOnBehalfOf() throws Exception {
+ // - Has the same restrictions as on_behalf_of (e.g. requires labels).
+ // - Takes the effective user from on_behalf_of (user).
+ // - Takes the real user from the real caller, not the intermediate
+ // X-Gerrit-RunAs user (user2).
+ allowRunAs();
+ allowCodeReviewOnBehalfOf();
+ TestAccount user2 = accounts.user2();
+
+ PushOneCommit.Result r = createChange();
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+
+ String endpoint =
+ "/changes/" + r.getChangeId() + "/revisions/current/review";
+ RestResponse res =
+ adminRestSession.postWithHeader(endpoint, in, runAsHeader(user2.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent()).isEqualTo(
+ "label required to post review on behalf of \"" + in.onBehalfOf + '"');
+
+ in.label("Code-Review", 1);
+ adminRestSession.postWithHeader(endpoint, in, runAsHeader(user2.id))
+ .assertOK();
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id); // not user2
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(user.id);
+ assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2
+ }
+
+ private void allowCodeReviewOnBehalfOf() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ LabelType codeReviewType = Util.codeReview();
+ String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid =
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(cfg, forCodeReviewAs, -1, 1, uuid, heads);
+ saveProjectConfig(project, cfg);
+ }
+
+ private void allowSubmitOnBehalfOf() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid =
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(cfg, Permission.SUBMIT_AS, uuid, heads);
+ Util.allow(cfg, Permission.SUBMIT, uuid, heads);
+ LabelType codeReviewType = Util.codeReview();
+ Util.allow(cfg, Permission.forLabel(codeReviewType.getName()),
+ -2, 2, uuid, heads);
+ saveProjectConfig(project, cfg);
+ }
+
+ private void blockRead(GroupInfo group) throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ Util.block(
+ cfg, Permission.READ, new AccountGroup.UUID(group.id), "refs/heads/master");
+ saveProjectConfig(project, cfg);
+ }
+
+ private void allowRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.allow(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private void removeRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.remove(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private static Header runAsHeader(Object user) {
+ return new BasicHeader("X-Gerrit-RunAs", user.toString());
+ }
+}
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..3d1377e 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,16 +20,19 @@
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.extensions.client.ListChangesOption.SUBMITTABLE;
+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;
-import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.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;
@@ -37,9 +40,9 @@
import com.google.gerrit.extensions.client.ListChangesOption;
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.LabelInfo;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
@@ -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;
@@ -65,13 +71,16 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
@NoHttpd
public abstract class AbstractSubmit extends AbstractDaemonTest {
@@ -110,9 +119,223 @@
@Test
@TestProjectInput(createEmptyCommit = false)
public void submitToEmptyRepo() throws Exception {
+ RevCommit initialHead = getRemoteHead();
PushOneCommit.Result change = createChange();
+ BinaryResult request = submitPreview(change.getChangeId());
+ RevCommit headAfterSubmitPreview = getRemoteHead();
+ assertThat(headAfterSubmitPreview).isEqualTo(initialHead);
+ Map<Branch.NameKey, RevTree> actual =
+ fetchFromBundles(request);
+ assertThat(actual).hasSize(1);
+
submit(change.getChangeId());
assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+ assertRevTrees(project, actual);
+ }
+
+ @Test
+ public void submitSingleChange() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result change = createChange();
+ BinaryResult request = submitPreview(change.getChangeId());
+ RevCommit headAfterSubmit = getRemoteHead();
+ assertThat(headAfterSubmit).isEqualTo(initialHead);
+ assertRefUpdatedEvents();
+ assertChangeMergedEvents();
+
+ Map<Branch.NameKey, RevTree> actual =
+ fetchFromBundles(request);
+
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ // The change is updated as well:
+ assertThat(actual).hasSize(2);
+ } else {
+ assertThat(actual).hasSize(1);
+ }
+
+ submit(change.getChangeId());
+ assertRevTrees(project, actual);
+ }
+
+ @Test
+ public void submitMultipleChangesOtherMergeConflictPreview()
+ throws Exception {
+ RevCommit initialHead = getRemoteHead();
+
+ PushOneCommit.Result change =
+ createChange("Change 1", "a.txt", "content");
+ submit(change.getChangeId());
+
+ RevCommit headAfterFirstSubmit = getRemoteHead();
+ testRepo.reset(initialHead);
+ PushOneCommit.Result change2 = createChange("Change 2",
+ "a.txt", "other content");
+ PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
+ PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
+ // change 2 is not approved, but we ignore labels
+ approve(change3.getChangeId());
+ BinaryResult request = null;
+ String msg = null;
+ try {
+ request = submitPreview(change4.getChangeId());
+ } catch (Exception e) {
+ msg = e.getMessage();
+ }
+
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ Map<Branch.NameKey, RevTree> s =
+ fetchFromBundles(request);
+ submit(change4.getChangeId());
+ assertRevTrees(project, s);
+ } else if (getSubmitType() == SubmitType.FAST_FORWARD_ONLY) {
+ assertThat(msg).isEqualTo(
+ "Failed to submit 3 changes due to the following problems:\n" +
+ "Change " + change2.getChange().getId() + ": internal error: " +
+ "change not processed by merge strategy\n" +
+ "Change " + change3.getChange().getId() + ": internal error: " +
+ "change not processed by merge strategy\n" +
+ "Change " + change4.getChange().getId() + ": Project policy " +
+ "requires all submissions to be a fast-forward. Please " +
+ "rebase the change locally and upload again for review.");
+ RevCommit headAfterSubmit = getRemoteHead();
+ assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
+ assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+ assertChangeMergedEvents(change.getChangeId(),
+ headAfterFirstSubmit.name());
+ } else if(getSubmitType() == SubmitType.REBASE_IF_NECESSARY) {
+ String change2hash = change2.getChange().currentPatchSet()
+ .getRevision().get();
+ assertThat(msg).isEqualTo(
+ "Cannot rebase " + change2hash + ": The change could " +
+ "not be rebased due to a conflict during merge.");
+ RevCommit headAfterSubmit = getRemoteHead();
+ assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
+ assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+ assertChangeMergedEvents(change.getChangeId(),
+ headAfterFirstSubmit.name());
+ } else {
+ assertThat(msg).isEqualTo(
+ "Failed to submit 3 changes due to the following problems:\n" +
+ "Change " + change2.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change " +
+ "locally and upload the rebased commit for review.\n" +
+ "Change " + change3.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change " +
+ "locally and upload the rebased commit for review.\n" +
+ "Change " + change4.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change " +
+ "locally and upload the rebased commit for review.");
+ RevCommit headAfterSubmit = getRemoteHead();
+ assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
+ assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
+ assertChangeMergedEvents(change.getChangeId(),
+ headAfterFirstSubmit.name());
+ }
+ }
+
+ @Test
+ public void submitMultipleChangesPreview() throws Exception {
+ RevCommit initialHead = getRemoteHead();
+ PushOneCommit.Result change2 = createChange("Change 2",
+ "a.txt", "other content");
+ PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
+ PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
+ // change 2 is not approved, but we ignore labels
+ approve(change3.getChangeId());
+ BinaryResult request = submitPreview(change4.getChangeId());
+
+ Map<String, Map<String, Integer>> expected = new HashMap<>();
+ expected.put(project.get(), new HashMap<String, Integer>());
+ expected.get(project.get()).put("refs/heads/master", 3);
+ Map<Branch.NameKey, RevTree> actual =
+ fetchFromBundles(request);
+
+ assertThat(actual).containsKey(
+ new Branch.NameKey(project, "refs/heads/master"));
+ if (getSubmitType() == SubmitType.CHERRY_PICK) {
+ assertThat(actual).hasSize(2);
+ } else {
+ assertThat(actual).hasSize(1);
+ }
+
+ // check that the submit preview did not actually submit
+ RevCommit headAfterSubmit = getRemoteHead();
+ assertThat(headAfterSubmit).isEqualTo(initialHead);
+ assertRefUpdatedEvents();
+ assertChangeMergedEvents();
+
+ // now check we actually have the same content:
+ approve(change2.getChangeId());
+ submit(change4.getChangeId());
+ assertRevTrees(project, actual);
+ }
+
+ @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
@@ -179,13 +402,8 @@
private void assertSubmitter(PushOneCommit.Result change) throws Exception {
ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
assertThat(info.messages).isNotNull();
- Iterable<String> messages = Iterables.transform(info.messages,
- new Function<ChangeMessageInfo, String>() {
- @Override
- public String apply(ChangeMessageInfo in) {
- return in.message;
- }
- });
+ Iterable<String> messages =
+ Iterables.transform(info.messages, i -> i.message);
assertThat(messages).hasSize(3);
String last = Iterables.getLast(messages);
if (getSubmitType() == SubmitType.CHERRY_PICK) {
@@ -252,8 +470,12 @@
assertMerged(change.changeId);
}
+ protected BinaryResult submitPreview(String changeId) throws Exception {
+ return gApi.changes().id(changeId).current().submitPreview();
+ }
+
protected void assertSubmittable(String changeId) throws Exception {
- assertThat(gApi.changes().id(changeId).info().submittable)
+ assertThat(get(changeId, SUBMITTABLE).submittable)
.named("submit bit on ChangeInfo")
.isEqualTo(true);
RevisionResource rsrc = parseCurrentRevisionResource(changeId);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
new file mode 100644
index 0000000..685cceb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -0,0 +1,153 @@
+// 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.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.testutil.TestTimeUtil;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.List;
+
+@NoHttpd
+public class AssigneeIT extends AbstractDaemonTest {
+
+ @BeforeClass
+ public static void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
+ }
+
+ @AfterClass
+ public static void restoreTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
+ @Test
+ public void testGetNoAssignee() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(getAssignee(r)).isNull();
+ }
+
+ @Test
+ public void testAddGetAssignee() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(setAssignee(r, user.email)._accountId)
+ .isEqualTo(user.getId().get());
+ assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get());
+ }
+
+ @Test
+ public void testSetNewAssigneeWhenExists() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setAssignee(r, user.email);
+ assertThat(setAssignee(r, user.email)._accountId)
+ .isEqualTo(user.getId().get());
+ }
+
+ @Test
+ public void testGetPastAssignees() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+ setAssignee(r, user.email);
+ setAssignee(r, admin.email);
+ List<AccountInfo> assignees = getPastAssignees(r);
+ assertThat(assignees).hasSize(2);
+ Iterator<AccountInfo> itr = assignees.iterator();
+ assertThat(itr.next()._accountId).isEqualTo(user.getId().get());
+ assertThat(itr.next()._accountId).isEqualTo(admin.getId().get());
+ }
+
+ @Test
+ public void testAssigneeAddedAsReviewer() throws Exception {
+ ReviewerState state;
+ // Assignee is added as CC, if back-end is reviewDb (that does not support
+ // CC) CC is stored as REVIEWER
+ if (notesMigration.readChanges()) {
+ state = ReviewerState.CC;
+ } else {
+ state = ReviewerState.REVIEWER;
+ }
+ PushOneCommit.Result r = createChange();
+ Iterable<AccountInfo> reviewers = getReviewers(r, state);
+ assertThat(reviewers).isNull();
+ assertThat(setAssignee(r, user.email)._accountId)
+ .isEqualTo(user.getId().get());
+ reviewers = getReviewers(r, state);
+ assertThat(reviewers).hasSize(1);
+ AccountInfo reviewer = Iterables.getFirst(reviewers, null);
+ assertThat(reviewer._accountId).isEqualTo(user.getId().get());
+ }
+
+ @Test
+ public void testSetAlreadyExistingAssignee() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setAssignee(r, user.email);
+ assertThat(setAssignee(r, user.email)._accountId)
+ .isEqualTo(user.getId().get());
+ }
+
+ @Test
+ public void testDeleteAssignee() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(setAssignee(r, user.email)._accountId)
+ .isEqualTo(user.getId().get());
+ assertThat(deleteAssignee(r)._accountId).isEqualTo(user.getId().get());
+ assertThat(getAssignee(r)).isNull();
+ }
+
+ @Test
+ public void testDeleteAssigneeWhenNoAssignee() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(deleteAssignee(r)).isNull();
+ }
+
+ private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
+ return gApi.changes().id(r.getChange().getId().get()).getAssignee();
+ }
+
+ private List<AccountInfo> getPastAssignees(PushOneCommit.Result r)
+ throws Exception {
+ return gApi.changes().id(r.getChange().getId().get()).getPastAssignees();
+ }
+
+ private Iterable<AccountInfo> getReviewers(PushOneCommit.Result r,
+ ReviewerState state) throws Exception {
+ return get(r.getChangeId()).reviewers.get(state);
+ }
+
+ private AccountInfo setAssignee(PushOneCommit.Result r, String identifieer)
+ throws Exception {
+ AssigneeInput input = new AssigneeInput();
+ input.assignee = identifieer;
+ return gApi.changes().id(r.getChange().getId().get()).setAssignee(input);
+ }
+
+ private AccountInfo deleteAssignee(PushOneCommit.Result r) throws Exception {
+ return gApi.changes().id(r.getChange().getId().get()).deleteAssignee();
+ }
+}
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/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 31e52f7..2474d68 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -33,7 +33,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
@@ -152,8 +152,8 @@
for (ChangeMessage m : cd.messages()) {
assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
}
- for (PatchLineComment c : cd.publishedComments()) {
- assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+ for (Comment c : cd.publishedComments()) {
+ assertThat(c.key.patchSetId).named(c.toString()).isNotEqualTo(delPsId.get());
}
}
@@ -187,8 +187,8 @@
for (ChangeMessage m : cd.messages()) {
assertThat(m.getPatchSetId()).named(m.toString()).isNotEqualTo(delPsId);
}
- for (PatchLineComment c : cd.publishedComments()) {
- assertThat(c.getPatchSetId()).named(c.toString()).isNotEqualTo(delPsId);
+ for (Comment c : cd.publishedComments()) {
+ assertThat(c.key.patchSetId).named(c.toString()).isNotEqualTo(delPsId.get());
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 29fda2d..e160374 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -24,14 +25,19 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
import java.util.List;
+import java.util.Map;
public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
@@ -144,6 +150,12 @@
approve(change2a.getChangeId());
approve(change2b.getChangeId());
approve(change3.getChangeId());
+
+ // get a preview before submitting:
+ BinaryResult request = submitPreview(change1b.getChangeId());
+ Map<Branch.NameKey, RevTree> preview =
+ fetchFromBundles(request);
+
submit(change1b.getChangeId());
RevCommit tip1 = getRemoteLog(p1, "master").get(0);
@@ -158,11 +170,28 @@
change2b.getCommit().getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(
change3.getCommit().getShortMessage());
+
+ // check that the preview matched what happened:
+ assertThat(preview).hasSize(3);
+
+ assertThat(preview).containsKey(
+ new Branch.NameKey(p1, "refs/heads/master"));
+ assertRevTrees(p1, preview);
+
+ assertThat(preview).containsKey(
+ new Branch.NameKey(p2, "refs/heads/master"));
+ assertRevTrees(p2, preview);
+
+ assertThat(preview).containsKey(
+ new Branch.NameKey(p3, "refs/heads/master"));
+ assertRevTrees(p3, preview);
} else {
assertThat(tip2.getShortMessage()).isEqualTo(
initialHead2.getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(
initialHead3.getShortMessage());
+ assertThat(preview).hasSize(1);
+ assertThat(preview.get(new Branch.NameKey(p1, "refs/heads/master"))).isNotNull();
}
}
@@ -215,11 +244,23 @@
approve(change3.getChangeId());
if (isSubmitWholeTopicEnabled()) {
- submitWithConflict(change1b.getChangeId(),
+ String msg =
"Failed to submit 5 changes due to the following problems:\n" +
"Change " + change3.getChange().getId() + ": Change could not be " +
"merged due to a path conflict. Please rebase the change locally " +
- "and upload the rebased commit for review.");
+ "and upload the rebased commit for review.";
+
+ // Get a preview before submitting:
+ try {
+ // We cannot just use the ExpectedException infrastructure as provided
+ // by AbstractDaemonTest, as then we'd stop early and not test the
+ // actual submit.
+ submitPreview(change1b.getChangeId());
+ fail("expected failure");
+ } catch (RestApiException e) {
+ assertThat(e.getMessage()).isEqualTo(msg);
+ }
+ submitWithConflict(change1b.getChangeId(), msg);
} else {
submit(change1b.getChangeId());
}
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/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index e4f56f8..0f251f5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -16,8 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
@@ -287,13 +285,9 @@
private TestAccount user(String name, String fullName, String emailName,
AccountGroup... groups) throws Exception {
- String[] groupNames = FluentIterable.from(Arrays.asList(groups))
- .transform(new Function<AccountGroup, String>() {
- @Override
- public String apply(AccountGroup in) {
- return in.getName();
- }
- }).toArray(String.class);
+ String[] groupNames = Arrays.stream(groups)
+ .map(AccountGroup::getName)
+ .toArray(String[]::new);
return accounts.create(name(name), name(emailName) + "@example.com",
fullName, groupNames);
}
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..ce7e8c9 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
@@ -64,8 +65,10 @@
// gerrit
@GerritConfig(name = "gerrit.allProjects", value = "Root"),
@GerritConfig(name = "gerrit.allUsers", value = "Users"),
- @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report"),
+ @GerritConfig(name = "gerrit.enableGwtUi", value = "true"),
+ @GerritConfig(name = "gerrit.enablePolyGerrit", value = "true"),
@GerritConfig(name = "gerrit.reportBugText", value = "REPORT BUG"),
+ @GerritConfig(name = "gerrit.reportBugUrl", value = "https://example.com/report"),
// suggest
@GerritConfig(name = "suggest.from", value = "3"),
@@ -74,12 +77,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");
@@ -107,6 +110,9 @@
assertThat(i.gerrit.reportBugUrl).isEqualTo("https://example.com/report");
assertThat(i.gerrit.reportBugText).isEqualTo("REPORT BUG");
+ // Acceptance tests force --headless even when UIs are specified in config.
+ assertThat(i.gerrit.webUis).isEmpty();
+
// plugin
assertThat(i.plugin.jsResourcePaths).isEmpty();
@@ -121,9 +127,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 +140,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 +148,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 +195,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/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
index c860bf0..522836d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -48,12 +47,7 @@
}
private static Iterable<String> refs(Iterable<BranchInfo> infos) {
- return Iterables.transform(infos, new Function<BranchInfo, String>() {
- @Override
- public String apply(BranchInfo in) {
- return in.ref;
- }
- });
+ return Iterables.transform(infos, b -> b.ref);
}
private static boolean toBoolean(Boolean b) {
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/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index e86bb29..496e7fd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -19,7 +19,6 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static org.junit.Assert.fail;
-import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -208,15 +207,14 @@
}
private Iterable<ProjectInfo> filter(Iterable<ProjectInfo> infos) {
- final String prefix = name("");
- return Iterables.filter(infos, new Predicate<ProjectInfo>() {
- @Override
- public boolean apply(ProjectInfo in) {
- return in.name != null && (
- in.name.equals(allProjects.get())
- || in.name.equals(allUsers.get())
- || in.name.startsWith(prefix));
- }
- });
+ String prefix = name("");
+ return Iterables.filter(
+ infos,
+ p -> {
+ return p.name != null && (
+ p.name.equals(allProjects.get())
+ || p.name.equals(allUsers.get())
+ || p.name.startsWith(prefix));
+ });
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index db6df95..e081ce5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -45,13 +44,8 @@
.that(Url.decode(info.id))
.isEqualTo(info.name);
}
- return assertThat(Iterables.transform(actual,
- new Function<ProjectInfo, Project.NameKey>() {
- @Override
- public Project.NameKey apply(ProjectInfo in) {
- return new Project.NameKey(in.name);
- }
- }));
+ return assertThat(
+ Iterables.transform(actual, p -> new Project.NameKey(p.name)));
}
public static void assertProjectInfo(Project project, ProjectInfo info) {
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..77cb11b 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
@@ -32,9 +32,11 @@
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ChangesCollection;
import com.google.gerrit.server.change.PostReview;
@@ -52,6 +54,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -148,8 +151,8 @@
@Test
public void postCommentOnMergeCommitChange() throws Exception {
for (Integer line : lines) {
- final String file = "/COMMIT_MSG";
- PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+ String file = "foo";
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master", file);
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
ReviewInput input = new ReviewInput();
@@ -165,6 +168,39 @@
assertThat(Lists.transform(result.get(file), infoToInput(file)))
.containsExactly(c1, c2, c3, c4);
}
+
+ // for the commit message comments on the auto-merge are not possible
+ for (Integer line : lines) {
+ String file = Patch.COMMIT_MSG;
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ ReviewInput input = new ReviewInput();
+ CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1");
+ CommentInput c2 = newCommentOnParent(file, 1, line, "parent-1 of ps-1");
+ CommentInput c3 = newCommentOnParent(file, 2, line, "parent-2 of ps-1");
+ input.comments = new HashMap<>();
+ input.comments.put(file, ImmutableList.of(c1, c2, c3));
+ revision(r).review(input);
+ Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+ assertThat(result).isNotEmpty();
+ assertThat(Lists.transform(result.get(file), infoToInput(file)))
+ .containsExactly(c1, c2, c3);
+ }
+ }
+
+ @Test
+ public void postCommentOnCommitMessageOnAutoMerge() throws Exception {
+ PushOneCommit.Result r = createMergeCommitChange("refs/for/master");
+ ReviewInput input = new ReviewInput();
+ CommentInput c =
+ newComment(Patch.COMMIT_MSG, Side.PARENT, 0, "comment on auto-merge");
+ input.comments = new HashMap<>();
+ input.comments.put(Patch.COMMIT_MSG, ImmutableList.of(c));
+ exception.expect(BadRequestException.class);
+ exception.expectMessage(
+ "cannot comment on " + Patch.COMMIT_MSG + " on auto-merge");
+ revision(r).review(input);
}
@Test
@@ -504,8 +540,7 @@
assertThat(ps2List.get(2).message).isEqualTo("join lines");
assertThat(ps2List.get(3).message).isEqualTo("typo: content");
- ImmutableList<Message> messages =
- email.getMessages(r2.getChangeId(), "comment");
+ List<Message> messages = email.getMessages(r2.getChangeId(), "comment");
assertThat(messages).hasSize(1);
String url = canonicalWebUrl.get();
int c = r1.getChange().getId().get();
@@ -544,6 +579,7 @@
+ "\n"
+ "PS2, Line 2: nten\n"
+ "typo: content\n"
+ + "\n"
+ "\n");
}
@@ -687,29 +723,21 @@
return c;
}
- private static Function<CommentInfo, CommentInput> infoToInput(
- final String path) {
- return new Function<CommentInfo, CommentInput>() {
- @Override
- public CommentInput apply(CommentInfo info) {
- CommentInput ci = new CommentInput();
- ci.path = path;
- copy(info, ci);
- return ci;
- }
- };
+ private static Function<CommentInfo, CommentInput> infoToInput(String path) {
+ return infoToInput(path, CommentInput::new);
}
- private static Function<CommentInfo, DraftInput> infoToDraft(
- final String path) {
- return new Function<CommentInfo, DraftInput>() {
- @Override
- public DraftInput apply(CommentInfo info) {
- DraftInput di = new DraftInput();
- di.path = path;
- copy(info, di);
- return di;
- }
+ private static Function<CommentInfo, DraftInput> infoToDraft(String path) {
+ return infoToInput(path, DraftInput::new);
+ }
+
+ private static <I extends Comment> Function<CommentInfo, I> infoToInput(
+ String path, Supplier<I> supplier) {
+ return info -> {
+ I i = supplier.get();
+ i.path = path;
+ copy(info, i);
+ return i;
};
}
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-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 06170d0..b843721 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -23,8 +23,11 @@
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.testutil.ConfigSuite;
@@ -44,6 +47,63 @@
}
@Test
+ public void doesNotIncludeCurrentFiles() throws Exception {
+ RevCommit c1_1 = commitBuilder()
+ .add("a.txt", "1")
+ .message("subject: 1")
+ .create();
+ RevCommit c2_1 = commitBuilder()
+ .add("b.txt", "2")
+ .message("subject: 2")
+ .create();
+ String id2 = getChangeId(c2_1);
+ pushHead(testRepo, "refs/for/master", false);
+
+ SubmittedTogetherInfo info =
+ gApi.changes()
+ .id(id2)
+ .submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
+ assertThat(info.changes).hasSize(2);
+ assertThat(info.changes.get(0).currentRevision).isEqualTo(c2_1.name());
+ assertThat(info.changes.get(1).currentRevision).isEqualTo(c1_1.name());
+
+ assertThat(info.changes.get(0).currentRevision).isEqualTo(c2_1.name());
+ RevisionInfo rev = info.changes.get(0).revisions.get(c2_1.name());
+ assertThat(rev.files).isNull();
+ }
+
+ @Test
+ public void returnsCurrentFilesIfOptionRequested() throws Exception {
+ RevCommit c1_1 = commitBuilder()
+ .add("a.txt", "1")
+ .message("subject: 1")
+ .create();
+ RevCommit c2_1 = commitBuilder()
+ .add("b.txt", "2")
+ .message("subject: 2")
+ .create();
+ String id2 = getChangeId(c2_1);
+ pushHead(testRepo, "refs/for/master", false);
+
+ SubmittedTogetherInfo info =
+ gApi.changes()
+ .id(id2)
+ .submittedTogether(
+ EnumSet.of(ListChangesOption.CURRENT_FILES),
+ EnumSet.of(NON_VISIBLE_CHANGES));
+ assertThat(info.changes).hasSize(2);
+ assertThat(info.changes.get(0).currentRevision).isEqualTo(c2_1.name());
+ assertThat(info.changes.get(1).currentRevision).isEqualTo(c1_1.name());
+
+ assertThat(info.changes.get(0).currentRevision).isEqualTo(c2_1.name());
+ RevisionInfo rev = info.changes.get(0).revisions.get(c2_1.name());
+ assertThat(rev).isNotNull();
+ FileInfo file = rev.files.get("b.txt");
+ assertThat(file).isNotNull();
+ assertThat(file.status).isEqualTo('A');
+ }
+
+ @Test
public void returnsAncestors() throws Exception {
// Create two commits and push.
RevCommit c1_1 = commitBuilder()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index b443e66..dcdce92 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -18,22 +18,26 @@
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+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.OBJ_BLOB;
import static org.junit.Assert.fail;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
@@ -42,12 +46,13 @@
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.Rebuild;
@@ -55,14 +60,18 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeRebuilder.NoPatchSetsException;
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gerrit.server.project.Util;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.NoteDbChecker;
import com.google.gerrit.testutil.NoteDbMode;
@@ -72,6 +81,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -85,6 +96,7 @@
import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -109,7 +121,7 @@
private Provider<ReviewDb> dbProvider;
@Inject
- private PatchLineCommentsUtil plcUtil;
+ private CommentsUtil commentsUtil;
@Inject
private Provider<PostReview> postReview;
@@ -123,6 +135,9 @@
@Inject
private Sequences seq;
+ @Inject
+ private ChangeBundleReader bundleReader;
+
@Before
public void setUp() throws Exception {
assume().that(NoteDbMode.readWrite()).isFalse();
@@ -387,8 +402,8 @@
// Check that the bundles are equal.
ChangeBundle actual = ChangeBundle.fromNotes(
- plcUtil, notesFactory.create(dbProvider.get(), project, id));
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ commentsUtil, notesFactory.create(dbProvider.get(), project, id));
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
}
@@ -438,18 +453,13 @@
// Check that the bundles are equal.
ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id);
- ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
assertThat(
Iterables.transform(
notes.getChangeMessages(),
- new Function<ChangeMessage, String>() {
- @Override
- public String apply(ChangeMessage in) {
- return in.getMessage();
- }
- }))
+ ChangeMessage::getMessage))
.contains(msg);
}
@@ -477,8 +487,8 @@
// Check that the bundles are equal.
ChangeBundle actual = ChangeBundle.fromNotes(
- plcUtil, notesFactory.create(dbProvider.get(), project, id));
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ commentsUtil, notesFactory.create(dbProvider.get(), project, id));
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
}
@@ -507,8 +517,8 @@
// Not up to date, but the actual returned state matches anyway.
assertChangeUpToDate(false, id);
assertThat(getMetaRef(project, changeMetaRef(id))).isEqualTo(oldMetaId);
- ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
assertChangeUpToDate(false, id);
@@ -549,8 +559,8 @@
// Not up to date, but the actual returned state matches anyway.
assertDraftsUpToDate(false, id, user);
- ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
// Another rebuild attempt succeeds
@@ -604,8 +614,8 @@
// Not up to date, but the actual returned state matches anyway.
assertChangeUpToDate(true, id);
assertDraftsUpToDate(false, id, user);
- ChangeBundle actual = ChangeBundle.fromNotes(plcUtil, notes);
- ChangeBundle expected = ChangeBundle.fromReviewDb(getUnwrappedDb(), id);
+ ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes);
+ ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id);
assertThat(actual.differencesFrom(expected)).isEmpty();
// Another rebuild attempt succeeds
@@ -711,6 +721,8 @@
rin.message = "comment";
Timestamp ts = new Timestamp(c.getCreatedOn().getTime() + 2000);
+ assertThat(ts).isGreaterThan(c.getCreatedOn());
+ assertThat(ts).isLessThan(db.patchSets().get(psId).getCreatedOn());
RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId());
postReview.get().apply(revRsrc, rin, ts);
@@ -971,6 +983,94 @@
checker.rebuildAndCheckChanges(id);
}
+ @Test
+ public void rebuildEntitiesCreatedByImpersonation() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getPatchSetId().getParentKey();
+ PatchSet.Id psId = new PatchSet.Id(id, 1);
+ String prefix = "/changes/" + id + "/revisions/current/";
+
+ // For each of the entities that have a real user field, create one entity
+ // without impersonation and one with.
+ CommentInput ci = new CommentInput();
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "comment without impersonation";
+ ReviewInput ri = new ReviewInput();
+ ri.label("Code-Review", -1);
+ ri.message = "message without impersonation";
+ ri.drafts = DraftHandling.KEEP;
+ ri.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ userRestSession.post(prefix + "review", ri).assertOK();
+
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "draft without impersonation";
+ userRestSession.put(prefix + "drafts", di).assertCreated();
+
+ allowRunAs();
+ try {
+ Header runAs = new BasicHeader("X-Gerrit-RunAs", user.id.toString());
+ ci.message = "comment with impersonation";
+ ri.message = "message with impersonation";
+ ri.label("Code-Review", 1);
+ adminRestSession.postWithHeader(prefix + "review", ri, runAs).assertOK();
+
+ di.message = "draft with impersonation";
+ adminRestSession.putWithHeader(prefix + "drafts", runAs, di)
+ .assertCreated();
+ } finally {
+ removeRunAs();
+ }
+
+ List<ChangeMessage> msgs =
+ Ordering.natural().onResultOf(ChangeMessage::getWrittenOn)
+ .sortedCopy(db.changeMessages().byChange(id));
+ assertThat(msgs).hasSize(3);
+ assertThat(msgs.get(1).getMessage())
+ .endsWith("message without impersonation");
+ assertThat(msgs.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(1).getRealAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(2).getMessage()).endsWith("message with impersonation");
+ assertThat(msgs.get(2).getAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(2).getRealAuthor()).isEqualTo(admin.id);
+
+ List<PatchSetApproval> psas = db.patchSetApprovals().byChange(id).toList();
+ assertThat(psas).hasSize(1);
+ assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+ assertThat(psas.get(0).getValue()).isEqualTo(1);
+ assertThat(psas.get(0).getAccountId()).isEqualTo(user.id);
+ assertThat(psas.get(0).getRealAccountId()).isEqualTo(admin.id);
+
+ Ordering<PatchLineComment> commentOrder =
+ Ordering.natural().onResultOf(PatchLineComment::getWrittenOn);
+ List<PatchLineComment> drafts = commentOrder.sortedCopy(
+ db.patchComments().draftByPatchSetAuthor(psId, user.id));
+ assertThat(drafts).hasSize(2);
+ assertThat(drafts.get(0).getMessage())
+ .isEqualTo("draft without impersonation");
+ assertThat(drafts.get(0).getAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(0).getRealAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(1).getMessage())
+ .isEqualTo("draft with impersonation");
+ assertThat(drafts.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(1).getRealAuthor()).isEqualTo(admin.id);
+
+ List<PatchLineComment> pub = commentOrder.sortedCopy(
+ db.patchComments().publishedByPatchSet(psId));
+ assertThat(pub).hasSize(2);
+ assertThat(pub.get(0).getMessage())
+ .isEqualTo("comment without impersonation");
+ assertThat(pub.get(0).getAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(0).getRealAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(1).getMessage()).isEqualTo("comment with impersonation");
+ assertThat(pub.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(1).getRealAuthor()).isEqualTo(admin.id);
+ }
+
private void assertChangesReadOnly(RestApiException e) throws Exception {
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(UpdateException.class);
@@ -1086,4 +1186,19 @@
ReviewDb db = dbProvider.get();
return ReviewDbUtil.unwrapDb(db);
}
+
+ private void allowRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.allow(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private void removeRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.remove(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
}
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
index ff2562d..62a99e3 100644
--- a/gerrit-acceptance-tests/tests.bzl
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -11,7 +11,6 @@
flaky = 0,
deps = [],
labels = [],
- source_under_test = [], #unused
vm_args = ['-Xmx256m']):
junit_tests(
name = group,
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
index 85cc78b..648bd63 100644
--- a/gerrit-acceptance-tests/tests.defs
+++ b/gerrit-acceptance-tests/tests.defs
@@ -8,7 +8,6 @@
srcs,
deps = [],
labels = [],
- source_under_test = [],
vm_args = ['-Xmx256m']):
from os import path
if path.exists('/dev/urandom'):
@@ -20,11 +19,6 @@
deps = deps + BOUNCYCASTLE + [
'//gerrit-acceptance-tests:lib'
],
- source_under_test = [
- '//gerrit-httpd:httpd',
- '//gerrit-sshd:sshd',
- '//gerrit-server:server',
- ] + source_under_test,
labels = labels + [
'acceptance',
'slow',
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 5009771..f7381a3 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -119,19 +119,8 @@
public void start() {
if (executor != null) {
for (final H2CacheImpl<?, ?> cache : caches) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- cache.start();
- }
- });
-
- cleanup.schedule(new Runnable() {
- @Override
- public void run() {
- cache.prune(cleanup);
- }
- }, 30, TimeUnit.SECONDS);
+ executor.execute(cache::start);
+ cleanup.schedule(() -> cache.prune(cleanup), 30, TimeUnit.SECONDS);
}
}
}
@@ -189,7 +178,7 @@
public <K, V> LoadingCache<K, V> build(
CacheBinding<K, V> def,
CacheLoader<K, V> loader) {
- long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+ long limit = config.getLong("cache", def.name(), "diskLimit", def.diskLimit());
if (cacheDir == null || limit <= 0) {
return defaultFactory.build(def, loader);
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 838f42c..7e05236 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -144,24 +144,14 @@
final ValueHolder<V> h = new ValueHolder<>(val);
h.created = TimeUtil.nowMs();
mem.put(key, h);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- store.put(key, h);
- }
- });
+ executor.execute(() -> store.put(key, h));
}
@SuppressWarnings("unchecked")
@Override
public void invalidate(final Object key) {
if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- store.invalidate((K) key);
- }
- });
+ executor.execute(() -> store.invalidate((K) key));
}
mem.invalidate(key);
}
@@ -212,12 +202,7 @@
cal.add(Calendar.DAY_OF_MONTH, 1);
long delay = cal.getTimeInMillis() - TimeUtil.nowMs();
- service.schedule(new Runnable() {
- @Override
- public void run() {
- prune(service);
- }
- }, delay, TimeUnit.MILLISECONDS);
+ service.schedule(() -> prune(service), delay, TimeUnit.MILLISECONDS);
}
static class ValueHolder<V> {
@@ -252,12 +237,7 @@
final ValueHolder<V> h = new ValueHolder<>(loader.load(key));
h.created = TimeUtil.nowMs();
- executor.execute(new Runnable() {
- @Override
- public void run() {
- store.put(key, h);
- }
- });
+ executor.execute(() -> store.put(key, h));
return h;
}
}
@@ -280,14 +260,9 @@
}
}
- final ValueHolder<V> h = new ValueHolder<V>(loader.call());
+ final ValueHolder<V> h = new ValueHolder<>(loader.call());
h.created = TimeUtil.nowMs();
- executor.execute(new Runnable() {
- @Override
- public void run() {
- store.put(key, h);
- }
- });
+ executor.execute(() -> store.put(key, h));
return h;
}
}
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 847fd25..452b2fe 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -62,7 +62,6 @@
'//lib:guava',
'//lib:junit',
],
- source_under_test = [':client'],
)
java_test(
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index 43d4441..795ec6a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -96,6 +96,10 @@
return toChangeQuery(op("owner", fullname) + " " + status(status));
}
+ public static String toAssigneeQuery(String fullname) {
+ return toChangeQuery(op("assignee", fullname));
+ }
+
public static String toCustomDashboard(final String params) {
return "/dashboard/?" + params;
}
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/CommentDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
index 1b98b09..ff5402f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
@@ -14,7 +14,8 @@
package com.google.gerrit.common.data;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import java.util.ArrayList;
@@ -24,14 +25,14 @@
import java.util.Map;
public class CommentDetail {
- protected List<PatchLineComment> a;
- protected List<PatchLineComment> b;
+ protected List<Comment> a;
+ protected List<Comment> b;
protected AccountInfoCache accounts;
private transient PatchSet.Id idA;
private transient PatchSet.Id idB;
- private transient Map<Integer, List<PatchLineComment>> forA;
- private transient Map<Integer, List<PatchLineComment>> forB;
+ private transient Map<Integer, List<Comment>> forA;
+ private transient Map<Integer, List<Comment>> forB;
public CommentDetail(PatchSet.Id idA, PatchSet.Id idB) {
this.a = new ArrayList<>();
@@ -43,9 +44,9 @@
protected CommentDetail() {
}
- public boolean include(final PatchLineComment p) {
- final PatchSet.Id psId = p.getKey().getParentKey().getParentKey();
- switch (p.getSide()) {
+ public boolean include(Change.Id changeId, Comment p) {
+ PatchSet.Id psId = new PatchSet.Id(changeId, p.key.patchSetId);
+ switch (p.side) {
case 0:
if (idA == null && idB.equals(psId)) {
a.add(p);
@@ -76,11 +77,11 @@
return accounts;
}
- public List<PatchLineComment> getCommentsA() {
+ public List<Comment> getCommentsA() {
return a;
}
- public List<PatchLineComment> getCommentsB() {
+ public List<Comment> getCommentsB() {
return b;
}
@@ -88,24 +89,23 @@
return a.isEmpty() && b.isEmpty();
}
- public List<PatchLineComment> getForA(final int lineNbr) {
+ public List<Comment> getForA(int lineNbr) {
if (forA == null) {
forA = index(a);
}
return get(forA, lineNbr);
}
- public List<PatchLineComment> getForB(final int lineNbr) {
+ public List<Comment> getForB(int lineNbr) {
if (forB == null) {
forB = index(b);
}
return get(forB, lineNbr);
}
- private static List<PatchLineComment> get(
- final Map<Integer, List<PatchLineComment>> m, final int i) {
- final List<PatchLineComment> r = m.get(i);
- return r != null ? orderComments(r) : Collections.<PatchLineComment> emptyList();
+ private static List<Comment> get(Map<Integer, List<Comment>> m, int i) {
+ List<Comment> r = m.get(i);
+ return r != null ? orderComments(r) : Collections.<Comment> emptyList();
}
/**
@@ -116,21 +116,21 @@
* @param comments The list of comments for a given line.
* @return The comments sorted as they should appear in the UI
*/
- private static List<PatchLineComment> orderComments(List<PatchLineComment> comments) {
+ private static List<Comment> orderComments(List<Comment> comments) {
// Map of comments keyed by their parent. The values are lists of comments since it is
// possible for several comments to have the same parent (this can happen if two reviewers
// click Reply on the same comment at the same time). Such comments will be displayed under
// their correct parent in chronological order.
- Map<String, List<PatchLineComment>> parentMap = new HashMap<>();
+ Map<String, List<Comment>> parentMap = new HashMap<>();
// It's possible to have more than one root comment if two reviewers create a comment on the
// same line at the same time
- List<PatchLineComment> rootComments = new ArrayList<>();
+ List<Comment> rootComments = new ArrayList<>();
// Store all the comments in parentMap, keyed by their parent
- for (PatchLineComment c : comments) {
- String parentUuid = c.getParentUuid();
- List<PatchLineComment> l = parentMap.get(parentUuid);
+ for (Comment c : comments) {
+ String parentUuid = c.parentUuid;
+ List<Comment> l = parentMap.get(parentUuid);
if (l == null) {
l = new ArrayList<>();
parentMap.put(parentUuid, l);
@@ -143,7 +143,7 @@
// Add the comments in the list, starting with the head and then going through all the
// comments that have it as a parent, and so on
- List<PatchLineComment> result = new ArrayList<>();
+ List<Comment> result = new ArrayList<>();
addChildren(parentMap, rootComments, result);
return result;
@@ -152,24 +152,23 @@
/**
* Add the comments to {@code outResult}, depth first
*/
- private static void addChildren(Map<String, List<PatchLineComment>> parentMap,
- List<PatchLineComment> children, List<PatchLineComment> outResult) {
+ private static void addChildren(Map<String, List<Comment>> parentMap,
+ List<Comment> children, List<Comment> outResult) {
if (children != null) {
- for (PatchLineComment c : children) {
+ for (Comment c : children) {
outResult.add(c);
- addChildren(parentMap, parentMap.get(c.getKey().get()), outResult);
+ addChildren(parentMap, parentMap.get(c.key.uuid), outResult);
}
}
}
- private Map<Integer, List<PatchLineComment>> index(
- List<PatchLineComment> in) {
- HashMap<Integer, List<PatchLineComment>> r = new HashMap<>();
- for (final PatchLineComment p : in) {
- List<PatchLineComment> l = r.get(p.getLine());
+ private Map<Integer, List<Comment>> index(List<Comment> in) {
+ HashMap<Integer, List<Comment>> r = new HashMap<>();
+ for (Comment p : in) {
+ List<Comment> l = r.get(p.lineNbr);
if (l == null) {
l = new ArrayList<>();
- r.put(p.getLine(), l);
+ r.put(p.lineNbr, l);
}
l.add(p);
}
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..290b9f9 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,8 +24,12 @@
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_ASSIGNEE = "editAssignee";
public static final String EDIT_TOPIC_NAME = "editTopicName";
public static final String FORGE_AUTHOR = "forgeAuthor";
public static final String FORGE_COMMITTER = "forgeCommitter";
@@ -36,8 +40,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 +48,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 +58,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());
@@ -72,11 +75,12 @@
NAMES_LC.add(VIEW_DRAFTS.toLowerCase());
NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
+ NAMES_LC.add(EDIT_ASSIGNEE.toLowerCase());
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 +251,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-elasticsearch/BUCK b/gerrit-elasticsearch/BUCK
new file mode 100644
index 0000000..a2641df
--- /dev/null
+++ b/gerrit-elasticsearch/BUCK
@@ -0,0 +1,51 @@
+java_library(
+ name = 'elasticsearch',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-extension-api:api',
+ '//gerrit-lucene:lucene', # only for LuceneAccountIndex
+ '//gerrit-reviewdb:client',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-index:index',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:protobuf',
+ '//lib/commons:codec',
+ '//lib/commons:lang',
+ '//lib/elasticsearch:elasticsearch',
+ '//lib/elasticsearch:jest',
+ '//lib/elasticsearch:jest-common',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/joda:joda-time',
+ '//lib/log:api',
+ '//lib/lucene:lucene-analyzers-common',
+ '//lib/lucene:lucene-core',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_test(
+ name = 'elasticsearch_tests',
+ labels = ['elastic'],
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':elasticsearch',
+ '//gerrit-extension-api:api',
+ '//gerrit-server:server',
+ '//gerrit-server:testutil',
+ '//gerrit-server:query_tests',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:junit',
+ '//lib:truth',
+ '//lib/elasticsearch:elasticsearch',
+ '//lib/guice:guice',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/jgit/org.eclipse.jgit.junit:junit',
+ ],
+)
diff --git a/gerrit-elasticsearch/BUILD b/gerrit-elasticsearch/BUILD
new file mode 100644
index 0000000..8ea9f9a
--- /dev/null
+++ b/gerrit-elasticsearch/BUILD
@@ -0,0 +1,53 @@
+java_library(
+ name = 'elasticsearch',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-extension-api:api',
+ '//gerrit-lucene:lucene', # only for LuceneAccountIndex
+ '//gerrit-reviewdb:client',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-index:index',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:protobuf',
+ '//lib/commons:codec',
+ '//lib/commons:lang',
+ '//lib/elasticsearch:elasticsearch',
+ '//lib/elasticsearch:jest',
+ '//lib/elasticsearch:jest-common',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/joda:joda-time',
+ '//lib/log:api',
+ '//lib/lucene:lucene-analyzers-common',
+ '//lib/lucene:lucene-core',
+ ],
+ visibility = ['//visibility:public'],
+)
+
+load('//tools/bzl:junit.bzl', 'junit_tests')
+
+junit_tests(
+ name = 'elasticsearch_tests',
+ tags = ['elastic'],
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':elasticsearch',
+ '//gerrit-extension-api:api',
+ '//gerrit-server:server',
+ '//gerrit-server:testutil',
+ '//gerrit-server:query_tests_code',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:junit',
+ '//lib:truth',
+ '//lib/elasticsearch:elasticsearch',
+ '//lib/guice:guice',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/jgit/org.eclipse.jgit.junit:junit',
+ ],
+)
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
new file mode 100644
index 0000000..a46edc7
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -0,0 +1,207 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.index.IndexUtils;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.Schema.Values;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+import io.searchbox.client.JestClientFactory;
+import io.searchbox.client.JestResult;
+import io.searchbox.client.config.HttpClientConfig;
+import io.searchbox.client.http.JestHttpClient;
+import io.searchbox.core.Bulk;
+import io.searchbox.core.Delete;
+import io.searchbox.indices.CreateIndex;
+import io.searchbox.indices.DeleteIndex;
+import io.searchbox.indices.IndicesExists;
+
+abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
+ private static final String DEFAULT_INDEX_NAME = "gerrit";
+
+ private final Schema<V> schema;
+ private final FillArgs fillArgs;
+ private final SitePaths sitePaths;
+
+ protected final boolean refresh;
+ protected final String indexName;
+ protected final JestHttpClient client;
+
+
+ @Inject
+ AbstractElasticIndex(@GerritServerConfig Config cfg,
+ FillArgs fillArgs,
+ SitePaths sitePaths,
+ @Assisted Schema<V> schema) {
+ this.fillArgs = fillArgs;
+ this.sitePaths = sitePaths;
+ this.schema = schema;
+ String protocol = getRequiredConfigOption(cfg, "protocol");
+ String hostname = getRequiredConfigOption(cfg, "hostname");
+ String port = getRequiredConfigOption(cfg, "port");
+
+ this.indexName =
+ firstNonNull(cfg.getString("index", null, "name"), DEFAULT_INDEX_NAME);
+
+ // By default Elasticsearch has a 1s delay before changes are available in
+ // the index. Setting refresh(true) on calls to the index makes the index
+ // refresh immediately.
+ //
+ // Discovery should be disabled during test mode to prevent spurious
+ // connection failures caused by the client starting up and being ready
+ // before the test node.
+ //
+ // This setting should only be set to true during testing, and is not
+ // documented.
+ this.refresh = cfg.getBoolean("index", "elasticsearch", "test", false);
+
+ String url = buildUrl(protocol, hostname, port);
+ JestClientFactory factory = new JestClientFactory();
+ factory.setHttpClientConfig(new HttpClientConfig
+ .Builder(url)
+ .multiThreaded(true)
+ .discoveryEnabled(!refresh)
+ .discoveryFrequency(1L, TimeUnit.MINUTES)
+ .build());
+ client = (JestHttpClient) factory.getObject();
+ }
+
+ @Override
+ public Schema<V> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ client.shutdownClient();
+ }
+
+ @Override
+ public void markReady(boolean ready) throws IOException {
+ IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
+ }
+
+ @Override
+ public void delete(K c) throws IOException {
+ Bulk bulk = addActions(new Bulk.Builder(), c).refresh(refresh).build();
+ JestResult result = client.execute(bulk);
+ if (!result.isSucceeded()) {
+ throw new IOException(String.format(
+ "Failed to delete change %s in index %s: %s", c, indexName,
+ result.getErrorMessage()));
+ }
+ }
+
+ @Override
+ public void deleteAll() throws IOException {
+ // Delete the index, if it exists.
+ JestResult result = client.execute(
+ new IndicesExists.Builder(indexName).build());
+ if (result.isSucceeded()) {
+ result = client.execute(
+ new DeleteIndex.Builder(indexName).build());
+ if (!result.isSucceeded()) {
+ throw new IOException(String.format(
+ "Failed to delete index %s: %s", indexName,
+ result.getErrorMessage()));
+ }
+ }
+
+ // Recreate the index.
+ result = client.execute(
+ new CreateIndex.Builder(indexName).settings(getMappings()).build());
+ if (!result.isSucceeded()) {
+ String error = String.format("Failed to create index %s: %s",
+ indexName, result.getErrorMessage());
+ throw new IOException(error);
+ }
+ }
+
+ protected abstract Bulk.Builder addActions(Bulk.Builder builder, K c);
+
+ protected abstract String getMappings();
+
+ protected abstract String getId(V v);
+
+ protected Delete delete(String type, K c) {
+ String id = c.toString();
+ return new Delete.Builder(id)
+ .index(indexName)
+ .type(type)
+ .build();
+ }
+
+ protected io.searchbox.core.Index insert(String type, V v) throws IOException {
+ String id = getId(v);
+ String doc = toDoc(v);
+ return new io.searchbox.core.Index.Builder(doc)
+ .index(indexName)
+ .type(type)
+ .id(id)
+ .build();
+ }
+
+ private String toDoc(V v) throws IOException {
+ XContentBuilder builder = jsonBuilder().startObject();
+ for (Values<V> values : schema.buildFields(v, fillArgs)) {
+ String name = values.getField().getName();
+ if (values.getField().isRepeatable()) {
+ builder.array(name, values.getValues());
+ } else {
+ Object element = Iterables.getOnlyElement(values.getValues(), "");
+ if (!(element instanceof String) || !((String) element).isEmpty()) {
+ builder.field(name, element);
+ }
+ }
+ }
+ return builder.endObject().string();
+ }
+
+ private String getRequiredConfigOption(Config cfg, String name) {
+ String option = cfg.getString("index", null, name);
+ checkState(!Strings.isNullOrEmpty(option), "index." + name + " must be supplied");
+ return option;
+ }
+
+ private String buildUrl(String protocol, String hostname, String port) {
+ try {
+ return new URL(protocol, hostname, Integer.parseInt(port), "").toString();
+ } catch (MalformedURLException | NumberFormatException e) {
+ throw new RuntimeException(
+ "Cannot build url to Elasticsearch from values: protocol=" + protocol
+ + " hostname=" + hostname + " port=" + port, e);
+ }
+ }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
new file mode 100644
index 0000000..723b5e3
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -0,0 +1,389 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
+import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
+import com.google.gerrit.index.IndexUtils;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.QueryOptions;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeField.ChangeProtoField;
+import com.google.gerrit.server.index.change.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexRewriter;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import io.searchbox.client.JestResult;
+import io.searchbox.core.Bulk;
+import io.searchbox.core.Bulk.Builder;
+import io.searchbox.core.Search;
+import io.searchbox.core.search.sort.Sort;
+import io.searchbox.core.search.sort.Sort.Sorting;
+
+/** Secondary index implementation using Elasticsearch. */
+class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
+ implements ChangeIndex {
+ private static final Logger log =
+ LoggerFactory.getLogger(ElasticChangeIndex.class);
+
+ static class ChangeMapping {
+ MappingProperties openChanges;
+ MappingProperties closedChanges;
+
+ ChangeMapping(Schema<ChangeData> schema) {
+ ElasticMapping.Builder mappingBuilder = new ElasticMapping.Builder();
+ for (FieldDef<?, ?> field : schema.getFields().values()) {
+ String name = field.getName();
+ FieldType<?> fieldType = field.getType();
+ if (fieldType == FieldType.EXACT) {
+ mappingBuilder.addExactField(name);
+ } else if (fieldType == FieldType.TIMESTAMP) {
+ mappingBuilder.addTimestamp(name);
+ } else if (fieldType == FieldType.INTEGER
+ || fieldType == FieldType.INTEGER_RANGE
+ || fieldType == FieldType.LONG) {
+ mappingBuilder.addNumber(name);
+ } else if (fieldType == FieldType.PREFIX
+ || fieldType == FieldType.FULL_TEXT
+ || fieldType == FieldType.STORED_ONLY) {
+ mappingBuilder.addString(name);
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported filed type " + fieldType.getName());
+ }
+ }
+ MappingProperties mapping = mappingBuilder.build();
+ openChanges = mapping;
+ closedChanges = mapping;
+ }
+ }
+
+ static final String OPEN_CHANGES = "open_changes";
+ static final String CLOSED_CHANGES = "closed_changes";
+
+ private final Gson gson;
+ private final ChangeMapping mapping;
+ private final Provider<ReviewDb> db;
+ private final ElasticQueryBuilder queryBuilder;
+ private final ChangeData.Factory changeDataFactory;
+
+ @AssistedInject
+ ElasticChangeIndex(
+ @GerritServerConfig Config cfg,
+ Provider<ReviewDb> db,
+ ChangeData.Factory changeDataFactory,
+ FillArgs fillArgs,
+ SitePaths sitePaths,
+ @Assisted Schema<ChangeData> schema) {
+ super(cfg, fillArgs, sitePaths, schema);
+ this.db = db;
+ this.changeDataFactory = changeDataFactory;
+ mapping = new ChangeMapping(schema);
+
+ this.queryBuilder = new ElasticQueryBuilder();
+ this.gson = new GsonBuilder()
+ .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
+ }
+
+ private static <T> List<T> decodeProtos(JsonObject doc, String fieldName,
+ ProtobufCodec<T> codec) {
+ return FluentIterable.from(doc.getAsJsonArray(fieldName))
+ .transform(i -> codec.decode(Base64.decodeBase64(i.toString())))
+ .toList();
+ }
+
+ @Override
+ public void replace(ChangeData cd) throws IOException {
+ String deleteIndex;
+ String insertIndex;
+
+ try {
+ if (cd.change().getStatus().isOpen()) {
+ insertIndex = OPEN_CHANGES;
+ deleteIndex = CLOSED_CHANGES;
+ } else {
+ insertIndex = CLOSED_CHANGES;
+ deleteIndex = OPEN_CHANGES;
+ }
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+
+ Bulk bulk = new Bulk.Builder()
+ .defaultIndex(indexName)
+ .defaultType("changes")
+ .addAction(insert(insertIndex, cd))
+ .addAction(delete(deleteIndex, cd.getId()))
+ .refresh(refresh)
+ .build();
+ JestResult result = client.execute(bulk);
+ if (!result.isSucceeded()) {
+ throw new IOException(String.format(
+ "Failed to replace change %s in index %s: %s", cd.getId(), indexName,
+ result.getErrorMessage()));
+ }
+ }
+
+ @Override
+ public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+ throws QueryParseException {
+ Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
+ List<String> indexes = Lists.newArrayListWithCapacity(2);
+ if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+ indexes.add(OPEN_CHANGES);
+ }
+ if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+ indexes.add(CLOSED_CHANGES);
+ }
+ return new QuerySource(indexes, p, opts);
+ }
+
+ @Override
+ protected Builder addActions(Builder builder, Id c) {
+ return builder
+ .addAction(delete(OPEN_CHANGES, c))
+ .addAction(delete(OPEN_CHANGES, c));
+ }
+
+ @Override
+ protected String getMappings() {
+ return gson.toJson(ImmutableMap.of("mappings", mapping));
+ }
+
+ @Override
+ protected String getId(ChangeData cd) {
+ return cd.getId().toString();
+ }
+
+ private class QuerySource implements ChangeDataSource {
+ private final Search search;
+ private final Set<String> fields;
+
+ QuerySource(List<String> types, Predicate<ChangeData> p,
+ QueryOptions opts) throws QueryParseException {
+ List<Sort> sorts = ImmutableList.of(
+ new Sort(ChangeField.UPDATED.getName(), Sorting.DESC),
+ new Sort(ChangeField.LEGACY_ID.getName(), Sorting.DESC));
+ for (Sort sort : sorts) {
+ sort.setIgnoreUnmapped();
+ }
+ QueryBuilder qb = queryBuilder.toQueryBuilder(p);
+ fields = IndexUtils.fields(opts);
+ SearchSourceBuilder searchSource = new SearchSourceBuilder()
+ .query(qb)
+ .from(opts.start())
+ .size(opts.limit())
+ .fields(Lists.newArrayList(fields));
+
+ search = new Search.Builder(searchSource.toString())
+ .addType(types)
+ .addSort(sorts)
+ .addIndex(indexName)
+ .build();
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10;
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ try {
+ List<ChangeData> results = Collections.emptyList();
+ JestResult result = client.execute(search);
+ if (result.isSucceeded()) {
+ JsonObject obj = result.getJsonObject().getAsJsonObject("hits");
+ if (obj.get("hits") != null) {
+ JsonArray json = obj.getAsJsonArray("hits");
+ results = Lists.newArrayListWithCapacity(json.size());
+ for (int i = 0; i < json.size(); i++) {
+ results.add(toChangeData(json.get(i)));
+ }
+ }
+ } else {
+ log.error(result.getErrorMessage());
+ }
+ final List<ChangeData> r = Collections.unmodifiableList(results);
+ return new ResultSet<ChangeData>() {
+ @Override
+ public Iterator<ChangeData> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<ChangeData> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return search.toString();
+ }
+
+ private ChangeData toChangeData(JsonElement json) {
+ JsonElement sourceElement = json.getAsJsonObject().get("_source");
+ if (sourceElement == null) {
+ sourceElement = json.getAsJsonObject().get("fields");
+ }
+ JsonObject source = sourceElement.getAsJsonObject();
+ JsonElement c = source.get(ChangeField.CHANGE.getName());
+
+ if (c == null) {
+ int id = source.get(ChangeField.LEGACY_ID.getName()).getAsInt();
+ String projectName =
+ source.get(ChangeField.PROJECT.getName()).getAsString();
+ if (projectName == null) {
+ return changeDataFactory.createOnlyWhenNoteDbDisabled(
+ db.get(), new Change.Id(id));
+ }
+ return changeDataFactory.create(
+ db.get(), new Project.NameKey(projectName), new Change.Id(id));
+ }
+
+ ChangeData cd = changeDataFactory.create(db.get(),
+ ChangeProtoField.CODEC.decode(Base64.decodeBase64(c.getAsString())));
+
+ // Patch sets.
+ cd.setPatchSets(decodeProtos(
+ source, ChangeField.PATCH_SET.getName(), PatchSetProtoField.CODEC));
+
+ // Approvals.
+ if (source.get(ChangeField.APPROVAL.getName()) != null) {
+ cd.setCurrentApprovals(decodeProtos(source,
+ ChangeField.APPROVAL.getName(), PatchSetApprovalProtoField.CODEC));
+ } else if (fields.contains(ChangeField.APPROVAL.getName())) {
+ cd.setCurrentApprovals(Collections.emptyList());
+ }
+
+ JsonElement addedElement = source.get(ChangeField.ADDED.getName());
+ JsonElement deletedElement = source.get(ChangeField.DELETED.getName());
+ if (addedElement != null && deletedElement != null) {
+ // Changed lines.
+ int added = addedElement.getAsInt();
+ int deleted = deletedElement.getAsInt();
+ if (added != 0 && deleted != 0) {
+ cd.setChangedLines(added, deleted);
+ }
+ }
+
+ // Mergeable.
+ JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
+ if (mergeableElement != null) {
+ String mergeable = mergeableElement.getAsString();
+ if ("1".equals(mergeable)) {
+ cd.setMergeable(true);
+ } else if ("0".equals(mergeable)) {
+ cd.setMergeable(false);
+ }
+ }
+
+ // Reviewed-by.
+ if (source.get(ChangeField.REVIEWEDBY.getName()) != null) {
+ JsonArray reviewedBy =
+ source.get(ChangeField.REVIEWEDBY.getName()).getAsJsonArray();
+ if (reviewedBy.size() > 0) {
+ Set<Account.Id> accounts =
+ Sets.newHashSetWithExpectedSize(reviewedBy.size());
+ for (int i = 0; i < reviewedBy.size() ; i++) {
+ int aId = reviewedBy.get(i).getAsInt();
+ if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
+ break;
+ }
+ accounts.add(new Account.Id(aId));
+ }
+ cd.setReviewedBy(accounts);
+ }
+ } else if (fields.contains(ChangeField.REVIEWEDBY.getName())) {
+ cd.setReviewedBy(Collections.emptySet());
+ }
+
+ if (source.get(ChangeField.REVIEWER.getName()) != null) {
+ cd.setReviewers(
+ ChangeField.parseReviewerFieldValues(FluentIterable
+ .from(
+ source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.REVIEWER.getName())) {
+ cd.setReviewers(ReviewerSet.empty());
+ }
+
+ return cd;
+ }
+ }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
new file mode 100644
index 0000000..e108dca
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.index.SingleVersionModule;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneAccountIndex;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Map;
+
+public class ElasticIndexModule extends LifecycleModule {
+ private final int threads;
+ private final Map<String, Integer> singleVersions;
+
+ public static ElasticIndexModule singleVersionWithExplicitVersions(
+ Map<String, Integer> versions, int threads) {
+ return new ElasticIndexModule(versions, threads);
+ }
+
+ public static ElasticIndexModule latestVersionWithOnlineUpgrade() {
+ return new ElasticIndexModule(null, 0);
+ }
+
+ private ElasticIndexModule(Map<String, Integer> singleVersions, int threads) {
+ this.singleVersions = singleVersions;
+ this.threads = threads;
+ }
+
+ @Override
+ protected void configure() {
+ install(
+ new FactoryModuleBuilder()
+ .implement(ChangeIndex.class, ElasticChangeIndex.class)
+ .build(ChangeIndex.Factory.class));
+ install(
+ new FactoryModuleBuilder()
+ // until we implement Elasticsearch index for accounts we need to
+ // use Lucene to make all tests green and Gerrit server to work
+ .implement(AccountIndex.class, LuceneAccountIndex.class)
+ .build(AccountIndex.Factory.class));
+
+ install(new IndexModule(threads));
+ install(new SingleVersionModule(singleVersions));
+ }
+
+ @Provides
+ @Singleton
+ IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+ return IndexConfig.fromConfig(cfg);
+ }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
new file mode 100644
index 0000000..e3f7e96
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -0,0 +1,79 @@
+// 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.elasticsearch;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+class ElasticMapping {
+ static class Builder {
+ private final ImmutableMap.Builder<String, FieldProperties> fields =
+ new ImmutableMap.Builder<>();
+
+ MappingProperties build() {
+ MappingProperties properties = new MappingProperties();
+ properties.properties = fields.build();
+ return properties;
+ }
+
+ Builder addExactField(String name) {
+ FieldProperties key = new FieldProperties("string");
+ key.index = "not_analyzed";
+ FieldProperties properties = new FieldProperties("string");
+ properties.fields = ImmutableMap.of("key", key);
+ fields.put(name, properties);
+ return this;
+ }
+
+ Builder addTimestamp(String name) {
+ FieldProperties properties = new FieldProperties("date");
+ properties.type = "date";
+ properties.format = "dateOptionalTime";
+ fields.put(name, properties);
+ return this;
+ }
+
+ Builder addNumber(String name) {
+ fields.put(name, new FieldProperties("long"));
+ return this;
+ }
+
+ Builder addString(String name) {
+ fields.put(name, new FieldProperties("string"));
+ return this;
+ }
+
+ Builder add(String name, String type) {
+ fields.put(name, new FieldProperties(type));
+ return this;
+ }
+ }
+
+ static class MappingProperties {
+ Map<String, FieldProperties> properties;
+ }
+
+ static class FieldProperties {
+ String type;
+ String index;
+ String format;
+ Map<String, FieldProperties> fields;
+
+ FieldProperties(String type) {
+ this.type = type;
+ }
+ }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
new file mode 100644
index 0000000..51b14a4
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -0,0 +1,181 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.IntegerRangePredicate;
+import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.AfterPredicate;
+
+import org.apache.lucene.search.BooleanQuery;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.joda.time.DateTime;
+
+public class ElasticQueryBuilder {
+
+ protected <T> QueryBuilder toQueryBuilder(Predicate<T> p)
+ throws QueryParseException {
+ if (p instanceof AndPredicate) {
+ return and(p);
+ } else if (p instanceof OrPredicate) {
+ return or(p);
+ } else if (p instanceof NotPredicate) {
+ return not(p);
+ } else if (p instanceof IndexPredicate) {
+ return fieldQuery((IndexPredicate<T>) p);
+ } else {
+ throw new QueryParseException("cannot create query for index: " + p);
+ }
+ }
+
+ private <T> BoolQueryBuilder and(Predicate<T> p)
+ throws QueryParseException {
+ try {
+ BoolQueryBuilder b = QueryBuilders.boolQuery();
+ for (Predicate<T> c : p.getChildren()) {
+ b.must(toQueryBuilder(c));
+ }
+ return b;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
+ }
+ }
+
+ private <T> BoolQueryBuilder or(Predicate<T> p)
+ throws QueryParseException {
+ try {
+ BoolQueryBuilder q = QueryBuilders.boolQuery();
+ for (Predicate<T> c : p.getChildren()) {
+ q.should(toQueryBuilder(c));
+ }
+ return q;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
+ }
+ }
+
+ private <T> QueryBuilder not(Predicate<T> p)
+ throws QueryParseException {
+ Predicate<T> n = p.getChild(0);
+ if (n instanceof TimestampRangePredicate) {
+ return notTimestamp((TimestampRangePredicate<T>) n);
+ }
+
+ // Lucene does not support negation, start with all and subtract.
+ BoolQueryBuilder q = QueryBuilders.boolQuery();
+ q.must(QueryBuilders.matchAllQuery());
+ q.mustNot(toQueryBuilder(n));
+ return q;
+ }
+
+ private <T> QueryBuilder fieldQuery(IndexPredicate<T> p)
+ throws QueryParseException {
+ FieldType<?> type = p.getType();
+ FieldDef<?,?> field = p.getField();
+ String name = field.getName();
+ String value = p.getValue();
+
+ if (type == FieldType.INTEGER) {
+ // QueryBuilder encodes integer fields as prefix coded bits,
+ // which elasticsearch's queryString can't handle.
+ // Create integer terms with string representations instead.
+ return QueryBuilders.termQuery(name, value);
+ } else if (type == FieldType.INTEGER_RANGE) {
+ return intRangeQuery(p);
+ } else if (type == FieldType.TIMESTAMP) {
+ return timestampQuery(p);
+ } else if (type == FieldType.EXACT) {
+ return exactQuery(p);
+ } else if (type == FieldType.PREFIX) {
+ return QueryBuilders.matchPhrasePrefixQuery(name, value);
+ } else if (type == FieldType.FULL_TEXT) {
+ return QueryBuilders.matchPhraseQuery(name, value);
+ } else {
+ throw FieldType.badFieldType(p.getType());
+ }
+ }
+
+ private <T> QueryBuilder intRangeQuery(IndexPredicate<T> p)
+ throws QueryParseException {
+ if (p instanceof IntegerRangePredicate) {
+ IntegerRangePredicate<T> r = (IntegerRangePredicate<T>) p;
+ int minimum = r.getMinimumValue();
+ int maximum = r.getMaximumValue();
+ if (minimum == maximum) {
+ // Just fall back to a standard integer query.
+ return QueryBuilders.termQuery(p.getField().getName(), minimum);
+ }
+ return QueryBuilders.rangeQuery(p.getField().getName())
+ .gte(minimum)
+ .lte(maximum);
+ }
+ throw new QueryParseException("not an integer range: " + p);
+ }
+
+ private <T> QueryBuilder notTimestamp(TimestampRangePredicate<T> r)
+ throws QueryParseException {
+ if (r.getMinTimestamp().getTime() == 0) {
+ return QueryBuilders.rangeQuery(r.getField().getName())
+ .gt(new DateTime(r.getMaxTimestamp().getTime()));
+ }
+ throw new QueryParseException("cannot negate: " + r);
+ }
+
+ private <T> QueryBuilder timestampQuery(IndexPredicate<T> p)
+ throws QueryParseException {
+ if (p instanceof TimestampRangePredicate) {
+ TimestampRangePredicate<T> r =
+ (TimestampRangePredicate<T>) p;
+ if (p instanceof AfterPredicate) {
+ return QueryBuilders.rangeQuery(r.getField().getName())
+ .gte(new DateTime(r.getMinTimestamp().getTime()));
+ }
+ return QueryBuilders.rangeQuery(r.getField().getName())
+ .gte(new DateTime(r.getMinTimestamp().getTime()))
+ .lte(new DateTime(r.getMaxTimestamp().getTime()));
+ }
+ throw new QueryParseException("not a timestamp: " + p);
+ }
+
+ private <T> QueryBuilder exactQuery(IndexPredicate<T> p){
+ String name = p.getField().getName();
+ String value = p.getValue();
+
+ if (value.isEmpty()) {
+ return new BoolQueryBuilder().mustNot(QueryBuilders.existsQuery(name));
+ } else if (p instanceof RegexPredicate) {
+ if (value.startsWith("^")) {
+ value = value.substring(1);
+ }
+ if (value.endsWith("$") && !value.endsWith("\\$")
+ && !value.endsWith("\\\\$")) {
+ value = value.substring(0, value.length() - 1);
+ }
+ return QueryBuilders.regexpQuery(name + ".key", value);
+ } else {
+ return QueryBuilders.termQuery(name + ".key", value);
+ }
+ }
+}
diff --git a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
new file mode 100644
index 0000000..e2e7585
--- /dev/null
+++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CLOSED_CHANGES;
+import static com.google.gerrit.elasticsearch.ElasticChangeIndex.OPEN_CHANGES;
+
+import com.google.common.base.Strings;
+import com.google.common.io.Files;
+import com.google.gerrit.elasticsearch.ElasticChangeIndex.ChangeMapping;
+import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.node.Node;
+import org.elasticsearch.node.NodeBuilder;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+public class ElasticQueryChangesTest extends AbstractQueryChangesTest {
+ private static final Gson gson = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ private static Node node;
+ private static String port;
+ private static File elasticDir;
+
+ static class NodeInfo {
+ String httpAddress;
+ }
+
+ static class Info {
+ Map<String, NodeInfo> nodes;
+ }
+
+ @BeforeClass
+ public static void startIndexService()
+ throws InterruptedException, ExecutionException {
+ if (node != null) {
+ // do not start Elasticsearch twice
+ return;
+ }
+ elasticDir = Files.createTempDir();
+ Path elasticDirPath = elasticDir.toPath();
+ Settings settings = Settings.settingsBuilder()
+ .put("cluster.name", "gerrit")
+ .put("node.name", "Gerrit Elasticsearch Test Node")
+ .put("node.local", true)
+ .put("discovery.zen.ping.multicast.enabled", false)
+ .put("index.store.fs.memory.enabled", true)
+ .put("index.gateway.type", "none")
+ .put("index.max_result_window", Integer.MAX_VALUE)
+ .put("gateway.type", "default")
+ .put("http.port", 0)
+ .put("discovery.zen.ping.unicast.hosts", "[\"localhost\"]")
+ .put("path.home", elasticDirPath.toAbsolutePath())
+ .put("path.data", elasticDirPath.resolve("data").toAbsolutePath())
+ .put("path.work", elasticDirPath.resolve("work").toAbsolutePath())
+ .put("path.logs", elasticDirPath.resolve("logs").toAbsolutePath())
+ .build();
+
+ // Start the node
+ node = NodeBuilder.nodeBuilder()
+ .settings(settings)
+ .node();
+
+ // Wait for it to be ready
+ node.client()
+ .admin()
+ .cluster()
+ .prepareHealth()
+ .setWaitForYellowStatus()
+ .execute()
+ .actionGet();
+
+ createIndexes();
+
+ assertThat(node.isClosed()).isFalse();
+ port = getHttpPort();
+ }
+
+ @After
+ public void cleanupIndex() {
+ node.client().admin().indices().prepareDelete("gerrit").execute();
+ createIndexes();
+ }
+
+ @AfterClass
+ public static void stopElasticsearchServer() {
+ if (node != null) {
+ node.close();
+ node = null;
+ }
+ if (elasticDir != null && elasticDir.delete()) {
+ elasticDir = null;
+ }
+ }
+
+ @Override
+ protected Injector createInjector() {
+ Config elasticsearchConfig = new Config(config);
+ InMemoryModule.setDefaults(elasticsearchConfig);
+ elasticsearchConfig.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
+ elasticsearchConfig.setString("index", null, "protocol", "http");
+ elasticsearchConfig.setString("index", null, "hostname", "localhost");
+ elasticsearchConfig.setString("index", null, "port", port);
+ elasticsearchConfig.setString("index", null, "name", "gerrit");
+ elasticsearchConfig.setBoolean("index", "elasticsearch", "test", true);
+ return Guice.createInjector(
+ new InMemoryModule(elasticsearchConfig, notesMigration));
+ }
+
+ private static void createIndexes() {
+ ChangeMapping openChangesMapping =
+ new ChangeMapping(ChangeSchemaDefinitions.INSTANCE.getLatest());
+ ChangeMapping closedChangesMapping =
+ new ChangeMapping(ChangeSchemaDefinitions.INSTANCE.getLatest());
+ openChangesMapping.closedChanges = null;
+ closedChangesMapping.openChanges = null;
+ node.client()
+ .admin()
+ .indices()
+ .prepareCreate("gerrit")
+ .addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping))
+ .addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping))
+ .execute()
+ .actionGet();
+ }
+
+ private static String getHttpPort()
+ throws InterruptedException, ExecutionException {
+ String nodes = node.client().admin().cluster()
+ .nodesInfo(new NodesInfoRequest("*")).get().toString();
+ Gson gson = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ Info info = gson.fromJson(nodes, Info.class);
+
+ checkState(info.nodes != null && info.nodes.size() == 1);
+ Iterator<NodeInfo> values = info.nodes.values().iterator();
+ String httpAddress = values.next().httpAddress;
+
+ checkState(
+ !Strings.isNullOrEmpty(httpAddress) && httpAddress.indexOf(':') > 0);
+ return httpAddress.substring(httpAddress.indexOf(':') + 1,
+ httpAddress.length());
+ }
+}
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 61cd406..a0e7495 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -67,14 +67,13 @@
'//lib:truth',
'//lib/guice:guice',
],
- source_under_test = [':api'],
)
java_doc(
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/BUILD b/gerrit-extension-api/BUILD
index 4a5cfe3..b66617a 100644
--- a/gerrit-extension-api/BUILD
+++ b/gerrit-extension-api/BUILD
@@ -44,3 +44,12 @@
],
visibility = ['//visibility:public'],
)
+
+load('//tools/bzl:javadoc.bzl', 'java_doc')
+
+java_doc(
+ name = 'extension-api-javadoc',
+ title = 'Gerrit Review Extension API Documentation',
+ libs = [':api'],
+ pkgs = ['com.google.gerrit.extensions'],
+)
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 2ef4acd..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.1</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/AddReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
index ca61b1d..4c535d4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AddReviewerInput.java
@@ -24,6 +24,7 @@
public String reviewer;
public Boolean confirmed;
public ReviewerState state;
+ public NotifyHandling notify;
public boolean confirmed() {
return (confirmed != null) ? confirmed : false;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java
new file mode 100644
index 0000000..61b5b85
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.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.api.changes;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class AssigneeInput {
+ @DefaultInput
+ public String assignee;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index f656c2d..d9cd562 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EditInfo;
@@ -97,6 +98,9 @@
List<ChangeInfo> submittedTogether() throws RestApiException;
SubmittedTogetherInfo submittedTogether(
EnumSet<SubmittedTogetherOption> options) throws RestApiException;
+ SubmittedTogetherInfo submittedTogether(
+ EnumSet<ListChangesOption> listOptions,
+ EnumSet<SubmittedTogetherOption> submitOptions) throws RestApiException;
/**
* Publishes a draft change.
@@ -139,6 +143,26 @@
Set<String> getHashtags() throws RestApiException;
/**
+ * Set the assignee of a change.
+ */
+ AccountInfo setAssignee(AssigneeInput input) throws RestApiException;
+
+ /**
+ * Get the assignee of a change.
+ */
+ AccountInfo getAssignee() throws RestApiException;
+
+ /**
+ * Get all past assignees.
+ */
+ List<AccountInfo> getPastAssignees() throws RestApiException;
+
+ /**
+ * Delete the assignee of a change.
+ */
+ AccountInfo deleteAssignee() throws RestApiException;
+
+ /**
* Get all published comments on a change.
*
* @return comments in a map keyed by path; comments have the {@code revision}
@@ -326,6 +350,27 @@
}
@Override
+ public AccountInfo setAssignee(AssigneeInput input)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public AccountInfo getAssignee() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public List<AccountInfo> getPastAssignees() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public AccountInfo deleteAssignee() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public Map<String, List<CommentInfo>> comments() throws RestApiException {
throw new NotImplementedException();
}
@@ -360,5 +405,12 @@
EnumSet<SubmittedTogetherOption> options) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public SubmittedTogetherInfo submittedTogether(
+ EnumSet<ListChangesOption> a,
+ EnumSet<SubmittedTogetherOption> b) 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/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index cbe16ed..472559b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -34,6 +34,7 @@
public Map<String, Short> labels;
public Map<String, List<CommentInput>> comments;
+ public Map<String, List<RobotCommentInput>> robotComments;
/**
* If true require all labels to be within the user's permitted ranges based
@@ -48,8 +49,11 @@
/**
* How to process draft comments already in the database that were not also
* described in this input request.
+ * <p>
+ * Defaults to DELETE, unless {@link #onBehalfOf} is set, in which case it
+ * defaults to KEEP and any other value is disallowed.
*/
- public DraftHandling drafts = DraftHandling.DELETE;
+ public DraftHandling drafts;
/** Who to send email notifications to after review is stored. */
public NotifyHandling notify = NotifyHandling.ALL;
@@ -94,6 +98,13 @@
public static class CommentInput extends Comment {
}
+ public static class RobotCommentInput extends CommentInput {
+ public String robotId;
+ public String robotRunId;
+ public String url;
+ public Map<String, String> properties;
+ }
+
public ReviewInput message(String msg) {
message = msg != null && !msg.isEmpty() ? msg : null;
return this;
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..29bf00b 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,8 +17,10 @@
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.RobotCommentInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -35,6 +37,7 @@
void submit() throws RestApiException;
void submit(SubmitInput in) throws RestApiException;
+ BinaryResult submitPreview() throws RestApiException;
void publish() throws RestApiException;
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
ChangeApi rebase() throws RestApiException;
@@ -52,15 +55,18 @@
MergeableInfo mergeableOtherBranches() throws RestApiException;
Map<String, List<CommentInfo>> comments() throws RestApiException;
+ Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException;
Map<String, List<CommentInfo>> drafts() throws RestApiException;
List<CommentInfo> commentsAsList() throws RestApiException;
List<CommentInfo> draftsAsList() throws RestApiException;
+ List<RobotCommentInfo> robotCommentsAsList() throws RestApiException;
DraftApi createDraft(DraftInput in) throws RestApiException;
DraftApi draft(String id) throws RestApiException;
CommentApi comment(String id) throws RestApiException;
+ RobotCommentApi robotComment(String id) throws RestApiException;
/**
* Returns patch of revision.
@@ -72,6 +78,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.
@@ -168,6 +201,12 @@
}
@Override
+ public Map<String, List<RobotCommentInfo>> robotComments()
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public List<CommentInfo> commentsAsList() throws RestApiException {
throw new NotImplementedException();
}
@@ -178,6 +217,12 @@
}
@Override
+ public List<RobotCommentInfo> robotCommentsAsList()
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public Map<String, List<CommentInfo>> drafts() throws RestApiException {
throw new NotImplementedException();
}
@@ -198,6 +243,11 @@
}
@Override
+ public RobotCommentApi robotComment(String id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public BinaryResult patch() throws RestApiException {
throw new NotImplementedException();
}
@@ -213,9 +263,19 @@
}
@Override
+ public BinaryResult submitPreview() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public SubmitType testSubmitType(TestSubmitRuleInput in)
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/changes/RobotCommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
new file mode 100644
index 0000000..e1ed107
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.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.api.changes;
+
+import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface RobotCommentApi {
+ RobotCommentInfo get() throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility
+ * when adding new methods to the interface.
+ **/
+ class NotImplemented implements RobotCommentApi {
+ @Override
+ public RobotCommentInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
index 8649e91f..e2cab4d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
@@ -16,15 +16,5 @@
/** Output options available for submitted_together requests. */
public enum SubmittedTogetherOption {
- NON_VISIBLE_CHANGES(0);
-
- private final int value;
-
- SubmittedTogetherOption(int v) {
- value = v;
- }
-
- public int getValue() {
- return value;
- }
+ NON_VISIBLE_CHANGES;
}
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/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index 7c8a3e8..78ffb82 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -59,6 +59,13 @@
}
}
+ public short side() {
+ if (side == Side.PARENT) {
+ return (short) (parent == null ? 0 : -parent.shortValue());
+ }
+ return 1;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
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..8d82e3a 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"),
@@ -113,6 +132,7 @@
public DownloadCommand downloadCommand;
public DateFormat dateFormat;
public TimeFormat timeFormat;
+ public Boolean highlightAssigneeInChangeTable;
public Boolean relativeDateInChangeTable;
public DiffView diffView;
public Boolean sizeBarInChangeTable;
@@ -123,6 +143,7 @@
public List<MenuItem> my;
public Map<String, String> urlAliases;
public EmailStrategy emailStrategy;
+ public DefaultBase defaultBaseForMerges;
public boolean isShowInfoInReviewCategory() {
return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
@@ -174,12 +195,14 @@
p.downloadCommand = DownloadCommand.CHECKOUT;
p.dateFormat = DateFormat.STD;
p.timeFormat = TimeFormat.HHMM_12;
+ p.highlightAssigneeInChangeTable = true;
p.relativeDateInChangeTable = false;
p.diffView = DiffView.SIDE_BY_SIDE;
p.sizeBarInChangeTable = true;
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/client/GitBasicAuthPolicy.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java
new file mode 100644
index 0000000..6450b0d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.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.client;
+
+public enum GitBasicAuthPolicy {
+ HTTP,
+ LDAP,
+ HTTP_LDAP
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 8b6c5e6..787725c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -69,7 +69,10 @@
PUSH_CERTIFICATES(18),
/** Include change's reviewer updates. */
- REVIEWER_UPDATES(19);
+ REVIEWER_UPDATES(19),
+
+ /** Set the submittable boolean. */
+ SUBMITTABLE(20);
private final int value;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/UiType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/UiType.java
new file mode 100644
index 0000000..0d9df39
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/UiType.java
@@ -0,0 +1,32 @@
+// 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 UiType {
+ NONE,
+ GWT,
+ POLYGERRIT;
+
+ public static UiType parse(String str) {
+ if (str != null) {
+ for (UiType type : UiType.values()) {
+ if (type.name().equalsIgnoreCase(str)) {
+ return type;
+ }
+ }
+ }
+ return null;
+ }
+}
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..0a066c6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
@@ -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.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
+
+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;
+ public GitBasicAuthPolicy gitBasicAuthPolicy;
+}
\ 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..963edcd
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -0,0 +1,26 @@
+// 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 showAssignee;
+ 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/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 003ab24..ba22094 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -28,6 +28,7 @@
public String project;
public String branch;
public String topic;
+ public AccountInfo assignee;
public Collection<String> hashtags;
public String changeId;
public String subject;
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..0c10ec7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -0,0 +1,30 @@
+// 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.UiType;
+
+import java.util.Set;
+
+public class GerritInfo {
+ public String allProjects;
+ public String allUsers;
+ public Boolean docSearch;
+ public String docUrl;
+ public Boolean editGpgKeys;
+ public String reportBugUrl;
+ public String reportBugText;
+ public Set<UiType> webUis;
+}
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..845f7cb
--- /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/RobotCommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
new file mode 100644
index 0000000..9028a1d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
@@ -0,0 +1,24 @@
+// 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 RobotCommentInfo extends CommentInfo {
+ public String robotId;
+ public String robotRunId;
+ public String url;
+ public Map<String, String> properties;
+}
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/common/WebLinkInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
index d9a34bf..4dd8f02 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.common;
+import com.google.gerrit.extensions.webui.WebLink.Target;
+
public class WebLinkInfo {
public String name;
public String imageUrl;
@@ -26,4 +28,8 @@
this.url = url;
this.target = target;
}
+
+ public WebLinkInfo(String name, String imageUrl, String url) {
+ this(name, imageUrl, url, Target.SELF);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java
new file mode 100644
index 0000000..022640c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/AssigneeChangedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+/** Notified whenever a change assignee is changed. */
+@ExtensionPoint
+public interface AssigneeChangedListener {
+ interface Event extends ChangeEvent {
+ @Nullable AccountInfo getOldAssignee();
+ }
+
+ void onAssigneeChanged(Event event);
+}
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-gpg/BUCK b/gerrit-gpg/BUCK
index 73d9f04..fe93bf8 100644
--- a/gerrit-gpg/BUCK
+++ b/gerrit-gpg/BUCK
@@ -52,6 +52,5 @@
'//lib/bouncycastle:bcprov',
'//lib/jgit/org.eclipse.jgit.junit:junit',
],
- source_under_test = [':gpg'],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index db6cb7a..3bbe6eb 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -19,10 +19,8 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
-import com.google.common.collect.Ordering;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -274,9 +272,7 @@
private static String missingUserIds(Set<String> allowedUserIds) {
StringBuilder sb = new StringBuilder("Key must contain a valid"
+ " certification for one of the following identities:\n");
- Iterator<String> sorted = FluentIterable.from(allowedUserIds)
- .toSortedList(Ordering.natural())
- .iterator();
+ Iterator<String> sorted = allowedUserIds.stream().sorted().iterator();
while (sorted.hasNext()) {
sb.append(" ").append(sorted.next());
if (sorted.hasNext()) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index 49657c6..ddee18d 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -19,7 +19,6 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
-import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
@@ -211,14 +210,8 @@
@VisibleForTesting
public static FluentIterable<AccountExternalId> getGpgExtIds(ReviewDb db,
Account.Id accountId) throws OrmException {
- return FluentIterable
- .from(db.accountExternalIds().byAccount(accountId))
- .filter(new Predicate<AccountExternalId>() {
- @Override
- public boolean apply(AccountExternalId in) {
- return in.isScheme(SCHEME_GPGKEY);
- }
- });
+ return FluentIterable.from(db.accountExternalIds().byAccount(accountId))
+ .filter(in -> in.isScheme(SCHEME_GPGKEY));
}
private Iterable<AccountExternalId> getGpgExtIds(AccountResource rsrc)
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 2deae3f..7c5c6ea 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -18,7 +18,6 @@
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -161,13 +160,8 @@
if (!newExtIds.isEmpty()) {
db.get().accountExternalIds().insert(newExtIds);
}
- db.get().accountExternalIds().deleteKeys(Iterables.transform(toRemove,
- new Function<Fingerprint, AccountExternalId.Key>() {
- @Override
- public AccountExternalId.Key apply(Fingerprint fp) {
- return toExtIdKey(fp.get());
- }
- }));
+ db.get().accountExternalIds().deleteKeys(
+ Iterables.transform(toRemove, fp -> toExtIdKey(fp.get())));
accountCache.evict(rsrc.getUser().getAccountId());
return toJson(newKeys, toRemove, store, rsrc.getUser());
}
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
deleted file mode 100644
index 728f276..0000000
--- a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright 2011 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.gwt.dev.codeserver;
-
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.TreeLogger.Type;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.dev.codeserver.CompileDir.PolicyFile;
-import com.google.gwt.dev.codeserver.Pages.ErrorPage;
-import com.google.gwt.dev.json.JsonObject;
-
-import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.server.HttpConnection;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.servlets.GzipFilter;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.servlet.DispatcherType;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * The web server for Super Dev Mode, also known as the code server. The URLs handled include:
- * <ul>
- * <li>HTML pages for the front page and module pages</li>
- * <li>JavaScript that implementing the bookmarklets</li>
- * <li>The web API for recompiling a GWT app</li>
- * <li>The output files and log files from the GWT compiler</li>
- * <li>Java source code (for source-level debugging)</li>
- * </ul>
- *
- * <p>EXPERIMENTAL. There is no authentication, encryption, or XSS protection, so this server is
- * only safe to run on localhost.</p>
- */
-// This file was copied from GWT project and adjusted to run against
-// Jetty 9.2.2. The original diff can be found here:
-// https://gwt-review.googlesource.com/#/c/7857/13/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
-public class WebServer {
-
- private static final Pattern SAFE_DIRECTORY =
- Pattern.compile("([a-zA-Z0-9_-]+\\.)*[a-zA-Z0-9_-]+"); // no extension needed
-
- private static final Pattern SAFE_FILENAME =
- Pattern.compile("([a-zA-Z0-9_-]+\\.)+[a-zA-Z0-9_-]+"); // an extension is required
-
- private static final Pattern SAFE_MODULE_PATH =
- Pattern.compile("/(" + SAFE_DIRECTORY + ")/$");
-
- static final Pattern SAFE_DIRECTORY_PATH =
- Pattern.compile("/(" + SAFE_DIRECTORY + "/)+$");
-
- /* visible for testing */
- static final Pattern SAFE_FILE_PATH =
- Pattern.compile("/(" + SAFE_DIRECTORY + "/)+" + SAFE_FILENAME + "$");
-
- static final Pattern STRONG_NAME = Pattern.compile("[\\dA-F]{32}");
-
- private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$");
-
- private static final MimeTypes MIME_TYPES = new MimeTypes();
-
- private static final String TIME_IN_THE_PAST = "Mon, 01 Jan 1990 00:00:00 GMT";
-
- private final SourceHandler handler;
- private final JsonExporter jsonExporter;
- private final OutboxTable outboxes;
- private final JobRunner runner;
- private final JobEventTable eventTable;
-
- private final String bindAddress;
- private final int port;
-
- private Server server;
-
- WebServer(SourceHandler handler, JsonExporter jsonExporter, OutboxTable outboxes,
- JobRunner runner, JobEventTable eventTable, String bindAddress, int port) {
- this.handler = handler;
- this.jsonExporter = jsonExporter;
- this.outboxes = outboxes;
- this.runner = runner;
- this.eventTable = eventTable;
- this.bindAddress = bindAddress;
- this.port = port;
- }
-
- @SuppressWarnings("serial")
- void start(final TreeLogger logger) throws UnableToCompleteException {
-
- Server newServer = new Server();
- ServerConnector connector = new ServerConnector(newServer);
- connector.setHost(bindAddress);
- connector.setPort(port);
- connector.setReuseAddress(false);
- connector.setSoLingerTime(0);
-
- newServer.addConnector(connector);
-
- ServletContextHandler newHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
- newHandler.setContextPath("/");
- newHandler.addServlet(new ServletHolder(new HttpServlet() {
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- handleRequest(request.getPathInfo(), request, response, logger);
- }
- }), "/*");
- newHandler.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
- newServer.setHandler(newHandler);
- try {
- newServer.start();
- } catch (Exception e) {
- logger.log(TreeLogger.ERROR, "cannot start web server", e);
- throw new UnableToCompleteException();
- }
- this.server = newServer;
- }
-
- public int getPort() {
- return port;
- }
-
- public void stop() throws Exception {
- server.stop();
- server = null;
- }
-
- /**
- * Returns the location of the compiler output. (Changes after every recompile.)
- * @param outputModuleName the module name that the GWT compiler used in its output.
- */
- public File getCurrentWarDir(String outputModuleName) {
- return outboxes.findByOutputModuleName(outputModuleName).getWarDir();
- }
-
- private void handleRequest(String target, HttpServletRequest request,
- HttpServletResponse response, TreeLogger parentLogger)
- throws IOException {
-
- if (request.getMethod().equalsIgnoreCase("get")) {
-
- TreeLogger logger = parentLogger.branch(Type.TRACE, "GET " + target);
-
- Response page = doGet(target, request, logger);
- if (page == null) {
- logger.log(Type.WARN, "not handled: " + target);
- return;
- }
-
- setHandled(request);
- if (!target.endsWith(".cache.js")) {
- // Make sure IE9 doesn't cache any pages.
- // (Nearly all pages may change on server restart.)
- response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
- response.setHeader("Pragma", "no-cache");
- response.setHeader("Expires", TIME_IN_THE_PAST);
- response.setDateHeader("Date", new Date().getTime());
- }
- page.send(request, response, logger);
- }
- }
-
- /**
- * Returns the page that should be sent in response to a GET request, or null for no response.
- */
- private Response doGet(String target, HttpServletRequest request, TreeLogger logger)
- throws IOException {
-
- if (target.equals("/")) {
- JsonObject json = jsonExporter.exportFrontPageVars();
- return Pages.newHtmlPage("config", json, "frontpage.html");
- }
-
- if (target.equals("/dev_mode_on.js")) {
- JsonObject json = jsonExporter.exportDevModeOnVars();
- return Responses.newJavascriptResponse("__gwt_codeserver_config", json,
- "dev_mode_on.js");
- }
-
- // Recompile on request from the bookmarklet.
- // This is a GET because a bookmarklet can call it from a different origin (JSONP).
- if (target.startsWith("/recompile/")) {
- String moduleName = target.substring("/recompile/".length());
- Outbox box = outboxes.findByOutputModuleName(moduleName);
- if (box == null) {
- return new ErrorPage("No such module: " + moduleName);
- }
-
- // We are passing properties from an unauthenticated GET request directly to the compiler.
- // This should be safe, but only because these are binding properties. For each binding
- // property, you can only choose from a set of predefined values. So all an attacker can do is
- // cause a spurious recompile, resulting in an unexpected permutation being loaded later.
- //
- // It would be unsafe to allow a configuration property to be changed.
- Job job = box.makeJob(getBindingProperties(request), logger);
- runner.submit(job);
- Job.Result result = job.waitForResult();
- JsonObject json = jsonExporter.exportRecompileResponse(result);
- return Responses.newJsonResponse(json);
- }
-
- if (target.startsWith("/log/")) {
- String moduleName = target.substring("/log/".length());
- Outbox box = outboxes.findByOutputModuleName(moduleName);
- if (box == null) {
- return new ErrorPage("No such module: " + moduleName);
- } else if (box.containsStubCompile()) {
- return new ErrorPage("This module hasn't been compiled yet.");
- } else {
- return makeLogPage(box);
- }
- }
-
- if (target.equals("/favicon.ico")) {
- InputStream faviconStream = getClass().getResourceAsStream("favicon.ico");
- if (faviconStream == null) {
- return new ErrorPage("icon not found");
- }
- // IE8 will not load the favicon in an img tag with the default MIME type,
- // so use "image/x-icon" instead.
- return Responses.newBinaryStreamResponse("image/x-icon", faviconStream);
- }
-
- if (target.equals("/policies/")) {
- return makePolicyIndexPage();
- }
-
- if (target.equals("/progress")) {
- // TODO: return a list of progress objects here, one for each job.
- JobEvent event = eventTable.getCompilingJobEvent();
-
- JsonObject json;
- if (event == null) {
- json = new JsonObject();
- json.put("status", "idle");
- } else {
- json = jsonExporter.exportProgressResponse(event);
- }
- return Responses.newJsonResponse(json);
- }
-
- Matcher matcher = SAFE_MODULE_PATH.matcher(target);
- if (matcher.matches()) {
- return makeModulePage(matcher.group(1));
- }
-
- matcher = SAFE_DIRECTORY_PATH.matcher(target);
- if (matcher.matches() && SourceHandler.isSourceMapRequest(target)) {
- return handler.handle(target, request, logger);
- }
-
- matcher = SAFE_FILE_PATH.matcher(target);
- if (matcher.matches()) {
- if (SourceHandler.isSourceMapRequest(target)) {
- return handler.handle(target, request, logger);
- }
- if (target.startsWith("/policies/")) {
- return makePolicyFilePage(target);
- }
- return makeCompilerOutputPage(target);
- }
-
- logger.log(TreeLogger.WARN, "ignored get request: " + target);
- return null; // not handled
- }
-
- /**
- * Returns a file that the compiler wrote to its war directory.
- */
- private Response makeCompilerOutputPage(String target) {
-
- int secondSlash = target.indexOf('/', 1);
- String moduleName = target.substring(1, secondSlash);
- Outbox box = outboxes.findByOutputModuleName(moduleName);
- if (box == null) {
- return new ErrorPage("No such module: " + moduleName);
- }
-
- final String contentEncoding;
- File file = box.getOutputFile(target);
- if (!file.isFile()) {
- // perhaps it's compressed
- file = box.getOutputFile(target + ".gz");
- if (!file.isFile()) {
- return new ErrorPage("not found: " + file.toString());
- }
- contentEncoding = "gzip";
- } else {
- contentEncoding = null;
- }
-
- final String sourceMapUrl;
- Matcher match = CACHE_JS_FILE.matcher(target);
- if (match.matches()) {
- String strongName = match.group(1);
- String template = SourceHandler.sourceMapLocationTemplate(moduleName);
- sourceMapUrl = template.replace("__HASH__", strongName);
- } else {
- sourceMapUrl = null;
- }
-
- String mimeType = guessMimeType(target);
- final Response barePage = Responses.newFileResponse(mimeType, file);
-
- // Wrap the response to send the extra headers.
- return new Response() {
- @Override
- public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
- throws IOException {
- // TODO: why do we need this? Looks like Ray added it a long time ago.
- response.setHeader("Access-Control-Allow-Origin", "*");
-
- if (sourceMapUrl != null) {
- response.setHeader("X-SourceMap", sourceMapUrl);
- response.setHeader("SourceMap", sourceMapUrl);
- }
-
- if (contentEncoding != null) {
- if (!request.getHeader("Accept-Encoding").contains("gzip")) {
- response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
- logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing");
- return;
- }
- response.setHeader("Content-Encoding", "gzip");
- }
-
- barePage.send(request, response, logger);
- }
- };
- }
-
- private Response makeModulePage(String moduleName) {
- Outbox box = outboxes.findByOutputModuleName(moduleName);
- if (box == null) {
- return new ErrorPage("No such module: " + moduleName);
- }
-
- JsonObject json = jsonExporter.exportModulePageVars(box);
- return Pages.newHtmlPage("config", json, "modulepage.html");
- }
-
- private Response makePolicyIndexPage() {
-
- return new Response() {
-
- @Override
- public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
- throws IOException {
- response.setContentType("text/html");
-
- HtmlWriter out = new HtmlWriter(response.getWriter());
-
- out.startTag("html").nl();
- out.startTag("head").nl();
- out.startTag("title").text("Policy Files").endTag("title").nl();
- out.endTag("head");
- out.startTag("body");
-
- out.startTag("h1").text("Policy Files").endTag("h1").nl();
-
- for (Outbox box : outboxes.getOutboxes()) {
- List<PolicyFile> policies = box.readRpcPolicyManifest();
- if (!policies.isEmpty()) {
- out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
-
- out.startTag("table").nl();
- for (PolicyFile policy : policies) {
-
- out.startTag("tr");
-
- out.startTag("td");
-
- out.startTag("a", "href=", policy.getServiceSourceUrl());
- out.text(policy.getServiceName());
- out.endTag("a");
-
- out.endTag("td");
-
- out.startTag("td");
-
- out.startTag("a", "href=", policy.getUrl());
- out.text(policy.getName());
- out.endTag("a");
-
- out.endTag("td");
-
- out.endTag("tr").nl();
- }
- out.endTag("table").nl();
- }
- }
-
- out.endTag("body").nl();
- out.endTag("html").nl();
- }
- };
- }
-
- private Response makePolicyFilePage(String target) {
-
- int secondSlash = target.indexOf('/', 1);
- if (secondSlash < 1) {
- return new ErrorPage("invalid URL for policy file: " + target);
- }
-
- String rest = target.substring(secondSlash + 1);
- if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
- return new ErrorPage("invalid name for policy file: " + rest);
- }
-
- File fileToSend = outboxes.findPolicyFile(rest);
- if (fileToSend == null) {
- return new ErrorPage("Policy file not found: " + rest);
- }
-
- return Responses.newFileResponse("text/plain", fileToSend);
- }
-
- /**
- * Sends the log file as html with errors highlighted in red.
- */
- private Response makeLogPage(final Outbox box) {
- final File file = box.getCompileLog();
- if (!file.isFile()) {
- return new ErrorPage("log file not found");
- }
-
- return new Response() {
-
- @Override
- public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
- throws IOException {
- BufferedReader reader = new BufferedReader(new FileReader(file));
-
- response.setStatus(HttpServletResponse.SC_OK);
- response.setContentType("text/html");
- response.setHeader("Content-Style-Type", "text/css");
-
- HtmlWriter out = new HtmlWriter(response.getWriter());
- out.startTag("html").nl();
- out.startTag("head").nl();
- out.startTag("title").text(box.getOutputModuleName() + " compile log").endTag("title").nl();
- out.startTag("style").nl();
- out.text(".error { color: red; font-weight: bold; }").nl();
- out.endTag("style").nl();
- out.endTag("head").nl();
- out.startTag("body").nl();
- sendLogAsHtml(reader, out);
- out.endTag("body").nl();
- out.endTag("html").nl();
- }
- };
- }
-
- private static final Pattern ERROR_PATTERN = Pattern.compile("\\[ERROR\\]");
-
- /**
- * Copies in to out line by line, escaping each line for html characters and highlighting
- * error lines. Closes <code>in</code> when done.
- */
- private static void sendLogAsHtml(BufferedReader in, HtmlWriter out) throws IOException {
- try {
- out.startTag("pre").nl();
- String line = in.readLine();
- while (line != null) {
- Matcher m = ERROR_PATTERN.matcher(line);
- boolean error = m.find();
- if (error) {
- out.startTag("span", "class=", "error");
- }
- out.text(line);
- if (error) {
- out.endTag("span");
- }
- out.nl(); // the readLine doesn't include the newline.
- line = in.readLine();
- }
- out.endTag("pre").nl();
- } finally {
- in.close();
- }
- }
-
- /* visible for testing */
- static String guessMimeType(String filename) {
- String mimeType = MIME_TYPES.getMimeByExtension(filename);
- return mimeType != null ? mimeType : "";
- }
-
- /**
- * Returns the binding properties from the web page where dev mode is being used. (As passed in
- * by dev_mode_on.js in a JSONP request to "/recompile".)
- */
- private Map<String, String> getBindingProperties(HttpServletRequest request) {
- Map<String, String> result = new HashMap<>();
- for (Object key : request.getParameterMap().keySet()) {
- String propName = (String) key;
- if (!propName.equals("_callback")) {
- result.put(propName, request.getParameter(propName));
- }
- }
- return result;
- }
-
- private static void setHandled(HttpServletRequest request) {
- Request baseRequest = (request instanceof Request) ? (Request) request :
- HttpConnection.getCurrentConnection().getHttpChannel().getRequest();
- baseRequest.setHandled(true);
- }
-}
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index 79a97a9..23db13f 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -80,7 +80,6 @@
'//lib/gwt:user',
'//lib/gwt:dev',
],
- source_under_test = [':SafeHtml'],
)
gwt_module(
diff --git a/gerrit-gwtexpui/BUILD b/gerrit-gwtexpui/BUILD
index d3b03ef..d74fc8b 100644
--- a/gerrit-gwtexpui/BUILD
+++ b/gerrit-gwtexpui/BUILD
@@ -19,6 +19,10 @@
'//lib/gwt:user',
],
visibility = ['//visibility:public'],
+ data = [
+ '//lib:LICENSE-clippy',
+ '//lib:LICENSE-silk_icons',
+ ],
)
java_library(
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index ef78d98..d4d97a6 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -66,7 +66,6 @@
'//lib/gwt:user',
'//lib/jgit/org.eclipse.jgit:jgit',
],
- source_under_test = [':client'],
vm_args = ['-Xmx512m'],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-gwtui-common/BUILD b/gerrit-gwtui-common/BUILD
new file mode 100644
index 0000000..4bd2dfd
--- /dev/null
+++ b/gerrit-gwtui-common/BUILD
@@ -0,0 +1,73 @@
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+load('//tools/bzl:java.bzl', 'java_library2')
+load('//tools/bzl:junit.bzl', 'junit_tests')
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+
+EXPORTED_DEPS = [
+ '//gerrit-common:client',
+ '//gerrit-gwtexpui:Clippy',
+ '//gerrit-gwtexpui:GlobalKey',
+ '//gerrit-gwtexpui:Progress',
+ '//gerrit-gwtexpui:SafeHtml',
+ '//gerrit-gwtexpui:UserAgent',
+]
+DEPS = ['//lib/gwt:user']
+SRC = 'src/main/java/com/google/gerrit/'
+DIFFY = glob(['src/main/resources/com/google/gerrit/client/diffy*.png'])
+
+gwt_module(
+ name = 'client',
+ srcs = glob(['src/main/**/*.java']),
+ gwt_xml = SRC + 'GerritGwtUICommon.gwt.xml',
+ resources = glob(
+ ['src/main/**/*'],
+ exclude = [SRC + 'client/**/*.java'] +
+ [SRC + 'GerritGwtUICommon.gwt.xml']
+ ),
+ exported_deps = EXPORTED_DEPS,
+ deps = DEPS,
+ visibility = ['//visibility:public'],
+)
+
+java_library2(
+ name = 'client-lib',
+ srcs = glob(['src/main/**/*.java']),
+ resources = glob(['src/main/**/*']),
+ exported_deps = EXPORTED_DEPS,
+ deps = DEPS,
+ visibility = ['//visibility:public'],
+)
+
+java_import(
+ name = 'diffy_logo',
+ jars = [':diffy_image_files_ln'],
+ visibility = ['//visibility:public'],
+ data = [
+ '//lib:LICENSE-diffy',
+ '//lib:LICENSE-CC-BY3.0-unported',
+ ],
+)
+
+genrule2(
+ name = 'diffy_image_files_ln',
+ srcs = [':diffy_image_files'],
+ cmd = 'ln -s $$ROOT/$(location :diffy_image_files) $@',
+ out = 'diffy_images.jar',
+)
+
+java_library(
+ name = 'diffy_image_files',
+ resources = DIFFY,
+)
+
+junit_tests(
+ name = 'client_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':client',
+ '//lib:junit',
+ '//lib/gwt:dev',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ ],
+ visibility = ['//visibility:public'],
+)
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
index 0a339a1..eb10718 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
@@ -22,6 +22,7 @@
CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK,
+ CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS,
/* MyPasswordScreen */
PASSWORD_SCREEN_BOTTOM,
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
index 95751fa..c8e23e5 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -30,6 +30,9 @@
@Source("user_add.png")
ImageResource addUser();
+ @Source("user_edit.png")
+ ImageResource editUser();
+
// derived from resultset_next.png
@Source("resultset_down_gray.png")
ImageResource arrowDown();
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..ca3912c 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,11 @@
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.gerrit.extensions.client.GitBasicAuthPolicy;
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,34 +53,46 @@
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;
}
public final boolean isHttpPasswordSettingsEnabled() {
- if (isLdap() && isGitBasicAuth()) {
+ if (isGitBasicAuth() && gitBasicAuthPolicy() == GitBasicAuthPolicy.LDAP) {
return false;
}
return true;
}
+ public final GitBasicAuthPolicy gitBasicAuthPolicy() {
+ return GitBasicAuthPolicy.valueOf(gitBasicAuthPolicyRaw());
+ }
+
public final native boolean useContributorAgreements()
/*-{ return this.use_contributor_agreements || false; }-*/;
public final native String loginUrl() /*-{ return this.login_url; }-*/;
@@ -90,9 +103,13 @@
public final native String editFullNameUrl() /*-{ return this.edit_full_name_url; }-*/;
public final native String httpPasswordUrl() /*-{ return this.http_password_url; }-*/;
public final native boolean isGitBasicAuth() /*-{ return this.is_git_basic_auth || false; }-*/;
+ private native String gitBasicAuthPolicyRaw()
+ /*-{ return this.git_basic_auth_policy; }-*/;
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/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 9eea93e..ff060b8 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -115,6 +115,7 @@
private native String statusRaw() /*-{ return this.status; }-*/;
public final native String subject() /*-{ return this.subject; }-*/;
public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+ public final native AccountInfo assignee() /*-{ return this.assignee; }-*/;
private native String createdRaw() /*-{ return this.created; }-*/;
private native String updatedRaw() /*-{ return this.updated; }-*/;
private native String submittedRaw() /*-{ return this.submitted; }-*/;
@@ -414,6 +415,10 @@
return PatchSet.Id.toId(_number());
}
+ public final boolean isMerge() {
+ return commit().parents().length() > 1;
+ }
+
protected RevisionInfo () {
}
}
@@ -458,6 +463,7 @@
public final native AccountInfo author() /*-{ return this.author; }-*/;
public final native String message() /*-{ return this.message; }-*/;
public final native int _revisionNumber() /*-{ return this._revision_number || 0; }-*/;
+ public final native String tag() /*-{ return this.tag; }-*/;
private native String dateRaw() /*-{ return this.date; }-*/;
public final Timestamp date() {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
index 9b290a5..b21126d 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/FileInfo.java
@@ -56,6 +56,12 @@
} else if (Patch.COMMIT_MSG.equals(b.path())) {
return 1;
}
+ if (Patch.MERGE_LIST.equals(a.path())) {
+ return -1;
+ } else if (Patch.MERGE_LIST.equals(b.path())) {
+ return 1;
+ }
+
// Look at file suffixes to check if it makes sense to use a different order
int s1 = a.path().lastIndexOf('.');
int s2 = b.path().lastIndexOf('.');
@@ -76,9 +82,15 @@
}
public static String getFileName(String path) {
- String fileName = Patch.COMMIT_MSG.equals(path)
- ? "Commit Message"
- : path;
+ String fileName;
+ if (Patch.COMMIT_MSG.equals(path)) {
+ fileName = "Commit Message";
+ } else if (Patch.MERGE_LIST.equals(path)) {
+ fileName = "Merge List";
+ } else {
+ fileName = path;
+ }
+
int s = fileName.lastIndexOf('/');
return s >= 0 ? fileName.substring(s + 1) : fileName;
}
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..9c751ed0 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;
@@ -47,6 +48,7 @@
p.downloadCommand(d.downloadCommand);
p.dateFormat(d.getDateFormat());
p.timeFormat(d.getTimeFormat());
+ p.highlightAssigneeInChangeTable(d.highlightAssigneeInChangeTable);
p.relativeDateInChangeTable(d.relativeDateInChangeTable);
p.sizeBarInChangeTable(d.sizeBarInChangeTable);
p.legacycidInChangeTable(d.legacycidInChangeTable);
@@ -55,6 +57,7 @@
p.reviewCategoryStrategy(d.getReviewCategoryStrategy());
p.diffView(d.getDiffView());
p.emailStrategy(d.emailStrategy);
+ p.defaultBaseForMerges(d.defaultBaseForMerges);
return p;
}
@@ -98,6 +101,9 @@
private native String timeFormatRaw()
/*-{ return this.time_format }-*/;
+ public final native boolean highlightAssigneeInChangeTable()
+ /*-{ return this.highlight_assignee_in_change_table || false }-*/;
+
public final native boolean relativeDateInChangeTable()
/*-{ return this.relative_date_in_change_table || false }-*/;
@@ -135,6 +141,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; }-*/;
@@ -168,6 +182,9 @@
private native void timeFormatRaw(String f)
/*-{ this.time_format = f }-*/;
+ public final native void highlightAssigneeInChangeTable(boolean d)
+ /*-{ this.highlight_assignee_in_change_table = d }-*/;
+
public final native void relativeDateInChangeTable(boolean d)
/*-{ this.relative_date_in_change_table = d }-*/;
@@ -201,6 +218,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-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
index 750412d..a111896 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GerritInfo.java
@@ -14,8 +14,13 @@
package com.google.gerrit.client.info;
+import com.google.gerrit.extensions.client.UiType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+import java.util.ArrayList;
+import java.util.List;
public class GerritInfo extends JavaScriptObject {
public final Project.NameKey allProjectsNameKey() {
@@ -42,6 +47,19 @@
public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
public final native String reportBugText() /*-{ return this.report_bug_text; }-*/;
+ private native JsArrayString _webUis() /*-{ return this.web_uis; }-*/;
+ public final List<UiType> webUis() {
+ JsArrayString webUis = _webUis();
+ List<UiType> result = new ArrayList<>(webUis.length());
+ for (int i = 0; i < webUis.length(); i++) {
+ UiType t = UiType.parse(webUis.get(i));
+ if (t != null) {
+ result.add(t);
+ }
+ }
+ return result;
+ }
+
protected GerritInfo() {
}
}
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-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
index 112c4db..be8d076 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
@@ -59,6 +59,7 @@
public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
public final native String replyLabel() /*-{ return this.reply_label; }-*/;
public final native String replyTooltip() /*-{ return this.reply_tooltip; }-*/;
+ public final native boolean showAssignee() /*-{ return this.show_assignee || false; }-*/;
public final native int updateDelay() /*-{ return this.update_delay || 0; }-*/;
public final native boolean isSubmitWholeTopicEnabled() /*-{
return this.submit_whole_topic; }-*/;
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png
new file mode 100644
index 0000000..c1974cd
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/user_edit.png
Binary files differ
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index 1e39831..63d52b0 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -61,7 +61,6 @@
'//lib/gwt:dev',
'//lib/gwt:user',
],
- source_under_test = [':ui_module'],
vm_args = ['-Xmx512m'],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-gwtui/BUILD b/gerrit-gwtui/BUILD
new file mode 100644
index 0000000..cc5da12
--- /dev/null
+++ b/gerrit-gwtui/BUILD
@@ -0,0 +1,15 @@
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+load('//tools/bzl:license.bzl', 'license_test')
+load(':gwt.bzl', 'gwt_binary', 'gwt_genrule', 'gen_ui_module')
+
+gwt_genrule()
+gwt_genrule('_r')
+
+gen_ui_module(name = 'ui_module')
+gen_ui_module(name = 'ui_module', suffix = '_r')
+
+license_test(
+ name = "ui_module_license_test",
+ target = ":ui_module",
+)
diff --git a/gerrit-gwtui/gwt.bzl b/gerrit-gwtui/gwt.bzl
new file mode 100644
index 0000000..9df4ff8
--- /dev/null
+++ b/gerrit-gwtui/gwt.bzl
@@ -0,0 +1,196 @@
+# 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.
+
+# Port of Buck native gwt_binary() rule. See discussion in context of
+# https://github.com/facebook/buck/issues/109
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+load('//tools/bzl:gwt.bzl', 'gwt_module')
+
+jar_filetype = FileType(['.jar'])
+
+MODULE = 'com.google.gerrit.GerritGwtUI'
+
+GWT_COMPILER = "com.google.gwt.dev.Compiler"
+
+GWT_JVM_ARGS = ['-Xmx512m']
+
+GWT_COMPILER_ARGS = [
+ '-XdisableClassMetadata',
+]
+
+GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [
+ '-XdisableCastChecking',
+]
+
+GWT_TRANSITIVE_DEPS = [
+ '//lib/gwt:ant',
+ '//lib/gwt:colt',
+ '//lib/gwt:javax-validation',
+ '//lib/gwt:javax-validation_src',
+ '//lib/gwt:jsinterop-annotations',
+ '//lib/gwt:jsinterop-annotations_src',
+ '//lib/gwt:tapestry',
+ '//lib/gwt:w3c-css-sac',
+ '//lib/ow2:ow2-asm',
+ '//lib/ow2:ow2-asm-analysis',
+ '//lib/ow2:ow2-asm-commons',
+ '//lib/ow2:ow2-asm-tree',
+ '//lib/ow2:ow2-asm-util',
+]
+
+DEPS = GWT_TRANSITIVE_DEPS + [
+ '//gerrit-gwtexpui:CSS',
+ '//lib:gwtjsonrpc',
+ '//lib/gwt:dev',
+ '@jgit_src//file',
+]
+
+def _impl(ctx):
+ output_zip = ctx.outputs.output
+ output_dir = output_zip.path + '.gwt_output'
+ deploy_dir = output_zip.path + '.gwt_deploy'
+
+ deps = _get_transitive_closure(ctx)
+
+ paths = []
+ for dep in deps:
+ paths.append(dep.path)
+
+ cmd = "external/local_jdk/bin/java %s -Dgwt.normalizeTimestamps=true -cp %s %s -war %s -deploy %s " % (
+ " ".join(ctx.attr.jvm_args),
+ ":".join(paths),
+ GWT_COMPILER,
+ output_dir,
+ deploy_dir,
+ )
+ cmd += " ".join([
+ "-style %s" % ctx.attr.style,
+ "-optimize %s" % ctx.attr.optimize,
+ "-strict",
+ " ".join(ctx.attr.compiler_args),
+ " ".join(ctx.attr.modules) + "\n",
+ "rm -rf %s/gwt-unitCache\n" % output_dir,
+ "root=`pwd`\n",
+ "cd %s; $root/%s Cc ../%s $(find .)\n" % (
+ output_dir,
+ ctx.executable._zip.path,
+ output_zip.basename,
+ )
+ ])
+
+ ctx.action(
+ inputs = list(deps) + ctx.files._jdk + ctx.files._zip,
+ outputs = [output_zip],
+ mnemonic = "GwtBinary",
+ progress_message = "GWT compiling " + output_zip.short_path,
+ command = "set -e\n" + cmd,
+ )
+
+def _get_transitive_closure(ctx):
+ deps = set()
+ for dep in ctx.attr.module_deps:
+ deps += dep.java.transitive_runtime_deps
+ deps += dep.java.transitive_source_jars
+ for dep in ctx.attr.deps:
+ if hasattr(dep, 'java'):
+ deps += dep.java.transitive_runtime_deps
+ elif hasattr(dep, 'files'):
+ deps += dep.files
+
+ return deps
+
+gwt_binary = rule(
+ implementation = _impl,
+ attrs = {
+ "style": attr.string(default = "OBF"),
+ "optimize": attr.string(default = "9"),
+ "deps": attr.label_list(allow_files=jar_filetype),
+ "modules": attr.string_list(mandatory=True),
+ "module_deps": attr.label_list(allow_files=jar_filetype),
+ "compiler_args": attr.string_list(),
+ "jvm_args": attr.string_list(),
+ "_jdk": attr.label(
+ default=Label("//tools/defaults:jdk")),
+ "_zip": attr.label(
+ default=Label("@bazel_tools//tools/zip:zipper"),
+ executable=True,
+ single_file=True),
+ },
+ outputs = {
+ "output": "%{name}.zip",
+ },
+)
+
+def gwt_genrule(suffix = ""):
+ dbg = 'ui_dbg' + suffix
+ opt = 'ui_opt' + suffix
+ module_dep = ':ui_module' + suffix
+ args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS
+
+ genrule2(
+ name = 'ui_optdbg' + suffix,
+ srcs = [
+ ':' + dbg,
+ ':' + opt,
+ ],
+ cmd = 'cd $$TMP;' +
+ 'unzip -q $$ROOT/$(location :%s);' % dbg +
+ 'mv' +
+ ' gerrit_ui/gerrit_ui.nocache.js' +
+ ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
+ 'unzip -qo $$ROOT/$(location :%s);' % opt +
+ 'mkdir -p $$(dirname $@);' +
+ 'zip -qr $$ROOT/$@ .',
+ out = 'ui_optdbg' + suffix + '.zip',
+ visibility = ['//visibility:public'],
+ )
+
+ gwt_binary(
+ name = opt,
+ modules = [MODULE],
+ module_deps = [module_dep],
+ deps = DEPS,
+ compiler_args = args,
+ jvm_args = GWT_JVM_ARGS,
+ )
+
+ gwt_binary(
+ name = dbg,
+ modules = [MODULE],
+ style = 'PRETTY',
+ optimize = "0",
+ module_deps = [module_dep],
+ deps = DEPS,
+ compiler_args = GWT_COMPILER_ARGS,
+ jvm_args = GWT_JVM_ARGS,
+ )
+
+def gen_ui_module(name, suffix = ""):
+ gwt_module(
+ name = name + suffix,
+ srcs = native.glob(['src/main/java/**/*.java']),
+ gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+ resources = native.glob(
+ ['src/main/java/**/*'],
+ exclude = ['src/main/java/**/*.java'] +
+ ['src/main/java/%s.gwt.xml' % MODULE.replace('.', '/')]),
+ deps = [
+ '//gerrit-gwtui-common:diffy_logo',
+ '//gerrit-gwtui-common:client',
+ '//gerrit-gwtexpui:CSS',
+ '//lib/codemirror:codemirror' + suffix,
+ '//lib/gwt:user',
+ ],
+ visibility = ['//visibility:public'],
+ )
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java
new file mode 100644
index 0000000..0a1aadd
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffObject.java
@@ -0,0 +1,198 @@
+// 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;
+
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+/**
+ * Represent an object that can be diffed. This can be either a regular patch
+ * set, the base of a patch set, the parent of a merge, the auto-merge of a
+ * merge or an edit patch set.
+ */
+public class DiffObject {
+ public static final String AUTO_MERGE = "AutoMerge";
+
+ /**
+ * Parses a string that represents a diff object.
+ * <p>
+ * The following string representations are supported:
+ * <ul>
+ * <li>a positive integer: represents a patch set
+ * <li>a negative integer: represents a parent of a merge patch set
+ * <li>'0': represents the edit patch set
+ * <li>empty string or null: represents the parent of a 1-parent patch set,
+ * also called base
+ * <li>'AutoMerge': represents the auto-merge of a merge patch set
+ * </ul>
+ *
+ * @param changeId the ID of the change to which the diff object belongs
+ * @param str the string representation of the diff object
+ * @return the parsed diff object, {@code null} if str cannot be parsed as
+ * diff object
+ */
+ public static DiffObject parse(Change.Id changeId, String str) {
+ if (str == null || str.isEmpty()) {
+ return new DiffObject(false);
+ }
+
+ if (AUTO_MERGE.equals(str)) {
+ return new DiffObject(true);
+ }
+
+ try {
+ return new DiffObject(new PatchSet.Id(changeId, Integer.parseInt(str)));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Create a DiffObject that represents the parent of a 1-parent patch set.
+ */
+ public static DiffObject base() {
+ return new DiffObject(false);
+ }
+
+ /**
+ * Create a DiffObject that represents the auto-merge for a merge patch set.
+ */
+ public static DiffObject autoMerge() {
+ return new DiffObject(true);
+ }
+
+ /**
+ * Create a DiffObject that represents a patch set.
+ */
+ public static DiffObject patchSet(PatchSet.Id psId) {
+ return new DiffObject(psId);
+ }
+
+ private final PatchSet.Id psId;
+ private final boolean autoMerge;
+
+ private DiffObject(PatchSet.Id psId) {
+ this.psId = psId;
+ this.autoMerge = false;
+ }
+
+ private DiffObject(boolean autoMerge) {
+ this.psId = null;
+ this.autoMerge = autoMerge;
+ }
+
+ public boolean isBase() {
+ return psId == null && !autoMerge;
+ }
+
+ public boolean isAutoMerge() {
+ return psId == null && autoMerge;
+ }
+
+ public boolean isBaseOrAutoMerge() {
+ return psId == null;
+ }
+
+ public boolean isPatchSet() {
+ return psId != null && psId.get() > 0;
+ }
+
+ public boolean isParent() {
+ return psId != null && psId.get() < 0;
+ }
+
+ public boolean isEdit() {
+ return psId != null && psId.get() == 0;
+ }
+
+ /**
+ * Returns the DiffObject as PatchSet.Id.
+ *
+ * @return PatchSet.Id with an id > 0 for a regular patch set; PatchSet.Id
+ * with an id < 0 for a parent of a merge; PatchSet.Id with id == 0
+ * for an edit patch set; {@code null} for the base of a 1-parent
+ * patch set and for the auto-merge of a merge patch set
+ */
+ public PatchSet.Id asPatchSetId() {
+ return psId;
+ }
+
+ /**
+ * Returns the parent number for a parent of a merge.
+ *
+ * @return 1-based parent number, 0 if this DiffObject is not a parent of a
+ * merge
+ */
+ public int getParentNum() {
+ if (!isParent()) {
+ return 0;
+ }
+
+ return -psId.get();
+ }
+
+ /**
+ * Returns a string representation of this DiffObject that can be used in
+ * URLs.
+ * <p>
+ * The following string representations are returned:
+ * <ul>
+ * <li>a positive integer for a patch set
+ * <li>a negative integer for a parent of a merge patch set
+ * <li>'0' for the edit patch set
+ * <li>{@code null} for the parent of a 1-parent patch set, also called base
+ * <li>'AutoMerge' for the auto-merge of a merge patch set
+ * </ul>
+ *
+ * @return string representation of this DiffObject
+ */
+ public String asString() {
+ if (autoMerge) {
+ if (Gerrit.getUserPreferences()
+ .defaultBaseForMerges() != DefaultBase.AUTO_MERGE) {
+ return AUTO_MERGE;
+ }
+ return null;
+ }
+
+ if (psId != null) {
+ return psId.getId();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ if (isPatchSet()) {
+ return "Patch Set " + psId.getId();
+ }
+
+ if (isParent()) {
+ return "Parent " + psId.getId();
+ }
+
+ if (isEdit()) {
+ return "Edit Patch Set";
+ }
+
+ if (isAutoMerge()) {
+ return "Auto Merge";
+ }
+
+ return "Base";
+ }
+}
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..e2aba0a 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;
@@ -108,35 +108,35 @@
import com.google.gwtorm.client.KeyUtil;
public class Dispatcher {
- public static String toPatch(PatchSet.Id diffBase,
+ public static String toPatch(DiffObject diffBase,
PatchSet.Id revision, String fileName) {
return toPatch("", diffBase, revision, fileName, null, 0);
}
- public static String toPatch(PatchSet.Id diffBase,
+ public static String toPatch(DiffObject diffBase,
PatchSet.Id revision, String fileName, DisplaySide side, int line) {
return toPatch("", diffBase, revision, fileName, side, line);
}
- public static String toSideBySide(PatchSet.Id diffBase, Patch.Key id) {
+ public static String toSideBySide(DiffObject diffBase, Patch.Key id) {
return toPatch("sidebyside", diffBase, id);
}
- public static String toSideBySide(PatchSet.Id diffBase,
- PatchSet.Id revision, String fileName) {
+ public static String toSideBySide(DiffObject diffBase, PatchSet.Id revision,
+ String fileName) {
return toPatch("sidebyside", diffBase, revision, fileName, null, 0);
}
- public static String toUnified(PatchSet.Id diffBase,
+ public static String toUnified(DiffObject diffBase,
PatchSet.Id revision, String fileName) {
return toPatch("unified", diffBase, revision, fileName, null, 0);
}
- public static String toUnified(PatchSet.Id diffBase, Patch.Key id) {
+ public static String toUnified(DiffObject diffBase, Patch.Key id) {
return toPatch("unified", diffBase, id);
}
- public static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
+ public static String toPatch(String type, DiffObject diffBase, Patch.Key id) {
return toPatch(type, diffBase, id.getParentKey(), id.get(), null, 0);
}
@@ -145,16 +145,16 @@
}
public static String toEditScreen(PatchSet.Id revision, String fileName, int line) {
- return toPatch("edit", null, revision, fileName, null, line);
+ return toPatch("edit", DiffObject.base(), revision, fileName, null, line);
}
- private static String toPatch(String type, PatchSet.Id diffBase,
+ private static String toPatch(String type, DiffObject diffBase,
PatchSet.Id revision, String fileName, DisplaySide side, int line) {
Change.Id c = revision.getParentKey();
StringBuilder p = new StringBuilder();
p.append("/c/").append(c).append("/");
- if (diffBase != null) {
- p.append(diffBase.get()).append("..");
+ if (diffBase != null && diffBase.asString() != null) {
+ p.append(diffBase.asString()).append("..");
}
p.append(revision.getId()).append("/").append(KeyUtil.encode(fileName));
if (type != null && !type.isEmpty()
@@ -395,7 +395,7 @@
panel = null;
}
Gerrit.display(token, panel == null
- ? new ChangeScreen(id, null, null, false, mode)
+ ? new ChangeScreen(id, DiffObject.base(), null, false, mode)
: new NotFoundScreen());
return;
}
@@ -410,11 +410,14 @@
rest = "";
}
- PatchSet.Id base = null;
+ DiffObject base = DiffObject.base();
PatchSet.Id ps;
int dotdot = psIdStr.indexOf("..");
if (1 <= dotdot) {
- base = new PatchSet.Id(id, Integer.parseInt(psIdStr.substring(0, dotdot)));
+ base = DiffObject.parse(id, psIdStr.substring(0, dotdot));
+ if (base == null) {
+ Gerrit.display(token, new NotFoundScreen());
+ }
psIdStr = psIdStr.substring(dotdot + 2);
}
ps = toPsId(id, psIdStr);
@@ -438,9 +441,7 @@
if (panel == null) {
Gerrit.display(token,
new ChangeScreen(id,
- base != null
- ? String.valueOf(base.get())
- : null,
+ base,
String.valueOf(ps.get()), false, FileTable.Mode.REVIEW));
} else {
Gerrit.display(token, new NotFoundScreen());
@@ -464,7 +465,7 @@
}
private static void patch(String token,
- PatchSet.Id baseId,
+ DiffObject base,
Patch.Key id,
DisplaySide side,
int line,
@@ -477,16 +478,20 @@
if ("".equals(panel) || /* DEPRECATED URL */"cm".equals(panel)) {
if (preferUnified()) {
- unified(token, baseId, id, side, line);
+ unified(token, base, id, side, line);
} else {
- codemirror(token, baseId, id, side, line, false);
+ codemirror(token, base, id, side, line);
}
} else if ("sidebyside".equals(panel)) {
- codemirror(token, baseId, id, side, line, false);
+ codemirror(token, base, id, side, line);
} else if ("unified".equals(panel)) {
- unified(token, baseId, id, side, line);
+ unified(token, base, id, side, line);
} else if ("edit".equals(panel)) {
- codemirror(token, null, id, side, line, true);
+ if (!Patch.isMagic(id.get()) || Patch.COMMIT_MSG.equals(id.get())) {
+ codemirrorForEdit(token, id, line);
+ } else {
+ Gerrit.display(token, new NotFoundScreen());
+ }
} else {
Gerrit.display(token, new NotFoundScreen());
}
@@ -497,26 +502,34 @@
|| (UserAgent.isPortrait() && UserAgent.isMobile());
}
- private static void unified(final String token, final PatchSet.Id baseId,
+ private static void unified(final String token, final DiffObject base,
final Patch.Key id, final DisplaySide side, final int line) {
GWT.runAsync(new AsyncSplit(token) {
@Override
public void onSuccess() {
- Gerrit.display(token,
- new Unified(baseId, id.getParentKey(), id.get(), side, line));
+ Gerrit.display(token, new Unified(base,
+ DiffObject.patchSet(id.getParentKey()), id.get(), side, line));
}
});
}
- private static void codemirror(final String token, final PatchSet.Id baseId,
- final Patch.Key id, final DisplaySide side, final int line,
- final boolean edit) {
+ private static void codemirror(final String token, final DiffObject base,
+ final Patch.Key id, final DisplaySide side, final int line) {
GWT.runAsync(new AsyncSplit(token) {
@Override
public void onSuccess() {
- Gerrit.display(token, edit
- ? new EditScreen(baseId, id, line)
- : new SideBySide(baseId, id.getParentKey(), id.get(), side, line));
+ Gerrit.display(token, new SideBySide(base,
+ DiffObject.patchSet(id.getParentKey()), id.get(), side, line));
+ }
+ });
+ }
+
+ private static void codemirrorForEdit(final String token, final Patch.Key id,
+ final int line) {
+ GWT.runAsync(new AsyncSplit(token) {
+ @Override
+ public void onSuccess() {
+ Gerrit.display(token, new EditScreen(id, line));
}
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index dd1505c..3f0daa2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -17,7 +17,6 @@
import com.google.gerrit.client.change.Resources;
import com.google.gerrit.client.info.AccountInfo;
import com.google.gerrit.client.info.GeneralPreferences;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gwt.i18n.client.NumberFormat;
import java.util.Date;
@@ -84,17 +83,6 @@
return createAccountFormatter().name(info);
}
- public static AccountInfo asInfo(Account acct) {
- if (acct == null) {
- return AccountInfo.create(0, null, null, null);
- }
- return AccountInfo.create(
- acct.getId() != null ? acct.getId().get() : 0,
- acct.getFullName(),
- acct.getPreferredEmail(),
- acct.getUserName());
- }
-
public static AccountInfo asInfo(com.google.gerrit.common.data.AccountInfo acct) {
if (acct == null) {
return AccountInfo.create(0, null, null, null);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index d280e07..d52d192 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -44,12 +44,14 @@
import com.google.gerrit.client.ui.MorphingTabPanel;
import com.google.gerrit.client.ui.ProjectLinkMenuItem;
import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.common.data.SystemInfoService;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GerritTopMenu;
+import com.google.gerrit.extensions.client.UiType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.EntryPoint;
@@ -286,6 +288,7 @@
}
/** @return access token to prove user identity during REST API calls. */
+ @Nullable
public static String getXGerritAuth() {
return xGerritAuth;
}
@@ -537,6 +540,14 @@
btmmenu.add(new InlineHTML(M.poweredBy(vs)));
+ if (info().gerrit().webUis().contains(UiType.POLYGERRIT)) {
+ btmmenu.add(new InlineLabel(" | "));
+ Anchor a = new Anchor(
+ C.polyGerrit(), GWT.getHostPageBaseURL() + "?polygerrit=1");
+ a.setStyleName("");
+ btmmenu.add(a);
+ }
+
String reportBugUrl = info().gerrit().reportBugUrl();
if (reportBugUrl != null) {
String reportBugText = info().gerrit().reportBugText();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 4c8c58d..53d9260 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -130,4 +130,6 @@
String searchDropdownChanges();
String searchDropdownDoc();
+
+ String polyGerrit();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 10d7e1d..d50ab34 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -113,3 +113,5 @@
searchDropdownChanges = Changes
searchDropdownDoc = Docs
+
+polyGerrit = PolyGerrit
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 32e30d4..30f33f1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -32,6 +32,8 @@
String branchTableDeleteButton();
String branchTablePrevNextLinks();
String cAPPROVAL();
+ String cASSIGNEE();
+ String cASSIGNEDTOME();
String cLastUpdate();
String cOWNER();
String cSIZE();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 54c5b92..000e5fd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -31,7 +31,7 @@
new ProjectNameSuggestOracle()),
new ParamSuggester(Arrays.asList(
"owner:", "reviewer:", "commentby:", "reviewedby:", "author:",
- "committer:", "from:"),
+ "committer:", "from:", "assignee:"),
new AccountSuggestOracle() {
@Override
public void onRequestSuggestions(final Request request, final Callback done) {
@@ -139,6 +139,12 @@
suggestions.add("hashtag:");
}
+ if (Gerrit.info().change().showAssignee()) {
+ suggestions.add("is:assigned");
+ suggestions.add("is:unassigned");
+ suggestions.add("assignee:");
+ }
+
suggestions.add("AND");
suggestions.add("OR");
suggestions.add("NOT");
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..06d5df5 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
@@ -38,6 +38,7 @@
String messageShowInReviewCategoryUsername();
String messageShowInReviewCategoryAbbrev();
String buttonSaveChanges();
+ String highlightAssigneeInChangeTable();
String showRelativeDateInChangeTable();
String showSizeBarInChangeTable();
String showLegacycidInChangeTable();
@@ -141,11 +142,8 @@
String errorDialogTitleRegisterNewEmail();
String newAgreement();
- String agreementStatus();
String agreementName();
String agreementDescription();
- String agreementStatus_EXPIRED();
- String agreementStatus_VERIFIED();
String newAgreementSelectTypeHeading();
String newAgreementNoneAvailable();
@@ -171,4 +169,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..2479c87 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
@@ -15,15 +15,20 @@
messageShowInReviewCategoryAbbrev = Show Abbreviated Name
emailFieldLabel = Email Notifications:
-messageEnabled = Enabled
-messageCCMeOnMyComments = CC Me On Comments I Write
-messageDisabled = Disabled
+messageCCMeOnMyComments = Every Comment
+messageEnabled = Only Comments Left By Others
+messageDisabled = None
+
+defaultBaseForMerges = Default Base For Merges:
+autoMerge = Auto Merge
+firstParent = First Parent
maximumPageSizeFieldLabel = Maximum Page Size:
diffViewLabel = Diff View:
dateFormatLabel = Date/Time Format:
contextWholeFile = Whole File
buttonSaveChanges = Save Changes
+highlightAssigneeInChangeTable = Highlight Changes Assigned To Me In Changes Table
showRelativeDateInChangeTable = Show Relative Dates In Changes Table
showSizeBarInChangeTable = Show Change Sizes As Colored Bars
showLegacycidInChangeTable = Show Change Number In Changes Table
@@ -151,10 +156,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..0b8fe3e 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 (currentEmail != null && !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/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index 7c707b2..423d05f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -129,24 +129,24 @@
public final native void lineLength(int c) /*-{ this.line_length = c }-*/;
public final native void context(int c) /*-{ this.context = c }-*/;
public final native void cursorBlinkRate(int r) /*-{ this.cursor_blink_rate = r }-*/;
- public final native void intralineDifference(boolean i) /*-{ this.intraline_difference = i }-*/;
- public final native void showLineEndings(boolean s) /*-{ this.show_line_endings = s }-*/;
- public final native void showTabs(boolean s) /*-{ this.show_tabs = s }-*/;
- public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
- public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
- public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
- public final native void autoHideDiffTableHeader(boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
- public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
- public final native void expandAllComments(boolean e) /*-{ this.expand_all_comments = e }-*/;
- public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
- public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
- public final native void retainHeader(boolean r) /*-{ this.retain_header = r }-*/;
- public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
- public final native void skipUnchanged(boolean s) /*-{ this.skip_unchanged = s }-*/;
- public final native void skipUncommented(boolean s) /*-{ this.skip_uncommented = s }-*/;
- public final native void skipDeleted(boolean s) /*-{ this.skip_deleted = s }-*/;
- public final native void matchBrackets(boolean m) /*-{ this.match_brackets = m }-*/;
- public final native void lineWrapping(boolean w) /*-{ this.line_wrapping = w }-*/;
+ public final native void intralineDifference(Boolean i) /*-{ this.intraline_difference = i }-*/;
+ public final native void showLineEndings(Boolean s) /*-{ this.show_line_endings = s }-*/;
+ public final native void showTabs(Boolean s) /*-{ this.show_tabs = s }-*/;
+ public final native void showWhitespaceErrors(Boolean s) /*-{ this.show_whitespace_errors = s }-*/;
+ public final native void syntaxHighlighting(Boolean s) /*-{ this.syntax_highlighting = s }-*/;
+ public final native void hideTopMenu(Boolean s) /*-{ this.hide_top_menu = s }-*/;
+ public final native void autoHideDiffTableHeader(Boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
+ public final native void hideLineNumbers(Boolean s) /*-{ this.hide_line_numbers = s }-*/;
+ public final native void expandAllComments(Boolean e) /*-{ this.expand_all_comments = e }-*/;
+ public final native void manualReview(Boolean r) /*-{ this.manual_review = r }-*/;
+ public final native void renderEntireFile(Boolean r) /*-{ this.render_entire_file = r }-*/;
+ public final native void retainHeader(Boolean r) /*-{ this.retain_header = r }-*/;
+ public final native void hideEmptyPane(Boolean s) /*-{ this.hide_empty_pane = s }-*/;
+ public final native void skipUnchanged(Boolean s) /*-{ this.skip_unchanged = s }-*/;
+ public final native void skipUncommented(Boolean s) /*-{ this.skip_uncommented = s }-*/;
+ public final native void skipDeleted(Boolean s) /*-{ this.skip_deleted = s }-*/;
+ public final native void matchBrackets(Boolean m) /*-{ this.match_brackets = m }-*/;
+ public final native void lineWrapping(Boolean w) /*-{ this.line_wrapping = w }-*/;
public final native boolean intralineDifference() /*-{ return this.intraline_difference || false }-*/;
public final native boolean showLineEndings() /*-{ return this.show_line_endings || false }-*/;
public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
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..3bfc7da 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
@@ -50,6 +50,7 @@
public class MyPreferencesScreen extends SettingsScreen {
private CheckBox showSiteHeader;
private CheckBox useFlashClipboard;
+ private CheckBox highlightAssigneeInChangeTable;
private CheckBox relativeDateInChangeTable;
private CheckBox sizeBarInChangeTable;
private CheckBox legacycidInChangeTable;
@@ -61,6 +62,7 @@
private ListBox reviewCategoryStrategy;
private ListBox diffView;
private ListBox emailStrategy;
+ private ListBox defaultBaseForMerges;
private StringListPanel myMenus;
private Button save;
@@ -93,19 +95,25 @@
GeneralPreferencesInfo.ReviewCategoryStrategy.ABBREV.name());
emailStrategy = new ListBox();
- emailStrategy.addItem(Util.C.messageEnabled(),
- GeneralPreferencesInfo.EmailStrategy.ENABLED.name());
emailStrategy
.addItem(
Util.C.messageCCMeOnMyComments(),
GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS
.name());
+ emailStrategy.addItem(Util.C.messageEnabled(),
+ GeneralPreferencesInfo.EmailStrategy.ENABLED.name());
emailStrategy
.addItem(
Util.C.messageDisabled(),
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(),
@@ -148,7 +156,8 @@
dateTimePanel.add(dateFormat);
dateTimePanel.add(timeFormat);
}
-
+ highlightAssigneeInChangeTable = new CheckBox(Util.C.highlightAssigneeInChangeTable());
+ highlightAssigneeInChangeTable.setEnabled(Gerrit.info().change().showAssignee());
relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
sizeBarInChangeTable = new CheckBox(Util.C.showSizeBarInChangeTable());
legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
@@ -156,7 +165,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 +185,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++;
@@ -185,6 +198,10 @@
row++;
formGrid.setText(row, labelIdx, "");
+ formGrid.setWidget(row, fieldIdx, highlightAssigneeInChangeTable);
+ row++;
+
+ formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
row++;
@@ -231,6 +248,7 @@
e.listenTo(maximumPageSize);
e.listenTo(dateFormat);
e.listenTo(timeFormat);
+ e.listenTo(highlightAssigneeInChangeTable);
e.listenTo(relativeDateInChangeTable);
e.listenTo(sizeBarInChangeTable);
e.listenTo(legacycidInChangeTable);
@@ -239,6 +257,7 @@
e.listenTo(diffView);
e.listenTo(reviewCategoryStrategy);
e.listenTo(emailStrategy);
+ e.listenTo(defaultBaseForMerges);
}
@Override
@@ -264,6 +283,7 @@
maximumPageSize.setEnabled(on);
dateFormat.setEnabled(on);
timeFormat.setEnabled(on);
+ highlightAssigneeInChangeTable.setEnabled(Gerrit.info().change().showAssignee());
relativeDateInChangeTable.setEnabled(on);
sizeBarInChangeTable.setEnabled(on);
legacycidInChangeTable.setEnabled(on);
@@ -272,6 +292,7 @@
reviewCategoryStrategy.setEnabled(on);
diffView.setEnabled(on);
emailStrategy.setEnabled(on);
+ defaultBaseForMerges.setEnabled(on);
}
private void display(GeneralPreferences p) {
@@ -282,6 +303,7 @@
p.dateFormat());
setListBox(timeFormat, GeneralPreferencesInfo.TimeFormat.HHMM_12, //
p.timeFormat());
+ highlightAssigneeInChangeTable.setValue(p.highlightAssigneeInChangeTable());
relativeDateInChangeTable.setValue(p.relativeDateInChangeTable());
sizeBarInChangeTable.setValue(p.sizeBarInChangeTable());
legacycidInChangeTable.setValue(p.legacycidInChangeTable());
@@ -296,6 +318,9 @@
setListBox(emailStrategy,
GeneralPreferencesInfo.EmailStrategy.ENABLED,
p.emailStrategy());
+ setListBox(defaultBaseForMerges,
+ GeneralPreferencesInfo.DefaultBase.FIRST_PARENT,
+ p.defaultBaseForMerges());
display(p.my());
}
@@ -369,6 +394,7 @@
p.timeFormat(getListBox(timeFormat,
GeneralPreferencesInfo.TimeFormat.HHMM_12,
GeneralPreferencesInfo.TimeFormat.values()));
+ p.highlightAssigneeInChangeTable(highlightAssigneeInChangeTable.getValue());
p.relativeDateInChangeTable(relativeDateInChangeTable.getValue());
p.sizeBarInChangeTable(sizeBarInChangeTable.getValue());
p.legacycidInChangeTable(legacycidInChangeTable.getValue());
@@ -385,6 +411,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..eacff7b 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;
@@ -315,7 +315,7 @@
CheckBox checkBox = new CheckBox();
table.setWidget(row, 1, checkBox);
checkBox.setEnabled(enabled);
- table.setWidget(row, 2, new AccountLinkPanel(i));
+ table.setWidget(row, 2, AccountLinkPanel.create(i));
table.setText(row, 3, i.email());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
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..5e7b873 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,7 +123,11 @@
abandon, \
addPatchSet, \
create, \
+ createTag, \
+ createSignedTag, \
+ delete, \
deleteDrafts, \
+ editAssignee, \
editHashtags, \
editTopicName, \
forgeAuthor, \
@@ -133,8 +137,6 @@
publishDrafts, \
push, \
pushMerge, \
- pushTag, \
- pushSignedTag, \
read, \
rebase, \
removeReviewer, \
@@ -145,7 +147,11 @@
abandon = Abandon
addPatchSet = Add Patch Set
create = Create Reference
+createTag = Create Annotated Tag
+createSignedTag = Create Signed Tag
+delete = Delete Reference
deleteDrafts = Delete Drafts
+editAssignee = Edit Assignee
editHashtags = Edit Hashtags
editTopicName = Edit Topic Name
forgeAuthor = Forge Author Identity
@@ -155,8 +161,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/PaginatedProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
index 6349803..66738c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PaginatedProjectScreen.java
@@ -21,7 +21,7 @@
abstract class PaginatedProjectScreen extends ProjectScreen {
protected int pageSize;
- protected String match;
+ protected String match = "";
protected int start;
PaginatedProjectScreen(Project.NameKey toShow) {
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/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
new file mode 100644
index 0000000..2956ffc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -0,0 +1,221 @@
+// 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.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotSignedInDialog;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.UIObject;
+
+/**
+ * Edit assignee using auto-completion.
+ */
+public class Assignee extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, Assignee> {
+ }
+
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ @UiField Element show;
+ @UiField InlineHyperlink assigneeLink;
+ @UiField Image editAssigneeIcon;
+ @UiField Element form;
+ @UiField Element error;
+ @UiField(provided = true)
+ RemoteSuggestBox suggestBox;
+
+ private AssigneeSuggestOracle assigneeSuggestOracle;
+ private Change.Id changeId;
+ private boolean canEdit;
+ private AccountInfo currentAssignee;
+
+ Assignee() {
+ assigneeSuggestOracle = new AssigneeSuggestOracle();
+ suggestBox = new RemoteSuggestBox(assigneeSuggestOracle);
+ suggestBox.setVisibleLength(55);
+ suggestBox.setHintText(Util.C.approvalTableEditAssigneeHint());
+ suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+ @Override
+ public void onClose(CloseEvent<RemoteSuggestBox> event) {
+ Assignee.this.onCancel(null);
+ }
+ });
+ suggestBox.addSelectionHandler(new SelectionHandler<String>() {
+ @Override
+ public void onSelection(SelectionEvent<String> event) {
+ editAssignee(event.getSelectedItem());
+ }
+ });
+
+ initWidget(uiBinder.createAndBindUi(this));
+ editAssigneeIcon.addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ onOpenForm();
+ }
+ }, ClickEvent.getType());
+ }
+
+ void set(ChangeInfo info) {
+ this.changeId = info.legacyId();
+ this.canEdit = info.hasActions() && info.actions().containsKey("assignee");
+ setAssignee(info.assignee());
+ assigneeSuggestOracle.setChange(changeId);
+ editAssigneeIcon.setVisible(canEdit);
+ if (!canEdit) {
+ show.setTitle(null);
+ }
+ }
+
+ void onOpenForm() {
+ UIObject.setVisible(form, true);
+ UIObject.setVisible(show, false);
+ UIObject.setVisible(error, false);
+ editAssigneeIcon.setVisible(false);
+ suggestBox.setFocus(true);
+ suggestBox.setText(FormatUtil.nameEmail(currentAssignee));
+ suggestBox.selectAll();
+ }
+
+ void onCloseForm() {
+ UIObject.setVisible(form, false);
+ UIObject.setVisible(show, true);
+ UIObject.setVisible(error, false);
+ editAssigneeIcon.setVisible(true);
+ suggestBox.setFocus(false);
+ }
+
+ @UiHandler("assign")
+ void onEditAssignee(@SuppressWarnings("unused") ClickEvent e) {
+ if (canEdit) {
+ editAssignee(suggestBox.getText());
+ }
+ }
+
+ @UiHandler("cancel")
+ void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+ onCloseForm();
+ }
+
+ private void editAssignee(final String assignee) {
+ if (assignee.isEmpty()) {
+ ChangeApi.deleteAssignee(changeId.get(),
+ new GerritCallback<AccountInfo>() {
+ @Override
+ public void onSuccess(AccountInfo result) {
+ onCloseForm();
+ setAssignee(null);
+ }
+
+ @Override
+ public void onFailure(Throwable err) {
+ if (isSigninFailure(err)) {
+ new NotSignedInDialog().center();
+ } else {
+ UIObject.setVisible(error, true);
+ error.setInnerText(err instanceof StatusCodeException
+ ? ((StatusCodeException) err).getEncodedResponse()
+ : err.getMessage());
+ }
+ }
+ });
+ } else {
+ ChangeApi.setAssignee(changeId.get(), assignee,
+ new GerritCallback<AccountInfo>() {
+ @Override
+ public void onSuccess(AccountInfo result) {
+ onCloseForm();
+ setAssignee(result);
+ Reviewers reviewers = getReviewers();
+ if (reviewers != null) {
+ reviewers.updateReviewerList();
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable err) {
+ if (isSigninFailure(err)) {
+ new NotSignedInDialog().center();
+ } else {
+ UIObject.setVisible(error, true);
+ error.setInnerText(err instanceof StatusCodeException
+ ? ((StatusCodeException) err).getEncodedResponse()
+ : err.getMessage());
+ }
+ }
+ });
+ }
+ }
+
+ private void setAssignee(AccountInfo assignee) {
+ currentAssignee = assignee;
+ assigneeLink.setText(assignee != null ? getName(assignee) : null);
+ assigneeLink.setTargetHistoryToken(assignee != null
+ ? PageLinks.toAssigneeQuery(assignee.name() != null
+ ? assignee.name()
+ : assignee.email() != null
+ ? assignee.email()
+ : String.valueOf(assignee._accountId()))
+ : "");
+ }
+
+ private Reviewers getReviewers() {
+ Element e = DOM.getParent(getElement());
+ for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+ EventListener l = DOM.getEventListener(e);
+ if (l instanceof ChangeScreen) {
+ ChangeScreen screen = (ChangeScreen) l;
+ return screen.reviewers;
+ }
+ }
+ return null;
+ }
+
+ private String getName(AccountInfo info) {
+ if (info.name() != null) {
+ return info.name();
+ }
+ if (info.email() != null) {
+ return info.email();
+ }
+ return Gerrit.info().user().anonymousCowardName();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml
new file mode 100644
index 0000000..d5a7239
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.ui.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:u='urn:import:com.google.gerrit.client.ui'>
+ <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style gss='false'>
+ .suggestBox {
+ margin-bottom: 2px;
+ }
+
+ .error {
+ color: #D33D3D;
+ font-weight: bold;
+ }
+
+ .editAssignee,
+ .cancel {
+ cursor: pointer;
+ float: right;
+ }
+ </ui:style>
+ <g:HTMLPanel>
+ <div ui:field='show'>
+ <u:InlineHyperlink ui:field='assigneeLink'
+ title='Search for changes assigned to this user'/>
+ <g:Image ui:field='editAssigneeIcon'
+ resource='{ico.editUser}'
+ styleName='{style.editAssignee}'
+ title='Assign User to Change'/>
+ </div>
+ <div ui:field='form' style='display: none' aria-hidden='true'>
+ <u:RemoteSuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
+ <div ui:field='error'
+ class='{style.error}'
+ style='display: none' aria-hidden='true'/>
+ <div>
+ <g:Button ui:field='assign' styleName='{res.style.button}'>
+ <div>Assign</div>
+ </g:Button>
+ <g:Button ui:field='cancel'
+ styleName='{res.style.button}'
+ addStyleNames='{style.cancel}'>
+ <div>Cancel</div>
+ </g:Button>
+ </div>
+ </div>
+ </g:HTMLPanel>
+ </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
new file mode 100644
index 0000000..8dc5574
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
@@ -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.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.change.ReviewerSuggestOracle.RestReviewerSuggestion;
+import com.google.gerrit.client.change.ReviewerSuggestOracle.SuggestReviewerInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** REST API based suggestion Oracle for assignee */
+public class AssigneeSuggestOracle extends SuggestAfterTypingNCharsOracle {
+ private Change.Id changeId;
+
+ public void setChange(Change.Id changeId) {
+ this.changeId = changeId;
+ }
+
+ @Override
+ protected void _onRequestSuggestions(Request req, Callback cb) {
+ ChangeApi
+ .suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), true)
+ .get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
+ @Override
+ public void onSuccess(JsArray<SuggestReviewerInfo> result) {
+ List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
+ for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
+ r.add(new RestReviewerSuggestion(reviewer, req.getQuery()));
+ }
+ cb.onSuggestionsReady(req, new Response(r));
+ }
+
+ @Override
+ public void onFailure(Throwable err) {
+ List<Suggestion> r = Collections.emptyList();
+ cb.onSuggestionsReady(req, new Response(r));
+ }
+ });
+ }
+}
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..436e0c3 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
@@ -15,10 +15,12 @@
package com.google.gerrit.client.change;
import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GerritUiExtensionPoint;
+import com.google.gerrit.client.NotFoundScreen;
import com.google.gerrit.client.api.ChangeGlue;
import com.google.gerrit.client.api.ExtensionPanel;
import com.google.gerrit.client.changes.ChangeApi;
@@ -107,8 +109,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);
@@ -141,7 +148,7 @@
}
private final Change.Id changeId;
- private String base;
+ private DiffObject base;
private String revision;
private ChangeInfo changeInfo;
private boolean hasDraftComments;
@@ -165,6 +172,8 @@
@UiField ToggleButton star;
@UiField Anchor permalink;
+ @UiField Assignee assignee;
+ @UiField Element assigneeRow;
@UiField Element ccText;
@UiField Reviewers reviewers;
@UiField Hashtags hashtags;
@@ -199,6 +208,7 @@
@UiField FileTable files;
@UiField ListBox diffBase;
@UiField History history;
+ @UiField SimplePanel historyExtensionRight;
@UiField Button includedIn;
@UiField Button patchSets;
@@ -219,6 +229,8 @@
@UiField Button renameFile;
@UiField Button expandAll;
@UiField Button collapseAll;
+ @UiField Button hideTaggedComments;
+ @UiField Button showTaggedComments;
@UiField QuickApprove quickApprove;
private ReplyAction replyAction;
@@ -229,10 +241,10 @@
private DeleteFileAction deleteFileAction;
private RenameFileAction renameFileAction;
- public ChangeScreen(Change.Id changeId, String base, String revision,
+ public ChangeScreen(Change.Id changeId, DiffObject base, String revision,
boolean openReplyBox, FileTable.Mode mode) {
this.changeId = changeId;
- this.base = normalize(base);
+ this.base = base;
this.revision = normalize(revision);
this.openReplyBox = openReplyBox;
this.fileTableMode = mode;
@@ -282,14 +294,26 @@
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.isBase() && rev.isMerge()) {
+ base = DiffObject.parse(info.legacyId(),
+ Gerrit.getUserPreferences()
+ .defaultBaseForMerges().getBase());
+ }
loadConfigInfo(info, base);
+ JsArray<MessageInfo> mAr = info.messages();
+ for (int i = 0; i < mAr.length(); i++) {
+ if (mAr.get(i).tag() != null) {
+ hideTaggedComments.setVisible(true);
+ break;
+ }
+ }
}
});
group.done();
@@ -352,6 +376,9 @@
addExtensionPoint(
GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK,
commitExtension, change, rev);
+ addExtensionPoint(
+ GerritUiExtensionPoint.CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS,
+ historyExtensionRight, change, rev);
}
private void addExtensionPoint(GerritUiExtensionPoint extensionPoint,
@@ -569,35 +596,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 +648,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);
}
}
@@ -884,7 +918,31 @@
int idx = diffBase.getSelectedIndex();
if (0 <= idx) {
String n = diffBase.getValue(idx);
- loadConfigInfo(changeInfo, !n.isEmpty() ? n : null);
+ loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
+ }
+ }
+
+ @UiHandler("showTaggedComments")
+ void onShowTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
+ showTaggedComments.setVisible(false);
+ hideTaggedComments.setVisible(true);
+ int n = history.getWidgetCount();
+ for (int i = 0; i < n; i++) {
+ Message m = ((Message) history.getWidget(i));
+ m.setVisible(true);
+ }
+ }
+
+ @UiHandler("hideTaggedComments")
+ void onHideTaggedComments(@SuppressWarnings("unused") ClickEvent e) {
+ hideTaggedComments.setVisible(false);
+ showTaggedComments.setVisible(true);
+ int n = history.getWidgetCount();
+ for (int i = 0; i < n; i++) {
+ Message m = ((Message) history.getWidget(i));
+ if (m.getMessageInfo().tag() != null) {
+ m.setVisible(false);
+ }
}
}
@@ -913,13 +971,20 @@
int idx = diffBase.getSelectedIndex();
if (0 <= idx) {
String n = diffBase.getValue(idx);
- loadConfigInfo(changeInfo, !n.isEmpty() ? n : null);
+ loadConfigInfo(changeInfo, DiffObject.parse(changeInfo.legacyId(), n));
}
}
- private void loadConfigInfo(final ChangeInfo info, String base) {
- RevisionInfo rev = info.revision(revision);
- RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
+ private void loadConfigInfo(final ChangeInfo info, DiffObject base) {
+ final RevisionInfo rev = info.revision(revision);
+ if (base.isAutoMerge() && !initCurrentRevision(info).isMerge()) {
+ Gerrit.display(getToken(), new NotFoundScreen());
+ }
+
+ updateToken(info, base, rev);
+
+ RevisionInfo baseRev =
+ resolveRevisionOrPatchSetId(info, base.asString(), null);
CallbackGroup group = new CallbackGroup();
Timestamp lastReply = myLastReply(info);
@@ -929,20 +994,36 @@
RevisionInfo p = RevisionInfo.findEditParentRevision(
info.revisions().values());
List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(p, group);
- loadFileList(b, rev, lastReply, group, comments, null);
+ loadFileList(base, baseRev, rev, lastReply, group, comments, null);
} else {
- loadDiff(b, rev, lastReply, group);
+ loadDiff(base, baseRev, 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 +1032,22 @@
renderChangeInfo(info);
loadRevisionInfo();
}
- }));
+ });
+ }
+
+ private void updateToken(ChangeInfo info, DiffObject base, RevisionInfo rev) {
+ StringBuilder token = new StringBuilder("/c/")
+ .append(info._number())
+ .append("/");
+ if (base.asString() != null) {
+ token.append(base.asString())
+ .append("..");
+ }
+ if (base.asString() != null
+ || !rev.name().equals(info.currentRevision())) {
+ token.append(rev._number());
+ }
+ setToken(token.toString());
}
static Timestamp myLastReply(ChangeInfo info) {
@@ -967,11 +1063,11 @@
return null;
}
- private void loadDiff(RevisionInfo base, RevisionInfo rev,
+ private void loadDiff(DiffObject base, RevisionInfo baseRev, RevisionInfo rev,
Timestamp myLastReply, CallbackGroup group) {
List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
- loadFileList(base, rev, myLastReply, group, comments, drafts);
+ loadFileList(base, baseRev, rev, myLastReply, group, comments, drafts);
if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
ChangeApi.revision(changeId.get(), rev.name())
@@ -990,19 +1086,19 @@
}
}
- private void loadFileList(final RevisionInfo base, final RevisionInfo rev,
- final Timestamp myLastReply, CallbackGroup group,
+ private void loadFileList(final DiffObject base, final RevisionInfo baseRev,
+ final RevisionInfo rev, final Timestamp myLastReply, CallbackGroup group,
final List<NativeMap<JsArray<CommentInfo>>> comments,
final List<NativeMap<JsArray<CommentInfo>>> drafts) {
DiffApi.list(changeId.get(),
rev.name(),
- base,
+ baseRev,
group.add(
new AsyncCallback<NativeMap<FileInfo>>() {
@Override
public void onSuccess(NativeMap<FileInfo> m) {
files.set(
- base != null ? new PatchSet.Id(changeId, base._number()) : null,
+ base,
new PatchSet.Id(changeId, rev._number()),
style, reply, fileTableMode, edit != null);
files.setValue(m, myLastReply,
@@ -1012,6 +1108,7 @@
@Override
public void onFailure(Throwable caught) {
+ files.showError(caught);
}
}));
}
@@ -1224,6 +1321,11 @@
commit.set(commentLinkProcessor, info, revision);
related.set(info, revision);
reviewers.set(info);
+ if (Gerrit.info().change().showAssignee()) {
+ assignee.set(info);
+ } else {
+ setVisible(assigneeRow, false);
+ }
if (Gerrit.isNoteDbEnabled()) {
hashtags.set(info, revision);
} else {
@@ -1398,12 +1500,12 @@
RevisionInfo r = list.get(i);
diffBase.addItem(
r.id() + ": " + r.name().substring(0, 6),
- r.name());
+ r.id());
if (r.name().equals(revision)) {
SelectElement.as(diffBase.getElement()).getOptions()
.getItem(diffBase.getItemCount() - 1).setDisabled(true);
}
- if (base != null && base.equals(String.valueOf(r._number()))) {
+ if (base.isPatchSet() && base.asPatchSetId().get() == r._number()) {
selectedIdx = diffBase.getItemCount() - 1;
}
}
@@ -1411,15 +1513,15 @@
RevisionInfo rev = info.revisions().get(revision);
JsArray<CommitInfo> parents = rev.commit().parents();
if (parents.length() > 1) {
- diffBase.addItem(Util.C.autoMerge(), "");
+ diffBase.addItem(Util.C.autoMerge(), DiffObject.AUTO_MERGE);
for (int i = 0; i < parents.length(); i++) {
int parentNum = i + 1;
diffBase.addItem(Util.M.diffBaseParent(parentNum),
String.valueOf(-parentNum));
}
- int parentNum = toParentNum(base);
- if (parentNum > 0) {
- selectedIdx = list.length() + parentNum;
+
+ if (base.isParent()) {
+ selectedIdx = list.length() + base.getParentNum();
}
} else {
diffBase.addItem(Util.C.baseDiffItem(), "");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index a0d5405..da18317 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -355,6 +355,11 @@
padding-top: 5px;
}
+ .historyExtension {
+ display: inline-block;
+ float: right;
+ }
+
.pushCertStatus {
padding-left: 5px;
}
@@ -458,6 +463,12 @@
</g:FlowPanel>
</td>
</tr>
+ <tr ui:field='assigneeRow'>
+ <th><ui:msg>Assignee</ui:msg></th>
+ <td>
+ <c:Assignee ui:field='assignee'/>
+ </td>
+ </tr>
<tr>
<th><ui:msg>Reviewers</ui:msg></th>
<td>
@@ -601,6 +612,21 @@
<ui:attribute name='title'/>
<div><ui:msg>Collapse All</ui:msg></div>
</g:Button>
+ <g:Button ui:field='hideTaggedComments'
+ styleName=''
+ visible='false'
+ title='Hide tagged comments'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Hide tagged comments</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='showTaggedComments'
+ styleName=''
+ visible='false'
+ title='Show tagged comments'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Show tagged comments</ui:msg></div>
+ </g:Button>
+ <g:SimplePanel ui:field='historyExtensionRight' styleName='{style.historyExtension}'/>
</div>
</div>
<c:History ui:field='history'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 42963f7..9ec1356 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -25,6 +25,7 @@
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
@@ -174,7 +175,7 @@
// no avatar plugin is installed
if (change.owner().hasAvatarInfo()) {
AvatarImage avatar;
- if (change.owner().email().equals(person.email())) {
+ if (sameEmail(change.owner(), person)) {
avatar = new AvatarImage(change.owner());
} else {
avatar = new AvatarImage(
@@ -202,4 +203,11 @@
return "";
}
}
+
+ private static boolean sameEmail(
+ @Nullable AccountInfo p1, @Nullable GitPerson p2) {
+ return p1 != null && p2 != null &&
+ p1.email() != null && p2.email() != null &&
+ p1.email().equals(p2.email());
+ }
}
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..a95270b 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
@@ -19,6 +19,7 @@
import static com.google.gerrit.client.FormatUtil.formatBytes;
import static com.google.gerrit.client.FormatUtil.formatPercentage;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.VoidResult;
@@ -60,6 +61,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 +96,7 @@
String inserted();
String deleted();
String restoreDelete();
+ String error();
}
public enum Mode {
@@ -180,7 +183,7 @@
return null;
}
- private PatchSet.Id base;
+ private DiffObject base;
private PatchSet.Id curr;
private MyTable table;
private boolean register;
@@ -197,7 +200,7 @@
R.css().ensureInjected();
}
- public void set(PatchSet.Id base, PatchSet.Id curr, ChangeScreen.Style style,
+ public void set(DiffObject base, PatchSet.Id curr, ChangeScreen.Style style,
Widget replyButton, Mode mode, boolean editExists) {
this.base = base;
this.curr = curr;
@@ -222,6 +225,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);
@@ -258,11 +268,18 @@
if (table != null) {
String self = Gerrit.selfRedirect(null);
for (FileInfo info : Natives.asList(table.list)) {
- Window.open(self + "#" + url(info), "_blank", null);
+ if (canOpen(info.path())) {
+ Window.open(self + "#" + url(info), "_blank", null);
+ }
}
}
}
+ private boolean canOpen(String path) {
+ return mode != Mode.EDIT || !Patch.isMagic(path)
+ || Patch.COMMIT_MSG.equals(path);
+ }
+
private void setTable(MyTable table) {
clear();
add(table);
@@ -324,7 +341,7 @@
});
setSavePointerId(
- (base != null ? base.toString() + ".." : "")
+ (!base.isBase() ? base.asString() + ".." : "")
+ curr.toString());
}
@@ -420,7 +437,10 @@
@Override
protected void onOpenRow(int row) {
if (1 <= row && row <= list.length()) {
- Gerrit.display(url(list.get(row - 1)));
+ FileInfo info = list.get(row - 1);
+ if (canOpen(info.path())) {
+ Gerrit.display(url(info));
+ }
}
}
@@ -443,7 +463,10 @@
@Override
public void onKeyPress(KeyPressEvent event) {
- Gerrit.display(url(list.get(index)));
+ FileInfo info = list.get(index);
+ if (canOpen(info.path())) {
+ Gerrit.display(url(info));
+ }
}
}
}
@@ -529,7 +552,7 @@
bytesDeleted = 0;
for (int i = 0; i < list.length(); i++) {
FileInfo info = list.get(i);
- if (!Patch.COMMIT_MSG.equals(info.path())) {
+ if (!Patch.isMagic(info.path())) {
if (!info.binary()) {
hasNonBinaryFile = true;
inserted += info.linesInserted();
@@ -619,7 +642,7 @@
private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().restoreDelete());
if (hasUser) {
- if (!Patch.COMMIT_MSG.equals(info.path())) {
+ if (!Patch.isMagic(info.path())) {
boolean editable = isEditable(info);
sb.openDiv()
.openElement("button")
@@ -650,7 +673,7 @@
private void columnStatus(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().status());
- if (!Patch.COMMIT_MSG.equals(info.path())
+ if (!Patch.isMagic(info.path())
&& info.status() != null
&& !ChangeType.MODIFIED.matches(info.status())) {
sb.append(info.status());
@@ -659,20 +682,43 @@
}
private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
- sb.openTd()
- .setStyleName(R.css().pathColumn())
- .openAnchor();
-
String path = info.path();
+
+ sb.openTd()
+ .setStyleName(R.css().pathColumn());
+
+ if (!canOpen(path)) {
+ sb.openDiv();
+ appendPath(path);
+ sb.closeDiv();
+ sb.closeTd();
+ return;
+ }
+
+ sb.openAnchor();
+
if (mode == Mode.EDIT && !isEditable(info)) {
sb.setAttribute("onclick", RESTORE + "(event," + info._row() + ")");
} else {
sb.setAttribute("href", "#" + url(info))
.setAttribute("onclick", OPEN + "(event," + info._row() + ")");
}
+ appendPath(path);
+ sb.closeAnchor();
+ if (info.oldPath() != null) {
+ sb.br();
+ sb.openSpan().setStyleName(R.css().renameCopySource())
+ .append(info.oldPath())
+ .closeSpan();
+ }
+ sb.closeTd();
+ }
+ private void appendPath(String path) {
if (Patch.COMMIT_MSG.equals(path)) {
sb.append(Util.C.commitMessage());
+ } else if (Patch.MERGE_LIST.equals(path)) {
+ sb.append(Util.C.mergeList());
} else if (Gerrit.getUserPreferences().muteCommonPathPrefixes()) {
int commonPrefixLen = commonPrefix(path);
if (commonPrefixLen > 0) {
@@ -685,15 +731,6 @@
} else {
sb.append(path);
}
-
- sb.closeAnchor();
- if (info.oldPath() != null) {
- sb.br();
- sb.openSpan().setStyleName(R.css().renameCopySource())
- .append(info.oldPath())
- .closeSpan();
- }
- sb.closeTd();
}
private int commonPrefix(String path) {
@@ -753,9 +790,9 @@
for (CommentInfo c : Natives.asList(list)) {
if (c.side() == Side.REVISION) {
result.push(c);
- } else if (base == null && !c.hasParent()) {
+ } else if (base.isBaseOrAutoMerge() && !c.hasParent()) {
result.push(c);
- } else if (base != null && c.parent() == -base.get()) {
+ } else if (base.isParent() && c.parent() == base.getParentNum()) {
result.push(c);
}
}
@@ -775,7 +812,7 @@
private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().deltaColumn1());
- if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
+ if (!Patch.isMagic(info.path()) && !info.binary()) {
if (showChangeSizeBars) {
sb.append(info.linesInserted() + info.linesDeleted());
} else if (!ChangeType.DELETED.matches(info.status())) {
@@ -804,7 +841,7 @@
private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().deltaColumn2());
if (showChangeSizeBars
- && !Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
+ && !Patch.isMagic(info.path()) && !info.binary()
&& (info.linesInserted() != 0 || info.linesDeleted() != 0)) {
int w = 80;
int t = inserted + deleted;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index 6c27ed9..c8735b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -176,6 +176,10 @@
if (l != null) {
comments.add(new FileComments(clp, ps, Util.C.commitMessage(), l));
}
+ l = m.remove(Patch.MERGE_LIST);
+ if (l != null) {
+ comments.add(new FileComments(clp, ps, Util.C.mergeList(), l));
+ }
for (Map.Entry<String, List<CommentInfo>> e : m.entrySet()) {
comments.add(new FileComments(clp, ps, e.getKey(), e.getValue()));
}
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/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index 791effc..846ad53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -178,7 +178,7 @@
rows = new ArrayList<>(changes.length());
connectedPos = changes.length() - 1;
connected = showIndirectAncestors
- ? new HashSet<String>(Math.max(changes.length() * 4 / 3, 16))
+ ? new HashSet<>(Math.max(changes.length() * 4 / 3, 16))
: null;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index e29048a..b985f43 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -422,12 +422,17 @@
comments.add(new FileComments(clp, psId,
Util.C.commitMessage(), copyPath(Patch.COMMIT_MSG, l)));
}
+ l = m.get(Patch.MERGE_LIST);
+ if (l != null) {
+ comments.add(new FileComments(clp, psId, Util.C.commitMessage(),
+ copyPath(Patch.MERGE_LIST, l)));
+ }
List<String> paths = new ArrayList<>(m.keySet());
Collections.sort(paths);
for (String path : paths) {
- if (!path.equals(Patch.COMMIT_MSG)) {
+ if (!Patch.isMagic(path)) {
comments.add(new FileComments(clp, psId,
path, copyPath(path, m.get(path))));
}
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..404f3c8 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;
@@ -36,7 +36,8 @@
@Override
protected void _onRequestSuggestions(final Request req, final Callback cb) {
- ChangeApi.suggestReviewers(changeId.get(), req.getQuery(), req.getLimit())
+ ChangeApi
+ .suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), false)
.get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
@Override
public void onSuccess(JsArray<SuggestReviewerInfo> result) {
@@ -59,7 +60,7 @@
this.changeId = changeId;
}
- private static class RestReviewerSuggestion implements Suggestion {
+ public static class RestReviewerSuggestion implements Suggestion {
private final String displayString;
private final String replacementString;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index b69d1c0..aa30760 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -198,7 +198,7 @@
});
}
- private void updateReviewerList() {
+ void updateReviewerList() {
ChangeApi.detail(changeId.get(),
new GerritCallback<ChangeInfo>() {
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
index 025668f..a063b6c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -100,6 +100,7 @@
input.setText(text.getText());
input.setFocus(true);
+ input.selectAll();
}
}
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/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index b181341..4882b97 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AccountInfo;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
@@ -34,16 +35,17 @@
public class ChangeApi {
/** Abandon the change, ending its review. */
public static void abandon(int id, String msg, AsyncCallback<ChangeInfo> cb) {
- Input input = Input.create();
+ MessageInput input = MessageInput.create();
input.message(emptyToNull(msg));
call(id, "abandon").post(input, cb);
}
- /** Create a new change.
+ /**
+ * Create a new change.
*
- * The new change is created as DRAFT unless the draft workflow is disabled
- * by `change.allowDrafts = false` in the configuration, in which case the
- * new change is created as NEW.
+ * The new change is created as DRAFT unless the draft workflow is disabled by
+ * `change.allowDrafts = false` in the configuration, in which case the new
+ * change is created as NEW.
*
*/
public static void createChange(String project, String branch, String topic,
@@ -64,14 +66,14 @@
/** Restore a previously abandoned change to be open again. */
public static void restore(int id, String msg, AsyncCallback<ChangeInfo> cb) {
- Input input = Input.create();
+ MessageInput input = MessageInput.create();
input.message(emptyToNull(msg));
call(id, "restore").post(input, cb);
}
/** Create a new change that reverts the delta caused by this change. */
public static void revert(int id, String msg, AsyncCallback<ChangeInfo> cb) {
- Input input = Input.create();
+ MessageInput input = MessageInput.create();
input.message(emptyToNull(msg));
call(id, "revert").post(input, cb);
}
@@ -81,7 +83,7 @@
RestApi call = call(id, "topic");
topic = emptyToNull(topic);
if (topic != null) {
- Input input = Input.create();
+ TopicInput input = TopicInput.create();
input.topic(topic);
call.put(input, NativeString.unwrap(cb));
} else {
@@ -112,6 +114,17 @@
return call(id, revision, "actions");
}
+ public static void deleteAssignee(int id, AsyncCallback<AccountInfo> cb) {
+ change(id).view("assignee").delete(cb);
+ }
+
+ public static void setAssignee(int id, String user,
+ AsyncCallback<AccountInfo> cb) {
+ AssigneeInput input = AssigneeInput.create();
+ input.assignee(user);
+ change(id).view("assignee").put(user, cb);
+ }
+
public static RestApi comments(int id) {
return call(id, "comments");
}
@@ -157,10 +170,11 @@
return change(id).view("reviewers");
}
- public static RestApi suggestReviewers(int id, String q, int n) {
+ public static RestApi suggestReviewers(int id, String q, int n, boolean e) {
return change(id).view("suggest_reviewers")
.addParameter("q", q)
- .addParameter("n", n);
+ .addParameter("n", n)
+ .addParameter("e", e);
}
public static RestApi vote(int id, int reviewer, String vote) {
@@ -178,12 +192,14 @@
public static RestApi hashtags(int changeId) {
return change(changeId).view("hashtags");
}
+
public static RestApi hashtag(int changeId, String hashtag) {
return change(changeId).view("hashtags").id(hashtag);
}
/** Submit a specific revision of a change. */
- public static void cherrypick(int id, String commit, String destination, String message, AsyncCallback<ChangeInfo> cb) {
+ public static void cherrypick(int id, String commit, String destination,
+ String message, AsyncCallback<ChangeInfo> cb) {
CherryPickInput cherryPickInput = CherryPickInput.create();
cherryPickInput.setMessage(message);
cherryPickInput.setDestination(destination);
@@ -199,13 +215,15 @@
}
/** Submit a specific revision of a change. */
- public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
+ public static void submit(int id, String commit,
+ AsyncCallback<SubmitInfo> cb) {
JavaScriptObject in = JavaScriptObject.createObject();
call(id, commit, "submit").post(in, cb);
}
/** Publish a specific revision of a draft change. */
- public static void publish(int id, String commit, AsyncCallback<JavaScriptObject> cb) {
+ public static void publish(int id, String commit,
+ AsyncCallback<JavaScriptObject> cb) {
JavaScriptObject in = JavaScriptObject.createObject();
call(id, commit, "publish").post(in, cb);
}
@@ -216,7 +234,8 @@
}
/** Delete a specific draft patch set. */
- public static void deleteRevision(int id, String commit, AsyncCallback<JavaScriptObject> cb) {
+ public static void deleteRevision(int id, String commit,
+ AsyncCallback<JavaScriptObject> cb) {
revision(id, commit).delete(cb);
}
@@ -238,21 +257,43 @@
}
/** Rebase a revision onto the branch tip or another change. */
- public static void rebase(int id, String commit, String base, AsyncCallback<ChangeInfo> cb) {
+ public static void rebase(int id, String commit, String base,
+ AsyncCallback<ChangeInfo> cb) {
RebaseInput rebaseInput = RebaseInput.create();
rebaseInput.setBase(base);
call(id, commit, "rebase").post(rebaseInput, cb);
}
- private static class Input extends JavaScriptObject {
- final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
+ private static class MessageInput extends JavaScriptObject {
final native void message(String m) /*-{ if(m)this.message=m; }-*/;
- static Input create() {
- return (Input) createObject();
+ static MessageInput create() {
+ return (MessageInput) createObject();
}
- protected Input() {
+ protected MessageInput() {
+ }
+ }
+
+ private static class AssigneeInput extends JavaScriptObject {
+ final native void assignee(String a) /*-{ if(a)this.assignee=a; }-*/;
+
+ static AssigneeInput create() {
+ return (AssigneeInput) createObject();
+ }
+
+ protected AssigneeInput() {
+ }
+ }
+
+ private static class TopicInput extends JavaScriptObject {
+ final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
+
+ static TopicInput create() {
+ return (TopicInput) createObject();
+ }
+
+ protected TopicInput() {
}
}
@@ -265,8 +306,9 @@
public final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
- public final native void baseChange(String b) /*-{ if(b)this.base_change=b; }-*/;
- public final native void status(String s) /*-{ if(s)this.status=s; }-*/;
+ public final native void status(String s) /*-{ if(s)this.status=s; }-*/;
+ public final native void baseChange(
+ String b) /*-{ if(b)this.base_change=b; }-*/;
protected CreateChangeInput() {
}
@@ -276,7 +318,9 @@
static CherryPickInput create() {
return (CherryPickInput) createObject();
}
+
final native void setDestination(String d) /*-{ this.destination = d; }-*/;
+
final native void setMessage(String m) /*-{ this.message = m; }-*/;
protected CherryPickInput() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index b2334d1d..1c3026c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -37,6 +37,7 @@
String changeTableColumnSize();
String changeTableColumnStatus();
String changeTableColumnOwner();
+ String changeTableColumnAssignee();
String changeTableColumnProject();
String changeTableColumnBranch();
String changeTableColumnLastUpdate();
@@ -63,11 +64,14 @@
String patchTableColumnComments();
String patchTableColumnSize();
String commitMessage();
+ String mergeList();
String patchTablePrev();
String patchTableNext();
String patchTableOpenDiff();
+ String approvalTableEditAssigneeHint();
+
String approvalTableAddReviewerHint();
String approvalTableAddManyReviewersConfirmationDialogTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index b7e2677..01921de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -18,6 +18,7 @@
changeTableColumnSize = Size
changeTableColumnStatus = Status
changeTableColumnOwner = Owner
+changeTableColumnAssignee = Assignee
changeTableColumnProject = Project
changeTableColumnBranch = Branch
changeTableColumnLastUpdate = Updated
@@ -40,16 +41,18 @@
keyExpandAllMessages = Expand all messages
keyCollapseAllMessages = Collapse all messages
-
patchTableColumnName = File Path
patchTableColumnComments = Comments
patchTableColumnSize = Size
commitMessage = Commit Message
+mergeList = Merge List
patchTablePrev = Previous file
patchTableNext = Next file
patchTableOpenDiff = Open diff
+approvalTableEditAssigneeHint = Name or Email
+
approvalTableAddReviewerHint = Name or Email or Group
approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 9c78955..86b2a82 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -49,6 +49,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
public class ChangeTable extends NavigationTable<ChangeInfo> {
@@ -63,14 +64,16 @@
private static final int C_SUBJECT = 3;
private static final int C_STATUS = 4;
private static final int C_OWNER = 5;
- private static final int C_PROJECT = 6;
- private static final int C_BRANCH = 7;
- private static final int C_LAST_UPDATE = 8;
- private static final int C_SIZE = 9;
- private static final int BASE_COLUMNS = 10;
+ private static final int C_ASSIGNEE = 6;
+ private static final int C_PROJECT = 7;
+ private static final int C_BRANCH = 8;
+ private static final int C_LAST_UPDATE = 9;
+ private static final int C_SIZE = 10;
+ private static final int BASE_COLUMNS = 11;
private final List<Section> sections;
private int columns;
+ private final boolean showAssignee;
private final boolean showLegacyId;
private List<String> labelNames;
@@ -78,6 +81,7 @@
super(Util.C.changeItemHelp());
columns = BASE_COLUMNS;
labelNames = Collections.emptyList();
+ showAssignee = Gerrit.info().change().showAssignee();
showLegacyId = Gerrit.getUserPreferences().legacycidInChangeTable();
if (Gerrit.isSignedIn()) {
@@ -90,6 +94,7 @@
table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
+ table.setText(0, C_ASSIGNEE, Util.C.changeTableColumnAssignee());
table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
@@ -103,6 +108,9 @@
if (!showLegacyId) {
fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().dataHeaderHidden());
}
+ if (!showAssignee) {
+ fmt.addStyleName(0, C_ASSIGNEE, Gerrit.RESOURCES.css().dataHeaderHidden());
+ }
table.addClickHandler(new ClickHandler() {
@Override
@@ -163,6 +171,9 @@
fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
fmt.addStyleName(row, C_STATUS, Gerrit.RESOURCES.css().cSTATUS());
fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
+ fmt.addStyleName(row, C_ASSIGNEE,
+ showAssignee ? Gerrit.RESOURCES.css().cASSIGNEE()
+ : Gerrit.RESOURCES.css().dataCellHidden());
fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
fmt.addStyleName(row, C_SIZE, Gerrit.RESOURCES.css().cSIZE());
@@ -232,13 +243,30 @@
}
if (c.owner() != null) {
- table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
+ table.setWidget(row, C_OWNER,
+ AccountLinkPanel.withStatus(c.owner(), status));
} else {
table.setText(row, C_OWNER, "");
}
+ if (showAssignee) {
+ if (c.assignee() != null) {
+ table.setWidget(row, C_ASSIGNEE,
+ AccountLinkPanel.forAssignee(c.assignee()));
+ if (Gerrit.getUserPreferences().highlightAssigneeInChangeTable()
+ && Objects.equals(c.assignee().getId(),
+ Gerrit.getUserAccount().getId())) {
+ table.getRowFormatter().addStyleName(row,
+ Gerrit.RESOURCES.css().cASSIGNEDTOME());
+ }
+ } else {
+ table.setText(row, C_ASSIGNEE, "");
+ }
+ }
+
table.setWidget(row, C_PROJECT, new ProjectLink(c.projectNameKey()));
- table.setWidget(row, C_BRANCH, new BranchLink(c.projectNameKey(), c
+ table.setWidget(row, C_BRANCH,
+ new BranchLink(c.projectNameKey(), c
.status(), c.branch(), c.topic()));
if (Gerrit.getUserPreferences().relativeDateInChangeTable()) {
table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 2f3ead3..4c01941 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.patches.SkippedLine;
@@ -40,7 +41,7 @@
/** Tracks comment widgets for {@link DiffScreen}. */
abstract class CommentManager {
- private final PatchSet.Id base;
+ private final DiffObject base;
private final PatchSet.Id revision;
private final String path;
private final CommentLinkProcessor commentLinkProcessor;
@@ -55,7 +56,7 @@
CommentManager(
DiffScreen host,
- PatchSet.Id base,
+ DiffObject base,
PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
@@ -129,29 +130,30 @@
}
Side getStoredSideFromDisplaySide(DisplaySide side) {
- if (side == DisplaySide.A && (base == null || base.get() < 0)) {
+ if (side == DisplaySide.A && base.isBaseOrAutoMerge() || base.isParent()) {
return Side.PARENT;
}
return Side.REVISION;
}
int getParentNumFromDisplaySide(DisplaySide side) {
- if (side == DisplaySide.A && base != null && base.get() < 0) {
- return -base.get();
+ if (side == DisplaySide.A) {
+ return base.getParentNum();
}
return 0;
}
PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
- if (side == DisplaySide.A && base != null && base.get() >= 0) {
- return base;
+ if (side == DisplaySide.A && (base.isPatchSet() || base.isEdit())) {
+ return base.asPatchSetId();
}
return revision;
}
DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
if (info.side() == Side.PARENT) {
- return (base == null || base.get() < 0) ? DisplaySide.A : null;
+ return (base.isBaseOrAutoMerge() || base.isParent())
+ ? DisplaySide.A : null;
}
return forSide;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index ce1d294..0b8e141 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
@@ -31,7 +32,7 @@
/** Collection of published and draft comments loaded from the server. */
class CommentsCollections {
private final String path;
- private final PatchSet.Id base;
+ private final DiffObject base;
private final PatchSet.Id revision;
private NativeMap<JsArray<CommentInfo>> publishedBaseAll;
private NativeMap<JsArray<CommentInfo>> publishedRevisionAll;
@@ -40,28 +41,28 @@
JsArray<CommentInfo> draftsBase;
JsArray<CommentInfo> draftsRevision;
- CommentsCollections(PatchSet.Id base, PatchSet.Id revision, String path) {
+ CommentsCollections(DiffObject base, PatchSet.Id revision, String path) {
this.path = path;
this.base = base;
this.revision = revision;
}
void load(CallbackGroup group) {
- if (base != null && base.get() > 0) {
- CommentApi.comments(base, group.add(publishedBase()));
+ if (base.isPatchSet()) {
+ CommentApi.comments(base.asPatchSetId(), group.add(publishedBase()));
}
CommentApi.comments(revision, group.add(publishedRevision()));
if (Gerrit.isSignedIn()) {
- if (base != null && base.get() > 0) {
- CommentApi.drafts(base, group.add(draftsBase()));
+ if (base.isPatchSet()) {
+ CommentApi.drafts(base.asPatchSetId(), group.add(draftsBase()));
}
CommentApi.drafts(revision, group.add(draftsRevision()));
}
}
boolean hasCommentForPath(String filePath) {
- if (base != null && base.get() > 0) {
+ if (base.isPatchSet()) {
JsArray<CommentInfo> forBase = publishedBaseAll.get(filePath);
if (forBase != null && forBase.length() > 0) {
return true;
@@ -110,9 +111,9 @@
for (CommentInfo c : Natives.asList(list)) {
if (c.side() == Side.REVISION) {
result.push(c);
- } else if (base == null && !c.hasParent()) {
+ } else if (base.isBaseOrAutoMerge() && !c.hasParent()) {
result.push(c);
- } else if (base != null && c.parent() == -base.get()) {
+ } else if (base.isParent() && c.parent() == base.getParentNum()) {
result.push(c);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index 8935e36..a22d4bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
import static java.lang.Double.POSITIVE_INFINITY;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.DiffPreferences;
@@ -96,7 +97,7 @@
}
private final Change.Id changeId;
- final PatchSet.Id base;
+ final DiffObject base;
final PatchSet.Id revision;
final String path;
final DiffPreferences prefs;
@@ -123,15 +124,15 @@
Header header;
DiffScreen(
- PatchSet.Id base,
- PatchSet.Id revision,
+ DiffObject base,
+ DiffObject revision,
String path,
DisplaySide startSide,
int startLine,
DiffView diffScreenType) {
this.base = base;
- this.revision = revision;
- this.changeId = revision.getParentKey();
+ this.revision = revision.asPatchSetId();
+ this.changeId = revision.asPatchSetId().getParentKey();
this.path = path;
this.startSide = startSide;
this.startLine = startLine;
@@ -173,7 +174,7 @@
}));
DiffApi.diff(revision, path)
- .base(base)
+ .base(base.asPatchSetId())
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
@@ -639,7 +640,7 @@
}
private void toggleShowIntraline() {
- prefs.intralineDifference(!prefs.intralineDifference());
+ prefs.intralineDifference(!Boolean.valueOf(prefs.intralineDifference()));
setShowIntraline(prefs.intralineDifference());
prefsAction.update();
}
@@ -789,11 +790,10 @@
group.addListener(new GerritCallback<Void>() {
@Override
public void onSuccess(Void result) {
- String b = base != null ? String.valueOf(base.get()) : null;
String rev = String.valueOf(revision.get());
Gerrit.display(
- PageLinks.toChange(changeId, b, rev),
- new ChangeScreen(changeId, b, rev, openReplyBox,
+ PageLinks.toChange(changeId, base.asString(), rev),
+ new ChangeScreen(changeId, base, rev, openReplyBox,
FileTable.Mode.REVIEW));
}
});
@@ -901,7 +901,7 @@
String nextPath = header.getNextPath();
if (nextPath != null) {
DiffApi.diff(revision, nextPath)
- .base(base)
+ .base(base.asPatchSetId())
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
@@ -924,7 +924,7 @@
void reloadDiffInfo() {
final int id = ++reloadVersionId;
DiffApi.diff(revision, path)
- .base(base)
+ .base(base.asPatchSetId())
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 392ad2f..54b55f04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -66,11 +67,12 @@
private ChangeType changeType;
Scrollbar scrollbar;
- DiffTable(DiffScreen parent, PatchSet.Id base, PatchSet.Id revision, String path) {
- patchSetSelectBoxA = new PatchSetSelectBox(
- parent, DisplaySide.A, revision.getParentKey(), base, path);
- patchSetSelectBoxB = new PatchSetSelectBox(
- parent, DisplaySide.B, revision.getParentKey(), revision, path);
+ DiffTable(DiffScreen parent, DiffObject base, DiffObject revision,
+ String path) {
+ patchSetSelectBoxA = new PatchSetSelectBox(parent, DisplaySide.A,
+ revision.asPatchSetId().getParentKey(), base, path);
+ patchSetSelectBoxB = new PatchSetSelectBox(parent, DisplaySide.B,
+ revision.asPatchSetId().getParentKey(), revision, path);
PatchSetSelectBox.link(patchSetSelectBoxA, patchSetSelectBoxB);
this.scrollbar = new Scrollbar(this);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index f377038..f3b9886 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.DiffPreferences;
@@ -87,7 +88,7 @@
@UiField Image preferences;
private final KeyCommandSet keys;
- private final PatchSet.Id base;
+ private final DiffObject base;
private final PatchSet.Id patchSetId;
private final String path;
private final DiffView diffScreenType;
@@ -99,12 +100,12 @@
private PreferencesAction prefsAction;
private ReviewedState reviewedState;
- Header(KeyCommandSet keys, PatchSet.Id base, PatchSet.Id patchSetId,
+ Header(KeyCommandSet keys, DiffObject base, DiffObject patchSetId,
String path, DiffView diffSreenType, DiffPreferences prefs) {
initWidget(uiBinder.createAndBindUi(this));
this.keys = keys;
this.base = base;
- this.patchSetId = patchSetId;
+ this.patchSetId = patchSetId.asPatchSetId();
this.path = path;
this.diffScreenType = diffSreenType;
this.prefs = prefs;
@@ -113,15 +114,17 @@
reviewed.getElement().getStyle().setVisibility(Visibility.HIDDEN);
}
SafeHtml.setInnerHTML(filePath, formatPath(path));
- up.setTargetHistoryToken(PageLinks.toChange(
- patchSetId.getParentKey(),
- base != null ? base.getId() : null, patchSetId.getId()));
+ up.setTargetHistoryToken(
+ PageLinks.toChange(patchSetId.asPatchSetId().getParentKey(),
+ base.asString(), patchSetId.asPatchSetId().getId()));
}
public static SafeHtml formatPath(String path) {
SafeHtmlBuilder b = new SafeHtmlBuilder();
if (Patch.COMMIT_MSG.equals(path)) {
return b.append(Util.C.commitMessage());
+ } else if (Patch.MERGE_LIST.equals(path)) {
+ return b.append(Util.C.mergeList());
}
int s = path.lastIndexOf('/') + 1;
@@ -145,16 +148,17 @@
@Override
protected void onLoad() {
- DiffApi.list(patchSetId, base, new GerritCallback<NativeMap<FileInfo>>() {
- @Override
- public void onSuccess(NativeMap<FileInfo> result) {
- files = result.values();
- FileInfo.sortFileInfoByPath(files);
- fileNumber.setInnerText(
- Integer.toString(Natives.asList(files).indexOf(result.get(path)) + 1));
- fileCount.setInnerText(Integer.toString(files.length()));
- }
- });
+ DiffApi.list(patchSetId, base.asPatchSetId(),
+ new GerritCallback<NativeMap<FileInfo>>() {
+ @Override
+ public void onSuccess(NativeMap<FileInfo> result) {
+ files = result.values();
+ FileInfo.sortFileInfoByPath(files);
+ fileNumber.setInnerText(Integer
+ .toString(Natives.asList(files).indexOf(result.get(path)) + 1));
+ fileCount.setInnerText(Integer.toString(files.length()));
+ }
+ });
if (Gerrit.isSignedIn()) {
ChangeApi.revision(patchSetId).view("files")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index bc37abb..b07a199 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.blame.BlameInfo;
@@ -67,13 +68,13 @@
private String path;
private Change.Id changeId;
private PatchSet.Id revision;
- private PatchSet.Id idActive;
+ private DiffObject idActive;
private PatchSetSelectBox other;
PatchSetSelectBox(DiffScreen parent,
DisplaySide side,
Change.Id changeId,
- PatchSet.Id revision,
+ DiffObject diffObject,
String path) {
initWidget(uiBinder.createAndBindUi(this));
icon.setTitle(PatchUtil.C.addFileCommentToolTip());
@@ -83,8 +84,8 @@
this.side = side;
this.sideA = side == DisplaySide.A;
this.changeId = changeId;
- this.revision = revision;
- this.idActive = (sideA && revision == null) ? null : revision;
+ this.revision = diffObject.asPatchSetId();
+ this.idActive = diffObject;
this.path = path;
}
@@ -93,19 +94,22 @@
InlineHyperlink selectedLink = null;
if (sideA) {
if (parents <= 1) {
- InlineHyperlink link = createLink(PatchUtil.C.patchBase(), null);
+ InlineHyperlink link =
+ createLink(PatchUtil.C.patchBase(), DiffObject.base());
linkPanel.add(link);
selectedLink = link;
} else {
for (int i = parents; i > 0; i--) {
PatchSet.Id id = new PatchSet.Id(changeId, -i);
- InlineHyperlink link = createLink(Util.M.diffBaseParent(i), id);
+ InlineHyperlink link =
+ createLink(Util.M.diffBaseParent(i), DiffObject.patchSet(id));
linkPanel.add(link);
if (revision != null && id.equals(revision)) {
selectedLink = link;
}
}
- InlineHyperlink link = createLink(Util.C.autoMerge(), null);
+ InlineHyperlink link =
+ createLink(Util.C.autoMerge(), DiffObject.autoMerge());
linkPanel.add(link);
if (selectedLink == null) {
selectedLink = link;
@@ -115,7 +119,7 @@
for (int i = 0; i < list.length(); i++) {
RevisionInfo r = list.get(i);
InlineHyperlink link = createLink(r.id(),
- new PatchSet.Id(changeId, r._number()));
+ DiffObject.patchSet(new PatchSet.Id(changeId, r._number())));
linkPanel.add(link);
if (revision != null && r.id().equals(revision.getId())) {
selectedLink = link;
@@ -128,11 +132,11 @@
if (meta == null) {
return;
}
- if (!Patch.COMMIT_MSG.equals(path)) {
+ if (!Patch.isMagic(path)) {
linkPanel.add(createDownloadLink());
}
- if (!binary && open && idActive != null && Gerrit.isSignedIn()) {
- if ((editExists && idActive.get() == 0)
+ if (!binary && open && !idActive.isBaseOrAutoMerge() && Gerrit.isSignedIn()) {
+ if ((editExists && idActive.isEdit())
|| (!editExists && current)) {
linkPanel.add(createEditIcon());
}
@@ -147,7 +151,7 @@
void setUpBlame(final CodeMirror cm, final boolean isBase,
final PatchSet.Id rev, final String path) {
- if (!Patch.COMMIT_MSG.equals(path) && Gerrit.isSignedIn()
+ if (!Patch.isMagic(path) && Gerrit.isSignedIn()
&& Gerrit.info().change().allowBlame()) {
Anchor blameIcon = createBlameIcon();
blameIcon.addClickHandler(new ClickHandler() {
@@ -172,7 +176,9 @@
}
private Widget createEditIcon() {
- PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
+ PatchSet.Id id = idActive.isBaseOrAutoMerge()
+ ? other.idActive.asPatchSetId()
+ : idActive.asPatchSetId();
Anchor anchor = new Anchor(
new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()),
"#" + Dispatcher.toEditScreen(id, path));
@@ -192,27 +198,29 @@
b.other = a;
}
- private InlineHyperlink createLink(String label, PatchSet.Id id) {
+ private InlineHyperlink createLink(String label, DiffObject id) {
assert other != null;
if (sideA) {
- assert other.idActive != null;
+ assert !other.idActive.isBaseOrAutoMerge();
}
- PatchSet.Id diffBase = sideA ? id : other.idActive;
- PatchSet.Id revision = sideA ? other.idActive : id;
+ DiffObject diffBase = sideA ? id : other.idActive;
+ DiffObject revision = sideA ? other.idActive : id;
return new InlineHyperlink(label,
parent.isSideBySide()
- ? Dispatcher.toSideBySide(diffBase, revision, path)
- : Dispatcher.toUnified(diffBase, revision, path));
+ ? Dispatcher.toSideBySide(diffBase, revision.asPatchSetId(), path)
+ : Dispatcher.toUnified(diffBase, revision.asPatchSetId(), path));
}
private Anchor createDownloadLink() {
- PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
- String sideURL = (idActive == null) ? "1" : "0";
+ DiffObject diffObject = idActive.isBaseOrAutoMerge()
+ ? other.idActive : idActive;
+ String sideURL = idActive.isBaseOrAutoMerge() ? "1" : "0";
String base = GWT.getHostPageBaseURL() + "cat/";
Anchor anchor = new Anchor(
new ImageResourceRenderer().render(Gerrit.RESOURCES.downloadIcon()),
- base + KeyUtil.encode(id + "," + path) + "^" + sideURL);
+ base + KeyUtil.encode(diffObject.asPatchSetId() + "," + path) + "^"
+ + sideURL);
anchor.setTitle(PatchUtil.C.download());
return anchor;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index 78d01db..ef1d4bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -260,7 +260,7 @@
@UiHandler("intralineDifference")
void onIntralineDifference(ValueChangeEvent<Boolean> e) {
- prefs.intralineDifference(e.getValue());
+ prefs.intralineDifference(Boolean.valueOf(e.getValue()));
if (view != null) {
view.setShowIntraline(prefs.intralineDifference());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index dbe7e5d..6e2120a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -16,6 +16,7 @@
import static java.lang.Double.POSITIVE_INFINITY;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
@@ -25,7 +26,6 @@
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -69,8 +69,8 @@
private SideBySideCommentManager commentManager;
public SideBySide(
- PatchSet.Id base,
- PatchSet.Id revision,
+ DiffObject base,
+ DiffObject revision,
String path,
DisplaySide startSide,
int startLine) {
@@ -192,9 +192,8 @@
cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA);
cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB);
- boolean reviewingBase = base == null;
- getDiffTable().setUpBlameIconA(cmA, reviewingBase,
- reviewingBase ? revision : base, path);
+ getDiffTable().setUpBlameIconA(cmA, base.isBaseOrAutoMerge(),
+ base.isBaseOrAutoMerge() ? revision : base.asPatchSetId(), path);
getDiffTable().setUpBlameIconB(cmB, revision, path);
cmA.extras().side(DisplaySide.A);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
index bcb7dac..fab6e6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.ui.CommentLinkProcessor;
@@ -29,7 +30,7 @@
/** Tracks comment widgets for {@link SideBySide}. */
class SideBySideCommentManager extends CommentManager {
SideBySideCommentManager(SideBySide host,
- PatchSet.Id base, PatchSet.Id revision,
+ DiffObject base, PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
index 2296796..5e8d7cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -14,8 +14,8 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.CssResource;
@@ -46,7 +46,7 @@
private boolean visibleA;
- SideBySideTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
+ SideBySideTable(SideBySide parent, DiffObject base, DiffObject revision,
String path) {
super(parent, base, revision, path);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
index a231580..566d87c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -16,6 +16,7 @@
import static java.lang.Double.POSITIVE_INFINITY;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.diff.UnifiedChunkManager.LineRegionInfo;
@@ -25,7 +26,6 @@
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
@@ -69,8 +69,8 @@
private boolean autoHideDiffTableHeader;
public Unified(
- PatchSet.Id base,
- PatchSet.Id revision,
+ DiffObject base,
+ DiffObject revision,
String path,
DisplaySide startSide,
int startLine) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
index 8968bc7..21356fc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.diff;
+import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
@@ -43,7 +44,7 @@
private final Map<Integer, CommentGroup> duplicates;
UnifiedCommentManager(Unified host,
- PatchSet.Id base, PatchSet.Id revision,
+ DiffObject base, PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
index 72b3e49..e3317c4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
@@ -14,7 +14,7 @@
package com.google.gerrit.client.diff;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.client.DiffObject;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.CssResource;
@@ -45,7 +45,7 @@
@UiField Element cm;
@UiField static DiffTableStyle style;
- UnifiedTable(Unified parent, PatchSet.Id base, PatchSet.Id revision,
+ UnifiedTable(Unified parent, DiffObject base, DiffObject revision,
String path) {
super(parent, base, revision, path);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index da7ca44..490e028 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -99,7 +99,6 @@
String hideBase();
}
- private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
private final int startLine;
@@ -130,8 +129,7 @@
private HandlerRegistration closeHandler;
private int generation;
- public EditScreen(PatchSet.Id base, Patch.Key patch, int startLine) {
- this.base = base;
+ public EditScreen(Patch.Key patch, int startLine) {
this.revision = patch.getParentKey();
this.path = patch.get();
this.startLine = startLine - 1;
@@ -232,7 +230,6 @@
// TODO(davido): We probably want to create dedicated GET EditScreenMeta
// REST endpoint. Abuse GET diff for now, as it retrieves links we need.
DiffApi.diff(revision, path)
- .base(base)
.webLinksOnly()
.get(group1.addFinal(new AsyncCallback<DiffInfo>() {
@Override
@@ -614,7 +611,7 @@
sbs.setHTML(new ImageResourceRenderer()
.render(Gerrit.RESOURCES.sideBySideDiff()));
sbs.setTargetHistoryToken(
- Dispatcher.toPatch("sidebyside", base, new Patch.Key(revision, path)));
+ Dispatcher.toPatch("sidebyside", null, new Patch.Key(revision, path)));
sbs.setTitle(PatchUtil.C.sideBySideDiff());
linkPanel.add(sbs);
@@ -622,7 +619,7 @@
unified.setHTML(new ImageResourceRenderer()
.render(Gerrit.RESOURCES.unifiedDiff()));
unified.setTargetHistoryToken(
- Dispatcher.toPatch("unified", base, new Patch.Key(revision, path)));
+ Dispatcher.toPatch("unified", null, new Patch.Key(revision, path)));
unified.setTitle(PatchUtil.C.unifiedDiff());
linkPanel.add(unified);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 4190672..4076296 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -449,6 +449,11 @@
white-space: nowrap;
}
+.changeTable .cASSIGNEDTOME {
+ background: #ffe9d6 !important;
+}
+
+.changeTable .cASSIGNEE,
.changeTable .cOWNER,
.changeTable .cSTATUS {
white-space: nowrap;
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/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index eb3b1ff..dd9f369 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -22,30 +22,44 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.FlowPanel;
+import java.util.function.Function;
+
/** Link to any user's account dashboard. */
public class AccountLinkPanel extends FlowPanel {
- public AccountLinkPanel(AccountInfo info) {
- this(info, Change.Status.NEW);
+ public static AccountLinkPanel create(AccountInfo ai) {
+ return withStatus(ai, Change.Status.NEW);
}
- public AccountLinkPanel(AccountInfo info, Change.Status status) {
+ public static AccountLinkPanel withStatus(AccountInfo ai,
+ Change.Status status) {
+ return new AccountLinkPanel(
+ ai, name -> PageLinks.toAccountQuery(name, status));
+ }
+
+ public static AccountLinkPanel forAssignee(AccountInfo ai) {
+ return new AccountLinkPanel(ai, PageLinks::toAssigneeQuery);
+ }
+
+ private AccountLinkPanel(AccountInfo ai,
+ Function<String, String> nameToQuery) {
addStyleName(Gerrit.RESOURCES.css().accountLinkPanel());
InlineHyperlink l =
- new InlineHyperlink(FormatUtil.name(info), PageLinks.toAccountQuery(
- owner(info), status)) {
- @Override
- public void go() {
- Gerrit.display(getTargetHistoryToken());
- }
- };
- l.setTitle(FormatUtil.nameEmail(info));
+ new InlineHyperlink(
+ FormatUtil.name(ai),
+ nameToQuery.apply(name(ai))) {
+ @Override
+ public void go() {
+ Gerrit.display(getTargetHistoryToken());
+ }
+ };
+ l.setTitle(FormatUtil.nameEmail(ai));
- add(new AvatarImage(info));
+ add(new AvatarImage(ai));
add(l);
}
- public static String owner(AccountInfo ai) {
+ private static String name(AccountInfo ai) {
if (ai.email() != null) {
return ai.email();
} else if (ai.name() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
index 62b8f2e..084cb9a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
@@ -134,4 +134,8 @@
public HandlerRegistration addCloseHandler(CloseHandler<RemoteSuggestBox> h) {
return addHandler(h, CloseEvent.getType());
}
+
+ public void selectAll() {
+ suggestBox.getValueBox().selectAll();
+ }
}
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/BUCK b/gerrit-httpd/BUCK
index d52963a..0b0499c 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -73,7 +73,6 @@
'//lib/jgit/org.eclipse.jgit.junit:junit',
'//lib/joda:joda-time',
],
- source_under_test = [':httpd'],
# TODO(sop) Remove after Buck supports Eclipse
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index fa2e0e3..f34f488 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -17,6 +17,7 @@
import static java.util.concurrent.TimeUnit.HOURS;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.httpd.WebSessionManager.Key;
import com.google.gerrit.httpd.WebSessionManager.Val;
@@ -109,6 +110,7 @@
}
@Override
+ @Nullable
public String getXGerritAuth() {
return isSignedIn() ? val.getAuth() : null;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
deleted file mode 100644
index c1a0f44..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import org.eclipse.jgit.lib.Config;
-
-public class GerritOptions {
- private final boolean headless;
- private final boolean slave;
- private final boolean enablePolyGerrit;
- private final boolean forcePolyGerritDev;
-
- public GerritOptions(Config cfg, boolean headless, boolean slave,
- boolean forcePolyGerritDev) {
- this.headless = headless;
- this.slave = slave;
- this.enablePolyGerrit = forcePolyGerritDev
- || cfg.getBoolean("gerrit", null, "enablePolyGerrit", false);
- this.forcePolyGerritDev = forcePolyGerritDev;
- }
-
- public boolean enableDefaultUi() {
- return !headless && !enablePolyGerrit;
- }
-
- public boolean enableMasterFeatures() {
- return !slave;
- }
-
- public boolean enablePolyGerrit() {
- return !headless && enablePolyGerrit;
- }
-
- public boolean forcePolyGerritDev() {
- return !headless && forcePolyGerritDev;
- }
-}
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/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index b06f370..fab0aeb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -19,6 +19,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.AccessPath;
@@ -141,12 +142,16 @@
return false;
}
- if (!authConfig.isLdapAuthType()
- && !passwordMatchesTheUserGeneratedOne(who, username, password)) {
- log.warn("Authentication failed for " + username
- + ": password does not match the one stored in Gerrit");
- rsp.sendError(SC_UNAUTHORIZED);
- return false;
+ GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
+ if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
+ || gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
+ if (passwordMatchesTheUserGeneratedOne(who, username, password)) {
+ return succeedAuthentication(who);
+ }
+ }
+
+ if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP) {
+ return failAuthentication(rsp, username);
}
AuthRequest whoAuth = AuthRequest.forUser(username);
@@ -158,8 +163,7 @@
return true;
} catch (NoSuchUserException e) {
if (password.equals(who.getPassword(who.getUserName()))) {
- setUserIdentified(who.getAccount().getId());
- return true;
+ return succeedAuthentication(who);
}
log.warn("Authentication failed for " + username, e);
rsp.sendError(SC_UNAUTHORIZED);
@@ -175,6 +179,19 @@
}
}
+ private boolean succeedAuthentication(final AccountState who) {
+ setUserIdentified(who.getAccount().getId());
+ return true;
+ }
+
+ private boolean failAuthentication(Response rsp, String username)
+ throws IOException {
+ log.warn("Authentication failed for {}: password does not match the one"
+ + " stored in Gerrit", username);
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
private void setUserIdentified(Account.Id id) {
WebSession ws = session.get();
ws.setUserAccountId(id);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 210800d..7e71639 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -90,7 +90,10 @@
}
CurrentUser self = session.get().getUser();
- if (!self.getCapabilities().canRunAs()) {
+ if (!self.getCapabilities().canRunAs()
+ // Always disallow for anonymous users, even if permitted by the ACL,
+ // because that would be crazy.
+ || !self.isIdentifiedUser()) {
replyError(req, res,
SC_FORBIDDEN,
"not permitted to use " + RUN_AS,
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..18445b3 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,10 +31,10 @@
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;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gwtexpui.server.CacheControlFilter;
import com.google.inject.Key;
import com.google.inject.Provider;
@@ -62,8 +63,9 @@
filter("/*").through(Key.get(CacheControlFilter.class));
bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
- if (options.enableDefaultUi()) {
+ if (options.enableGwtUi()) {
filter("/").through(XsrfCookieFilter.class);
+ filter("/accounts/self/detail").through(XsrfCookieFilter.class);
serve("/").with(HostPageServlet.class);
serve("/Gerrit").with(LegacyGerritServlet.class);
serve("/Gerrit/*").with(legacyGerritScreen());
@@ -96,7 +98,17 @@
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
filter("/a/*").through(RequireIdentifiedUserFilter.class);
+
+ // Must be after RequireIdentifiedUserFilter so auth happens before checking
+ // for RunAs capability.
+ install(new RunAsFilter.Module());
+
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/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 3e3b7c4..d5a4b00 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -25,6 +25,7 @@
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.GitwebCgiConfig;
import com.google.gerrit.server.git.AsyncReceiveCommits;
@@ -55,8 +56,6 @@
bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
bind(HttpRequestContext.class);
- install(new RunAsFilter.Module());
-
installAuthModule();
if (options.enableMasterFeatures()) {
install(new UrlModule(options, authConfig));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 327aaa3..cddd04f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -14,6 +14,7 @@
package com.google.gerrit.httpd;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath;
@@ -22,7 +23,7 @@
public interface WebSession {
boolean isSignedIn();
- String getXGerritAuth();
+ @Nullable String getXGerritAuth();
boolean isValidXGerritAuth(String keyIn);
AccountExternalId.Key getLastLoginExternalId();
CurrentUser getUser();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/XsrfCookieFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/XsrfCookieFilter.java
index 842b2b4..0c2565c2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/XsrfCookieFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd;
+import static com.google.common.base.Strings.nullToEmpty;
+
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.CurrentUser;
@@ -61,11 +63,11 @@
private void setXsrfTokenCookie(HttpServletRequest req,
HttpServletResponse rsp, WebSession session) {
- String v = session != null ? session.getXGerritAuth() : "";
- Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, v);
+ String v = session != null ? session.getXGerritAuth() : null;
+ Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, nullToEmpty(v));
c.setPath("/");
c.setSecure(authConfig.getCookieSecure() && isSecure(req));
- c.setMaxAge(session != null
+ c.setMaxAge(v != null
? -1 // Set the cookie for this browser session.
: 0); // Remove the cookie (expire immediately).
rsp.addCookie(c);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index fe556ac..ac7c7e7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -609,45 +609,39 @@
final OutputStream dst) throws IOException {
final int contentLength = req.getContentLength();
final InputStream src = req.getInputStream();
- new Thread(new Runnable() {
- @Override
- public void run() {
+ new Thread(() -> {
+ try {
try {
- try {
- final byte[] buf = new byte[bufferSize];
- int remaining = contentLength;
- while (0 < remaining) {
- final int max = Math.max(buf.length, remaining);
- final int n = src.read(buf, 0, max);
- if (n < 0) {
- throw new EOFException("Expected " + remaining + " more bytes");
- }
- dst.write(buf, 0, n);
- remaining -= n;
+ final byte[] buf = new byte[bufferSize];
+ int remaining = contentLength;
+ while (0 < remaining) {
+ final int max = Math.max(buf.length, remaining);
+ final int n = src.read(buf, 0, max);
+ if (n < 0) {
+ throw new EOFException("Expected " + remaining + " more bytes");
}
- } finally {
- dst.close();
+ dst.write(buf, 0, n);
+ remaining -= n;
}
- } catch (IOException e) {
- log.debug("Unexpected error copying input to CGI", e);
+ } finally {
+ dst.close();
}
+ } catch (IOException e) {
+ log.debug("Unexpected error copying input to CGI", e);
}
}, "Gitweb-InputFeeder").start();
}
private void copyStderrToLog(final InputStream in) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- try (BufferedReader br =
- new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
- String line;
- while ((line = br.readLine()) != null) {
- log.error("CGI: " + line);
- }
- } catch (IOException e) {
- log.debug("Unexpected error copying stderr from CGI", e);
+ new Thread(() -> {
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(in, ISO_8859_1.name()))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ log.error("CGI: " + line);
}
+ } catch (IOException e) {
+ log.debug("Unexpected error copying stderr from CGI", e);
}
}, "Gitweb-ErrorLogger").start();
}
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..cd70143 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
@@ -18,14 +18,13 @@
import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
import com.google.common.base.CharMatcher;
import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
@@ -74,6 +73,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
+import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -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('-', ' ');
}
@@ -379,34 +378,30 @@
List<PluginEntry> docs = new ArrayList<>();
PluginEntry about = null;
- Predicate<PluginEntry> filter = new Predicate<PluginEntry>() {
- @Override
- public boolean apply(PluginEntry entry) {
- String name = entry.getName();
- Optional<Long> size = entry.getSize();
- if (name.startsWith(prefix)
- && (name.endsWith(".md") || name.endsWith(".html"))
- && size.isPresent()) {
- if (size.get() <= 0 || size.get() > SMALL_RESOURCE) {
- log.warn(String.format(
- "Plugin %s: %s omitted from document index. "
- + "Size %d out of range (0,%d).",
- pluginName,
- name.substring(prefix.length()),
- size.get(),
- SMALL_RESOURCE));
- return false;
+ Predicate<PluginEntry> filter =
+ entry -> {
+ String name = entry.getName();
+ Optional<Long> size = entry.getSize();
+ if (name.startsWith(prefix)
+ && (name.endsWith(".md") || name.endsWith(".html"))
+ && size.isPresent()) {
+ if (size.get() <= 0 || size.get() > SMALL_RESOURCE) {
+ log.warn(String.format(
+ "Plugin %s: %s omitted from document index. "
+ + "Size %d out of range (0,%d).",
+ pluginName,
+ name.substring(prefix.length()),
+ size.get(),
+ SMALL_RESOURCE));
+ return false;
+ }
+ return true;
}
- return true;
- }
- return false;
- }
- };
+ return false;
+ };
- List<PluginEntry> entries = FluentIterable
- .from(Collections.list(scanner.entries()))
- .filter(filter)
- .toList();
+ List<PluginEntry> entries = Collections.list(scanner.entries()).stream()
+ .filter(filter).collect(toList());
for (PluginEntry entry: entries) {
String name = entry.getName().substring(prefix.length());
if (name.startsWith("cmd-")) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java
index ef55e34..3e0f833 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java
@@ -36,7 +36,7 @@
} else {
bowerComponents = GerritLauncher
.newZipFileSystem(zip)
- .getPath("bower_components/");
+ .getPath("/");
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index 4f07ac2..c35738b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -294,17 +294,14 @@
}
private Callable<Resource> newLoader(final Path p) {
- return new Callable<Resource>() {
- @Override
- public Resource call() throws IOException {
- try {
- return new Resource(
- getLastModifiedTime(p),
- contentType(p.toString()),
- Files.readAllBytes(p));
- } catch (NoSuchFileException e) {
- return Resource.NOT_FOUND;
- }
+ return () -> {
+ try {
+ return new Resource(
+ getLastModifiedTime(p),
+ contentType(p.toString()),
+ Files.readAllBytes(p));
+ } catch (NoSuchFileException e) {
+ return Resource.NOT_FOUND;
}
};
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
index 7916ed0..31e337e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -15,16 +15,18 @@
package com.google.gerrit.httpd.raw;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.isReadable;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
-import com.google.gerrit.httpd.GerritOptions;
+import com.google.gerrit.extensions.client.UiType;
import com.google.gerrit.httpd.XsrfCookieFilter;
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
@@ -46,8 +48,16 @@
import java.nio.file.FileSystem;
import java.nio.file.Path;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
public class StaticModule extends ServletModule {
@@ -55,20 +65,42 @@
LoggerFactory.getLogger(StaticModule.class);
public static final String CACHE = "static_content";
+ public static final String GERRIT_UI_COOKIE = "GERRIT_UI";
+ /**
+ * Paths at which we should serve the main PolyGerrit application {@code
+ * index.html}.
+ * <p>
+ * Supports {@code "/*"} as a trailing wildcard.
+ */
public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
ImmutableList.of(
- "/",
- "/c/*",
- "/q/*",
- "/x/*",
- "/admin/*",
- "/dashboard/*",
- "/settings/*",
- // TODO(dborowitz): These fragments conflict with the REST API
- // namespace, so they will need to use a different path.
- "/groups/*",
- "/projects/*");
+ "/",
+ "/c/*",
+ "/q/*",
+ "/x/*",
+ "/admin/*",
+ "/dashboard/*",
+ "/settings/*");
+ // TODO(dborowitz): These fragments conflict with the REST API
+ // namespace, so they will need to use a different path.
+ //"/groups/*",
+ //"/projects/*");
+ //
+
+ /**
+ * Paths that should be treated as static assets when serving PolyGerrit.
+ * <p>
+ * Supports {@code "/*"} as a trailing wildcard.
+ */
+ private static final ImmutableList<String> POLYGERRIT_ASSET_PATHS =
+ ImmutableList.of(
+ "/behaviors/*",
+ "/bower_components/*",
+ "/elements/*",
+ "/fonts/*",
+ "/scripts/*",
+ "/styles/*");
private static final String DOC_SERVLET = "DocServlet";
private static final String FAVICON_SERVLET = "FaviconServlet";
@@ -77,6 +109,8 @@
"PolyGerritUiIndexServlet";
private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
+ private static final int GERRIT_UI_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
+
private final GerritOptions options;
private Paths paths;
@@ -85,9 +119,11 @@
this.options = options;
}
+ @Provides
+ @Singleton
private Paths getPaths() {
if (paths == null) {
- paths = new Paths();
+ paths = new Paths(options);
}
return paths;
}
@@ -104,11 +140,13 @@
.weigher(ResourceServlet.Weigher.class);
}
});
+ if (!options.headless()) {
+ install(new CoreStaticModule());
+ }
if (options.enablePolyGerrit()) {
- install(new CoreStaticModule());
- install(new PolyGerritUiModule());
- } else if (options.enableDefaultUi()) {
- install(new CoreStaticModule());
+ install(new PolyGerritModule());
+ }
+ if (options.enableGwtUi()) {
install(new GwtUiModule());
}
}
@@ -211,25 +249,17 @@
}
}
- private class PolyGerritUiModule extends ServletModule {
+ private class PolyGerritModule extends ServletModule {
@Override
public void configureServlets() {
- Path buckOut = getPaths().buckOut;
- if (buckOut != null) {
- serve("/bower_components/*").with(BowerComponentsServlet.class);
- serve("/fonts/*").with(FontsServlet.class);
- } else {
- // In the war case, bower_components and fonts are either inlined
- // by vulcanize, or live under /polygerrit_ui in the war file,
- // so we don't need a separate servlet.
- }
-
- Key<HttpServlet> indexKey = named(POLYGERRIT_INDEX_SERVLET);
for (String p : POLYGERRIT_INDEX_PATHS) {
- filter(p).through(XsrfCookieFilter.class);
- serve(p).with(indexKey);
+ // Skip XsrfCookieFilter for /, since that is already done in the GWT UI
+ // path (UrlModule).
+ if (!p.equals("/")) {
+ filter(p).through(XsrfCookieFilter.class);
+ }
}
- serve("/*").with(PolyGerritUiServlet.class);
+ filter("/*").through(PolyGerritFilter.class);
}
@Provides
@@ -281,13 +311,13 @@
}
}
- private class Paths {
+ private static class Paths {
private final FileSystem warFs;
private final Path buckOut;
private final Path unpackedWar;
private final boolean development;
- private Paths() {
+ private Paths(GerritOptions options) {
try {
File launcherLoadedFrom = getLauncherLoadedFrom();
if (launcherLoadedFrom != null
@@ -393,4 +423,200 @@
private static Key<HttpServlet> named(String name) {
return Key.get(HttpServlet.class, Names.named(name));
}
+
+ @Singleton
+ private static class PolyGerritFilter implements Filter {
+ private final GerritOptions options;
+ private final Paths paths;
+ private final HttpServlet polyGerritIndex;
+ private final PolyGerritUiServlet polygerritUI;
+ private final BowerComponentsServlet bowerComponentServlet;
+ private final FontsServlet fontServlet;
+
+ @Inject
+ PolyGerritFilter(GerritOptions options,
+ Paths paths,
+ @Named(POLYGERRIT_INDEX_SERVLET) HttpServlet polyGerritIndex,
+ PolyGerritUiServlet polygerritUI,
+ BowerComponentsServlet bowerComponentServlet,
+ FontsServlet fontServlet) {
+ this.paths = paths;
+ this.options = options;
+ this.polyGerritIndex = polyGerritIndex;
+ this.polygerritUI = polygerritUI;
+ this.bowerComponentServlet = bowerComponentServlet;
+ this.fontServlet = fontServlet;
+ checkState(options.enablePolyGerrit(),
+ "can't install PolyGerritFilter when PolyGerrit is disabled");
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ HttpServletResponse res = (HttpServletResponse) response;
+ if (handlePolyGerritParam(req, res)) {
+ return;
+ }
+ if (!isPolyGerritEnabled(req)) {
+ chain.doFilter(req, res);
+ return;
+ }
+
+ GuiceFilterRequestWrapper reqWrapper =
+ new GuiceFilterRequestWrapper(req);
+ String path = pathInfo(req);
+
+ // Special case assets during development that are built by Buck and not
+ // served out of the source tree.
+ //
+ // In the war case, these are either inlined by vulcanize, or live under
+ // /polygerrit_ui in the war file, so we can just treat them as normal
+ // assets.
+ if (paths.isDev()) {
+ if (path.startsWith("/bower_components/")) {
+ bowerComponentServlet.service(reqWrapper, res);
+ return;
+ } else if (path.startsWith("/fonts/")) {
+ fontServlet.service(reqWrapper, res);
+ return;
+ }
+ }
+
+ if (isPolyGerritIndex(path)) {
+ polyGerritIndex.service(reqWrapper, res);
+ return;
+ }
+ if (isPolyGerritAsset(path)) {
+ polygerritUI.service(reqWrapper, res);
+ return;
+ }
+
+ chain.doFilter(req, res);
+ }
+
+ private static String pathInfo(HttpServletRequest req) {
+ String uri = req.getRequestURI();
+ String ctx = req.getContextPath();
+ return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
+ }
+
+ private boolean handlePolyGerritParam(HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ if (!options.enableGwtUi()) {
+ return false;
+ }
+ boolean redirect = false;
+ String param = req.getParameter("polygerrit");
+ if ("1".equals(param)) {
+ setPolyGerritCookie(req, res, UiType.POLYGERRIT);
+ redirect = true;
+ } else if ("0".equals(param)) {
+ setPolyGerritCookie(req, res, UiType.GWT);
+ redirect = true;
+ }
+ if (redirect) {
+ // Strip polygerrit param from URL. This actually strips all params,
+ // which is a similar behavior to the JS PolyGerrit redirector code.
+ // Stripping just one param is frustratingly difficult without the use
+ // of Apache httpclient, which is a dep we don't want here:
+ // https://gerrit-review.googlesource.com/#/c/57570/57/gerrit-httpd/BUCK@32
+ res.sendRedirect(req.getRequestURL().toString());
+ }
+ return redirect;
+ }
+
+ private boolean isPolyGerritEnabled(HttpServletRequest req) {
+ return !options.enableGwtUi() || isPolyGerritCookie(req);
+ }
+
+ private boolean isPolyGerritCookie(HttpServletRequest req) {
+ UiType type = options.defaultUi();
+ Cookie[] all = req.getCookies();
+ if (all != null) {
+ for (Cookie c : all) {
+ if (GERRIT_UI_COOKIE.equals(c.getName())) {
+ UiType t = UiType.parse(c.getValue());
+ if (t != null) {
+ type = t;
+ break;
+ }
+ }
+ }
+ }
+ return type == UiType.POLYGERRIT;
+ }
+
+ private void setPolyGerritCookie(HttpServletRequest req,
+ HttpServletResponse res, UiType pref) {
+ // Only actually set a cookie if both UIs are enabled in the server;
+ // otherwise clear it.
+ Cookie cookie = new Cookie(GERRIT_UI_COOKIE, pref.name());
+ if (options.enablePolyGerrit() && options.enableGwtUi()) {
+ cookie.setPath("/");
+ cookie.setSecure(isSecure(req));
+ cookie.setMaxAge(GERRIT_UI_COOKIE_MAX_AGE);
+ } else {
+ cookie.setValue("");
+ cookie.setMaxAge(0);
+ }
+ res.addCookie(cookie);
+ }
+
+ private static boolean isSecure(HttpServletRequest req) {
+ return req.isSecure() || "https".equals(req.getScheme());
+ }
+
+ private static boolean isPolyGerritAsset(String path) {
+ return matchPath(POLYGERRIT_ASSET_PATHS, path);
+ }
+
+ private static boolean isPolyGerritIndex(String path) {
+ return matchPath(POLYGERRIT_INDEX_PATHS, path);
+ }
+
+ private static boolean matchPath(Iterable<String> paths, String path) {
+ for (String p : paths) {
+ if (p.endsWith("/*")) {
+ if (path.regionMatches(0, p, 0, p.length() - 1)) {
+ return true;
+ }
+ } else if(p.equals(path)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private static class GuiceFilterRequestWrapper
+ extends HttpServletRequestWrapper {
+ GuiceFilterRequestWrapper(HttpServletRequest req) {
+ super(req);
+ }
+
+ @Override
+ public String getPathInfo() {
+ String uri = getRequestURI();
+ String ctx = getContextPath();
+ // This is a workaround for long standing guice filter bug:
+ // https://github.com/google/guice/issues/807
+ String res = uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
+
+ // Match the logic in the ResourceServlet, that re-add "/"
+ // for null path info
+ if ("/".equals(res)) {
+ return null;
+ }
+ return res;
+ }
+ }
}
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..c1a3eec 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,9 +15,18 @@
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;
+import static java.util.stream.Collectors.joining;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
@@ -33,11 +42,12 @@
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import com.google.common.base.CharMatcher;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
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 +95,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 +114,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 +143,8 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
import java.util.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,74 @@
}
}
+ 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 =
+ StreamSupport.stream(
+ Splitter.on(',').trimResults().split(headers).spliterator(),
+ false)
+ .filter(h -> !ALLOWED_CORS_REQUEST_HEADERS.contains(h))
+ .findFirst()
+ .orElse(null);
+ if (badHeader != null) {
+ throw new BadRequestException(badHeader + " not allowed in CORS");
+ }
+ }
+
+ 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 +539,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 +570,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:
@@ -935,16 +1036,12 @@
} else if (r.isEmpty()) {
throw new ResourceNotFoundException(projection);
} else {
- throw new AmbiguousViewException(String.format(
- "Projection %s is ambiguous: %s",
- name,
- Joiner.on(", ").join(
- Iterables.transform(r.keySet(), new Function<String, String>() {
- @Override
- public String apply(String in) {
- return in + "~" + projection;
- }
- }))));
+ throw new AmbiguousViewException(
+ String.format(
+ "Projection %s is ambiguous: %s",
+ name,
+ r.keySet().stream().map(in -> in + "~" + projection)
+ .collect(joining(", "))));
}
}
@@ -972,25 +1069,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 +1121,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-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index ed2a4f9..bd88e6a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -14,7 +14,6 @@
package com.google.gerrit.httpd.rpc.project;
-import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AccessSection;
@@ -238,14 +237,7 @@
}
}
}
- return Maps.filterEntries(
- infos,
- new Predicate<Map.Entry<AccountGroup.UUID, GroupInfo>>() {
- @Override
- public boolean apply(Map.Entry<AccountGroup.UUID, GroupInfo> in) {
- return in.getValue() != null;
- }
- });
+ return Maps.filterEntries(infos, in -> in.getValue() != null);
}
private ProjectControl open() throws NoSuchProjectException {
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
index 94f3768..6ccfc38 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -17,7 +17,6 @@
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.newCapture;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
@@ -145,7 +144,7 @@
IMocksControl mockControl = ems.createStrictControl();
FilterChain chain = mockControl.createMock(FilterChain.class);
- Capture<FilterChain> capturedChain = newCapture();
+ Capture<FilterChain> capturedChain = new Capture<>();
AllRequestFilter filter = mockControl.createMock(AllRequestFilter.class);
filter.init(config);
@@ -210,8 +209,8 @@
IMocksControl mockControl = ems.createStrictControl();
FilterChain chain = mockControl.createMock(FilterChain.class);
- Capture<FilterChain> capturedChainA = newCapture();
- Capture<FilterChain> capturedChainB = newCapture();
+ Capture<FilterChain> capturedChainA = new Capture<>();
+ Capture<FilterChain> capturedChainB = new Capture<>();
AllRequestFilter filterA = mockControl.createMock(AllRequestFilter.class);
AllRequestFilter filterB = mockControl.createMock(AllRequestFilter.class);
@@ -252,9 +251,9 @@
IMocksControl mockControl = ems.createStrictControl();
FilterChain chain = mockControl.createMock("chain", FilterChain.class);
- Capture<FilterChain> capturedChainA1 = newCapture();
- Capture<FilterChain> capturedChainA2 = newCapture();
- Capture<FilterChain> capturedChainB = newCapture();
+ Capture<FilterChain> capturedChainA1 = new Capture<>();
+ Capture<FilterChain> capturedChainA2 = new Capture<>();
+ Capture<FilterChain> capturedChainB = new Capture<>();
AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
@@ -308,9 +307,9 @@
IMocksControl mockControl = ems.createStrictControl();
FilterChain chain = mockControl.createMock("chain", FilterChain.class);
- Capture<FilterChain> capturedChainA1 = newCapture();
- Capture<FilterChain> capturedChainB1 = newCapture();
- Capture<FilterChain> capturedChainB2 = newCapture();
+ Capture<FilterChain> capturedChainA1 = new Capture<>();
+ Capture<FilterChain> capturedChainB1 = new Capture<>();
+ Capture<FilterChain> capturedChainB2 = new Capture<>();
AllRequestFilter filterA = mockControl.createMock("filterA", AllRequestFilter.class);
AllRequestFilter filterB = mockControl.createMock("filterB", AllRequestFilter.class);
diff --git a/gerrit-index/BUCK b/gerrit-index/BUCK
new file mode 100644
index 0000000..ea97f88
--- /dev/null
+++ b/gerrit-index/BUCK
@@ -0,0 +1,13 @@
+java_library(
+ name = 'index',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-server:server',
+ '//gerrit-patch-jgit:server',
+ '//lib/guice:guice',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib:guava',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-index/BUILD b/gerrit-index/BUILD
new file mode 100644
index 0000000..119d5c4
--- /dev/null
+++ b/gerrit-index/BUILD
@@ -0,0 +1,13 @@
+java_library(
+ name = 'index',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-server:server',
+ '//gerrit-patch-jgit:server',
+ '//lib/guice:guice',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib:guava',
+ ],
+ visibility = ['//visibility:public'],
+)
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
similarity index 84%
rename from gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
rename to gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
index f43e385..cafd30e 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/GerritIndexStatus.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.lucene;
+package com.google.gerrit.index;
import com.google.common.primitives.Ints;
import com.google.gerrit.server.config.SitePaths;
@@ -24,13 +24,13 @@
import java.io.IOException;
-class GerritIndexStatus {
+public class GerritIndexStatus {
private static final String SECTION = "index";
private static final String KEY_READY = "ready";
private final FileBasedConfig cfg;
- GerritIndexStatus(SitePaths sitePaths)
+ public GerritIndexStatus(SitePaths sitePaths)
throws ConfigInvalidException, IOException {
cfg = new FileBasedConfig(
sitePaths.index_dir.resolve("gerrit_index.config").toFile(),
@@ -39,16 +39,16 @@
convertLegacyConfig();
}
- void setReady(String indexName, int version, boolean ready) {
+ public void setReady(String indexName, int version, boolean ready) {
cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready);
}
- boolean getReady(String indexName, int version) {
+ public boolean getReady(String indexName, int version) {
return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY,
false);
}
- void save() throws IOException {
+ public void save() throws IOException {
cfg.save();
}
@@ -62,8 +62,8 @@
if (ready != null) {
dirty = false;
cfg.unset(SECTION, subsection, KEY_READY);
- cfg.setString(SECTION,
- indexDirName(ChangeSchemaDefinitions.NAME, v), KEY_READY, ready);
+ cfg.setString(SECTION, indexDirName(ChangeSchemaDefinitions.NAME, v),
+ KEY_READY, ready);
}
}
}
diff --git a/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java b/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java
new file mode 100644
index 0000000..f00f5c2
--- /dev/null
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/IndexUtils.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index;
+
+import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
+import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
+import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.QueryOptions;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+public final class IndexUtils {
+ public static final Map<String, String> CUSTOM_CHAR_MAPPING =
+ ImmutableMap.of("_", " ", ".", " ");
+
+ public static void setReady(SitePaths sitePaths, String name, int version,
+ boolean ready) throws IOException {
+ try {
+ GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
+ cfg.setReady(name, version, ready);
+ cfg.save();
+ } catch (ConfigInvalidException e) {
+ throw new IOException(e);
+ }
+ }
+
+ public static Set<String> fields(QueryOptions opts) {
+ // Ensure we request enough fields to construct a ChangeData. We need both
+ // change ID and project, which can either come via the Change field or
+ // separate fields.
+ Set<String> fs = opts.fields();
+ if (fs.contains(CHANGE.getName())) {
+ // A Change is always sufficient.
+ return fs;
+ }
+ if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID.getName())) {
+ return fs;
+ }
+ return Sets.union(fs,
+ ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
+ }
+
+ private IndexUtils() {
+ // hide default constructor
+ }
+}
diff --git a/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java b/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java
new file mode 100644
index 0000000..d547b06
--- /dev/null
+++ b/gerrit-index/src/main/java/com/google/gerrit/index/SingleVersionModule.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.index;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.Schema;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class SingleVersionModule extends LifecycleModule {
+ static final String SINGLE_VERSIONS = "IndexModule/SingleVersions";
+
+ private final Map<String, Integer> singleVersions;
+
+ public SingleVersionModule(Map<String, Integer> singleVersions) {
+ this.singleVersions = singleVersions;
+ }
+
+ @Override
+ public void configure() {
+ listener().to(SingleVersionListener.class);
+ bind(new TypeLiteral<Map<String, Integer>>() {})
+ .annotatedWith(Names.named(SINGLE_VERSIONS))
+ .toInstance(singleVersions);
+ }
+
+ @Singleton
+ static class SingleVersionListener implements LifecycleListener {
+ private final Set<String> disabled;
+ private final Collection<IndexDefinition<?, ?, ?>> defs;
+ private final Map<String, Integer> singleVersions;
+
+ @Inject
+ SingleVersionListener(
+ @GerritServerConfig Config cfg,
+ Collection<IndexDefinition<?, ?, ?>> defs,
+ @Named(SINGLE_VERSIONS) Map<String, Integer> singleVersions) {
+ this.defs = defs;
+ this.singleVersions = singleVersions;
+
+ disabled = ImmutableSet.copyOf(
+ cfg.getStringList("index", null, "testDisable"));
+ }
+
+ @Override
+ public void start() {
+ for (IndexDefinition<?, ?, ?> def : defs) {
+ start(def);
+ }
+ }
+
+ private <K, V, I extends Index<K, V>> void start(
+ IndexDefinition<K, V, I> def) {
+ if (disabled.contains(def.getName())) {
+ return;
+ }
+ Schema<V> schema;
+ Integer v = singleVersions.get(def.getName());
+ if (v == null) {
+ schema = def.getLatest();
+ } else {
+ schema = def.getSchemas().get(v);
+ if (schema == null) {
+ throw new ProvisionException(String.format(
+ "Unrecognized %s schema version: %s", def.getName(), v));
+ }
+ }
+ I index = def.getIndexFactory().create(schema);
+ def.getIndexCollection().setSearchIndex(index);
+ def.getIndexCollection().addWriteIndex(index);
+ }
+
+ @Override
+ public void stop() {
+ // Do nothing; indexes are closed by IndexCollection.
+ }
+ }
+}
\ No newline at end of file
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index bef57d0..a272864 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -55,6 +55,8 @@
private static final String pkg = "com.google.gerrit.pgm";
public static final String NOT_ARCHIVED = "NOT_ARCHIVED";
+ private static ClassLoader daemonClassLoader;
+
public static void main(final String[] argv) throws Exception {
System.exit(mainImpl(argv));
}
@@ -102,6 +104,44 @@
return invokeProgram(cl, argv);
}
+ public static void daemonStart(final String[] argv) throws Exception {
+ if (daemonClassLoader != null) {
+ throw new IllegalStateException(
+ "daemonStart can be called only once per JVM instance");
+ }
+ final ClassLoader cl = libClassLoader(false);
+ Thread.currentThread().setContextClassLoader(cl);
+
+ daemonClassLoader = cl;
+
+ String[] daemonArgv = new String[argv.length + 1];
+ daemonArgv[0] = "daemon";
+ for (int i = 0; i < argv.length; i++) {
+ daemonArgv[i + 1] = argv[i];
+ }
+ int res = invokeProgram(cl, daemonArgv);
+ if (res != 0) {
+ throw new Exception("Unexpected return value: " + res);
+ }
+ }
+
+ public static void daemonStop(final String[] argv) throws Exception {
+ if (daemonClassLoader == null) {
+ throw new IllegalStateException(
+ "daemonStop can be called only after call to daemonStop");
+ }
+ String[] daemonArgv = new String[argv.length + 2];
+ daemonArgv[0] = "daemon";
+ daemonArgv[1] = "--stop-only";
+ for (int i = 0; i < argv.length; i++) {
+ daemonArgv[i + 2] = argv[i];
+ }
+ int res = invokeProgram(daemonClassLoader, daemonArgv);
+ if (res != 0) {
+ throw new Exception("Unexpected return value: " + res);
+ }
+ }
+
private static boolean isProlog(String cn) {
return "PrologShell".equals(cn) || "Rulec".equals(cn);
}
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 771a021..f4f097c 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -27,6 +27,7 @@
'//gerrit-extension-api:api',
'//gerrit-reviewdb:server',
'//gerrit-server:server',
+ '//gerrit-index:index',
'//lib:guava',
'//lib:gwtorm',
'//lib/guice:guice',
diff --git a/gerrit-lucene/BUILD b/gerrit-lucene/BUILD
index 2f1cba7..de010eb 100644
--- a/gerrit-lucene/BUILD
+++ b/gerrit-lucene/BUILD
@@ -25,6 +25,7 @@
'//gerrit-common:annotations',
'//gerrit-common:server',
'//gerrit-extension-api:api',
+ '//gerrit-index:index',
'//gerrit-reviewdb:server',
'//gerrit-server:server',
'//lib:guava',
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index eb0dfaa..e869afb 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -25,6 +25,7 @@
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.index.IndexUtils;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldDef.FillArgs;
@@ -51,7 +52,6 @@
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,17 +75,6 @@
return f.getName() + "_SORT";
}
- public static void setReady(SitePaths sitePaths, String name, int version,
- boolean ready) throws IOException {
- try {
- GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
- cfg.setReady(name, version, ready);
- cfg.save();
- } catch (ConfigInvalidException e) {
- throw new IOException(e);
- }
- }
-
private final Schema<V> schema;
private final SitePaths sitePaths;
private final Directory dir;
@@ -198,7 +187,7 @@
@Override
public void markReady(boolean ready) throws IOException {
- setReady(sitePaths, name, schema.getVersion(), ready);
+ IndexUtils.setReady(sitePaths, name, schema.getVersion(), ready);
}
@Override
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index b5b391e..4775ac4 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -18,17 +18,14 @@
import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
import static com.google.gerrit.lucene.LuceneVersionManager.CHANGES_PREFIX;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import static com.google.gerrit.server.index.change.ChangeField.CHANGE;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
-import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
@@ -36,6 +33,7 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.index.IndexUtils;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -129,8 +127,6 @@
private static final String HASHTAG_FIELD =
ChangeField.HASHTAG_CASE_AWARE.getName();
private static final String STAR_FIELD = ChangeField.STAR.getName();
- @Deprecated
- private static final String STARREDBY_FIELD = ChangeField.STARREDBY.getName();
static Term idTerm(ChangeData cd) {
return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
@@ -320,7 +316,7 @@
throw new OrmException("interrupted");
}
- final Set<String> fields = fields(opts);
+ final Set<String> fields = IndexUtils.fields(opts);
return new ChangeDataResults(
executor.submit(new Callable<List<Document>>() {
@Override
@@ -408,31 +404,6 @@
}
}
- private Set<String> fields(QueryOptions opts) {
- // Ensure we request enough fields to construct a ChangeData.
- Set<String> fs = opts.fields();
- if (fs.contains(CHANGE.getName())) {
- // A Change is always sufficient.
- return fs;
- }
-
- if (!schema.hasField(PROJECT)) {
- // Schema is not new enough to have project field. Ensure we have ID
- // field, and call createOnlyWhenNoteDbDisabled from toChangeData below.
- if (fs.contains(LEGACY_ID.getName())) {
- return fs;
- }
- return Sets.union(fs, ImmutableSet.of(LEGACY_ID.getName()));
- }
-
- // New enough schema to have project field, so ensure that is requested.
- if (fs.contains(PROJECT.getName()) && fs.contains(LEGACY_ID.getName())) {
- return fs;
- }
- return Sets.union(fs,
- ImmutableSet.of(LEGACY_ID.getName(), PROJECT.getName()));
- }
-
private static Multimap<String, IndexableField> fields(Document doc,
Set<String> fields) {
Multimap<String, IndexableField> stored =
@@ -488,9 +459,6 @@
if (fields.contains(HASHTAG_FIELD)) {
decodeHashtags(doc, cd);
}
- if (fields.contains(STARREDBY_FIELD)) {
- decodeStarredBy(doc, cd);
- }
if (fields.contains(STAR_FIELD)) {
decodeStar(doc, cd);
}
@@ -568,17 +536,6 @@
cd.setHashtags(hashtags);
}
- @Deprecated
- private void decodeStarredBy(Multimap<String, IndexableField> doc, ChangeData cd) {
- Collection<IndexableField> starredBy = doc.get(STARREDBY_FIELD);
- Set<Account.Id> accounts =
- Sets.newHashSetWithExpectedSize(starredBy.size());
- for (IndexableField r : starredBy) {
- accounts.add(new Account.Id(r.numericValue().intValue()));
- }
- cd.setStarredBy(accounts);
- }
-
private void decodeStar(Multimap<String, IndexableField> doc, ChangeData cd) {
Collection<IndexableField> star = doc.get(STAR_FIELD);
Multimap<Account.Id, String> stars = ArrayListMultimap.create();
@@ -592,17 +549,12 @@
cd.setStars(stars);
}
- private void decodeReviewers(Multimap<String, IndexableField> doc, ChangeData cd) {
+ private void decodeReviewers(Multimap<String, IndexableField> doc,
+ ChangeData cd) {
cd.setReviewers(
ChangeField.parseReviewerFieldValues(
FluentIterable.from(doc.get(REVIEWER_FIELD))
- .transform(
- new Function<IndexableField, String>() {
- @Override
- public String apply(IndexableField in) {
- return in.stringValue();
- }
- })));
+ .transform(IndexableField::stringValue)));
}
private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index f5d5146..58890176 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -15,37 +15,23 @@
package com.google.gerrit.lucene;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.SingleVersionModule;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexConfig;
-import com.google.gerrit.server.index.IndexDefinition;
import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.inject.Inject;
import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.google.inject.name.Named;
-import com.google.inject.name.Names;
import org.apache.lucene.search.BooleanQuery;
import org.eclipse.jgit.lib.Config;
-import java.util.Collection;
import java.util.Map;
-import java.util.Set;
public class LuceneIndexModule extends LifecycleModule {
- private static final String SINGLE_VERSIONS =
- "LuceneIndexModule/SingleVersions";
-
public static LuceneIndexModule singleVersionAllLatest(int threads) {
return new LuceneIndexModule(ImmutableMap.<String, Integer> of(), threads);
}
@@ -86,7 +72,7 @@
if (singleVersions == null) {
install(new MultiVersionModule());
} else {
- install(new SingleVersionModule());
+ install(new SingleVersionModule(singleVersions));
}
}
@@ -104,66 +90,4 @@
listener().to(LuceneVersionManager.class);
}
}
-
- private class SingleVersionModule extends LifecycleModule {
- @Override
- public void configure() {
- listener().to(SingleVersionListener.class);
- bind(new TypeLiteral<Map<String, Integer>>() {})
- .annotatedWith(Names.named(SINGLE_VERSIONS))
- .toInstance(singleVersions);
- }
- }
-
- @Singleton
- static class SingleVersionListener implements LifecycleListener {
- private final Set<String> disabled;
- private final Collection<IndexDefinition<?, ?, ?>> defs;
- private final Map<String, Integer> singleVersions;
-
- @Inject
- SingleVersionListener(
- @GerritServerConfig Config cfg,
- Collection<IndexDefinition<?, ?, ?>> defs,
- @Named(SINGLE_VERSIONS) Map<String, Integer> singleVersions) {
- this.defs = defs;
- this.singleVersions = singleVersions;
-
- disabled = ImmutableSet.copyOf(
- cfg.getStringList("index", null, "testDisable"));
- }
-
- @Override
- public void start() {
- for (IndexDefinition<?, ?, ?> def : defs) {
- start(def);
- }
- }
-
- private <K, V, I extends Index<K, V>> void start(
- IndexDefinition<K, V, I> def) {
- if (disabled.contains(def.getName())) {
- return;
- }
- Schema<V> schema;
- Integer v = singleVersions.get(def.getName());
- if (v == null) {
- schema = def.getLatest();
- } else {
- schema = def.getSchemas().get(v);
- if (schema == null) {
- throw new ProvisionException(String.format(
- "Unrecognized %s schema version: %s", def.getName(), v));
- }
- }
- I index = def.getIndexFactory().create(schema);
- def.getIndexCollection().setSearchIndex(index);
- def.getIndexCollection().addWriteIndex(index);
- }
-
- @Override
- public void stop() {
- // Do nothing; indexes are closed by IndexCollection.
- }
- }
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index b46f1f6..2f871fc 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -20,6 +20,7 @@
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.GerritIndexStatus;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.Index;
diff --git a/gerrit-main/BUILD b/gerrit-main/BUILD
new file mode 100644
index 0000000..67863c5
--- /dev/null
+++ b/gerrit-main/BUILD
@@ -0,0 +1,13 @@
+java_binary(
+ name = 'main_bin',
+ main_class = 'Main',
+ runtime_deps = [':main_lib'],
+ visibility = ['//visibility:public'],
+)
+
+java_library(
+ name = 'main_lib',
+ srcs = ['src/main/java/Main.java'],
+ deps = ['//gerrit-launcher:launcher'],
+ visibility = ['//visibility:public'],
+)
diff --git a/gerrit-main/src/main/java/Main.java b/gerrit-main/src/main/java/Main.java
index a29f1c6..58de6a4 100644
--- a/gerrit-main/src/main/java/Main.java
+++ b/gerrit-main/src/main/java/Main.java
@@ -31,11 +31,11 @@
private static boolean onSupportedJavaVersion() {
final String version = System.getProperty("java.specification.version");
- if (1.7 <= parse(version)) {
+ if (1.8 <= parse(version)) {
return true;
}
- System.err.println("fatal: Gerrit Code Review requires Java 7 or later");
+ System.err.println("fatal: Gerrit Code Review requires Java 8 or later");
System.err.println(" (trying to run on Java " + version + ")");
return false;
}
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-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
index 09ccf9c..4a4929e 100644
--- a/gerrit-patch-jgit/BUCK
+++ b/gerrit-patch-jgit/BUCK
@@ -33,7 +33,7 @@
'org/eclipse/jgit/diff/Edit.java;' +
'cd $TMP;' +
'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
- out = 'edit.src.zip',
+ out = 'edit-sources.jar',
)
java_library(
@@ -61,6 +61,5 @@
'//lib/jgit/org.eclipse.jgit:jgit',
'//lib:junit',
],
- source_under_test = [':server'],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 4be941c..5f2ef43 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -47,7 +47,7 @@
':init-api',
':util',
'//gerrit-common:annotations',
- '//gerrit-lucene:lucene',
+ '//gerrit-index:index',
'//lib:args4j',
'//lib:derby',
'//lib:gwtjsonrpc',
@@ -66,6 +66,7 @@
REST_UTIL_DEPS = [
'//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
'//gerrit-util-cli:cli',
'//lib:args4j',
'//lib:gwtorm',
@@ -120,6 +121,7 @@
':init-api',
':util',
'//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
'//gerrit-gpg:gpg',
'//gerrit-lucene:lucene',
'//gerrit-oauth:oauth',
@@ -180,5 +182,4 @@
'//lib/jgit/org.eclipse.jgit:jgit',
'//lib/jgit/org.eclipse.jgit.junit:junit',
],
- source_under_test = [':pgm'],
)
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 59b371a..8e3cbcf 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -1,5 +1,6 @@
load('//tools/bzl:java.bzl', 'java_library2')
load('//tools/bzl:junit.bzl', 'junit_tests')
+load('//tools/bzl:license.bzl', 'license_test')
SRCS = 'src/main/java/com/google/gerrit/pgm/'
RSRCS = 'src/main/resources/com/google/gerrit/pgm/'
@@ -19,6 +20,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',
]
@@ -43,6 +45,7 @@
':init-api',
':util',
'//gerrit-common:annotations',
+ '//gerrit-index:index',
'//gerrit-launcher:launcher', # We want this dep to be provided_deps
'//gerrit-lucene:lucene',
'//lib:args4j',
@@ -108,6 +111,7 @@
':init-api',
':util',
'//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
'//gerrit-gpg:gpg',
'//gerrit-lucene:lucene',
'//gerrit-oauth:oauth',
@@ -159,3 +163,8 @@
'//lib/jgit/org.eclipse.jgit.junit:junit',
],
)
+
+license_test(
+ name = "pgm_license_test",
+ target = ":pgm",
+)
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 3ff6451..ba280c3 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,9 +20,10 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
+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;
import com.google.gerrit.httpd.GetUserFilter;
import com.google.gerrit.httpd.GitOverHttpModule;
import com.google.gerrit.httpd.H2CacheBasedWebSession;
@@ -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;
@@ -56,6 +56,7 @@
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.RestCacheAdminModule;
import com.google.gerrit.server.events.StreamEventsApiListener;
@@ -125,7 +126,7 @@
private boolean sshd = true;
@Option(name = "--disable-sshd", usage = "Disable the internal SSH daemon")
- void setDisableSshd(@SuppressWarnings("unused") boolean arg) {
+ void setDisableSshd(@SuppressWarnings("unused") boolean arg) {
sshd = false;
}
@@ -151,6 +152,9 @@
usage = "Init site before starting the daemon")
private boolean doInit;
+ @Option(name = "--stop-only", usage = "Stop the daemon", hidden = true)
+ private boolean stopOnly;
+
private final LifecycleManager manager = new LifecycleManager();
private Injector dbInjector;
private Injector cfgInjector;
@@ -171,7 +175,8 @@
}
@VisibleForTesting
- public Daemon(Runnable serverStarted) {
+ public Daemon(Runnable serverStarted, Path sitePath) {
+ super (sitePath);
this.serverStarted = serverStarted;
}
@@ -181,6 +186,10 @@
@Override
public int run() throws Exception {
+ if (stopOnly) {
+ RuntimeShutdown.manualShutdown();
+ return 0;
+ }
if (doInit) {
try {
new Init(getSitePath()).run();
@@ -214,14 +223,7 @@
@Override
public void run() {
log.info("caught shutdown, cleaning up");
- if (runId != null) {
- try {
- Files.delete(runFile);
- } catch (IOException err) {
- log.warn("failed to delete " + runFile, err);
- }
- }
- manager.stop();
+ stop();
}
});
@@ -313,6 +315,13 @@
@VisibleForTesting
public void stop() {
+ if (runId != null) {
+ try {
+ Files.delete(runFile);
+ } catch (IOException err) {
+ log.warn("failed to delete " + runFile, err);
+ }
+ }
manager.stop();
}
@@ -401,15 +410,18 @@
return cfgInjector.createChildInjector(modules);
}
- private AbstractModule createIndexModule() {
+ private Module createIndexModule() {
if (slave) {
return new DummyIndexModule();
}
+ if (luceneModule != null) {
+ return luceneModule;
+ }
switch (indexType) {
case LUCENE:
- return luceneModule != null
- ? luceneModule
- : LuceneIndexModule.latestVersionWithOnlineUpgrade();
+ return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+ case ELASTICSEARCH:
+ return ElasticIndexModule.latestVersionWithOnlineUpgrade();
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
}
@@ -419,6 +431,7 @@
indexType = IndexModule.getIndexType(cfgInjector);
switch (indexType) {
case LUCENE:
+ case ELASTICSEARCH:
break;
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 05a0d70..b3813f6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -14,9 +14,8 @@
package com.google.gerrit.pgm;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.IoUtil;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.PluginData;
@@ -42,6 +41,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/** Initialize a new Gerrit installation. */
public class Init extends BaseInit {
@@ -247,16 +247,10 @@
if (nullOrEmpty(installPlugins) || nullOrEmpty(plugins)) {
return;
}
- ArrayList<String> copy = Lists.newArrayList(installPlugins);
- List<String> pluginNames = Lists.transform(plugins, new Function<PluginData, String>() {
- @Override
- public String apply(PluginData input) {
- return input.name;
- }
- });
- copy.removeAll(pluginNames);
- if (!copy.isEmpty()) {
- ui.message("Cannot find plugin(s): %s\n", Joiner.on(", ").join(copy));
+ Set<String> missing = Sets.newHashSet(installPlugins);
+ plugins.stream().forEach(p -> missing.remove(p.name));
+ if (!missing.isEmpty()) {
+ ui.message("Cannot find plugin(s): %s\n", Joiner.on(", ").join(missing));
listPlugins = true;
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 0adb1af..030ac30 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ArrayListMultimap;
@@ -47,8 +46,8 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.DummyIndexModule;
import com.google.gerrit.server.index.change.ReindexAfterUpdate;
-import com.google.gerrit.server.notedb.ChangeRebuilder;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -234,13 +233,8 @@
ArrayListMultimap.create();
try (ReviewDb db = schemaFactory.open()) {
if (projects.isEmpty() && !changes.isEmpty()) {
- Iterable<Change> todo = unwrapDb(db).changes().get(
- Iterables.transform(changes, new Function<Integer, Change.Id>() {
- @Override
- public Change.Id apply(Integer in) {
- return new Change.Id(in);
- }
- }));
+ Iterable<Change> todo = unwrapDb(db).changes()
+ .get(Iterables.transform(changes, Change.Id::new));
for (Change c : todo) {
changesByProject.put(c.getProject(), c.getId());
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 2e7d88a..ee0d02f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -16,12 +16,11 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+import static java.util.stream.Collectors.toSet;
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Die;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lucene.LuceneIndexModule;
@@ -134,14 +133,8 @@
}
checkNotNull(indexDefs, "Called this method before injectMembers?");
- Set<String> valid = FluentIterable.from(indexDefs).transform(
- new Function<IndexDefinition<?, ?, ?>, String>() {
- @Override
- public String apply(IndexDefinition<?, ?, ?> input) {
- return input.getName();
- }
- }).toSortedSet(Ordering.natural());
-
+ Set<String> valid = indexDefs.stream()
+ .map(IndexDefinition::getName).sorted().collect(toSet());
Set<String> invalid = Sets.difference(Sets.newHashSet(indices), valid);
if (invalid.isEmpty()) {
return;
@@ -169,6 +162,10 @@
indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(
versions, threads);
break;
+ case ELASTICSEARCH:
+ indexModule = ElasticIndexModule
+ .singleVersionWithExplicitVersions(versions, threads);
+ break;
default:
throw new IllegalStateException("unsupported index.type");
}
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..f4bcd86 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;
@@ -135,8 +135,4 @@
libraries.bouncyCastlePGP.downloadRequired();
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
index 33dc204..aac2b36 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
@@ -89,8 +89,4 @@
}
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
index 36754a1..03ddd7b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -117,8 +117,4 @@
private static String javaHome() {
return System.getProperty("java.home");
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 7e4d3c1..47783e4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -102,8 +102,4 @@
GerritServerIdProvider.KEY, GerritServerIdProvider.generate());
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
index d8fd509..19eaa3c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -47,8 +47,4 @@
}
FileUtil.mkdirsOrDie(d, "Cannot create");
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
index a907d46..72a70c9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -202,8 +202,4 @@
throw die("Cannot delete " + tmpdir, e);
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 018211b..c8d8edb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -15,7 +15,8 @@
package com.google.gerrit.pgm.init;
import com.google.common.collect.Iterables;
-import com.google.gerrit.lucene.AbstractLuceneIndex;
+import com.google.common.collect.Sets;
+import com.google.gerrit.index.IndexUtils;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep;
@@ -61,9 +62,17 @@
type = index.select("Type", "type", type);
}
+ if (type == IndexType.ELASTICSEARCH) {
+ index.select("Transport protocol", "protocol", "http",
+ Sets.newHashSet("http", "https"));
+ index.string("Hostname", "hostname", "localhost");
+ index.string("Port", "port", "9200");
+ index.string("Index Name", "name", "gerrit");
+ }
+
if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
for (SchemaDefinitions<?> def : IndexModule.ALL_SCHEMA_DEFS) {
- AbstractLuceneIndex.setReady(
+ IndexUtils.setReady(
site, def.getName(), def.getLatest().getVersion(), true);
}
} else {
@@ -87,8 +96,4 @@
return true;
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
index 5c7eefd..ed5f7d2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -64,8 +64,4 @@
sendemail.string("SMTP username", "smtpUser", username);
sendemail.password("smtpUser", "smtpPass");
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index cb4439a..904d4f2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -23,6 +23,7 @@
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.Section;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.HostPlatform;
import com.google.gerrit.server.util.SocketUtil;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -31,6 +32,7 @@
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -103,25 +105,30 @@
//
final String comment = "gerrit-code-review@" + hostname();
+ // Workaround for JDK-6518827 - zero-length argument ignored on Win32
+ String emptyPassphraseArg = HostPlatform.isWin32() ? "\"\"" : "";
+
System.err.print(" rsa...");
System.err.flush();
- Runtime.getRuntime().exec(new String[] {"ssh-keygen",
+ new ProcessBuilder("ssh-keygen",
"-q" /* quiet */,
"-t", "rsa",
- "-P", "",
+ "-P", emptyPassphraseArg,
"-C", comment,
- "-f", site.ssh_rsa.toAbsolutePath().toString(),
- }).waitFor();
+ "-f", site.ssh_rsa.toAbsolutePath().toString()
+ ).redirectError(Redirect.INHERIT).redirectOutput(Redirect.INHERIT)
+ .start().waitFor();
System.err.print(" dsa...");
System.err.flush();
- Runtime.getRuntime().exec(new String[] {"ssh-keygen",
+ new ProcessBuilder("ssh-keygen",
"-q" /* quiet */,
"-t", "dsa",
- "-P", "",
+ "-P", emptyPassphraseArg,
"-C", comment,
- "-f", site.ssh_dsa.toAbsolutePath().toString(),
- }).waitFor();
+ "-f", site.ssh_dsa.toAbsolutePath().toString()
+ ).redirectError(Redirect.INHERIT).redirectOutput(Redirect.INHERIT)
+ .start().waitFor();
} else {
// Generate the SSH daemon host key ourselves. This is complex
@@ -163,8 +170,4 @@
System.err.println(" done");
}
}
-
- @Override
- public void postRun() throws Exception {
- }
}
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..c3d516b 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,32 @@
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("AbandonedHtml.soy");
+ extractMailExample("AddKey.soy");
+ extractMailExample("ChangeFooter.soy");
+ extractMailExample("ChangeFooterHtml.soy");
+ extractMailExample("ChangeSubject.soy");
+ extractMailExample("Comment.soy");
+ extractMailExample("CommentFooter.soy");
+ extractMailExample("DeleteReviewer.soy");
+ extractMailExample("DeleteReviewerHtml.soy");
+ extractMailExample("DeleteVote.soy");
+ extractMailExample("DeleteVoteHtml.soy");
+ extractMailExample("Footer.soy");
+ extractMailExample("FooterHtml.soy");
+ extractMailExample("HeaderHtml.soy");
+ extractMailExample("Merged.soy");
+ extractMailExample("MergedHtml.soy");
+ extractMailExample("NewChange.soy");
+ extractMailExample("NewChangeHtml.soy");
+ extractMailExample("RegisterNewEmail.soy");
+ extractMailExample("ReplacePatchSet.soy");
+ extractMailExample("ReplacePatchSetHtml.soy");
+ extractMailExample("Restored.soy");
+ extractMailExample("RestoredHtml.soy");
+ extractMailExample("Reverted.soy");
+ extractMailExample("RevertedHtml.soy");
if (!ui.isBatch()) {
System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
index 52f9096..87b24f9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -286,8 +286,4 @@
}
return null;
}
-
- @Override
- public void postRun() throws Exception {
- }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
index fd28399..9d4becc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
@@ -19,5 +19,5 @@
void run() throws Exception;
/** Executed after the site has been initialized */
- void postRun() throws Exception;
+ default void postRun() throws Exception {}
}
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-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
index dc3a915..86fef21 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
@@ -39,6 +39,10 @@
cb.waitForShutdown();
}
+ public static void manualShutdown() {
+ cb.manualShutdown();
+ }
+
private RuntimeShutdown() {
}
@@ -96,6 +100,11 @@
}
}
+ void manualShutdown() {
+ Runtime.getRuntime().removeShutdownHook(this);
+ run();
+ }
+
void waitForShutdown() {
synchronized (this) {
while (!shutdownComplete) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 9e2da5c..a2e0450 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -78,6 +78,10 @@
protected SiteProgram() {
}
+ protected SiteProgram(Path sitePath) {
+ this.sitePath = sitePath;
+ }
+
protected SiteProgram(Path sitePath, final Provider<DataSource> dsProvider) {
this.sitePath = sitePath;
this.dsProvider = dsProvider;
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index c18e497..cd688a9 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,31 @@
'//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/log:log4j',
'//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 +84,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 +93,4 @@
'//lib/bouncycastle:bcpkix',
],
visibility = ['PUBLIC'],
- do_it_wrong = True,
)
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index 2c18ca6..e2d8372 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -11,6 +11,45 @@
'//gerrit-sshd:sshd',
]
+EXPORTS = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-antlr:query_parser',
+ '//gerrit-common:annotations',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-gwtexpui:server',
+ '//gerrit-reviewdb:server',
+ '//lib/commons:lang',
+ '//lib/dropwizard:dropwizard-core',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:guice-servlet',
+ '//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/log:log4j',
+ '//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:gson',
+ '//lib:gwtorm',
+ '//lib:icu4j',
+ '//lib:jsch',
+ '//lib:mime-util',
+ '//lib:protobuf',
+ '//lib:servlet-api-3_1',
+ '//lib:soy',
+ '//lib:velocity',
+]
+
java_binary(
name = 'plugin-api',
main_class = 'Dummy',
@@ -20,7 +59,42 @@
java_library(
name = 'lib',
- exports = PLUGIN_API + [
+ exports = PLUGIN_API + EXPORTS,
+ visibility = ['//visibility:public'],
+)
+
+java_library(
+ name = 'lib-neverlink',
+ neverlink = 1,
+ exports = PLUGIN_API + EXPORTS,
+ visibility = ['//visibility:public'],
+)
+
+java_binary(
+ name = 'plugin-api-sources',
+ main_class = 'Dummy',
+ runtime_deps = [
+ '//gerrit-antlr:libquery_exception-src.jar',
+ '//gerrit-antlr:libquery_parser-src.jar',
+ '//gerrit-common:libannotations-src.jar',
+ '//gerrit-extension-api:libapi-src.jar',
+ '//gerrit-gwtexpui:libserver-src.jar',
+ '//gerrit-httpd:libhttpd-src.jar',
+ '//gerrit-pgm:libinit-api-src.jar',
+ '//gerrit-reviewdb:libserver-src.jar',
+ '//gerrit-server:libserver-src.jar',
+ '//gerrit-sshd:libsshd-src.jar',
+ ],
+ visibility = ['//visibility:public'],
+)
+
+load('//tools/bzl:javadoc.bzl', 'java_doc')
+
+java_doc(
+ name = 'plugin-api-javadoc',
+ title = 'Gerrit Review Plugin API Documentation',
+ pkgs = ['com.google.gerrit'],
+ libs = PLUGIN_API + [
'//gerrit-antlr:query_exception',
'//gerrit-antlr:query_parser',
'//gerrit-common:annotations',
@@ -28,24 +102,5 @@
'//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/guice:guice',
- '//lib/guice:guice-assistedinject',
- '//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',
],
- visibility = ['//visibility:public'],
)
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 77f3638..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.1</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 bb9c015..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.1</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-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
index 2a585e4..602b029 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
@@ -4,8 +4,8 @@
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@@ -85,7 +85,7 @@
org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index 026e21d..f0cc120 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -66,8 +66,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
- <source>1.7</source>
- <target>1.7</target>
+ <source>1.8</source>
+ <target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 6310e8d..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.1</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-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
index 2a585e4..602b029 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
@@ -4,8 +4,8 @@
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@@ -85,7 +85,7 @@
org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
index 511a8ec..21bc45c 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
@@ -13,20 +13,5 @@
name = 'dev',
id = 'com.google.gwt:gwt-dev:' + VERSION,
license = 'Apache2.0',
- deps = [
- ':javax-validation',
- ':javax-validation_src',
- ],
attach_source = False,
- exclude = ['org/eclipse/jetty/*'],
)
-
-maven_jar(
- name = 'javax-validation',
- id = 'javax.validation:validation-api:1.0.0.GA',
- bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
- src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
- license = 'Apache2.0',
- visibility = [],
-)
-
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index 2c7fe88..baec648 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -61,8 +61,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
- <source>1.7</source>
- <target>1.7</target>
+ <source>1.8</source>
+ <target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
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 4ab0a25..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.1</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 d87b3f6..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.1</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-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
index 2a585e4..602b029 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs
@@ -4,8 +4,8 @@
org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@@ -85,7 +85,7 @@
org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index 8f4aadd..7a38260 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -60,8 +60,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
- <source>1.7</source>
- <target>1.7</target>
+ <source>1.8</source>
+ <target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
diff --git a/gerrit-prettify/BUILD b/gerrit-prettify/BUILD
index 063feee..b8d4dd6 100644
--- a/gerrit-prettify/BUILD
+++ b/gerrit-prettify/BUILD
@@ -33,3 +33,8 @@
],
visibility = ['//visibility:public'],
)
+
+exports_files([
+ 'src/main/resources/com/google/gerrit/prettify/client/prettify.css',
+ 'src/main/resources/com/google/gerrit/prettify/client/prettify.js',
+])
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
index 82e0135..a5fb1f5 100644
--- a/gerrit-reviewdb/BUCK
+++ b/gerrit-reviewdb/BUCK
@@ -33,6 +33,5 @@
'//lib:gwtorm',
'//lib:truth',
],
- source_under_test = [':client'],
visibility = ['//tools/eclipse:classpath'],
)
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-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 1864c56..0f3d45c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -149,6 +149,7 @@
}
int ce = nextNonDigit(ref, cs);
if (ref.substring(ce).equals(RefNames.META_SUFFIX)
+ || ref.substring(ce).equals(RefNames.ROBOT_COMMENTS_SUFFIX)
|| PatchSet.Id.fromRef(ref, ce) >= 0) {
return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
}
@@ -481,6 +482,13 @@
@Column(id = 18, notNull = false)
protected String submissionId;
+ /**
+ * Allows assigning a change to a user.
+ */
+ @Column(id = 19, notNull = false)
+ protected Account.Id assignee;
+
+
/** @see com.google.gerrit.server.notedb.NoteDbChangeState */
@Column(id = 101, notNull = false, length = Integer.MAX_VALUE)
protected String noteDbState;
@@ -500,6 +508,7 @@
}
public Change(Change other) {
+ assignee = other.assignee;
changeId = other.changeId;
changeKey = other.changeKey;
rowVersion = other.rowVersion;
@@ -535,6 +544,14 @@
changeKey = k;
}
+ public Account.Id getAssignee() {
+ return assignee;
+ }
+
+ public void setAssignee(Account.Id a) {
+ assignee = a;
+ }
+
public Timestamp getCreatedOn() {
return createdOn;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index 898dc94..db44d33 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -18,6 +18,7 @@
import com.google.gwtorm.client.StringKey;
import java.sql.Timestamp;
+import java.util.Objects;
/** A message attached to a {@link Change}. */
public final class ChangeMessage {
@@ -78,6 +79,13 @@
@Column(id = 6, notNull = false)
protected String tag;
+ /**
+ * Real user that added this message on behalf of the user recorded in {@link
+ * #author}.
+ */
+ @Column(id = 7, notNull = false)
+ protected Account.Id realAuthor;
+
protected ChangeMessage() {
}
@@ -105,6 +113,15 @@
author = accountId;
}
+ public Account.Id getRealAuthor() {
+ return realAuthor != null ? realAuthor : getAuthor();
+ }
+
+ public void setRealAuthor(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAuthor = Objects.equals(getAuthor(), id) ? null : id;
+ }
+
public Timestamp getWrittenOn() {
return writtenOn;
}
@@ -142,6 +159,7 @@
return "ChangeMessage{"
+ "key=" + key
+ ", author=" + author
+ + ", realAuthor=" + realAuthor
+ ", writtenOn=" + writtenOn
+ ", patchset=" + patchset
+ ", tag=" + tag
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
new file mode 100644
index 0000000..5ec3e47
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -0,0 +1,279 @@
+// 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.reviewdb.client;
+
+import java.sql.Timestamp;
+import java.util.Objects;
+
+/**
+ * This class represents inline comments in NoteDb. This means it determines the
+ * JSON format for inline comments in the revision notes that NoteDb uses to
+ * persist inline comments.
+ * <p>
+ * Changing fields in this class changes the storage format of inline comments
+ * in NoteDb and may require a corresponding data migration (adding new optional
+ * fields is generally okay).
+ * <p>
+ * {@link PatchLineComment} also represents inline comments, but in ReviewDb.
+ * There are a few notable differences:
+ * <ul>
+ * <li>PatchLineComment knows the comment status (published or draft). For
+ * comments in NoteDb the status is determined by the branch in which they are
+ * stored (published comments are stored in the change meta ref; draft comments
+ * are store in refs/draft-comments branches in All-Users). Hence Comment
+ * doesn't need to contain the status, but the status is implicitly known by
+ * where the comments are read from.
+ * <li>PatchLineComment knows the change ID. For comments in NoteDb, the change
+ * ID is determined by the branch in which they are stored (the ref name
+ * contains the change ID). Hence Comment doesn't need to contain the change ID,
+ * but the change ID is implicitly known by where the comments are read from.
+ * </ul>
+ * <p>
+ * For all utility classes and middle layer functionality using Comment over
+ * PatchLineComment is preferred, as PatchLineComment will go away together with
+ * ReviewDb. This means Comment should be used everywhere and only for storing
+ * inline comment in ReviewDb a conversion to PatchLineComment is done.
+ * Converting Comments to PatchLineComments and vice verse is done by
+ * CommentsUtil#toPatchLineComments(Change.Id, PatchLineComment.Status, Iterable)
+ * and CommentsUtil#toComments(String, Iterable).
+ */
+public class Comment {
+ public static class Key {
+ public String uuid;
+ public String filename;
+ public int patchSetId;
+
+ public Key(Key k) {
+ this(k.uuid, k.filename, k.patchSetId);
+ }
+
+ public Key(String uuid, String filename, int patchSetId) {
+ this.uuid = uuid;
+ this.filename = filename;
+ this.patchSetId = patchSetId;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("Comment.Key{")
+ .append("uuid=").append(uuid).append(',')
+ .append("filename=").append(filename).append(',')
+ .append("patchSetId=").append(patchSetId)
+ .append('}')
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Key) {
+ Key k = (Key) o;
+ return Objects.equals(uuid, k.uuid)
+ && Objects.equals(filename, k.filename)
+ && Objects.equals(patchSetId, k.patchSetId);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid, filename, patchSetId);
+ }
+ }
+
+ public static class Identity {
+ int id;
+
+ public Identity(Account.Id id) {
+ this.id = id.get();
+ }
+
+ public Account.Id getId() {
+ return new Account.Id(id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Identity) {
+ return Objects.equals(id, ((Identity) o).id);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("Comment.Identity{")
+ .append("id=").append(id)
+ .append('}')
+ .toString();
+ }
+ }
+
+ public static class Range {
+ public int startLine;
+ public int startChar;
+ public int endLine;
+ public int endChar;
+
+ public Range(Range r) {
+ this(r.startLine, r.startChar, r.endLine, r.endChar);
+ }
+
+ public Range(com.google.gerrit.extensions.client.Comment.Range r) {
+ this(r.startLine, r.startCharacter, r.endLine, r.endCharacter);
+ }
+
+ public Range(int startLine, int startChar, int endLine, int endChar) {
+ this.startLine = startLine;
+ this.startChar = startChar;
+ this.endLine = endLine;
+ this.endChar = endChar;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Range) {
+ Range r = (Range) o;
+ return Objects.equals(startLine, r.startLine)
+ && Objects.equals(startChar, r.startChar)
+ && Objects.equals(endLine, r.endLine)
+ && Objects.equals(endChar, r.endChar);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(startLine, startChar, endLine, endChar);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("Comment.Range{")
+ .append("startLine=").append(startLine).append(',')
+ .append("startChar=").append(startChar).append(',')
+ .append("endLine=").append(endLine).append(',')
+ .append("endChar=").append(endChar)
+ .append('}')
+ .toString();
+ }
+ }
+
+ public Key key;
+ public int lineNbr;
+ public Identity author;
+ protected Identity realAuthor;
+ public Timestamp writtenOn;
+ public short side;
+ public String message;
+ public String parentUuid;
+ public Range range;
+ public String tag;
+ public String revId;
+ public String serverId;
+
+ public Comment(Comment c) {
+ this(new Key(c.key), c.author.getId(), new Timestamp(c.writtenOn.getTime()),
+ c.side, c.message, c.serverId);
+ this.lineNbr = c.lineNbr;
+ this.realAuthor = c.realAuthor;
+ this.range = c.range != null ? new Range(c.range) : null;
+ this.tag = c.tag;
+ this.revId = c.revId;
+ }
+
+ public Comment(Key key, Account.Id author, Timestamp writtenOn,
+ short side, String message, String serverId) {
+ this.key = key;
+ this.author = new Comment.Identity(author);
+ this.realAuthor = this.author;
+ this.writtenOn = writtenOn;
+ this.side = side;
+ this.message = message;
+ this.serverId = serverId;
+ }
+
+ public void setLineNbrAndRange(Integer lineNbr,
+ com.google.gerrit.extensions.client.Comment.Range range) {
+ this.lineNbr = lineNbr != null
+ ? lineNbr
+ : range != null
+ ? range.endLine
+ : 0;
+ if (range != null) {
+ this.range = new Comment.Range(range);
+ }
+ }
+
+ public void setRange(CommentRange range) {
+ this.range = range != null ? range.asCommentRange() : null;
+ }
+
+ public void setRevId(RevId revId) {
+ this.revId = revId != null ? revId.get() : null;
+ }
+
+ public void setRealAuthor(Account.Id id) {
+ realAuthor = id != null && id.get() != author.id
+ ? new Comment.Identity(id)
+ : null;
+ }
+
+ public Identity getRealAuthor() {
+ return realAuthor != null ? realAuthor : author;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Comment) {
+ return Objects.equals(key, ((Comment) o).key);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("Comment{")
+ .append("key=").append(key).append(',')
+ .append("lineNbr=").append(lineNbr).append(',')
+ .append("author=").append(author.getId().get()).append(',')
+ .append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.getId().get() : "")
+ .append(',')
+ .append("writtenOn=").append(writtenOn.toString()).append(',')
+ .append("side=").append(side).append(',')
+ .append("message=").append(Objects.toString(message, "")).append(',')
+ .append("parentUuid=")
+ .append(Objects.toString(parentUuid, "")).append(',')
+ .append("range=").append(Objects.toString(range, "")).append(',')
+ .append("revId=").append(revId != null ? revId : "")
+ .append("tag=").append(Objects.toString(tag, ""))
+ .append('}')
+ .toString();
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
index 5a98d94..0cc3e58 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
@@ -72,6 +72,10 @@
endCharacter = ec;
}
+ public Comment.Range asCommentRange() {
+ return new Comment.Range(startLine, startCharacter, endLine, endCharacter);
+ }
+
@Override
public boolean equals(Object obj) {
if (obj instanceof CommentRange) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 6a55965..309bda4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -22,6 +22,22 @@
/** Magical file name which represents the commit message. */
public static final String COMMIT_MSG = "/COMMIT_MSG";
+ /** Magical file name which represents the merge list of a merge commit. */
+ public static final String MERGE_LIST = "/MERGE_LIST";
+
+ /**
+ * Checks if the given path represents a magic file. A magic file is a
+ * generated file that is automatically included into changes. It does not
+ * exist in the commit of the patch set.
+ *
+ * @param path the file path
+ * @return {@code true} if the path represents a magic file, otherwise
+ * {@code false}.
+ */
+ public static boolean isMagic(String path) {
+ return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path);
+ }
+
public static class Key extends StringKey<PatchSet.Id> {
private static final long serialVersionUID = 1L;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index 16b2d61..5d2f3bb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -21,11 +21,24 @@
import java.sql.Timestamp;
import java.util.Objects;
-/** A comment left by a user on a specific line of a {@link Patch}. */
+/**
+ * A comment left by a user on a specific line of a {@link Patch}.
+ *
+ * This class represents an inline comment in ReviewDb. It should only be used
+ * for writing/reading inline comments to/from ReviewDb. For all other purposes
+ * inline comments should be represented by {@link Comment}.
+ *
+ * @see Comment
+ */
public final class PatchLineComment {
public static class Key extends StringKey<Patch.Key> {
private static final long serialVersionUID = 1L;
+ public static Key from(Change.Id changeId, Comment.Key key) {
+ return new Key(new Patch.Key(new PatchSet.Id(changeId, key.patchSetId),
+ key.filename), key.uuid);
+ }
+
@Column(id = 1, name = Column.NONE)
protected Patch.Key patchKey;
@@ -55,6 +68,12 @@
public void set(String newValue) {
uuid = newValue;
}
+
+ public Comment.Key asCommentKey() {
+ return new Comment.Key(get(),
+ getParentKey().getFileName(),
+ getParentKey().getParentKey().get());
+ }
}
public static final char STATUS_DRAFT = 'd';
@@ -85,6 +104,29 @@
}
}
+ public static PatchLineComment from(Change.Id changeId,
+ PatchLineComment.Status status, Comment c) {
+ PatchLineComment.Key key = new PatchLineComment.Key(
+ new Patch.Key(new PatchSet.Id(changeId, c.key.patchSetId),
+ c.key.filename),
+ c.key.uuid);
+
+ PatchLineComment plc = new PatchLineComment(key, c.lineNbr,
+ c.author.getId(), c.parentUuid, c.writtenOn);
+ plc.setSide(c.side);
+ plc.setMessage(c.message);
+ if (c.range != null) {
+ Comment.Range r = c.range;
+ plc.setRange(
+ new CommentRange(r.startLine, r.startChar, r.endLine, r.endChar));
+ }
+ plc.setTag(c.tag);
+ plc.setRevId(new RevId(c.revId));
+ plc.setStatus(status);
+ plc.setRealAuthor(c.getRealAuthor().getId());
+ return plc;
+ }
+
@Column(id = 1, name = Column.NONE)
protected Key key;
@@ -126,6 +168,13 @@
protected String tag;
/**
+ * Real user that added this comment on behalf of the user recorded in {@link
+ * #author}.
+ */
+ @Column(id = 11, notNull = false)
+ protected Account.Id realAuthor;
+
+ /**
* The RevId for the commit to which this comment is referring.
*
* Note that this field is not stored in the database. It is just provided
@@ -151,6 +200,7 @@
key = o.key;
lineNbr = o.lineNbr;
author = o.author;
+ realAuthor = o.realAuthor;
writtenOn = o.writtenOn;
status = o.status;
side = o.side;
@@ -186,6 +236,15 @@
return author;
}
+ public Account.Id getRealAuthor() {
+ return realAuthor != null ? realAuthor : getAuthor();
+ }
+
+ public void setRealAuthor(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAuthor = Objects.equals(getAuthor(), id) ? null : id;
+ }
+
public Timestamp getWrittenOn() {
return writtenOn;
}
@@ -260,6 +319,18 @@
return tag;
}
+ public Comment asComment(String serverId) {
+ Comment c = new Comment(key.asCommentKey(), author, writtenOn, side,
+ message, serverId);
+ c.setRevId(revId);
+ c.setRange(range);
+ c.lineNbr = lineNbr;
+ c.parentUuid = parentUuid;
+ c.tag = tag;
+ c.setRealAuthor(getRealAuthor());
+ return c;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof PatchLineComment) {
@@ -291,6 +362,8 @@
builder.append("key=").append(key).append(',');
builder.append("lineNbr=").append(lineNbr).append(',');
builder.append("author=").append(author.get()).append(',');
+ builder.append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.get() : "").append(',');
builder.append("writtenOn=").append(writtenOn.toString()).append(',');
builder.append("status=").append(status).append(',');
builder.append("side=").append(side).append(',');
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index b9cd813..c87c66c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -93,6 +93,13 @@
@Column(id = 6, notNull = false)
protected String tag;
+ /**
+ * Real user that made this approval on behalf of the user recorded in {@link
+ * Key#accountId}.
+ */
+ @Column(id = 7, notNull = false)
+ protected Account.Id realAccountId;
+
// DELETED: id = 4 (changeOpen)
// DELETED: id = 5 (changeSortKey)
@@ -110,6 +117,7 @@
new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId());
value = src.getValue();
granted = src.granted;
+ realAccountId = src.realAccountId;
tag = src.tag;
}
@@ -125,6 +133,15 @@
return key.accountId;
}
+ public Account.Id getRealAccountId() {
+ return realAccountId != null ? realAccountId : getAccountId();
+ }
+
+ public void setRealAccountId(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAccountId = Objects.equals(getAccountId(), id) ? null : id;
+ }
+
public LabelId getLabelId() {
return key.categoryId;
}
@@ -167,8 +184,12 @@
@Override
public String toString() {
- return new StringBuilder().append('[').append(key).append(": ")
- .append(value).append(",tag:").append(tag).append(']').toString();
+ return "["
+ + key + ": "
+ + value
+ + ",tag:" + tag
+ + ",realAccountId:" + realAccountId
+ + ']';
}
@Override
@@ -178,7 +199,8 @@
return Objects.equals(key, p.key)
&& Objects.equals(value, p.value)
&& Objects.equals(granted, p.granted)
- && Objects.equals(tag, p.tag);
+ && Objects.equals(tag, p.tag)
+ && Objects.equals(realAccountId, p.realAccountId);
}
return false;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index b2bd818..7629705 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -68,6 +68,9 @@
/** Suffix of a meta ref in the NoteDb. */
public static final String META_SUFFIX = "/meta";
+ /** Suffix of a ref that stores robot comments in the NoteDb. */
+ public static final String ROBOT_COMMENTS_SUFFIX = "/robot-comments";
+
public static final String EDIT_PREFIX = "edit-";
public static String fullName(String ref) {
@@ -92,6 +95,14 @@
return r.toString();
}
+ public static String robotCommentsRef(Change.Id id) {
+ StringBuilder r = new StringBuilder();
+ r.append(REFS_CHANGES);
+ r.append(shard(id.get()));
+ r.append(ROBOT_COMMENTS_SUFFIX);
+ return r.toString();
+ }
+
public static String refsUsers(Account.Id accountId) {
StringBuilder r = new StringBuilder();
r.append(REFS_USERS);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
new file mode 100644
index 0000000..ecb952a
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
@@ -0,0 +1,60 @@
+// 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.reviewdb.client;
+
+import java.sql.Timestamp;
+import java.util.Map;
+import java.util.Objects;
+
+public class RobotComment extends Comment {
+ public String robotId;
+ public String robotRunId;
+ public String url;
+ public Map<String, String> properties;
+
+ public RobotComment(Key key, Account.Id author, Timestamp writtenOn,
+ short side, String message, String serverId, String robotId,
+ String robotRunId) {
+ super(key, author, writtenOn, side, message, serverId);
+ this.robotId = robotId;
+ this.robotRunId = robotRunId;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("RobotComment{")
+ .append("key=").append(key).append(',')
+ .append("robotId=").append(robotId).append(',')
+ .append("robotRunId=").append(robotRunId).append(',')
+ .append("lineNbr=").append(lineNbr).append(',')
+ .append("author=").append(author.getId().get()).append(',')
+ .append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.getId().get() : "")
+ .append(',')
+ .append("writtenOn=").append(writtenOn.toString()).append(',')
+ .append("side=").append(side).append(',')
+ .append("message=").append(Objects.toString(message, "")).append(',')
+ .append("parentUuid=")
+ .append(Objects.toString(parentUuid, "")).append(',')
+ .append("range=").append(Objects.toString(range, "")).append(',')
+ .append("revId=").append(revId != null ? revId : "").append(',')
+ .append("tag=").append(Objects.toString(tag, "")).append(',')
+ .append("url=").append(url).append(',')
+ .append("properties=").append(properties != null ? properties : "")
+ .append('}')
+ .toString();
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
index 42d0993..7e2a9b0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbUtil.java
@@ -14,41 +14,37 @@
package com.google.gerrit.reviewdb.server;
-import com.google.common.base.Function;
import com.google.common.collect.Ordering;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.client.IntKey;
/** Static utilities for ReviewDb types. */
public class ReviewDbUtil {
- public static final Function<IntKey<?>, Integer> INT_KEY_FUNCTION =
- new Function<IntKey<?>, Integer>() {
- @Override
- public Integer apply(IntKey<?> in) {
- return in.get();
- }
- };
-
- private static final Function<Change, Change.Id> CHANGE_ID_FUNCTION =
- new Function<Change, Change.Id>() {
- @Override
- public Change.Id apply(Change in) {
- return in.getId();
- }
- };
-
private static final Ordering<? extends IntKey<?>> INT_KEY_ORDERING =
- Ordering.natural().nullsFirst().onResultOf(INT_KEY_FUNCTION).nullsFirst();
+ Ordering.natural()
+ .nullsFirst()
+ .<IntKey<?>>onResultOf(IntKey::get)
+ .nullsFirst();
+ /**
+ * Null-safe ordering over arbitrary subclass of {@code IntKey}.
+ * <p>
+ * In some cases, {@code Comparator.comparing(Change.Id::get)} may be shorter
+ * and cleaner. However, this method may be preferable in some cases:
+ * <ul>
+ * <li>This ordering is null-safe over both input and the result of {@link
+ * IntKey#get()}; {@code comparing} is only a good idea if all inputs are
+ * obviously non-null.</li>
+ * <li>{@code intKeyOrdering().sortedCopy(iterable)} is shorter than the
+ * stream equivalent.</li>
+ * <li>Creating derived comparators may be more readable with {@link Ordering}
+ * method chaining rather than static {@code Comparator} methods.
+ * </ul>
+ */
@SuppressWarnings("unchecked")
public static <K extends IntKey<?>> Ordering<K> intKeyOrdering() {
return (Ordering<K>) INT_KEY_ORDERING;
}
- public static Function<Change, Change.Id> changeIdFunction() {
- return CHANGE_ID_FUNCTION;
- }
-
public static ReviewDb unwrapDb(ReviewDb db) {
if (db instanceof DisabledChangesReviewDbWrapper) {
return ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 4fc578c..a50df82 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',
@@ -90,6 +91,7 @@
':server',
'//gerrit-common:server',
'//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
'//gerrit-extension-api:api',
'//gerrit-gpg:gpg',
'//gerrit-lucene:lucene',
@@ -180,7 +182,7 @@
'//gerrit-server/src/main/prolog:common',
'//lib/antlr:java_runtime',
],
- source_under_test = [':server'],
+ visibility = ['PUBLIC'],
)
java_test(
@@ -203,11 +205,9 @@
'//lib:guava',
'//lib:guava-retrying',
'//lib:protobuf',
- '//lib/commons:validator',
'//lib/dropwizard:dropwizard-core',
'//lib/guice:guice-assistedinject',
'//lib/prolog:runtime',
],
- source_under_test = [':server'],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index 5a6b50f..3874fc9 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',
@@ -168,6 +169,20 @@
['src/test/java/com/google/gerrit/server/query/**/*.java'],
)
+java_library(
+ name = 'query_tests_code',
+ srcs = QUERY_TESTS,
+ deps = TESTUTIL_DEPS + [
+ ':testutil',
+ '//gerrit-antlr:query_exception',
+ '//gerrit-antlr:query_parser',
+ '//gerrit-common:annotations',
+ '//gerrit-server/src/main/prolog:common',
+ '//lib/antlr:java_runtime',
+ ],
+ visibility = ['//visibility:public'],
+)
+
junit_tests(
name = 'query_tests',
srcs = QUERY_TESTS,
@@ -188,6 +203,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',
@@ -206,3 +222,12 @@
],
visibility = ['//visibility:public'],
)
+
+load('//tools/bzl:javadoc.bzl', 'java_doc')
+
+java_doc(
+ name = 'doc',
+ title = 'Gerrit Review Server Documentation',
+ libs = [':server'],
+ pkgs = ['com.google.gerrit'],
+)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
index 364f4f8..95fbf04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
@@ -161,17 +161,10 @@
private static <T> Function<T, String> initFormatter(Class<T> keyType) {
if (keyType == String.class) {
return (Function<T, String>) Functions.<String> identity();
-
} else if (keyType == Integer.class || keyType == Boolean.class) {
return (Function<T, String>) Functions.toStringFunction();
-
} else if (Enum.class.isAssignableFrom(keyType)) {
- return new Function<T, String>() {
- @Override
- public String apply(T in) {
- return ((Enum<?>) in).name();
- }
- };
+ return in -> ((Enum<?>) in).name();
}
throw new IllegalStateException("unsupported type " + keyType.getName());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
index e7ab75c..d3fe6ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
@@ -14,7 +14,6 @@
package com.google.gerrit.metrics.dropwizard;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.gerrit.metrics.Description;
@@ -124,14 +123,7 @@
@Override
public Map<Object, Metric> getCells() {
- return Maps.transformValues(
- cells,
- new Function<ValueGauge, Metric> () {
- @Override
- public Metric apply(ValueGauge in) {
- return in;
- }
- });
+ return Maps.transformValues(cells, in -> (Metric) in);
}
final class ValueGauge implements Gauge<V> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
index 10b92e6..7894a84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
@@ -14,7 +14,6 @@
package com.google.gerrit.metrics.dropwizard;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.gerrit.metrics.Description;
@@ -98,13 +97,6 @@
@Override
public Map<Object, Metric> getCells() {
- return Maps.transformValues(
- cells,
- new Function<CounterImpl, Metric> () {
- @Override
- public Metric apply(CounterImpl in) {
- return in.metric;
- }
- });
+ return Maps.transformValues(cells, c -> c.metric);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
index 071c678..ff38cd4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
@@ -14,7 +14,6 @@
package com.google.gerrit.metrics.dropwizard;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.gerrit.metrics.Description;
@@ -96,13 +95,6 @@
@Override
public Map<Object, Metric> getCells() {
- return Maps.transformValues(
- cells,
- new Function<HistogramImpl, Metric> () {
- @Override
- public Metric apply(HistogramImpl in) {
- return in.metric;
- }
- });
+ return Maps.transformValues(cells, h -> h.metric);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
index 6981ef1..aff6c4a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
@@ -14,7 +14,6 @@
package com.google.gerrit.metrics.dropwizard;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.gerrit.metrics.Description;
@@ -96,13 +95,6 @@
@Override
public Map<Object, Metric> getCells() {
- return Maps.transformValues(
- cells,
- new Function<TimerImpl, Metric> () {
- @Override
- public Metric apply(TimerImpl in) {
- return in.metric;
- }
- });
+ return Maps.transformValues(cells, t -> t.metric);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
index e159c82..ee2ce29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -18,7 +18,6 @@
import static com.google.gerrit.metrics.dropwizard.MetricResource.METRIC_KIND;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -304,14 +303,8 @@
@Override
public synchronized RegistrationHandle newTrigger(
Set<CallbackMetric<?>> metrics, Runnable trigger) {
- final ImmutableSet<CallbackMetricGlue> all = FluentIterable.from(metrics)
- .transform(
- new Function<CallbackMetric<?>, CallbackMetricGlue>() {
- @Override
- public CallbackMetricGlue apply(CallbackMetric<?> input) {
- return (CallbackMetricGlue) input;
- }
- })
+ ImmutableSet<CallbackMetricGlue> all = FluentIterable.from(metrics)
+ .transform(m -> (CallbackMetricGlue) m)
.toSet();
trigger = new CallbackGroup(trigger, all);
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..1fcb5b6 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);
@@ -123,7 +161,8 @@
continue;
}
- ChangeKind kind = changeKindCache.getChangeKind(project, repo,
+ ChangeKind kind = changeKindCache.getChangeKind(
+ project.getProject().getNameKey(), repo,
ObjectId.fromString(priorPs.getRevision().get()),
ObjectId.fromString(ps.getRevision().get()));
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..67f07bc 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
@@ -14,12 +14,12 @@
package com.google.gerrit.server;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static java.util.Comparator.comparing;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
@@ -27,6 +27,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
+import com.google.common.primitives.Shorts;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
@@ -43,12 +44,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 java.sql.Timestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -73,39 +77,50 @@
*/
@Singleton
public class ApprovalsUtil {
+ private static final Logger log =
+ LoggerFactory.getLogger(ApprovalsUtil.class);
+
private static final Ordering<PatchSetApproval> SORT_APPROVALS =
- Ordering.natural()
- .onResultOf(
- new Function<PatchSetApproval, Timestamp>() {
- @Override
- public Timestamp apply(PatchSetApproval a) {
- return a.getGranted();
- }
- });
+ Ordering.from(comparing(PatchSetApproval::getGranted));
public static List<PatchSetApproval> sortApprovals(
Iterable<PatchSetApproval> approvals) {
return SORT_APPROVALS.sortedCopy(approvals);
}
+ public static PatchSetApproval newApproval(PatchSet.Id psId, CurrentUser user,
+ LabelId labelId, int value, Date when) {
+ PatchSetApproval psa = new PatchSetApproval(
+ new PatchSetApproval.Key(
+ psId,
+ user.getAccountId(),
+ labelId),
+ Shorts.checkedCast(value),
+ when);
+ user.updateRealAccountId(psa::setRealAccountId);
+ return psa;
+ }
+
private static Iterable<PatchSetApproval> filterApprovals(
Iterable<PatchSetApproval> psas, final Account.Id accountId) {
- return Iterables.filter(psas, new Predicate<PatchSetApproval>() {
- @Override
- public boolean apply(PatchSetApproval input) {
- return Objects.equals(input.getAccountId(), accountId);
- }
- });
+ return Iterables.filter(
+ psas, a -> Objects.equals(a.getAccountId(), accountId));
}
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 +179,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 +204,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 +219,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());
@@ -225,10 +240,21 @@
(short) 0, update.getWhen()));
update.putReviewer(account, REVIEWER);
}
- db.patchSetApprovals().insert(cells);
+ db.patchSetApprovals().upsert(cells);
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 +280,42 @@
return need;
}
- public void 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);
+ /**
+ * Adds approvals to ChangeUpdate for a new patch set, 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> addApprovalsForNewPatchSet(ReviewDb db,
+ ChangeUpdate update, LabelTypes labelTypes, PatchSet ps,
+ ChangeControl changeCtl, Map<String, Short> approvals)
+ throws OrmException {
+ Account.Id accountId = changeCtl.getUser().getAccountId();
+ checkArgument(accountId.equals(ps.getUploader()),
+ "expected user %s to match patch set uploader %s",
+ accountId, ps.getUploader());
+ 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(
+ newApproval(ps.getId(), changeCtl.getUser(), 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/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
index f3fdbcb..13b289f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NotesMigration;
@@ -27,6 +30,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -39,6 +43,26 @@
*/
@Singleton
public class ChangeMessagesUtil {
+ public static ChangeMessage newMessage(BatchUpdate.ChangeContext ctx,
+ String body) throws OrmException {
+ return newMessage(
+ ctx.getDb(), ctx.getChange().currentPatchSetId(),
+ ctx.getUser(), ctx.getWhen(), body);
+ }
+
+ public static ChangeMessage newMessage(
+ ReviewDb db, PatchSet.Id psId, CurrentUser user, Timestamp when,
+ String body) throws OrmException {
+ checkNotNull(psId);
+ Account.Id accountId = user.isInternalUser() ? null : user.getAccountId();
+ ChangeMessage m = new ChangeMessage(
+ new ChangeMessage.Key(psId.getParentKey(), ChangeUtil.messageUUID(db)),
+ accountId, when, psId);
+ m.setMessage(body);
+ user.updateRealAccountId(m::setRealAuthor);
+ return m;
+ }
+
private static List<ChangeMessage> sortChangeMessages(
Iterable<ChangeMessage> changeMessage) {
return ChangeNotes.MESSAGE_BY_TIME.sortedCopy(changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 11a3d81..7866ed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,7 +14,8 @@
package com.google.gerrit.server;
-import com.google.common.base.Function;
+import static java.util.Comparator.comparingInt;
+
import com.google.common.collect.Ordering;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -40,16 +41,8 @@
private static final String SUBJECT_CROP_APPENDIX = "...";
private static final int SUBJECT_CROP_RANGE = 10;
- public static final Function<PatchSet, Integer> TO_PS_ID =
- new Function<PatchSet, Integer>() {
- @Override
- public Integer apply(PatchSet in) {
- return in.getId().get();
- }
- };
-
- public static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
- .onResultOf(TO_PS_ID);
+ public static final Ordering<PatchSet> PS_ID_ORDER =
+ Ordering.from(comparingInt(PatchSet::getPatchSetId));
/**
* Generate a new unique identifier for change message entities.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
new file mode 100644
index 0000000..81ec4eb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -0,0 +1,466 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+/**
+ * Utility functions to manipulate Comments.
+ * <p>
+ * These methods either query for and update Comments in the NoteDb or
+ * ReviewDb, depending on the state of the NotesMigration.
+ */
+@Singleton
+public class CommentsUtil {
+ public static final Ordering<Comment> COMMENT_ORDER =
+ new Ordering<Comment>() {
+ @Override
+ public int compare(Comment c1, Comment c2) {
+ return ComparisonChain.start()
+ .compare(c1.key.filename, c2.key.filename)
+ .compare(c1.key.patchSetId, c2.key.patchSetId)
+ .compare(c1.side, c2.side)
+ .compare(c1.lineNbr, c2.lineNbr)
+ .compare(c1.writtenOn, c2.writtenOn)
+ .result();
+ }
+ };
+
+ public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
+ new Ordering<CommentInfo>() {
+ @Override
+ public int compare(CommentInfo a, CommentInfo b) {
+ return ComparisonChain.start()
+ .compare(a.path, b.path, NULLS_FIRST)
+ .compare(a.patchSet, b.patchSet, NULLS_FIRST)
+ .compare(side(a), side(b))
+ .compare(a.line, b.line, NULLS_FIRST)
+ .compare(a.id, b.id)
+ .result();
+ }
+
+ private int side(CommentInfo c) {
+ return firstNonNull(c.side, Side.REVISION).ordinal();
+ }
+ };
+
+ public static PatchSet.Id getCommentPsId(Change.Id changeId,
+ Comment comment) {
+ return new PatchSet.Id(changeId, comment.key.patchSetId);
+ }
+
+ private static final Ordering<Comparable<?>> NULLS_FIRST =
+ Ordering.natural().nullsFirst();
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+ private final NotesMigration migration;
+ private final String serverId;
+
+ @Inject
+ CommentsUtil(GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ NotesMigration migration,
+ @GerritServerId String serverId) {
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ this.migration = migration;
+ this.serverId = serverId;
+ }
+
+ public Comment newComment(ChangeContext ctx, String path, PatchSet.Id psId,
+ short side, String message) throws OrmException {
+ Comment c = new Comment(
+ new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path, psId.get()),
+ ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId);
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
+ return c;
+ }
+
+ public RobotComment newRobotComment(ChangeContext ctx, String path,
+ PatchSet.Id psId, short side, String message, String robotId,
+ String robotRunId) throws OrmException {
+ RobotComment c = new RobotComment(
+ new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path, psId.get()),
+ ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId,
+ robotId, robotRunId);
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
+ return c;
+ }
+
+ public Optional<Comment> get(ReviewDb db, ChangeNotes notes,
+ Comment.Key key) throws OrmException {
+ if (!migration.readChanges()) {
+ PatchLineComment plc = db.patchComments()
+ .get(PatchLineComment.Key.from(notes.getChangeId(), key));
+ Comment c = plc != null ? plc.asComment(serverId) : null;
+ return Optional.fromNullable(c);
+ }
+ for (Comment c : publishedByChange(db, notes)) {
+ if (key.equals(c.key)) {
+ return Optional.of(c);
+ }
+ }
+ for (Comment c : draftByChange(db, notes)) {
+ if (key.equals(c.key)) {
+ return Optional.of(c);
+ }
+ }
+ return Optional.absent();
+ }
+
+ public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(byCommentStatus(
+ db.patchComments().byChange(notes.getChangeId()), Status.PUBLISHED));
+ }
+
+ notes.load();
+ return sort(Lists.newArrayList(notes.getComments().values()));
+ }
+
+ public List<RobotComment> robotCommentsByChange(ChangeNotes notes)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return ImmutableList.of();
+ }
+
+ notes.load();
+ return sort(Lists.newArrayList(notes.getRobotComments().values()));
+ }
+
+ public List<Comment> draftByChange(ReviewDb db, ChangeNotes notes)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(byCommentStatus(
+ db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
+ }
+
+ List<Comment> comments = new ArrayList<>();
+ for (Ref ref : getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ comments.addAll(draftByChangeAuthor(db, notes, account));
+ }
+ }
+ return sort(comments);
+ }
+
+ private List<Comment> byCommentStatus(ResultSet<PatchLineComment> comments,
+ final PatchLineComment.Status status) {
+ return toComments(serverId, Lists.newArrayList(
+ Iterables.filter(comments, c -> c.getStatus() == status)));
+ }
+
+ public List<Comment> byPatchSet(ReviewDb db,
+ ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(toComments(serverId,
+ db.patchComments().byPatchSet(psId).toList()));
+ }
+ List<Comment> comments = new ArrayList<>();
+ comments.addAll(publishedByPatchSet(db, notes, psId));
+
+ for (Ref ref : getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null) {
+ comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
+ }
+ }
+ return sort(comments);
+ }
+
+ public List<Comment> publishedByChangeFile(ReviewDb db, ChangeNotes notes,
+ Change.Id changeId, String file) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(toComments(serverId,
+ db.patchComments().publishedByChangeFile(changeId, file).toList()));
+ }
+ return commentsOnFile(notes.load().getComments().values(), file);
+ }
+
+ public List<Comment> publishedByPatchSet(ReviewDb db,
+ ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+ if (!migration.readChanges()) {
+ return removeCommentsOnAncestorOfCommitMessage(sort(toComments(serverId,
+ db.patchComments().publishedByPatchSet(psId).toList())));
+ }
+ return removeCommentsOnAncestorOfCommitMessage(
+ commentsOnPatchSet(notes.load().getComments().values(), psId));
+ }
+
+ public List<RobotComment> robotCommentsByPatchSet(ChangeNotes notes,
+ PatchSet.Id psId) throws OrmException {
+ if (!migration.readChanges()) {
+ return ImmutableList.of();
+ }
+ return commentsOnPatchSet(notes.load().getRobotComments().values(), psId);
+ }
+
+ /**
+ * For the commit message the A side in a diff view is always empty when a
+ * comparison against an ancestor is done, so there can't be any comments on
+ * this ancestor. However earlier we showed the auto-merge commit message on
+ * side A when for a merge commit a comparison against the auto-merge was
+ * done. From that time there may still be comments on the auto-merge commit
+ * message and those we want to filter out.
+ */
+ private List<Comment> removeCommentsOnAncestorOfCommitMessage(
+ List<Comment> list) {
+ return list.stream()
+ .filter(c -> c.side != 0 || !Patch.COMMIT_MSG.equals(c.key.filename))
+ .collect(toList());
+ }
+
+ public List<Comment> draftByPatchSetAuthor(ReviewDb db, PatchSet.Id psId,
+ Account.Id author, ChangeNotes notes) throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(toComments(serverId,
+ db.patchComments().draftByPatchSetAuthor(psId, author).toList()));
+ }
+ return commentsOnPatchSet(notes.load().getDraftComments(author).values(), psId);
+ }
+
+ public List<Comment> draftByChangeFileAuthor(ReviewDb db,
+ ChangeNotes notes, String file, Account.Id author)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return sort(toComments(serverId,
+ db.patchComments()
+ .draftByChangeFileAuthor(notes.getChangeId(), file, author)
+ .toList()));
+ }
+ return commentsOnFile(notes.load().getDraftComments(author).values(), file);
+ }
+
+ public List<Comment> draftByChangeAuthor(ReviewDb db,
+ ChangeNotes notes, Account.Id author)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return StreamSupport
+ .stream(db.patchComments().draftByAuthor(author).spliterator(), false)
+ .filter(c -> c.getPatchSetId().getParentKey()
+ .equals(notes.getChangeId()))
+ .map(plc -> plc.asComment(serverId))
+ .sorted(COMMENT_ORDER)
+ .collect(toList());
+ }
+ List<Comment> comments = new ArrayList<>();
+ comments.addAll(notes.getDraftComments(author).values());
+ return sort(comments);
+ }
+
+ @Deprecated // To be used only by HasDraftByLegacyPredicate.
+ public List<Change.Id> changesWithDraftsByAuthor(ReviewDb db,
+ Account.Id author) throws OrmException {
+ if (!migration.readChanges()) {
+ return FluentIterable.from(db.patchComments().draftByAuthor(author))
+ .transform(plc -> plc.getPatchSetId().getParentKey()).toList();
+ }
+
+ List<Change.Id> changes = new ArrayList<>();
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ for (String refName : repo.getRefDatabase()
+ .getRefs(RefNames.REFS_DRAFT_COMMENTS).keySet()) {
+ Account.Id accountId = Account.Id.fromRefSuffix(refName);
+ Change.Id changeId = Change.Id.fromRefPart(refName);
+ if (accountId == null || changeId == null) {
+ continue;
+ }
+ changes.add(changeId);
+ }
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ return changes;
+ }
+
+ public void putComments(ReviewDb db, ChangeUpdate update,
+ PatchLineComment.Status status, Iterable<Comment> comments)
+ throws OrmException {
+ for (Comment c : comments) {
+ update.putComment(status, c);
+ }
+ db.patchComments()
+ .upsert(toPatchLineComments(update.getId(), status, comments));
+ }
+
+ public void putRobotComments(ChangeUpdate update,
+ Iterable<RobotComment> comments) {
+ for (RobotComment c : comments) {
+ update.putRobotComment(c);
+ }
+ }
+
+ public void deleteComments(ReviewDb db, ChangeUpdate update,
+ Iterable<Comment> comments) throws OrmException {
+ for (Comment c : comments) {
+ update.deleteComment(c);
+ }
+ db.patchComments().delete(toPatchLineComments(update.getId(),
+ PatchLineComment.Status.DRAFT, comments));
+ }
+
+ public void deleteAllDraftsFromAllUsers(Change.Id changeId)
+ throws IOException {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ for (Ref ref : getDraftRefs(repo, changeId)) {
+ bru.addCommand(new ReceiveCommand(
+ ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
+ }
+ bru.setRefLogMessage("Delete drafts from NoteDb", false);
+ bru.execute(rw, NullProgressMonitor.INSTANCE);
+ for (ReceiveCommand cmd : bru.getCommands()) {
+ if (cmd.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException(String.format(
+ "Failed to delete draft comment ref %s at %s: %s (%s)",
+ cmd.getRefName(), cmd.getOldId(), cmd.getResult(),
+ cmd.getMessage()));
+ }
+ }
+ }
+ }
+
+ private static List<Comment> commentsOnFile(Collection<Comment> allComments,
+ String file) {
+ List<Comment> result = new ArrayList<>(allComments.size());
+ for (Comment c : allComments) {
+ String currentFilename = c.key.filename;
+ if (currentFilename.equals(file)) {
+ result.add(c);
+ }
+ }
+ return sort(result);
+ }
+
+ private static <T extends Comment> List<T> commentsOnPatchSet(
+ Collection<T> allComments,
+ PatchSet.Id psId) {
+ List<T> result = new ArrayList<>(allComments.size());
+ for (T c : allComments) {
+ if (c.key.patchSetId == psId.get()) {
+ result.add(c);
+ }
+ }
+ return sort(result);
+ }
+
+ public static void setCommentRevId(Comment c,
+ PatchListCache cache, Change change, PatchSet ps) throws OrmException {
+ checkArgument(c.key.patchSetId == ps.getId().get(),
+ "cannot set RevId for patch set %s on comment %s", ps.getId(), c);
+ if (c.revId == null) {
+ try {
+ if (Side.fromShort(c.side) == Side.PARENT) {
+ if (c.side < 0) {
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, -c.side));
+ } else {
+ c.revId = ObjectId.toString(cache.getOldId(change, ps, null));
+ }
+ } else {
+ c.revId = ps.getRevision().get();
+ }
+ } catch (PatchListNotAvailableException e) {
+ throw new OrmException(e);
+ }
+ }
+ }
+
+ public Collection<Ref> getDraftRefs(Change.Id changeId)
+ throws OrmException {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getDraftRefs(repo, changeId);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId)
+ throws IOException {
+ return repo.getRefDatabase().getRefs(
+ RefNames.refsDraftCommentsPrefix(changeId)).values();
+ }
+
+ private static <T extends Comment> List<T> sort(List<T> comments) {
+ Collections.sort(comments, COMMENT_ORDER);
+ return comments;
+ }
+
+ public static Iterable<PatchLineComment> toPatchLineComments(
+ Change.Id changeId, PatchLineComment.Status status,
+ Iterable<Comment> comments) {
+ return FluentIterable.from(comments)
+ .transform(c -> PatchLineComment.from(changeId, status, c));
+ }
+
+ public static List<Comment> toComments(final String serverId,
+ Iterable<PatchLineComment> comments) {
+ return COMMENT_ORDER.sortedCopy(FluentIterable.from(comments)
+ .transform(plc -> plc.asComment(serverId)));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 34a2d02..668b344 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -20,6 +20,8 @@
import com.google.gerrit.server.account.GroupMembership;
import com.google.inject.servlet.RequestScoped;
+import java.util.function.Consumer;
+
/**
* Information about the currently logged in user.
* <p>
@@ -72,6 +74,16 @@
}
/**
+ * If the {@link #getRealUser()} has an account ID associated with it, call
+ * the given setter with that ID.
+ */
+ public void updateRealAccountId(Consumer<Account.Id> setter) {
+ if (getRealUser().isIdentifiedUser()) {
+ setter.accept(getRealUser().getAccountId());
+ }
+ }
+
+ /**
* Get the set of groups the user is currently a member of.
* <p>
* The returned set may be a subset of the user's actual groups; if the user's
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
index 24d10f7..c050a61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OptionUtil.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server;
import com.google.common.base.CharMatcher;
-import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
@@ -24,16 +23,10 @@
private static final Splitter COMMA_OR_SPACE =
Splitter.on(CharMatcher.anyOf(", ")).omitEmptyStrings().trimResults();
- private static final Function<String, String> TO_LOWER_CASE =
- new Function<String, String>() {
- @Override
- public String apply(String input) {
- return input.toLowerCase();
- }
- };
-
public static Iterable<String> splitOptionValue(String value) {
- return Iterables.transform(COMMA_OR_SPACE.split(value), TO_LOWER_CASE);
+ return Iterables.transform(
+ COMMA_OR_SPACE.split(value),
+ String::toLowerCase);
}
private OptionUtil() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
deleted file mode 100644
index 603f528..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ /dev/null
@@ -1,405 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
-import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import com.google.gerrit.extensions.client.Side;
-import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.DraftCommentNotes;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Utility functions to manipulate PatchLineComments.
- * <p>
- * These methods either query for and update PatchLineComments in the NoteDb or
- * ReviewDb, depending on the state of the NotesMigration.
- */
-@Singleton
-public class PatchLineCommentsUtil {
- public static final Ordering<PatchLineComment> PLC_ORDER =
- new Ordering<PatchLineComment>() {
- @Override
- public int compare(PatchLineComment c1, PatchLineComment c2) {
- String filename1 = c1.getKey().getParentKey().get();
- String filename2 = c2.getKey().getParentKey().get();
- return ComparisonChain.start()
- .compare(filename1, filename2)
- .compare(getCommentPsId(c1).get(), getCommentPsId(c2).get())
- .compare(c1.getSide(), c2.getSide())
- .compare(c1.getLine(), c2.getLine())
- .compare(c1.getWrittenOn(), c2.getWrittenOn())
- .result();
- }
- };
-
- public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
- new Ordering<CommentInfo>() {
- @Override
- public int compare(CommentInfo a, CommentInfo b) {
- return ComparisonChain.start()
- .compare(a.path, b.path, NULLS_FIRST)
- .compare(a.patchSet, b.patchSet, NULLS_FIRST)
- .compare(side(a), side(b))
- .compare(a.line, b.line, NULLS_FIRST)
- .compare(a.id, b.id)
- .result();
- }
-
- private int side(CommentInfo c) {
- return firstNonNull(c.side, Side.REVISION).ordinal();
- }
- };
-
- public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
- return plc.getKey().getParentKey().getParentKey();
- }
-
- private static final Ordering<Comparable<?>> NULLS_FIRST =
- Ordering.natural().nullsFirst();
-
- private final GitRepositoryManager repoManager;
- private final AllUsersName allUsers;
- private final DraftCommentNotes.Factory draftFactory;
- private final NotesMigration migration;
-
- @Inject
- PatchLineCommentsUtil(GitRepositoryManager repoManager,
- AllUsersName allUsers,
- DraftCommentNotes.Factory draftFactory,
- NotesMigration migration) {
- this.repoManager = repoManager;
- this.allUsers = allUsers;
- this.draftFactory = draftFactory;
- this.migration = migration;
- }
-
- public Optional<PatchLineComment> get(ReviewDb db, ChangeNotes notes,
- PatchLineComment.Key key) throws OrmException {
- if (!migration.readChanges()) {
- return Optional.fromNullable(db.patchComments().get(key));
- }
- for (PatchLineComment c : publishedByChange(db, notes)) {
- if (key.equals(c.getKey())) {
- return Optional.of(c);
- }
- }
- for (PatchLineComment c : draftByChange(db, notes)) {
- if (key.equals(c.getKey())) {
- return Optional.of(c);
- }
- }
- return Optional.absent();
- }
-
- public List<PatchLineComment> publishedByChange(ReviewDb db,
- ChangeNotes notes) throws OrmException {
- if (!migration.readChanges()) {
- return sort(byCommentStatus(
- db.patchComments().byChange(notes.getChangeId()), Status.PUBLISHED));
- }
-
- notes.load();
- List<PatchLineComment> comments = new ArrayList<>();
- comments.addAll(notes.getComments().values());
- return sort(comments);
- }
-
- public List<PatchLineComment> draftByChange(ReviewDb db,
- ChangeNotes notes) throws OrmException {
- if (!migration.readChanges()) {
- return sort(byCommentStatus(
- db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
- }
-
- List<PatchLineComment> comments = new ArrayList<>();
- for (Ref ref : getDraftRefs(notes.getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null) {
- comments.addAll(draftByChangeAuthor(db, notes, account));
- }
- }
- return sort(comments);
- }
-
- private static List<PatchLineComment> byCommentStatus(
- ResultSet<PatchLineComment> comments,
- final PatchLineComment.Status status) {
- return Lists.newArrayList(
- Iterables.filter(comments, new Predicate<PatchLineComment>() {
- @Override
- public boolean apply(PatchLineComment input) {
- return (input.getStatus() == status);
- }
- })
- );
- }
-
- public List<PatchLineComment> byPatchSet(ReviewDb db,
- ChangeNotes notes, PatchSet.Id psId) throws OrmException {
- if (!migration.readChanges()) {
- return sort(db.patchComments().byPatchSet(psId).toList());
- }
- List<PatchLineComment> comments = new ArrayList<>();
- comments.addAll(publishedByPatchSet(db, notes, psId));
-
- for (Ref ref : getDraftRefs(notes.getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null) {
- comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
- }
- }
- return sort(comments);
- }
-
- public List<PatchLineComment> publishedByChangeFile(ReviewDb db,
- ChangeNotes notes, Change.Id changeId, String file) throws OrmException {
- if (!migration.readChanges()) {
- return sort(
- db.patchComments().publishedByChangeFile(changeId, file).toList());
- }
- return commentsOnFile(notes.load().getComments().values(), file);
- }
-
- public List<PatchLineComment> publishedByPatchSet(ReviewDb db,
- ChangeNotes notes, PatchSet.Id psId) throws OrmException {
- if (!migration.readChanges()) {
- return sort(
- db.patchComments().publishedByPatchSet(psId).toList());
- }
- return commentsOnPatchSet(notes.load().getComments().values(), psId);
- }
-
- public List<PatchLineComment> draftByPatchSetAuthor(ReviewDb db,
- PatchSet.Id psId, Account.Id author, ChangeNotes notes)
- throws OrmException {
- if (!migration.readChanges()) {
- return sort(
- db.patchComments().draftByPatchSetAuthor(psId, author).toList());
- }
- return commentsOnPatchSet(
- notes.load().getDraftComments(author).values(), psId);
- }
-
- public List<PatchLineComment> draftByChangeFileAuthor(ReviewDb db,
- ChangeNotes notes, String file, Account.Id author)
- throws OrmException {
- if (!migration.readChanges()) {
- return sort(
- db.patchComments()
- .draftByChangeFileAuthor(notes.getChangeId(), file, author)
- .toList());
- }
- return commentsOnFile(
- notes.load().getDraftComments(author).values(), file);
- }
-
- public List<PatchLineComment> draftByChangeAuthor(ReviewDb db,
- ChangeNotes notes, Account.Id author)
- throws OrmException {
- if (!migration.readChanges()) {
- final Change.Id matchId = notes.getChangeId();
- return FluentIterable
- .from(db.patchComments().draftByAuthor(author))
- .filter(new Predicate<PatchLineComment>() {
- @Override
- public boolean apply(PatchLineComment in) {
- Change.Id changeId =
- in.getKey().getParentKey().getParentKey().getParentKey();
- return changeId.equals(matchId);
- }
- }).toSortedList(PLC_ORDER);
- }
- List<PatchLineComment> comments = new ArrayList<>();
- comments.addAll(notes.getDraftComments(author).values());
- return sort(comments);
- }
-
- @Deprecated // To be used only by HasDraftByLegacyPredicate.
- public List<PatchLineComment> draftByAuthor(ReviewDb db,
- Account.Id author) throws OrmException {
- if (!migration.readChanges()) {
- return sort(db.patchComments().draftByAuthor(author).toList());
- }
-
- List<PatchLineComment> comments = new ArrayList<>();
- try (Repository repo = repoManager.openRepository(allUsers)) {
- for (String refName : repo.getRefDatabase()
- .getRefs(RefNames.REFS_DRAFT_COMMENTS).keySet()) {
- Account.Id accountId = Account.Id.fromRefSuffix(refName);
- Change.Id changeId = Change.Id.fromRefPart(refName);
- if (accountId == null || changeId == null) {
- continue;
- }
- // Avoid loading notes for all affected changes just to be able to auto-
- // rebuild. This is only used in a corner case in the search codepath,
- // so returning slightly stale values is ok.
- DraftCommentNotes notes =
- draftFactory.createWithAutoRebuildingDisabled(changeId, author);
- comments.addAll(notes.load().getComments().values());
- }
- } catch (IOException e) {
- throw new OrmException(e);
- }
- return sort(comments);
- }
-
- public void putComments(ReviewDb db, ChangeUpdate update,
- Iterable<PatchLineComment> comments) throws OrmException {
- for (PatchLineComment c : comments) {
- update.putComment(c);
- }
- db.patchComments().upsert(comments);
- }
-
- public void deleteComments(ReviewDb db, ChangeUpdate update,
- Iterable<PatchLineComment> comments) throws OrmException {
- for (PatchLineComment c : comments) {
- update.deleteComment(c);
- }
- db.patchComments().delete(comments);
- }
-
- public void deleteAllDraftsFromAllUsers(Change.Id changeId)
- throws IOException {
- try (Repository repo = repoManager.openRepository(allUsers);
- RevWalk rw = new RevWalk(repo)) {
- BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
- for (Ref ref : getDraftRefs(repo, changeId)) {
- bru.addCommand(new ReceiveCommand(
- ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
- }
- bru.setRefLogMessage("Delete drafts from NoteDb", false);
- bru.execute(rw, NullProgressMonitor.INSTANCE);
- for (ReceiveCommand cmd : bru.getCommands()) {
- if (cmd.getResult() != ReceiveCommand.Result.OK) {
- throw new IOException(String.format(
- "Failed to delete draft comment ref %s at %s: %s (%s)",
- cmd.getRefName(), cmd.getOldId(), cmd.getResult(),
- cmd.getMessage()));
- }
- }
- }
- }
-
- private static List<PatchLineComment> commentsOnFile(
- Collection<PatchLineComment> allComments,
- String file) {
- List<PatchLineComment> result = new ArrayList<>(allComments.size());
- for (PatchLineComment c : allComments) {
- String currentFilename = c.getKey().getParentKey().getFileName();
- if (currentFilename.equals(file)) {
- result.add(c);
- }
- }
- return sort(result);
- }
-
- private static List<PatchLineComment> commentsOnPatchSet(
- Collection<PatchLineComment> allComments,
- PatchSet.Id psId) {
- List<PatchLineComment> result = new ArrayList<>(allComments.size());
- for (PatchLineComment c : allComments) {
- if (getCommentPsId(c).equals(psId)) {
- result.add(c);
- }
- }
- return sort(result);
- }
-
- public static RevId setCommentRevId(PatchLineComment c,
- PatchListCache cache, Change change, PatchSet ps) throws OrmException {
- checkArgument(c.getPatchSetId().equals(ps.getId()),
- "cannot set RevId for patch set %s on comment %s", ps.getId(), c);
- if (c.getRevId() == null) {
- try {
- if (Side.fromShort(c.getSide()) == Side.PARENT) {
- if (c.getSide() < 0) {
- c.setRevId(new RevId(ObjectId.toString(
- cache.getOldId(change, ps, -c.getSide()))));
- } else {
- c.setRevId(new RevId(ObjectId.toString(
- cache.getOldId(change, ps, null))));
- }
- } else {
- c.setRevId(ps.getRevision());
- }
- } catch (PatchListNotAvailableException e) {
- throw new OrmException(e);
- }
- }
- return c.getRevId();
- }
-
- public Collection<Ref> getDraftRefs(Change.Id changeId)
- throws OrmException {
- try (Repository repo = repoManager.openRepository(allUsers)) {
- return getDraftRefs(repo, changeId);
- } catch (IOException e) {
- throw new OrmException(e);
- }
- }
-
- private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId)
- throws IOException {
- return repo.getRefDatabase().getRefs(
- RefNames.refsDraftCommentsPrefix(changeId)).values();
- }
-
- private static List<PatchLineComment> sort(List<PatchLineComment> comments) {
- Collections.sort(comments, PLC_ORDER);
- return comments;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index e1f786b..b01c233 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -14,14 +14,14 @@
package com.google.gerrit.server;
-import com.google.common.base.Function;
+import static java.util.Comparator.comparing;
+
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -68,19 +68,17 @@
public class ReviewersUtil {
private static final String MAX_SUFFIX = "\u9fa5";
private static final Ordering<SuggestedReviewerInfo> ORDERING =
- Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
- @Nullable
- @Override
- public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
- if (suggestedReviewerInfo == null) {
- return null;
- }
- return suggestedReviewerInfo.account != null
- ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
- Strings.nullToEmpty(suggestedReviewerInfo.account.name))
- : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
- }
- });
+ Ordering.<SuggestedReviewerInfo> from(comparing(
+ suggestedReviewerInfo -> {
+ if (suggestedReviewerInfo == null) {
+ return null;
+ }
+ return suggestedReviewerInfo.account != null
+ ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
+ Strings.nullToEmpty(suggestedReviewerInfo.account.name))
+ : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
+ }));
+
private final AccountLoader accountLoader;
private final AccountCache accountCache;
private final AccountIndexCollection indexes;
@@ -123,7 +121,7 @@
public List<SuggestedReviewerInfo> suggestReviewers(
SuggestReviewers suggestReviewers, ProjectControl projectControl,
- VisibilityControl visibilityControl)
+ VisibilityControl visibilityControl, boolean excludeGroups)
throws IOException, OrmException, BadRequestException {
String query = suggestReviewers.getQuery();
boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
@@ -149,20 +147,22 @@
reviewer.add(info);
}
- for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
- GroupAsReviewer result = suggestGroupAsReviewer(
- suggestReviewers, projectControl.getProject(), g, visibilityControl);
- if (result.allowed || result.allowedWithConfirmation) {
- GroupBaseInfo info = new GroupBaseInfo();
- info.id = Url.encode(g.getUUID().get());
- info.name = g.getName();
- SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
- suggestedReviewerInfo.group = info;
- suggestedReviewerInfo.count = result.size;
- if (result.allowedWithConfirmation) {
- suggestedReviewerInfo.confirm = true;
+ if (!excludeGroups) {
+ for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
+ GroupAsReviewer result = suggestGroupAsReviewer(
+ suggestReviewers, projectControl.getProject(), g, visibilityControl);
+ if (result.allowed || result.allowedWithConfirmation) {
+ GroupBaseInfo info = new GroupBaseInfo();
+ info.id = Url.encode(g.getUUID().get());
+ info.name = g.getName();
+ SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
+ suggestedReviewerInfo.group = info;
+ suggestedReviewerInfo.count = result.size;
+ if (result.allowedWithConfirmation) {
+ suggestedReviewerInfo.confirm = true;
+ }
+ reviewer.add(suggestedReviewerInfo);
}
- reviewer.add(suggestedReviewerInfo);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 5a89afa..8f25e43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -15,14 +15,12 @@
package com.google.gerrit.server;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
-import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
@@ -249,29 +247,11 @@
public Set<Account.Id> byChange(final Change.Id changeId,
final String label) throws OrmException {
try (final Repository repo = repoManager.openRepository(allUsers)) {
- return FluentIterable
- .from(getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId)))
- .transform(new Function<String, Account.Id>() {
- @Override
- public Account.Id apply(String refPart) {
- return Account.Id.parse(refPart);
- }
- })
- .filter(new Predicate<Account.Id>() {
- @Override
- public boolean apply(Account.Id accountId) {
- try {
- return readLabels(repo,
- RefNames.refsStarredChanges(changeId, accountId))
- .contains(label);
- } catch (IOException e) {
- log.error(String.format(
- "Cannot query stars by account %d on change %d",
- accountId.get(), changeId.get()), e);
- return false;
- }
- }
- }).toSet();
+ return getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))
+ .stream()
+ .map(Account.Id::parse)
+ .filter(accountId -> hasStar(repo, changeId, accountId, label))
+ .collect(toSet());
} catch (IOException e) {
throw new OrmException(
String.format("Get accounts that starred change %d failed",
@@ -283,36 +263,12 @@
// To be used only for IsStarredByLegacyPredicate.
public Set<Change.Id> byAccount(final Account.Id accountId,
final String label) throws OrmException {
- try (final Repository repo = repoManager.openRepository(allUsers)) {
- return FluentIterable
- .from(getRefNames(repo, RefNames.REFS_STARRED_CHANGES))
- .filter(new Predicate<String>() {
- @Override
- public boolean apply(String refPart) {
- return refPart.endsWith("/" + accountId.get());
- }
- })
- .transform(new Function<String, Change.Id>() {
- @Override
- public Change.Id apply(String refPart) {
- return Change.Id.fromRefPart(refPart);
- }
- })
- .filter(new Predicate<Change.Id>() {
- @Override
- public boolean apply(Change.Id changeId) {
- try {
- return readLabels(repo,
- RefNames.refsStarredChanges(changeId, accountId))
- .contains(label);
- } catch (IOException e) {
- log.error(String.format(
- "Cannot query stars by account %d on change %d",
- accountId.get(), changeId.get()), e);
- return false;
- }
- }
- }).toSet();
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getRefNames(repo, RefNames.REFS_STARRED_CHANGES).stream()
+ .filter(refPart -> refPart.endsWith("/" + accountId.get()))
+ .map(Change.Id::fromRefPart)
+ .filter(changeId -> hasStar(repo, changeId, accountId, label))
+ .collect(toSet());
} catch (IOException e) {
throw new OrmException(
String.format("Get changes that were starred by %d failed",
@@ -320,6 +276,20 @@
}
}
+ private boolean hasStar(Repository repo, Change.Id changeId,
+ Account.Id accountId, String label) {
+ try {
+ return readLabels(repo,
+ RefNames.refsStarredChanges(changeId, accountId))
+ .contains(label);
+ } catch (IOException e) {
+ log.error(String.format(
+ "Cannot query stars by account %d on change %d",
+ accountId.get(), changeId.get()), e);
+ return false;
+ }
+ }
+
public ImmutableMultimap<Account.Id, String> byChangeFromIndex(
Change.Id changeId) throws OrmException, NoSuchChangeException {
Set<String> fields = ImmutableSet.of(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index 761f2a3..6dccbc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -39,37 +39,31 @@
@Singleton
public class WebLinks {
private static final Logger log = LoggerFactory.getLogger(WebLinks.class);
+
private static final Predicate<WebLinkInfo> INVALID_WEBLINK =
- new Predicate<WebLinkInfo>() {
-
- @Override
- public boolean apply(WebLinkInfo link) {
- if (link == null) {
- return false;
- } else if (Strings.isNullOrEmpty(link.name)
- || Strings.isNullOrEmpty(link.url)) {
- log.warn(String.format("%s is missing name and/or url",
- link.getClass().getName()));
- return false;
- }
- return true;
+ link -> {
+ if (link == null) {
+ return false;
+ } else if (Strings.isNullOrEmpty(link.name)
+ || Strings.isNullOrEmpty(link.url)) {
+ log.warn(String.format("%s is missing name and/or url",
+ link.getClass().getName()));
+ return false;
}
+ return true;
};
- private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
- new Predicate<WebLinkInfoCommon>() {
- @Override
- public boolean apply(WebLinkInfoCommon link) {
- if (link == null) {
- return false;
- } else if (Strings.isNullOrEmpty(link.name)
- || Strings.isNullOrEmpty(link.url)) {
- log.warn(String.format("%s is missing name and/or url", link
- .getClass().getName()));
- return false;
- }
- return true;
+ private static final Predicate<WebLinkInfoCommon> INVALID_WEBLINK_COMMON =
+ link -> {
+ if (link == null) {
+ return false;
+ } else if (Strings.isNullOrEmpty(link.name)
+ || Strings.isNullOrEmpty(link.url)) {
+ log.warn(String.format("%s is missing name and/or url", link
+ .getClass().getName()));
+ return false;
}
+ return true;
};
private final DynamicSet<PatchSetWebLink> patchSetLinks;
@@ -85,8 +79,7 @@
DynamicSet<FileHistoryWebLink> fileLogLinks,
DynamicSet<DiffWebLink> diffLinks,
DynamicSet<ProjectWebLink> projectLinks,
- DynamicSet<BranchWebLink> branchLinks
- ) {
+ DynamicSet<BranchWebLink> branchLinks) {
this.patchSetLinks = patchSetLinks;
this.fileLinks = fileLinks;
this.fileHistoryLinks = fileLogLinks;
@@ -101,15 +94,11 @@
* @param commit SHA1 of commit.
* @return Links for patch sets.
*/
- public FluentIterable<WebLinkInfo> getPatchSetLinks(final Project.NameKey project,
- final String commit) {
- return filterLinks(patchSetLinks, new Function<WebLink, WebLinkInfo>() {
-
- @Override
- public WebLinkInfo apply(WebLink webLink) {
- return ((PatchSetWebLink)webLink).getPatchSetWebLink(project.get(), commit);
- }
- });
+ public FluentIterable<WebLinkInfo> getPatchSetLinks(Project.NameKey project,
+ String commit) {
+ return filterLinks(
+ patchSetLinks,
+ webLink -> webLink.getPatchSetWebLink(project.get(), commit));
}
/**
@@ -119,15 +108,11 @@
* @param file File name.
* @return Links for files.
*/
- public FluentIterable<WebLinkInfo> getFileLinks(final String project, final String revision,
- final String file) {
- return filterLinks(fileLinks, new Function<WebLink, WebLinkInfo>() {
-
- @Override
- public WebLinkInfo apply(WebLink webLink) {
- return ((FileWebLink)webLink).getFileWebLink(project, revision, file);
- }
- });
+ public FluentIterable<WebLinkInfo> getFileLinks(String project,
+ String revision, String file) {
+ return filterLinks(
+ fileLinks,
+ webLink -> webLink.getFileWebLink(project, revision, file));
}
/**
@@ -137,39 +122,31 @@
* @param file File name.
* @return Links for file history
*/
- public FluentIterable<WebLinkInfo> getFileHistoryLinks(final String project,
- final String revision, final String file) {
- return filterLinks(fileHistoryLinks, new Function<WebLink, WebLinkInfo>() {
-
- @Override
- public WebLinkInfo apply(WebLink webLink) {
- return ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
- revision, file);
- }
- });
+ public FluentIterable<WebLinkInfo> getFileHistoryLinks(String project,
+ String revision, String file) {
+ return filterLinks(
+ fileHistoryLinks,
+ webLink -> webLink.getFileHistoryWebLink(project, revision, file));
}
public FluentIterable<WebLinkInfoCommon> getFileHistoryLinksCommon(
- final String project, final String revision, final String file) {
+ String project, String revision, String file) {
return FluentIterable
.from(fileHistoryLinks)
- .transform(new Function<WebLink, WebLinkInfoCommon>() {
- @Override
- public WebLinkInfoCommon apply(WebLink webLink) {
- WebLinkInfo info =
- ((FileHistoryWebLink) webLink).getFileHistoryWebLink(project,
- revision, file);
- if (info == null) {
- return null;
- }
- WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
- commonInfo.name = info.name;
- commonInfo.imageUrl = info.imageUrl;
- commonInfo.url = info.url;
- commonInfo.target = info.target;
- return commonInfo;
- }
- })
+ .transform(
+ webLink -> {
+ WebLinkInfo info =
+ webLink.getFileHistoryWebLink(project, revision, file);
+ if (info == null) {
+ return null;
+ }
+ WebLinkInfoCommon commonInfo = new WebLinkInfoCommon();
+ commonInfo.name = info.name;
+ commonInfo.imageUrl = info.imageUrl;
+ commonInfo.url = info.url;
+ commonInfo.target = info.target;
+ return commonInfo;
+ })
.filter(INVALID_WEBLINK_COMMON);
}
@@ -190,14 +167,10 @@
final int patchSetIdB, final String revisionB, final String fileB) {
return FluentIterable
.from(diffLinks)
- .transform(new Function<WebLink, DiffWebLinkInfo>() {
- @Override
- public DiffWebLinkInfo apply(WebLink webLink) {
- return ((DiffWebLink) webLink).getDiffLink(project, changeId,
+ .transform(webLink ->
+ webLink.getDiffLink(project, changeId,
patchSetIdA, revisionA, fileA,
- patchSetIdB, revisionB, fileB);
- }
- })
+ patchSetIdB, revisionB, fileB))
.filter(INVALID_WEBLINK);
}
@@ -207,13 +180,9 @@
* @return Links for projects.
*/
public FluentIterable<WebLinkInfo> getProjectLinks(final String project) {
- return filterLinks(projectLinks, new Function<WebLink, WebLinkInfo>() {
-
- @Override
- public WebLinkInfo apply(WebLink webLink) {
- return ((ProjectWebLink)webLink).getProjectWeblink(project);
- }
- });
+ return filterLinks(
+ projectLinks,
+ webLink -> webLink.getProjectWeblink(project));
}
/**
@@ -223,17 +192,13 @@
* @return Links for branches.
*/
public FluentIterable<WebLinkInfo> getBranchLinks(final String project, final String branch) {
- return filterLinks(branchLinks, new Function<WebLink, WebLinkInfo>() {
-
- @Override
- public WebLinkInfo apply(WebLink webLink) {
- return ((BranchWebLink)webLink).getBranchWebLink(project, branch);
- }
- });
+ return filterLinks(
+ branchLinks,
+ webLink -> webLink.getBranchWebLink(project, branch));
}
- private FluentIterable<WebLinkInfo> filterLinks(DynamicSet<? extends WebLink> links,
- Function<WebLink, WebLinkInfo> transformer) {
+ private <T extends WebLink> FluentIterable<WebLinkInfo> filterLinks(DynamicSet<T> links,
+ Function<T, WebLinkInfo> transformer) {
return FluentIterable
.from(links)
.transform(transformer)
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/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index c5b0699..db2a98f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Sets;
+import static java.util.stream.Collectors.toSet;
+
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
@@ -28,7 +28,6 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.HashSet;
import java.util.Set;
/** Access control management for one account's access to other accounts. */
@@ -186,14 +185,9 @@
}
private Set<AccountGroup.UUID> groupsOf(IdentifiedUser user) {
- return new HashSet<>(Sets.filter(
- user.getEffectiveGroups().getKnownGroups(),
- new Predicate<AccountGroup.UUID>() {
- @Override
- public boolean apply(AccountGroup.UUID in) {
- return !SystemGroupBackend.isSystemGroup(in);
- }
- }));
+ return user.getEffectiveGroups().getKnownGroups().stream()
+ .filter(a -> !SystemGroupBackend.isSystemGroup(a))
+ .collect(toSet());
}
private abstract static class OtherUser {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountJson.java
new file mode 100644
index 0000000..7193564
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountJson.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+
+public class AccountJson {
+
+ public static AccountInfo toAccountInfo(Account account) {
+ if (account == null || account.getId() == null) {
+ return null;
+ }
+ AccountInfo accountInfo = new AccountInfo(account.getId().get());
+ accountInfo.email = account.getPreferredEmail();
+ accountInfo.name = account.getFullName();
+ accountInfo.username = account.getUserName();
+ return accountInfo;
+ }
+
+}
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 9cbd1b0..7c69224 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/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 5a18269..b400eb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
+import static java.util.stream.Collectors.toSet;
+
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -191,14 +191,9 @@
// At this point we have no clue. Just perform a whole bunch of suggestions
// and pray we come up with a reasonable result list.
- return FluentIterable
- .from(accountQueryProvider.get().byDefault(nameOrEmail))
- .transform(new Function<AccountState, Account.Id>() {
- @Override
- public Account.Id apply(AccountState accountState) {
- return accountState.getAccount().getId();
- }
- }).toSet();
+ return accountQueryProvider.get().byDefault(nameOrEmail).stream()
+ .map(a -> a.getAccount().getId())
+ .collect(toSet());
}
List<Account> m = db.accounts().byFullName(nameOrEmail).toList();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 05a7179..ed99266 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -36,12 +36,7 @@
public class AccountState {
public static final Function<AccountState, Account.Id> ACCOUNT_ID_FUNCTION =
- new Function<AccountState, Account.Id>() {
- @Override
- public Account.Id apply(AccountState in) {
- return in.getAccount().getId();
- }
- };
+ a -> a.getAccount().getId();
private final Account account;
private final Set<AccountGroup.UUID> internalGroups;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 04ebc87..c7ce1b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -90,15 +91,7 @@
*/
public IdentifiedUser parse(String id) throws AuthException,
UnprocessableEntityException, OrmException {
- IdentifiedUser user = parseId(id);
- if (user == null) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", id));
- } else if (!accountControlFactory.get().canSee(user.getAccount())) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", id));
- }
- return user;
+ return parseOnBehalfOf(null, id);
}
/**
@@ -115,6 +108,29 @@
* @throws OrmException
*/
public IdentifiedUser parseId(String id) throws AuthException, OrmException {
+ return parseIdOnBehalfOf(null, id);
+ }
+
+ /**
+ * Like {@link #parse(String)}, but also sets the {@link
+ * CurrentUser#getRealUser()} on the result.
+ */
+ public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller,
+ String id)
+ throws AuthException, UnprocessableEntityException, OrmException {
+ IdentifiedUser user = parseIdOnBehalfOf(caller, id);
+ if (user == null) {
+ throw new UnprocessableEntityException(String.format(
+ "Account Not Found: %s", id));
+ } else if (!accountControlFactory.get().canSee(user.getAccount())) {
+ throw new UnprocessableEntityException(String.format(
+ "Account Not Found: %s", id));
+ }
+ return user;
+ }
+
+ private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller,
+ String id) throws AuthException, OrmException {
if (id.equals("self")) {
CurrentUser user = self.get();
if (user.isIdentifiedUser()) {
@@ -130,7 +146,8 @@
if (match == null) {
return null;
}
- return userFactory.create(match.getId());
+ CurrentUser realUser = caller != null ? caller.getRealUser() : null;
+ return userFactory.runAs(null, match.getId(), realUser);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index e348e73..d86d27c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -14,15 +14,14 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Function;
+import static com.google.common.base.Predicates.not;
+
import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
+import com.google.common.collect.FluentIterable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.git.QueueProvider;
@@ -32,6 +31,7 @@
import com.google.inject.assistedinject.Assisted;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -98,7 +98,7 @@
if (canEmailReviewers == null) {
canEmailReviewers =
matchAny(capabilities.emailReviewers, ALLOWED_RULE)
- || !matchAny(capabilities.emailReviewers, Predicates.not(ALLOWED_RULE));
+ || !matchAny(capabilities.emailReviewers, not(ALLOWED_RULE));
}
return canEmailReviewers;
@@ -279,23 +279,16 @@
return mine;
}
- private static final Predicate<PermissionRule> ALLOWED_RULE = new Predicate<PermissionRule>() {
- @Override
- public boolean apply(PermissionRule rule) {
- return rule.getAction() == Action.ALLOW;
- }
- };
+ private static final Predicate<PermissionRule> ALLOWED_RULE =
+ r -> r.getAction() == Action.ALLOW;
- private boolean matchAny(Iterable<PermissionRule> rules, Predicate<PermissionRule> predicate) {
- Iterable<AccountGroup.UUID> ids = Iterables.transform(
- Iterables.filter(rules, predicate),
- new Function<PermissionRule, AccountGroup.UUID>() {
- @Override
- public AccountGroup.UUID apply(PermissionRule rule) {
- return rule.getGroup().getUUID();
- }
- });
- return user.getEffectiveGroups().containsAnyOf(ids);
+ private boolean matchAny(Collection<PermissionRule> rules,
+ Predicate<PermissionRule> predicate) {
+ return user.getEffectiveGroups()
+ .containsAnyOf(
+ FluentIterable.from(rules)
+ .filter(predicate)
+ .transform(r -> r.getGroup().getUUID()));
}
private static boolean match(GroupMembership groups,
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 713154c..c4ab7a5 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/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
index e2fbc3c..0e9bc2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
+import static java.util.stream.Collectors.toList;
+
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
@@ -105,13 +105,10 @@
private void deleteFromGit(Account.Id accountId, List<ProjectWatchInfo> input)
throws IOException, ConfigInvalidException {
- watchConfig.deleteProjectWatches(accountId, Lists.transform(input,
- new Function<ProjectWatchInfo, ProjectWatchKey>() {
- @Override
- public ProjectWatchKey apply(ProjectWatchInfo info) {
- return ProjectWatchKey.create(new Project.NameKey(info.project),
- info.filter);
- }
- }));
+ watchConfig.deleteProjectWatches(
+ accountId,
+ input.stream().map(w -> ProjectWatchKey.create(
+ new Project.NameKey(w.project), w.filter))
+ .collect(toList()));
}
}
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/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index bf1a3af..df125e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -60,13 +59,9 @@
public List<SshKeyInfo> apply(IdentifiedUser user)
throws RepositoryNotFoundException, IOException, ConfigInvalidException {
- return Lists.transform(authorizedKeys.getKeys(user.getAccountId()),
- new Function<AccountSshKey, SshKeyInfo>() {
- @Override
- public SshKeyInfo apply(AccountSshKey key) {
- return newSshKeyInfo(key);
- }
- });
+ return Lists.transform(
+ authorizedKeys.getKeys(user.getAccountId()),
+ GetSshKeys::newSshKeyInfo);
}
public static SshKeyInfo newSshKeyInfo(AccountSshKey sshKey) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index c47d6f8..84660ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -14,10 +14,8 @@
package com.google.gerrit.server.account;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
+import static java.util.stream.Collectors.toList;
+
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.data.GroupReference;
@@ -30,18 +28,11 @@
import org.eclipse.jgit.lib.ObjectId;
import java.util.Collection;
+import java.util.stream.StreamSupport;
/** Implementation of GroupBackend for the internal group system. */
@Singleton
public class InternalGroupBackend implements GroupBackend {
- private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
- new Function<AccountGroup, GroupReference>() {
- @Override
- public GroupReference apply(AccountGroup group) {
- return GroupReference.forGroup(group);
- }
- };
-
private final GroupControl.Factory groupControlFactory;
private final GroupCache groupCache;
private final IncludingGroupMembership.Factory groupMembershipFactory;
@@ -77,16 +68,13 @@
@Override
public Collection<GroupReference> suggest(final String name,
final ProjectControl project) {
- Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
- new Predicate<AccountGroup>() {
- @Override
- public boolean apply(AccountGroup group) {
+ return StreamSupport.stream(groupCache.all().spliterator(), false)
+ .filter(group ->
// startsWithIgnoreCase && isVisible
- return group.getName().regionMatches(true, 0, name, 0, name.length())
- && groupControlFactory.controlFor(group).isVisible();
- }
- });
- return Lists.newArrayList(Iterables.transform(filtered, ACT_GROUP_TO_GROUP_REF));
+ group.getName().regionMatches(true, 0, name, 0, name.length())
+ && groupControlFactory.controlFor(group).isVisible())
+ .map(GroupReference::forGroup)
+ .collect(toList());
}
@Override
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/account/VersionedAuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index bb744ce..aa32d27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -15,8 +15,8 @@
package com.google.gerrit.server.account;
import static com.google.common.base.Preconditions.checkState;
+import static java.util.Comparator.comparing;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
@@ -278,13 +278,7 @@
* @param newKeys the new public SSH keys
*/
public void setKeys(Collection<AccountSshKey> newKeys) {
- Ordering<AccountSshKey> o =
- Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
- @Override
- public Integer apply(AccountSshKey sshKey) {
- return sshKey.getKey().get();
- }
- });
+ Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get()));
keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(),
Optional.<AccountSshKey> absent()));
for (AccountSshKey key : newKeys) {
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/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 9bfb342..f7cb8f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AssigneeInput;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.api.changes.FixInput;
@@ -28,6 +29,7 @@
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EditInfo;
@@ -40,8 +42,11 @@
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.Check;
+import com.google.gerrit.server.change.DeleteAssignee;
import com.google.gerrit.server.change.DeleteDraftChange;
+import com.google.gerrit.server.change.GetAssignee;
import com.google.gerrit.server.change.GetHashtags;
+import com.google.gerrit.server.change.GetPastAssignees;
import com.google.gerrit.server.change.GetTopic;
import com.google.gerrit.server.change.Index;
import com.google.gerrit.server.change.ListChangeComments;
@@ -50,6 +55,7 @@
import com.google.gerrit.server.change.PostHashtags;
import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.PublishDraftPatchSet;
+import com.google.gerrit.server.change.PutAssignee;
import com.google.gerrit.server.change.PutTopic;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.change.Revert;
@@ -61,6 +67,7 @@
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 java.io.IOException;
@@ -84,7 +91,7 @@
private final Abandon abandon;
private final Revert revert;
private final Restore restore;
- private final SubmittedTogether submittedTogether;
+ private final Provider<SubmittedTogether> submittedTogether;
private final PublishDraftPatchSet.CurrentRevision
publishDraftChange;
private final DeleteDraftChange deleteDraftChange;
@@ -94,6 +101,10 @@
private final ChangeJson.Factory changeJson;
private final PostHashtags postHashtags;
private final GetHashtags getHashtags;
+ private final PutAssignee putAssignee;
+ private final GetAssignee getAssignee;
+ private final GetPastAssignees getPastAssignees;
+ private final DeleteAssignee deleteAssignee;
private final ListChangeComments listComments;
private final ListChangeDrafts listDrafts;
private final Check check;
@@ -111,7 +122,7 @@
Abandon abandon,
Revert revert,
Restore restore,
- SubmittedTogether submittedTogether,
+ Provider<SubmittedTogether> submittedTogether,
PublishDraftPatchSet.CurrentRevision publishDraftChange,
DeleteDraftChange deleteDraftChange,
GetTopic getTopic,
@@ -120,6 +131,10 @@
ChangeJson.Factory changeJson,
PostHashtags postHashtags,
GetHashtags getHashtags,
+ PutAssignee putAssignee,
+ GetAssignee getAssignee,
+ GetPastAssignees getPastAssignees,
+ DeleteAssignee deleteAssignee,
ListChangeComments listComments,
ListChangeDrafts listDrafts,
Check check,
@@ -145,6 +160,10 @@
this.changeJson = changeJson;
this.postHashtags = postHashtags;
this.getHashtags = getHashtags;
+ this.putAssignee = putAssignee;
+ this.getAssignee = getAssignee;
+ this.getPastAssignees = getPastAssignees;
+ this.deleteAssignee = deleteAssignee;
this.listComments = listComments;
this.listDrafts = listDrafts;
this.check = check;
@@ -248,21 +267,29 @@
}
}
- @SuppressWarnings("unchecked")
@Override
public List<ChangeInfo> submittedTogether() throws RestApiException {
- try {
- return (List<ChangeInfo>) submittedTogether.apply(change);
- } catch (IOException | OrmException e) {
- throw new RestApiException("Cannot query submittedTogether", e);
- }
+ SubmittedTogetherInfo info = submittedTogether(
+ EnumSet.noneOf(ListChangesOption.class),
+ EnumSet.noneOf(SubmittedTogetherOption.class));
+ return info.changes;
}
@Override
public SubmittedTogetherInfo submittedTogether(
EnumSet<SubmittedTogetherOption> options) throws RestApiException {
+ return submittedTogether(EnumSet.noneOf(ListChangesOption.class), options);
+ }
+
+ @Override
+ public SubmittedTogetherInfo submittedTogether(
+ EnumSet<ListChangesOption> listOptions,
+ EnumSet<SubmittedTogetherOption> submitOptions) throws RestApiException {
try {
- return submittedTogether.apply(change, options);
+ return submittedTogether.get()
+ .addListChangesOption(listOptions)
+ .addSubmittedTogetherOption(submitOptions)
+ .applyInfo(change);
} catch (IOException | OrmException e) {
throw new RestApiException("Cannot query submittedTogether", e);
}
@@ -394,6 +421,45 @@
}
@Override
+ public AccountInfo setAssignee(AssigneeInput input)
+ throws RestApiException {
+ try {
+ return putAssignee.apply(change, input).value();
+ } catch (UpdateException | IOException | OrmException e) {
+ throw new RestApiException("Cannot set assignee", e);
+ }
+ }
+
+ @Override
+ public AccountInfo getAssignee() throws RestApiException {
+ try {
+ Response<AccountInfo> r = getAssignee.apply(change);
+ return r.isNone() ? null : r.value();
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot get assignee", e);
+ }
+ }
+
+ @Override
+ public List<AccountInfo> getPastAssignees() throws RestApiException {
+ try {
+ return getPastAssignees.apply(change).value();
+ } catch (Exception e) {
+ throw new RestApiException("Cannot get past assignees", e);
+ }
+ }
+
+ @Override
+ public AccountInfo deleteAssignee() throws RestApiException {
+ try {
+ Response<AccountInfo> r = deleteAssignee.apply(change, null);
+ return r.isNone() ? null : r.value();
+ } catch (UpdateException e) {
+ throw new RestApiException("Cannot delete assignee", e);
+ }
+ }
+
+ @Override
public Map<String, List<CommentInfo>> comments() throws RestApiException {
try {
return listComments.apply(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
index 228dad6..bc38df2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
@@ -24,6 +24,7 @@
factory(ChangeApiImpl.Factory.class);
factory(CommentApiImpl.Factory.class);
+ factory(RobotCommentApiImpl.Factory.class);
factory(DraftApiImpl.Factory.class);
factory(RevisionApiImpl.Factory.class);
factory(FileApiImpl.Factory.class);
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..4e847a1 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
@@ -26,12 +26,15 @@
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.changes.RobotCommentApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
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.RobotCommentInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
@@ -44,24 +47,30 @@
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;
import com.google.gerrit.server.change.ListRevisionDrafts;
+import com.google.gerrit.server.change.ListRobotComments;
import com.google.gerrit.server.change.Mergeable;
import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.PreviewSubmit;
import com.google.gerrit.server.change.PublishDraftPatchSet;
import com.google.gerrit.server.change.Rebase;
import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.Reviewed;
import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.RobotComments;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.change.TestSubmitType;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.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;
@@ -84,6 +93,7 @@
private final Rebase rebase;
private final RebaseUtil rebaseUtil;
private final Submit submit;
+ private final PreviewSubmit submitPreview;
private final PublishDraftPatchSet publish;
private final Reviewed.PutReviewed putReviewed;
private final Reviewed.DeleteReviewed deleteReviewed;
@@ -95,15 +105,19 @@
private final Mergeable mergeable;
private final FileApiImpl.Factory fileApi;
private final ListRevisionComments listComments;
+ private final ListRobotComments listRobotComments;
private final ListRevisionDrafts listDrafts;
private final CreateDraftComment createDraft;
private final DraftComments drafts;
private final DraftApiImpl.Factory draftFactory;
private final Comments comments;
private final CommentApiImpl.Factory commentFactory;
+ private final RobotComments robotComments;
+ private final RobotCommentApiImpl.Factory robotCommentFactory;
private final GetRevisionActions revisionActions;
private final TestSubmitType testSubmitType;
private final TestSubmitType.Get getSubmitType;
+ private final Provider<GetMergeList> getMergeList;
@Inject
RevisionApiImpl(GitRepositoryManager repoManager,
@@ -113,6 +127,7 @@
Rebase rebase,
RebaseUtil rebaseUtil,
Submit submit,
+ PreviewSubmit submitPreview,
PublishDraftPatchSet publish,
Reviewed.PutReviewed putReviewed,
Reviewed.DeleteReviewed deleteReviewed,
@@ -123,15 +138,19 @@
Mergeable mergeable,
FileApiImpl.Factory fileApi,
ListRevisionComments listComments,
+ ListRobotComments listRobotComments,
ListRevisionDrafts listDrafts,
CreateDraftComment createDraft,
DraftComments drafts,
DraftApiImpl.Factory draftFactory,
Comments comments,
CommentApiImpl.Factory commentFactory,
+ RobotComments robotComments,
+ RobotCommentApiImpl.Factory robotCommentFactory,
GetRevisionActions revisionActions,
TestSubmitType testSubmitType,
TestSubmitType.Get getSubmitType,
+ Provider<GetMergeList> getMergeList,
@Assisted RevisionResource r) {
this.repoManager = repoManager;
this.changes = changes;
@@ -141,6 +160,7 @@
this.rebaseUtil = rebaseUtil;
this.review = review;
this.submit = submit;
+ this.submitPreview = submitPreview;
this.publish = publish;
this.files = files;
this.putReviewed = putReviewed;
@@ -150,15 +170,19 @@
this.mergeable = mergeable;
this.fileApi = fileApi;
this.listComments = listComments;
+ this.robotComments = robotComments;
+ this.listRobotComments = listRobotComments;
this.listDrafts = listDrafts;
this.createDraft = createDraft;
this.drafts = drafts;
this.draftFactory = draftFactory;
this.comments = comments;
this.commentFactory = commentFactory;
+ this.robotCommentFactory = robotCommentFactory;
this.revisionActions = revisionActions;
this.testSubmitType = testSubmitType;
this.getSubmitType = getSubmitType;
+ this.getMergeList = getMergeList;
this.revision = r;
}
@@ -187,6 +211,12 @@
}
@Override
+ public BinaryResult submitPreview() throws RestApiException {
+ submitPreview.setFormat("zip");
+ return submitPreview.apply(revision);
+ }
+
+ @Override
public void publish() throws RestApiException {
try {
publish.apply(revision, new PublishDraftPatchSet.Input());
@@ -264,7 +294,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 +323,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 +334,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 +345,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);
}
}
@@ -336,6 +366,15 @@
}
@Override
+ public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
+ try {
+ return listRobotComments.apply(revision);
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot retrieve robot comments", e);
+ }
+ }
+
+ @Override
public List<CommentInfo> commentsAsList() throws RestApiException {
try {
return listComments.getComments(revision);
@@ -354,6 +393,15 @@
}
@Override
+ public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
+ try {
+ return listRobotComments.getComments(revision);
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot retrieve robot comments", e);
+ }
+ }
+
+ @Override
public List<CommentInfo> draftsAsList() throws RestApiException {
try {
return listDrafts.getComments(revision);
@@ -396,6 +444,16 @@
}
@Override
+ public RobotCommentApi robotComment(String id) throws RestApiException {
+ try {
+ return robotCommentFactory
+ .create(robotComments.parse(revision, IdString.fromDecoded(id)));
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot retrieve robot comment", e);
+ }
+ }
+
+ @Override
public BinaryResult patch() throws RestApiException {
try {
return getPatch.apply(revision);
@@ -427,4 +485,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/changes/RobotCommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
new file mode 100644
index 0000000..9169a4f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
@@ -0,0 +1,49 @@
+// 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.api.changes;
+
+import com.google.gerrit.extensions.api.changes.RobotCommentApi;
+import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.GetRobotComment;
+import com.google.gerrit.server.change.RobotCommentResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class RobotCommentApiImpl implements RobotCommentApi {
+ interface Factory {
+ RobotCommentApiImpl create(RobotCommentResource c);
+ }
+
+ private final GetRobotComment getComment;
+ private final RobotCommentResource comment;
+
+ @Inject
+ RobotCommentApiImpl(GetRobotComment getComment,
+ @Assisted RobotCommentResource comment) {
+ this.getComment = getComment;
+ this.comment = comment;
+ }
+
+ @Override
+ public RobotCommentInfo get() throws RestApiException {
+ try {
+ return getComment.apply(comment);
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot retrieve robot comment", e);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index 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/cache/CacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
index 7062871..343827c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -26,6 +26,9 @@
/** Set the total size of the cache. */
CacheBinding<K, V> maximumWeight(long weight);
+ /** Set the total on-disk limit of the cache */
+ CacheBinding<K, V> diskLimit(long limit);
+
/** Set the time an element lives before being expired. */
CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
@@ -39,6 +42,7 @@
TypeLiteral<K> keyType();
TypeLiteral<V> valueType();
long maximumWeight();
+ long diskLimit();
@Nullable Long expireAfterWrite(TimeUnit unit);
@Nullable Weigher<K, V> weigher();
@Nullable CacheLoader<K, V> loader();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 6d9ae0f..c73760c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -38,6 +38,7 @@
private final TypeLiteral<V> valType;
private boolean persist;
private long maximumWeight;
+ private long diskLimit;
private Long expireAfterWrite;
private Provider<CacheLoader<K, V>> loader;
private Provider<Weigher<K, V>> weigher;
@@ -86,6 +87,15 @@
}
@Override
+ public CacheBinding<K, V> diskLimit(long limit) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ Preconditions.checkState(persist,
+ "diskLimit supported for persistent caches only");
+ diskLimit = limit;
+ return this;
+ }
+
+ @Override
public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
expireAfterWrite = SECONDS.convert(duration, unit);
@@ -130,6 +140,14 @@
}
@Override
+ public long diskLimit() {
+ if (diskLimit > 0) {
+ return diskLimit;
+ }
+ return 128 << 20;
+ }
+
+ @Override
@Nullable
public Long expireAfterWrite(TimeUnit unit) {
return expireAfterWrite != null
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..45f0afe 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,9 +28,9 @@
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;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeAbandoned;
@@ -50,6 +50,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 +93,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,29 +105,69 @@
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(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(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 Change change;
private PatchSet patchSet;
private ChangeMessage message;
- private NotifyHandling notifyHandling;
- private Op(String msgTxt, Account account, NotifyHandling notifyHandling) {
- this.account = account;
+ private Op(String msgTxt, NotifyHandling notifyHandling) {
this.msgTxt = msgTxt;
this.notifyHandling = notifyHandling;
}
@@ -155,19 +202,14 @@
msg.append(msgTxt.trim());
}
- ChangeMessage message = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- account != null ? account.getId() : null,
- ctx.getWhen(),
- change.currentPatchSetId());
- message.setMessage(msg.toString());
- return message;
+ return ChangeMessagesUtil.newMessage(ctx, msg.toString());
}
@Override
public void postUpdate(Context ctx) throws OrmException {
+ Account account = ctx.getUser().isIdentifiedUser()
+ ? ctx.getUser().asIdentifiedUser().getAccount()
+ : null;
try {
ReplyToChangeSender cm =
abandonedSenderFactory.create(ctx.getProject(), change.getId());
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..d1cc73b 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,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -37,10 +41,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 +54,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 +72,64 @@
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 =
+ getValidChanges(abandons.get(project), query);
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)
+ private Collection<ChangeControl> getValidChanges(
+ Collection<ChangeControl> changeControls, 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());
+ Collection<ChangeControl> validChanges = new ArrayList<>();
+ for (ChangeControl cc : changeControls) {
+ String newQuery = query + " change:" + cc.getId();
+ List<ChangeData> changesToAbandon =
+ queryProcessor.enforceVisibility(false)
+ .query(queryBuilder.parse(newQuery)).entities();
+ if (!changesToAbandon.isEmpty()) {
+ validChanges.add(cc);
+ } else {
+ log.debug(
+ "Change data with id \"{}\" does not satisfy the query \"{}\""
+ + " any more, hence skipping it in clean up",
+ cc.getId(), query);
+ }
+ }
+ return validChanges;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AllowedFormats.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AllowedFormats.java
new file mode 100644
index 0000000..756ce88
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AllowedFormats.java
@@ -0,0 +1,58 @@
+// 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.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.DownloadConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class AllowedFormats {
+ final ImmutableMap<String, ArchiveFormat> extensions;
+ final ImmutableSet<ArchiveFormat> allowed;
+
+ @Inject
+ AllowedFormats(DownloadConfig cfg) {
+ Map<String, ArchiveFormat> exts = new HashMap<>();
+ for (ArchiveFormat format : cfg.getArchiveFormats()) {
+ for (String ext : format.getSuffixes()) {
+ exts.put(ext, format);
+ }
+ exts.put(format.name().toLowerCase(), format);
+ }
+ extensions = ImmutableMap.copyOf(exts);
+
+ // Zip is not supported because it may be interpreted by a Java plugin as a
+ // valid JAR file, whose code would have access to cookies on the domain.
+ allowed = Sets.immutableEnumSet(
+ Iterables.filter(cfg.getArchiveFormats(), f -> f != ArchiveFormat.ZIP));
+ }
+
+ public Set<ArchiveFormat> getAllowed() {
+ return allowed;
+ }
+
+ public ImmutableMap<String, ArchiveFormat> getExtensions() {
+ return extensions;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
index 335f201..9b8f2b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.change;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.eclipse.jgit.api.ArchiveCommand;
import org.eclipse.jgit.archive.TarFormat;
import org.eclipse.jgit.archive.Tbz2Format;
@@ -21,12 +25,40 @@
import org.eclipse.jgit.archive.TxzFormat;
import org.eclipse.jgit.archive.ZipFormat;
+import java.io.IOException;
+import java.io.OutputStream;
+
public enum ArchiveFormat {
- TGZ("application/x-gzip", new TgzFormat()),
- TAR("application/x-tar", new TarFormat()),
- TBZ2("application/x-bzip2", new Tbz2Format()),
- TXZ("application/x-xz", new TxzFormat()),
- ZIP("application/x-zip", new ZipFormat());
+ TGZ("application/x-gzip", new TgzFormat()) {
+ @Override
+ public ArchiveEntry prepareArchiveEntry(String fileName) {
+ return new TarArchiveEntry(fileName);
+ }
+ },
+ TAR("application/x-tar", new TarFormat()) {
+ @Override
+ public ArchiveEntry prepareArchiveEntry(String fileName) {
+ return new TarArchiveEntry(fileName);
+ }
+ },
+ TBZ2("application/x-bzip2", new Tbz2Format()) {
+ @Override
+ public ArchiveEntry prepareArchiveEntry(String fileName) {
+ return new TarArchiveEntry(fileName);
+ }
+ },
+ TXZ("application/x-xz", new TxzFormat()) {
+ @Override
+ public ArchiveEntry prepareArchiveEntry(String fileName) {
+ return new TarArchiveEntry(fileName);
+ }
+ },
+ ZIP("application/x-zip", new ZipFormat()) {
+ @Override
+ public ArchiveEntry prepareArchiveEntry(String fileName) {
+ return new ZipArchiveEntry(fileName);
+ }
+ };
private final ArchiveCommand.Format<?> format;
private final String mimeType;
@@ -52,4 +84,11 @@
Iterable<String> getSuffixes() {
return format.suffixes();
}
-}
+
+ public ArchiveOutputStream createArchiveOutputStream(OutputStream o)
+ throws IOException {
+ return (ArchiveOutputStream)this.format.createArchiveOutputStream(o);
+ }
+
+ public abstract ArchiveEntry prepareArchiveEntry(final String fileName);
+}
\ No newline at end of file
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 0d7a1bf..6f6e964 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
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
+import static java.util.stream.Collectors.toSet;
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.FooterConstants;
@@ -33,12 +34,11 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.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 +48,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 +63,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 +86,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 +129,7 @@
@Inject
ChangeInserter(ProjectControl.GenericFactory projectControlFactory,
+ IdentifiedUser.GenericFactory userFactory,
ChangeControl.GenericFactory changeControlFactory,
PatchSetInfoFactory patchSetInfoFactory,
PatchSetUtil psUtil,
@@ -142,6 +144,7 @@
@Assisted RevCommit commit,
@Assisted String refName) {
this.projectControlFactory = projectControlFactory;
+ this.userFactory = userFactory;
this.changeControlFactory = changeControlFactory;
this.patchSetInfoFactory = patchSetInfoFactory;
this.psUtil = psUtil;
@@ -353,21 +356,42 @@
update.fixStatus(change.getStatus());
LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
- approvalsUtil.addReviewers(db, update, labelTypes, change,
- patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
- approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
- ctx.getControl(), approvals);
+ approvalsUtil.addReviewers(db, update, labelTypes, change, patchSet,
+ patchSetInfo,
+ filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
+ Collections.<Account.Id> emptySet());
+ approvalsUtil.addApprovalsForNewPatchSet(
+ db, update, labelTypes, patchSet, ctx.getControl(), approvals);
if (message != null) {
- changeMessage =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db)), ctx.getAccountId(),
- patchSet.getCreatedOn(), patchSet.getId());
- changeMessage.setMessage(message);
+ changeMessage = ChangeMessagesUtil.newMessage(
+ db, patchSet.getId(), ctx.getUser(), patchSet.getCreatedOn(),
+ message);
cmUtil.addChangeMessage(db, update, changeMessage);
}
return true;
}
+ private Set<Account.Id> filterOnChangeVisibility(final ReviewDb db,
+ final ChangeNotes notes, Set<Account.Id> accounts) {
+ return accounts.stream()
+ .filter(
+ 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;
+ }
+ })
+ .collect(toSet());
+ }
+
@Override
public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException {
if (sendMail) {
@@ -440,9 +464,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 +474,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..bf7e6b8 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
@@ -32,11 +32,12 @@
import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
+import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
import static com.google.gerrit.server.CommonConverters.toGitPerson;
+import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
@@ -45,6 +46,7 @@
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -100,6 +102,8 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LabelNormalizer;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -141,6 +145,9 @@
public static final Set<ListChangesOption> NO_OPTIONS =
Collections.emptySet();
+ public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
+ ImmutableSet.of(ALL_REVISIONS, MESSAGES);
+
public interface Factory {
ChangeJson create(Set<ListChangesOption> options);
}
@@ -167,7 +174,9 @@
private final ChangeNotes.Factory notesFactory;
private final ChangeResource.Factory changeResourceFactory;
private final ChangeKindCache changeKindCache;
+ private final ChangeIndexCollection indexes;
+ private boolean lazyLoad = true;
private AccountLoader accountLoader;
private Map<Change.Id, List<SubmitRecord>> submitRecords;
private FixInput fix;
@@ -195,6 +204,7 @@
ChangeNotes.Factory notesFactory,
ChangeResource.Factory changeResourceFactory,
ChangeKindCache changeKindCache,
+ ChangeIndexCollection indexes,
@Assisted Set<ListChangesOption> options) {
this.db = db;
this.labelNormalizer = ln;
@@ -217,11 +227,17 @@
this.notesFactory = notesFactory;
this.changeResourceFactory = changeResourceFactory;
this.changeKindCache = changeKindCache;
+ this.indexes = indexes;
this.options = options.isEmpty()
? EnumSet.noneOf(ListChangesOption.class)
: EnumSet.copyOf(options);
}
+ public ChangeJson lazyLoad(boolean load) {
+ lazyLoad = load;
+ return this;
+ }
+
public ChangeJson fix(FixInput fix) {
this.fix = fix;
return this;
@@ -282,13 +298,8 @@
public List<List<ChangeInfo>> formatQueryResults(
List<QueryResult<ChangeData>> in) throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
- ensureLoaded(FluentIterable.from(in).transformAndConcat(
- new Function<QueryResult<ChangeData>, List<ChangeData>>() {
- @Override
- public List<ChangeData> apply(QueryResult<ChangeData> in) {
- return in.entities();
- }
- }));
+ ensureLoaded(
+ FluentIterable.from(in).transformAndConcat(QueryResult::entities));
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
Map<Change.Id, ChangeInfo> out = new HashMap<>();
@@ -316,16 +327,22 @@
}
private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
- ChangeData.ensureChangeLoaded(all);
- if (has(ALL_REVISIONS)) {
- ChangeData.ensureAllPatchSetsLoaded(all);
- } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
- ChangeData.ensureCurrentPatchSetLoaded(all);
+ if (lazyLoad) {
+ ChangeData.ensureChangeLoaded(all);
+ if (has(ALL_REVISIONS)) {
+ ChangeData.ensureAllPatchSetsLoaded(all);
+ } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
+ ChangeData.ensureCurrentPatchSetLoaded(all);
+ }
+ if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
+ ChangeData.ensureReviewedByLoadedForOpenChanges(all);
+ }
+ ChangeData.ensureCurrentApprovalsLoaded(all);
+ } else {
+ for (ChangeData cd : all) {
+ cd.setLazyLoad(false);
+ }
}
- if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
- ChangeData.ensureReviewedByLoadedForOpenChanges(all);
- }
- ChangeData.ensureCurrentApprovalsLoaded(all);
}
private boolean has(ListChangesOption option) {
@@ -419,16 +436,23 @@
out.project = in.getProject().get();
out.branch = in.getDest().getShortName();
out.topic = in.getTopic();
+ if (indexes.getSearchIndex().getSchema().hasField(ChangeField.ASSIGNEE)) {
+ if (in.getAssignee() != null) {
+ out.assignee = accountLoader.get(in.getAssignee());
+ }
+ }
out.hashtags = cd.hashtags();
out.changeId = in.getKey().get();
- if (in.getStatus() != Change.Status.MERGED) {
+ if (in.getStatus().isOpen()) {
SubmitTypeRecord str = cd.submitTypeRecord();
if (str.isOk()) {
out.submitType = str.type;
}
out.mergeable = cd.isMergeable();
+ if (has(SUBMITTABLE)) {
+ out.submittable = submittable(cd);
+ }
}
- out.submittable = Submit.submittable(cd);
Optional<ChangedLines> changedLines = cd.changedLines();
if (changedLines.isPresent()) {
out.insertions = changedLines.get().insertions;
@@ -529,6 +553,18 @@
return result;
}
+ private boolean submittable(ChangeData cd) throws OrmException {
+ List<SubmitRecord> records = new SubmitRuleEvaluator(cd)
+ .setFastEvalLabels(true)
+ .evaluate();
+ for (SubmitRecord sr : records) {
+ if (sr.status == SubmitRecord.Status.OK) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
// Maintain our own cache rather than using cd.getSubmitRecords(),
// since the latter may not have used the same values for
@@ -563,7 +599,7 @@
? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed)
: labelsForClosedChange(cd, labelTypes, standard, detailed);
return ImmutableMap.copyOf(
- Maps.transformValues(withStatus, LabelWithStatus.TO_LABEL_INFO));
+ Maps.transformValues(withStatus, LabelWithStatus::label));
}
private Map<String, LabelWithStatus> labelsForOpenChange(ChangeControl ctl,
@@ -663,7 +699,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());
}
@@ -916,22 +952,25 @@
private Collection<AccountInfo> toAccountInfo(
Collection<Account.Id> accounts) {
- return FluentIterable.from(accounts)
- .transform(new Function<Account.Id, AccountInfo>() {
- @Override
- public AccountInfo apply(Account.Id id) {
- return accountLoader.get(id);
- }
- })
- .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST);
+ return accounts.stream()
+ .map(accountLoader::get)
+ .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
+ .collect(toList());
+ }
+
+ @Nullable
+ private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
+ if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
+ 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 +1014,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 +1023,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();
@@ -1022,6 +1060,7 @@
if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
out.files = fileInfoJson.toFileInfoMap(c, in);
out.files.remove(Patch.COMMIT_MSG);
+ out.files.remove(Patch.MERGE_LIST);
}
if ((out.isCurrent || (out.draft != null && out.draft))
@@ -1143,14 +1182,6 @@
@AutoValue
abstract static class LabelWithStatus {
- private static final Function<LabelWithStatus, LabelInfo> TO_LABEL_INFO =
- new Function<LabelWithStatus, LabelInfo>() {
- @Override
- public LabelInfo apply(LabelWithStatus in) {
- return in.label();
- }
- };
-
private static LabelWithStatus create(LabelInfo label,
SubmitRecord.Label.Status status) {
return new AutoValue_ChangeJson_LabelWithStatus(label, status);
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..e971eff 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,11 +14,12 @@
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;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import org.eclipse.jgit.lib.ObjectId;
@@ -31,10 +32,11 @@
* implementation changes, which might invalidate old entries).
*/
public interface ChangeKindCache {
- ChangeKind getChangeKind(ProjectState project, Repository repo,
+ ChangeKind getChangeKind(Project.NameKey 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..c0c0492 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,17 +22,17 @@
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;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -83,7 +83,6 @@
public static class NoCache implements ChangeKindCache {
private final boolean useRecursiveMerge;
private final ChangeData.Factory changeDataFactory;
- private final ProjectCache projectCache;
private final GitRepositoryManager repoManager;
@@ -91,23 +90,21 @@
NoCache(
@GerritServerConfig Config serverConfig,
ChangeData.Factory changeDataFactory,
- ProjectCache projectCache,
GitRepositoryManager repoManager) {
this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
this.changeDataFactory = changeDataFactory;
- this.projectCache = projectCache;
this.repoManager = repoManager;
}
@Override
- public ChangeKind getChangeKind(ProjectState project, Repository repo,
- ObjectId prior, ObjectId next) {
+ public ChangeKind getChangeKind(Project.NameKey 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, repo).call();
} catch (IOException e) {
log.warn("Cannot check trivial rebase of new patch set " + next.name()
- + " in " + project.getProject().getName(), e);
+ + " in " + project, e);
return ChangeKind.REWORK;
}
}
@@ -116,13 +113,13 @@
public ChangeKind getChangeKind(ReviewDb db, Change change,
PatchSet patch) {
return getChangeKindInternal(this, db, change, patch, changeDataFactory,
- projectCache, repoManager);
+ repoManager);
}
@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);
+ return getChangeKindInternal(this, repo, cd, patch);
}
}
@@ -191,11 +188,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 +206,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 +254,10 @@
// it was a rework.
}
return ChangeKind.REWORK;
+ } finally {
+ if (close) {
+ repo.close();
+ }
}
}
@@ -303,7 +315,6 @@
private final Cache<Key, ChangeKind> cache;
private final boolean useRecursiveMerge;
private final ChangeData.Factory changeDataFactory;
- private final ProjectCache projectCache;
private final GitRepositoryManager repoManager;
@Inject
@@ -311,24 +322,22 @@
@GerritServerConfig Config serverConfig,
@Named(ID_CACHE) Cache<Key, ChangeKind> cache,
ChangeData.Factory changeDataFactory,
- ProjectCache projectCache,
GitRepositoryManager repoManager) {
this.cache = cache;
this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
this.changeDataFactory = changeDataFactory;
- this.projectCache = projectCache;
this.repoManager = repoManager;
}
@Override
- public ChangeKind getChangeKind(ProjectState project, Repository repo,
- ObjectId prior, ObjectId next) {
+ public ChangeKind getChangeKind(Project.NameKey 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, repo));
} catch (ExecutionException e) {
log.warn("Cannot check trivial rebase of new patch set " + next.name()
- + " in " + project.getProject().getName(), e);
+ + " in " + project, e);
return ChangeKind.REWORK;
}
}
@@ -336,27 +345,25 @@
@Override
public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
return getChangeKindInternal(this, db, change, patch, changeDataFactory,
- projectCache, repoManager);
+ repoManager);
}
@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);
+ return getChangeKindInternal(this, repo, cd, patch);
}
private static ChangeKind getChangeKindInternal(
ChangeKindCache cache,
- Repository repo,
+ @Nullable Repository repo,
ChangeData change,
- PatchSet patch,
- ProjectCache projectCache) {
+ PatchSet patch) {
ChangeKind kind = ChangeKind.REWORK;
// Trivial case: if we're on the first patch, we don't need to use
// the repository.
if (patch.getId().get() > 1) {
try {
- ProjectState projectState = projectCache.checkedGet(change.project());
Collection<PatchSet> patchSetCollection = change.patchSets();
PatchSet priorPs = patch;
for (PatchSet ps : patchSetCollection) {
@@ -374,11 +381,11 @@
// and deletes the draft.
if (priorPs != patch) {
kind =
- cache.getChangeKind(projectState, repo,
+ cache.getChangeKind(change.project(), repo,
ObjectId.fromString(priorPs.getRevision().get()),
ObjectId.fromString(patch.getRevision().get()));
}
- } catch (IOException | OrmException e) {
+ } catch (OrmException e) {
// Do nothing; assume we have a complex change
log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
"of change " + change.getId(), e);
@@ -393,7 +400,6 @@
Change change,
PatchSet patch,
ChangeData.Factory changeDataFactory,
- ProjectCache projectCache,
GitRepositoryManager repoManager) {
// TODO - dborowitz: add NEW_CHANGE type for default.
ChangeKind kind = ChangeKind.REWORK;
@@ -402,8 +408,7 @@
if (patch.getId().get() > 1) {
try (Repository repo = repoManager.openRepository(change.getProject())) {
kind = getChangeKindInternal(cache, repo,
- changeDataFactory.create(db, change), patch,
- projectCache);
+ changeDataFactory.create(db, change), patch);
} catch (IOException e) {
// Do nothing; assume we have a complex change
log.warn("Unable to get change kind for patchSet " + patch.getPatchSetId() +
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..248acd3 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();
}
@@ -271,10 +272,6 @@
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
- ChangeMessage changeMessage = new ChangeMessage(
- new ChangeMessage.Key(
- ctx.getChange().getId(), ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), psId);
StringBuilder sb = new StringBuilder("Patch Set ")
.append(psId.get())
.append(": Cherry Picked")
@@ -283,8 +280,8 @@
.append(destBranch)
.append(" as commit ")
.append(cherryPickCommit.name());
- changeMessage.setMessage(sb.toString());
-
+ ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), sb.toString());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index d1ce453..54f73bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -14,17 +14,17 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.PatchLineCommentsUtil.COMMENT_INFO_ORDER;
+import static com.google.gerrit.server.CommentsUtil.COMMENT_INFO_ORDER;
-import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.gerrit.extensions.client.Comment.Range;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -57,104 +57,135 @@
return this;
}
- CommentInfo format(PatchLineComment c) throws OrmException {
- AccountLoader loader = null;
- if (fillAccounts) {
- loader = accountLoaderFactory.create(true);
- }
- CommentInfo commentInfo = toCommentInfo(c, loader);
- if (fillAccounts) {
- loader.fill();
- }
- return commentInfo;
+ public CommentFormatter newCommentFormatter() {
+ return new CommentFormatter();
}
- Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l)
- throws OrmException {
- Map<String, List<CommentInfo>> out = new TreeMap<>();
- AccountLoader accountLoader = fillAccounts
- ? accountLoaderFactory.create(true)
- : null;
+ public RobotCommentFormatter newRobotCommentFormatter() {
+ return new RobotCommentFormatter();
+ }
- for (PatchLineComment c : l) {
- CommentInfo o = toCommentInfo(c, accountLoader);
- List<CommentInfo> list = out.get(o.path);
- if (list == null) {
- list = new ArrayList<>();
- out.put(o.path, list);
+ private abstract class BaseCommentFormatter<F extends Comment,
+ T extends CommentInfo> {
+ public T format(F comment) throws OrmException {
+ AccountLoader loader =
+ fillAccounts ? accountLoaderFactory.create(true) : null;
+ T info = toInfo(comment, loader);
+ if (loader != null) {
+ loader.fill();
}
- o.path = null;
- list.add(o);
+ return info;
}
- for (List<CommentInfo> list : out.values()) {
- Collections.sort(list, COMMENT_INFO_ORDER);
+ public Map<String, List<T>> format(Iterable<F> comments)
+ throws OrmException {
+ AccountLoader loader =
+ fillAccounts ? accountLoaderFactory.create(true) : null;
+
+ Map<String, List<T>> out = new TreeMap<>();
+
+ for (F c : comments) {
+ T o = toInfo(c, loader);
+ List<T> list = out.get(o.path);
+ if (list == null) {
+ list = new ArrayList<>();
+ out.put(o.path, list);
+ }
+ o.path = null;
+ list.add(o);
+ }
+
+ for (List<T> list : out.values()) {
+ Collections.sort(list, COMMENT_INFO_ORDER);
+ }
+
+ if (loader != null) {
+ loader.fill();
+ }
+ return out;
}
- if (accountLoader != null) {
- accountLoader.fill();
+ public List<T> formatAsList(Iterable<F> comments) throws OrmException {
+ AccountLoader loader =
+ fillAccounts ? accountLoaderFactory.create(true) : null;
+
+ List<T> out = FluentIterable.from(comments)
+ .transform(c -> toInfo(c, loader))
+ .toSortedList(COMMENT_INFO_ORDER);
+
+ if (loader != null) {
+ loader.fill();
+ }
+ return out;
}
- return out;
- }
+ protected abstract T toInfo(F comment, AccountLoader loader);
- List<CommentInfo> formatAsList(Iterable<PatchLineComment> l)
- throws OrmException {
- final AccountLoader accountLoader = fillAccounts
- ? accountLoaderFactory.create(true)
- : null;
- List<CommentInfo> out = FluentIterable
- .from(l)
- .transform(new Function<PatchLineComment, CommentInfo>() {
- @Override
- public CommentInfo apply(PatchLineComment c) {
- return toCommentInfo(c, accountLoader);
- }
- }).toSortedList(COMMENT_INFO_ORDER);
-
- if (accountLoader != null) {
- accountLoader.fill();
- }
-
- return out;
- }
-
- private CommentInfo toCommentInfo(PatchLineComment c, AccountLoader loader) {
- CommentInfo r = new CommentInfo();
- if (fillPatchSet) {
- r.patchSet = c.getKey().getParentKey().getParentKey().get();
- }
- r.id = Url.encode(c.getKey().get());
- r.path = c.getKey().getParentKey().getFileName();
- if (c.getSide() <= 0) {
- r.side = Side.PARENT;
- if (c.getSide() < 0) {
- r.parent = -c.getSide();
+ protected void fillCommentInfo(Comment c, CommentInfo r,
+ AccountLoader loader) {
+ if (fillPatchSet) {
+ r.patchSet = c.key.patchSetId;
+ }
+ r.id = Url.encode(c.key.uuid);
+ r.path = c.key.filename;
+ if (c.side <= 0) {
+ r.side = Side.PARENT;
+ if (c.side < 0) {
+ r.parent = -c.side;
+ }
+ }
+ if (c.lineNbr > 0) {
+ r.line = c.lineNbr;
+ }
+ r.inReplyTo = Url.encode(c.parentUuid);
+ r.message = Strings.emptyToNull(c.message);
+ r.updated = c.writtenOn;
+ r.range = toRange(c.range);
+ r.tag = c.tag;
+ if (loader != null) {
+ r.author = loader.get(c.author.getId());
}
}
- if (c.getLine() > 0) {
- r.line = c.getLine();
+
+ private Range toRange(Comment.Range commentRange) {
+ Range range = null;
+ if (commentRange != null) {
+ range = new Range();
+ range.startLine = commentRange.startLine;
+ range.startCharacter = commentRange.startChar;
+ range.endLine = commentRange.endLine;
+ range.endCharacter = commentRange.endChar;
+ }
+ return range;
}
- r.inReplyTo = Url.encode(c.getParentUuid());
- r.message = Strings.emptyToNull(c.getMessage());
- r.updated = c.getWrittenOn();
- r.range = toRange(c.getRange());
- r.tag = c.getTag();
- if (loader != null) {
- r.author = loader.get(c.getAuthor());
- }
- return r;
}
- private Range toRange(CommentRange commentRange) {
- Range range = null;
- if (commentRange != null) {
- range = new Range();
- range.startLine = commentRange.getStartLine();
- range.startCharacter = commentRange.getStartCharacter();
- range.endLine = commentRange.getEndLine();
- range.endCharacter = commentRange.getEndCharacter();
+ class CommentFormatter extends BaseCommentFormatter<Comment, CommentInfo> {
+ @Override
+ protected CommentInfo toInfo(Comment c, AccountLoader loader) {
+ CommentInfo ci = new CommentInfo();
+ fillCommentInfo(c, ci, loader);
+ return ci;
}
- return range;
+
+ private CommentFormatter() {
+ }
+ }
+
+ class RobotCommentFormatter
+ extends BaseCommentFormatter<RobotComment, RobotCommentInfo> {
+ @Override
+ protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) {
+ RobotCommentInfo rci = new RobotCommentInfo();
+ rci.robotId = c.robotId;
+ rci.robotRunId = c.robotRunId;
+ rci.url = c.url;
+ rci.properties = c.properties;
+ fillCommentInfo(c, rci, loader);
+ return rci;
+ }
+
+ private RobotCommentFormatter() {
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
index c535e9e..40c8515 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -17,7 +17,7 @@
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.inject.TypeLiteral;
@@ -26,9 +26,9 @@
new TypeLiteral<RestView<CommentResource>>() {};
private final RevisionResource rev;
- private final PatchLineComment comment;
+ private final Comment comment;
- public CommentResource(RevisionResource rev, PatchLineComment c) {
+ public CommentResource(RevisionResource rev, Comment c) {
this.rev = rev;
this.comment = c;
}
@@ -37,15 +37,15 @@
return rev.getPatchSet();
}
- PatchLineComment getComment() {
+ Comment getComment() {
return comment;
}
String getId() {
- return comment.getKey().get();
+ return comment.key.uuid;
}
Account.Id getAuthorId() {
- return comment.getAuthor();
+ return comment.author.getId();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
index 8f78f0e..6ce7dda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -19,9 +19,9 @@
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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -33,16 +33,16 @@
private final DynamicMap<RestView<CommentResource>> views;
private final ListRevisionComments list;
private final Provider<ReviewDb> dbProvider;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
@Inject
Comments(DynamicMap<RestView<CommentResource>> views,
ListRevisionComments list, Provider<ReviewDb> dbProvider,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.views = views;
this.list = list;
this.dbProvider = dbProvider;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
@Override
@@ -61,9 +61,9 @@
String uuid = id.get();
ChangeNotes notes = rev.getNotes();
- for (PatchLineComment c : plcUtil.publishedByPatchSet(dbProvider.get(),
+ for (Comment c : commentsUtil.publishedByPatchSet(dbProvider.get(),
notes, rev.getPatchSet().getId())) {
- if (uuid.equals(c.getKey().get())) {
+ if (uuid.equals(c.key.uuid)) {
return new CommentResource(rev, c);
}
}
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..30ed82f 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
@@ -19,19 +19,17 @@
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
-import static com.google.gerrit.server.ChangeUtil.TO_PS_ID;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.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;
@@ -254,13 +252,10 @@
Map<String, Ref> refs;
try {
- refs = repo.getRefDatabase().exactRef(
- Lists.transform(all, new Function<PatchSet, String>() {
- @Override
- public String apply(PatchSet ps) {
- return ps.getId().toRefName();
- }
- }).toArray(new String[all.size()]));
+ refs = repo.getRefDatabase().exactRef(
+ all.stream()
+ .map(ps -> ps.getId().toRefName())
+ .toArray(String[]::new));
} catch (IOException e) {
error("error reading refs", e);
refs = Collections.emptyMap();
@@ -318,7 +313,7 @@
if (e.getValue().size() > 1) {
problem(String.format("Multiple patch sets pointing to %s: %s",
e.getKey().name(),
- Collections2.transform(e.getValue(), TO_PS_ID)));
+ Collections2.transform(e.getValue(), PatchSet::getPatchSetId)));
}
}
@@ -530,7 +525,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/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 7cb2aac..47821f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -14,8 +14,7 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
-import static com.google.gerrit.server.change.PutDraftComment.side;
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
@@ -27,12 +26,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -50,7 +48,7 @@
private final Provider<ReviewDb> db;
private final BatchUpdate.Factory updateFactory;
private final Provider<CommentJson> commentJson;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final PatchListCache patchListCache;
@@ -58,13 +56,13 @@
CreateDraftComment(Provider<ReviewDb> db,
BatchUpdate.Factory updateFactory,
Provider<CommentJson> commentJson,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache) {
this.db = db;
this.updateFactory = updateFactory;
this.commentJson = commentJson;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
}
@@ -87,8 +85,8 @@
Op op = new Op(rsrc.getPatchSet().getId(), in);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
- return Response.created(
- commentJson.get().setFillAccounts(false).format(op.comment));
+ return Response.created(commentJson.get().setFillAccounts(false)
+ .newCommentFormatter().format(op.comment));
}
}
@@ -96,7 +94,7 @@
private final PatchSet.Id psId;
private final DraftInput in;
- private PatchLineComment comment;
+ private Comment comment;
private Op(PatchSet.Id psId, DraftInput in) {
this.psId = psId;
@@ -110,23 +108,16 @@
if (ps == null) {
throw new ResourceNotFoundException("patch set not found: " + psId);
}
- int line = in.line != null
- ? in.line
- : in.range != null ? in.range.endLine : 0;
- comment = new PatchLineComment(
- new PatchLineComment.Key(
- new Patch.Key(ps.getId(), in.path),
- ChangeUtil.messageUUID(ctx.getDb())),
- line, ctx.getAccountId(), Url.decode(in.inReplyTo),
- ctx.getWhen());
- comment.setSide(side(in));
- comment.setMessage(in.message.trim());
- comment.setRange(in.range);
- comment.setTag(in.tag);
+ comment = commentsUtil.newComment(
+ ctx, in.path, ps.getId(), in.side(), in.message.trim());
+ comment.parentUuid = Url.decode(in.inReplyTo);
+ comment.setLineNbrAndRange(in.line, in.range);
+ comment.tag = in.tag;
setCommentRevId(
comment, patchListCache, ctx.getChange(), ps);
- plcUtil.putComments(
- ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(comment));
+
+ commentsUtil.putComments(ctx.getDb(), ctx.getUpdate(psId), Status.DRAFT,
+ Collections.singleton(comment));
ctx.bumpLastUpdatedOn(false);
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
new file mode 100644
index 0000000..70c2ae7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -0,0 +1,127 @@
+// 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.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountJson;
+import com.google.gerrit.server.change.DeleteAssignee.Input;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.extensions.events.AssigneeChanged;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class DeleteAssignee implements RestModifyView<ChangeResource, Input> {
+ public static class Input {
+
+ }
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final ChangeMessagesUtil cmUtil;
+ private final Provider<ReviewDb> db;
+ private final AccountInfoCacheFactory.Factory accountInfos;
+ private final AssigneeChanged assigneeChanged;
+ private final String anonymousCowardName;
+
+ @Inject
+ DeleteAssignee(BatchUpdate.Factory batchUpdateFactory,
+ ChangeMessagesUtil cmUtil,
+ Provider<ReviewDb> db,
+ AccountInfoCacheFactory.Factory accountInfosFactory,
+ AssigneeChanged assigneeChanged,
+ @AnonymousCowardName String anonymousCowardName) {
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.cmUtil = cmUtil;
+ this.db = db;
+ this.accountInfos = accountInfosFactory;
+ this.assigneeChanged = assigneeChanged;
+ this.anonymousCowardName = anonymousCowardName;
+ }
+
+ @Override
+ public Response<AccountInfo> apply(ChangeResource rsrc, Input input)
+ throws RestApiException, UpdateException {
+ try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+ rsrc.getProject(),
+ rsrc.getUser(), TimeUtil.nowTs())) {
+ Op op = new Op();
+ bu.addOp(rsrc.getChange().getId(), op);
+ bu.execute();
+ if (op.getDeletedAssignee() == null) {
+ return Response.none();
+ }
+ return Response.ok(AccountJson.toAccountInfo(op.getDeletedAssignee()));
+ }
+ }
+
+ private class Op extends BatchUpdate.Op {
+ private Change change;
+ private Account deletedAssignee;
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException{
+ if (!ctx.getControl().canEditAssignee()) {
+ throw new AuthException("Delete Assignee not permitted");
+ }
+ change = ctx.getChange();
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+ Account.Id currentAssigneeId = change.getAssignee();
+ if (currentAssigneeId == null) {
+ return false;
+ }
+ deletedAssignee = accountInfos.create().get(currentAssigneeId);
+ // noteDb
+ update.removeAssignee();
+ // reviewDb
+ change.setAssignee(null);
+ addMessage(ctx, update, deletedAssignee);
+ return true;
+ }
+
+ public Account getDeletedAssignee() {
+ return deletedAssignee;
+ }
+
+ private void addMessage(BatchUpdate.ChangeContext ctx,
+ ChangeUpdate update, Account deleted) throws OrmException {
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(
+ ctx, "Assignee deleted: " + deleted.getName(anonymousCowardName));
+ cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws OrmException {
+ assigneeChanged.fire(change, ctx.getAccount(), deletedAssignee, ctx.getWhen());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
index 56dbfa7..9a4c02d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import com.google.common.base.Optional;
import com.google.gerrit.common.TimeUtil;
@@ -23,10 +23,10 @@
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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.DeleteDraftComment.Input;
import com.google.gerrit.server.git.BatchUpdate;
@@ -47,19 +47,19 @@
}
private final Provider<ReviewDb> db;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final BatchUpdate.Factory updateFactory;
private final PatchListCache patchListCache;
@Inject
DeleteDraftComment(Provider<ReviewDb> db,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
BatchUpdate.Factory updateFactory,
PatchListCache patchListCache) {
this.db = db;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.updateFactory = updateFactory;
this.patchListCache = patchListCache;
@@ -71,7 +71,7 @@
try (BatchUpdate bu = updateFactory.create(
db.get(), rsrc.getChange().getProject(), rsrc.getControl().getUser(),
TimeUtil.nowTs())) {
- Op op = new Op(rsrc.getComment().getKey());
+ Op op = new Op(rsrc.getComment().key);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
}
@@ -79,28 +79,29 @@
}
private class Op extends BatchUpdate.Op {
- private final PatchLineComment.Key key;
+ private final Comment.Key key;
- private Op(PatchLineComment.Key key) {
+ private Op(Comment.Key key) {
this.key = key;
}
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceNotFoundException, OrmException {
- Optional<PatchLineComment> maybeComment =
- plcUtil.get(ctx.getDb(), ctx.getNotes(), key);
+ Optional<Comment> maybeComment =
+ commentsUtil.get(ctx.getDb(), ctx.getNotes(), key);
if (!maybeComment.isPresent()) {
return false; // Nothing to do.
}
- PatchSet.Id psId = key.getParentKey().getParentKey();
+ PatchSet.Id psId =
+ new PatchSet.Id(ctx.getChange().getId(), key.patchSetId);
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (ps == null) {
throw new ResourceNotFoundException("patch set not found: " + psId);
}
- PatchLineComment c = maybeComment.get();
+ Comment c = maybeComment.get();
setCommentRevId(c, patchListCache, ctx.getChange(), ps);
- plcUtil.deleteComments(
+ commentsUtil.deleteComments(
ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c));
ctx.bumpLastUpdatedOn(false);
return true;
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..72ffa77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -14,12 +14,13 @@
package com.google.gerrit.server.change;
-import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
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;
@@ -35,10 +36,8 @@
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
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.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 +61,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 +101,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 +123,7 @@
private class Op extends BatchUpdate.Op {
private final Account reviewer;
+ private final DeleteReviewerInput input;
ChangeMessage changeMessage;
Change currChange;
PatchSet currPs;
@@ -126,8 +131,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,57 +154,57 @@
}
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 = ChangeMessagesUtil.newMessage(ctx, 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());
}
private Iterable<PatchSetApproval> approvals(ChangeContext ctx,
- final Account.Id accountId) throws OrmException {
+ Account.Id accountId) throws OrmException {
Change.Id changeId = ctx.getNotes().getChangeId();
Iterable<PatchSetApproval> approvals;
@@ -218,13 +224,7 @@
}
return Iterables.filter(
- approvals,
- new Predicate<PatchSetApproval>() {
- @Override
- public boolean apply(PatchSetApproval input) {
- return accountId.equals(input.getAccountId());
- }
- });
+ approvals, psa -> accountId.equals(psa.getAccountId()));
}
private String formatLabelValue(short value) {
@@ -233,30 +233,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..eda2fcf 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,12 +29,12 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.VoteDeleted;
@@ -44,6 +45,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 +139,62 @@
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)));
+
+ StringBuilder msg = new StringBuilder();
+ msg.append("Removed ");
+ LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label)));
+ msg.append(" by ")
+ .append(userFactory.create(accountId).getNameEmail())
+ .append("\n");
+ changeMessage = ChangeMessagesUtil.newMessage(ctx, msg.toString());
+ cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
+ changeMessage);
+
return true;
}
+ private PatchSetApproval deletedApproval(ChangeContext ctx) {
+ // Set the effective user to the account we're trying to remove, and don't
+ // set the real user; this preserves the calling user as the NoteDb
+ // committer.
+ 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 +220,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/DraftCommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
index 3dc0c78..781216c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -18,7 +18,7 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
@@ -28,9 +28,9 @@
new TypeLiteral<RestView<DraftCommentResource>>() {};
private final RevisionResource rev;
- private final PatchLineComment comment;
+ private final Comment comment;
- public DraftCommentResource(RevisionResource rev, PatchLineComment c) {
+ public DraftCommentResource(RevisionResource rev, Comment c) {
this.rev = rev;
this.comment = c;
}
@@ -47,12 +47,12 @@
return rev.getPatchSet();
}
- PatchLineComment getComment() {
+ Comment getComment() {
return comment;
}
String getId() {
- return comment.getKey().get();
+ return comment.key.uuid;
}
Account.Id getAuthorId() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
index acb50ac..fe82d66 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
@@ -20,10 +20,10 @@
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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -35,19 +35,19 @@
private final Provider<CurrentUser> user;
private final ListRevisionDrafts list;
private final Provider<ReviewDb> dbProvider;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
@Inject
DraftComments(DynamicMap<RestView<DraftCommentResource>> views,
Provider<CurrentUser> user,
ListRevisionDrafts list,
Provider<ReviewDb> dbProvider,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.views = views;
this.user = user;
this.list = list;
this.dbProvider = dbProvider;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
@Override
@@ -66,9 +66,9 @@
throws ResourceNotFoundException, OrmException, AuthException {
checkIdentifiedUser();
String uuid = id.get();
- for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(dbProvider.get(),
+ for (Comment c : commentsUtil.draftByPatchSetAuthor(dbProvider.get(),
rev.getPatchSet().getId(), rev.getAccountId(), rev.getNotes())) {
- if (uuid.equals(c.getKey().get())) {
+ if (uuid.equals(c.key.uuid)) {
return new DraftCommentResource(rev, c);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index 390f416..0b9a877 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -14,11 +14,11 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
+import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
@@ -52,7 +52,7 @@
PatchSet patchSet,
IdentifiedUser user,
ChangeMessage message,
- List<PatchLineComment> comments);
+ List<Comment> comments);
}
private final ExecutorService sendEmailsExecutor;
@@ -66,7 +66,7 @@
private final PatchSet patchSet;
private final IdentifiedUser user;
private final ChangeMessage message;
- private List<PatchLineComment> comments;
+ private final List<Comment> comments;
private ReviewDb db;
@Inject
@@ -81,7 +81,7 @@
@Assisted PatchSet patchSet,
@Assisted IdentifiedUser user,
@Assisted ChangeMessage message,
- @Assisted List<PatchLineComment> comments) {
+ @Assisted List<Comment> comments) {
this.sendEmailsExecutor = executor;
this.patchSetInfoFactory = patchSetInfoFactory;
this.commentSenderFactory = commentSenderFactory;
@@ -92,7 +92,7 @@
this.patchSet = patchSet;
this.user = user;
this.message = message;
- this.comments = PLC_ORDER.sortedCopy(comments);
+ this.comments = COMMENT_ORDER.sortedCopy(comments);
}
void sendAsync() {
@@ -110,7 +110,7 @@
cm.setPatchSet(patchSet,
patchSetInfoFactory.get(notes.getProjectName(), patchSet));
cm.setChangeMessage(message.getMessage(), message.getWrittenOn());
- cm.setPatchLineComments(comments);
+ cm.setComments(comments);
cm.setNotify(notify);
cm.send();
} catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index d145ddf..d617a70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -54,6 +54,7 @@
@Singleton
public class FileContentUtil {
public static final String TEXT_X_GERRIT_COMMIT_MESSAGE = "text/x-gerrit-commit-message";
+ public static final String TEXT_X_GERRIT_MERGE_LIST = "text/x-gerrit-merge-list";
private static final String X_GIT_SYMLINK = "x-git/symlink";
private static final String X_GIT_GITLINK = "x-git/gitlink";
private static final int MAX_SIZE = 5 << 20;
@@ -264,6 +265,9 @@
if (Patch.COMMIT_MSG.equals(path)) {
return TEXT_X_GERRIT_COMMIT_MESSAGE;
}
+ if (Patch.MERGE_LIST.equals(path)) {
+ return TEXT_X_GERRIT_MERGE_LIST;
+ }
if (project != null) {
for (ProjectState p : project.tree()) {
String t = p.getConfig().getMimeTypes().getMimeType(path);
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/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index a2fd004..e99eb87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -14,18 +14,13 @@
package com.google.gerrit.server.change;
-import com.google.common.base.Predicate;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Sets;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
import org.eclipse.jgit.api.ArchiveCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -37,48 +32,8 @@
import java.io.IOException;
import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
public class GetArchive implements RestReadView<RevisionResource> {
- @Singleton
- public static class AllowedFormats {
- final ImmutableMap<String, ArchiveFormat> extensions;
- final Set<ArchiveFormat> allowed;
-
- @Inject
- AllowedFormats(DownloadConfig cfg) {
- Map<String, ArchiveFormat> exts = new HashMap<>();
- for (ArchiveFormat format : cfg.getArchiveFormats()) {
- for (String ext : format.getSuffixes()) {
- exts.put(ext, format);
- }
- exts.put(format.name().toLowerCase(), format);
- }
- extensions = ImmutableMap.copyOf(exts);
-
- // Zip is not supported because it may be interpreted by a Java plugin as a
- // valid JAR file, whose code would have access to cookies on the domain.
- allowed = Sets.filter(
- cfg.getArchiveFormats(),
- new Predicate<ArchiveFormat>() {
- @Override
- public boolean apply(ArchiveFormat format) {
- return (format != ArchiveFormat.ZIP);
- }
- });
- }
-
- public Set<ArchiveFormat> getAllowed() {
- return allowed;
- }
-
- public ImmutableMap<String, ArchiveFormat> getExtensions() {
- return extensions;
- }
- }
-
private final GitRepositoryManager repoManager;
private final AllowedFormats allowedFormats;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
new file mode 100644
index 0000000..d57107b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
@@ -0,0 +1,48 @@
+// 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.base.Optional;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountJson;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetAssignee implements RestReadView<ChangeResource> {
+ private final AccountInfoCacheFactory.Factory accountInfo;
+
+ @Inject
+ GetAssignee(AccountInfoCacheFactory.Factory accountInfo) {
+ this.accountInfo = accountInfo;
+ }
+
+ @Override
+ public Response<AccountInfo> apply(ChangeResource rsrc) throws OrmException {
+
+ Optional<Account.Id> assignee =
+ Optional.fromNullable(rsrc.getChange().getAssignee());
+ if (assignee.isPresent()) {
+ Account account = accountInfo.create().get(assignee.get());
+ return Response.ok(AccountJson.toAccountInfo(account));
+ }
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
index d87c7eb..d601737 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -33,6 +33,6 @@
@Override
public CommentInfo apply(CommentResource rsrc) throws OrmException {
- return commentJson.get().format(rsrc.getComment());
+ return commentJson.get().newCommentFormatter().format(rsrc.getComment());
}
}
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/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index 5a546f3..c7044e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -24,6 +24,8 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.ComparisonType;
+import com.google.gerrit.server.patch.Text;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -68,6 +70,12 @@
return BinaryResult.create(msg)
.setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
.base64();
+ } else if (Patch.MERGE_LIST.equals(path)) {
+ byte[] mergeList = getMergeList(
+ rsrc.getRevision().getChangeResource().getNotes());
+ return BinaryResult.create(mergeList)
+ .setContentType(FileContentUtil.TEXT_X_GERRIT_MERGE_LIST)
+ .base64();
}
return fileContentUtil.getContent(
rsrc.getRevision().getControl().getProjectControl().getProjectState(),
@@ -92,4 +100,22 @@
throw new NoSuchChangeException(changeId, e);
}
}
+
+ private byte[] getMergeList(ChangeNotes notes)
+ throws NoSuchChangeException, OrmException, IOException {
+ Change.Id changeId = notes.getChangeId();
+ PatchSet ps = psUtil.current(db.get(), notes);
+ if (ps == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ try (Repository git = gitManager.openRepository(notes.getProjectName());
+ RevWalk revWalk = new RevWalk(git)) {
+ return Text.forMergeList(ComparisonType.againstAutoMerge(),
+ revWalk.getObjectReader(),
+ ObjectId.fromString(ps.getRevision().get())).getContent();
+ } catch (RepositoryNotFoundException e) {
+ throw new NoSuchChangeException(changeId, e);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
index 22f90c9..a380ce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
@@ -33,6 +33,6 @@
@Override
public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
- return commentJson.get().format(rsrc.getComment());
+ return commentJson.get().newCommentFormatter().format(rsrc.getComment());
}
}
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..b15810c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.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.gerrit.server.patch.MergeListBuilder;
+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 {
+ 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 createResponse(rsrc, ImmutableList.<CommitInfo> of());
+ }
+
+ List<RevCommit> commits =
+ MergeListBuilder.build(rw, commit, uninterestingParent);
+ List<CommitInfo> result = new ArrayList<>(commits.size());
+ ChangeJson changeJson = json.create(ChangeJson.NO_OPTIONS);
+ for (RevCommit c : commits) {
+ result.add(changeJson.toCommit(rsrc.getControl(), rw, c, addLinks, true));
+ }
+ return createResponse(rsrc, result);
+ }
+ }
+
+ private static Response<List<CommitInfo>> createResponse(
+ RevisionResource rsrc, List<CommitInfo> result) {
+ 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/GetPastAssignees.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java
new file mode 100644
index 0000000..fa9c0e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.java
@@ -0,0 +1,56 @@
+// 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 static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountJson;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class GetPastAssignees implements RestReadView<ChangeResource> {
+ private final AccountInfoCacheFactory.Factory accountInfos;
+
+ @Inject
+ GetPastAssignees(AccountInfoCacheFactory.Factory accountInfosFactory) {
+ this.accountInfos = accountInfosFactory;
+ }
+
+ @Override
+ public Response<List<AccountInfo>> apply(ChangeResource rsrc)
+ throws OrmException {
+
+ Set<Account.Id> pastAssignees =
+ rsrc.getControl().getNotes().load().getPastAssignees();
+ if (pastAssignees == null) {
+ return Response.ok(Collections.emptyList());
+ }
+ AccountInfoCacheFactory accountInfoFactory = accountInfos.create();
+
+ return Response.ok(pastAssignees.stream()
+ .map(accountInfoFactory::get)
+ .map(AccountJson::toAccountInfo)
+ .collect(toList()));
+ }
+}
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/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index eae67a2..478d9c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -41,14 +41,14 @@
private final ActionJson delegate;
private final Config config;
private final Provider<ReviewDb> dbProvider;
- private final MergeSuperSet mergeSuperSet;
+ private final Provider<MergeSuperSet> mergeSuperSet;
private final ChangeResource.Factory changeResourceFactory;
@Inject
GetRevisionActions(
ActionJson delegate,
Provider<ReviewDb> dbProvider,
- MergeSuperSet mergeSuperSet,
+ Provider<MergeSuperSet> mergeSuperSet,
ChangeResource.Factory changeResourceFactory,
@GerritServerConfig Config config) {
this.delegate = delegate;
@@ -72,7 +72,7 @@
h.putBoolean(Submit.wholeTopicEnabled(config));
ReviewDb db = dbProvider.get();
ChangeSet cs =
- mergeSuperSet.completeChangeSet(db, rsrc.getChange(), user);
+ mergeSuperSet.get().completeChangeSet(db, rsrc.getChange(), user);
for (ChangeData cd : cs.changes()) {
changeResourceFactory.create(cd.changeControl()).prepareETag(h, user);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRobotComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRobotComment.java
new file mode 100644
index 0000000..c10cd2e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRobotComment.java
@@ -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.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetRobotComment implements RestReadView<RobotCommentResource> {
+
+ private final Provider<CommentJson> commentJson;
+
+ @Inject
+ GetRobotComment(Provider<CommentJson> commentJson) {
+ this.commentJson = commentJson;
+ }
+
+ @Override
+ public RobotCommentInfo apply(RobotCommentResource rsrc) throws OrmException {
+ return commentJson.get().newRobotCommentFormatter()
+ .format(rsrc.getComment());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
index 97befa0..32b5ae8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
@@ -18,7 +18,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -33,17 +33,17 @@
private final Provider<ReviewDb> db;
private final ChangeData.Factory changeDataFactory;
private final Provider<CommentJson> commentJson;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
@Inject
ListChangeComments(Provider<ReviewDb> db,
ChangeData.Factory changeDataFactory,
Provider<CommentJson> commentJson,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.db = db;
this.changeDataFactory = changeDataFactory;
this.commentJson = commentJson;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
@Override
@@ -53,6 +53,7 @@
return commentJson.get()
.setFillAccounts(true)
.setFillPatchSet(true)
- .format(plcUtil.publishedByChange(db.get(), cd.notes()));
+ .newCommentFormatter()
+ .format(commentsUtil.publishedByChange(db.get(), cd.notes()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
index 561a040..6a3e237 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
@@ -17,9 +17,9 @@
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -34,17 +34,17 @@
private final Provider<ReviewDb> db;
private final ChangeData.Factory changeDataFactory;
private final Provider<CommentJson> commentJson;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
@Inject
ListChangeDrafts(Provider<ReviewDb> db,
ChangeData.Factory changeDataFactory,
Provider<CommentJson> commentJson,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.db = db;
this.changeDataFactory = changeDataFactory;
this.commentJson = commentJson;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
@Override
@@ -54,11 +54,11 @@
throw new AuthException("Authentication required");
}
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
- List<PatchLineComment> drafts = plcUtil.draftByChangeAuthor(
+ List<Comment> drafts = commentsUtil.draftByChangeAuthor(
db.get(), cd.notes(), rsrc.getControl().getUser().getAccountId());
return commentJson.get()
.setFillAccounts(false)
.setFillPatchSet(true)
- .format(drafts);
+ .newCommentFormatter().format(drafts);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
index 2392781..8524b8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
@@ -14,9 +14,9 @@
package com.google.gerrit.server.change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -28,8 +28,8 @@
@Inject
ListRevisionComments(Provider<ReviewDb> db,
Provider<CommentJson> commentJson,
- PatchLineCommentsUtil plcUtil) {
- super(db, commentJson, plcUtil);
+ CommentsUtil commentsUtil) {
+ super(db, commentJson, commentsUtil);
}
@Override
@@ -38,9 +38,10 @@
}
@Override
- protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+ protected Iterable<Comment> listComments(RevisionResource rsrc)
throws OrmException {
ChangeNotes notes = rsrc.getNotes();
- return plcUtil.publishedByPatchSet(db.get(), notes, rsrc.getPatchSet().getId());
+ return commentsUtil.publishedByPatchSet(db.get(), notes,
+ rsrc.getPatchSet().getId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
index ef12b2a..21d427c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
@@ -16,9 +16,9 @@
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -31,20 +31,20 @@
public class ListRevisionDrafts implements RestReadView<RevisionResource> {
protected final Provider<ReviewDb> db;
protected final Provider<CommentJson> commentJson;
- protected final PatchLineCommentsUtil plcUtil;
+ protected final CommentsUtil commentsUtil;
@Inject
ListRevisionDrafts(Provider<ReviewDb> db,
Provider<CommentJson> commentJson,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.db = db;
this.commentJson = commentJson;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
- protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+ protected Iterable<Comment> listComments(RevisionResource rsrc)
throws OrmException {
- return plcUtil.draftByPatchSetAuthor(db.get(), rsrc.getPatchSet().getId(),
+ return commentsUtil.draftByPatchSetAuthor(db.get(), rsrc.getPatchSet().getId(),
rsrc.getAccountId(), rsrc.getNotes());
}
@@ -57,13 +57,13 @@
throws OrmException {
return commentJson.get()
.setFillAccounts(includeAuthorInfo())
- .format(listComments(rsrc));
+ .newCommentFormatter().format(listComments(rsrc));
}
public List<CommentInfo> getComments(RevisionResource rsrc)
throws OrmException {
return commentJson.get()
.setFillAccounts(includeAuthorInfo())
- .formatAsList(listComments(rsrc));
+ .newCommentFormatter().formatAsList(listComments(rsrc));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRobotComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRobotComments.java
new file mode 100644
index 0000000..01ad9ee
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRobotComments.java
@@ -0,0 +1,67 @@
+// 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.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListRobotComments implements RestReadView<RevisionResource> {
+ protected final Provider<ReviewDb> db;
+ protected final Provider<CommentJson> commentJson;
+ protected final CommentsUtil commentsUtil;
+
+ @Inject
+ ListRobotComments(Provider<ReviewDb> db,
+ Provider<CommentJson> commentJson,
+ CommentsUtil commentsUtil) {
+ this.db = db;
+ this.commentJson = commentJson;
+ this.commentsUtil = commentsUtil;
+ }
+
+ @Override
+ public Map<String, List<RobotCommentInfo>> apply(RevisionResource rsrc)
+ throws OrmException {
+ return commentJson.get()
+ .setFillAccounts(true)
+ .newRobotCommentFormatter()
+ .format(listComments(rsrc));
+ }
+
+ public List<RobotCommentInfo> getComments(RevisionResource rsrc)
+ throws OrmException {
+ return commentJson.get()
+ .setFillAccounts(true)
+ .newRobotCommentFormatter()
+ .formatAsList(listComments(rsrc));
+ }
+
+ private Iterable<RobotComment> listComments(RevisionResource rsrc)
+ throws OrmException {
+ return commentsUtil.robotCommentsByPatchSet(
+ rsrc.getNotes(), rsrc.getPatchSet().getId());
+ }
+}
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..a52920a 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
@@ -21,6 +21,7 @@
import static com.google.gerrit.server.change.FileResource.FILE_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND;
import static com.google.gerrit.server.change.VoteResource.VOTE_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -37,11 +38,13 @@
bind(Reviewers.class);
bind(DraftComments.class);
bind(Comments.class);
+ bind(RobotComments.class);
bind(Files.class);
bind(Votes.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
+ DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND);
DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND);
DynamicMap.mapOf(binder(), FILE_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
@@ -53,6 +56,10 @@
get(CHANGE_KIND, "detail").to(GetDetail.class);
get(CHANGE_KIND, "topic").to(GetTopic.class);
get(CHANGE_KIND, "in").to(IncludedIn.class);
+ get(CHANGE_KIND, "assignee").to(GetAssignee.class);
+ get(CHANGE_KIND, "past_assignees").to(GetPastAssignees.class);
+ put(CHANGE_KIND, "assignee").to(PutAssignee.class);
+ delete(CHANGE_KIND, "assignee").to(DeleteAssignee.class);
get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
get(CHANGE_KIND, "comments").to(ListChangeComments.class);
get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
@@ -78,6 +85,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);
@@ -92,6 +100,7 @@
get(REVISION_KIND, "related").to(GetRelated.class);
get(REVISION_KIND, "review").to(GetReview.class);
post(REVISION_KIND, "review").to(PostReview.class);
+ get(REVISION_KIND, "preview_submit").to(PreviewSubmit.class);
post(REVISION_KIND, "submit").to(Submit.class);
post(REVISION_KIND, "rebase").to(Rebase.class);
get(REVISION_KIND, "patch").to(GetPatch.class);
@@ -99,6 +108,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);
@@ -109,6 +119,9 @@
child(REVISION_KIND, "comments").to(Comments.class);
get(COMMENT_KIND).to(GetComment.class);
+ child(REVISION_KIND, "robotcomments").to(RobotComments.class);
+ get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
+
child(REVISION_KIND, "files").to(Files.class);
put(FILE_KIND, "reviewed").to(PutReviewed.class);
delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
@@ -136,6 +149,7 @@
factory(PatchSetInserter.Factory.class);
factory(RebaseChangeOp.Factory.class);
factory(ReviewerResource.Factory.class);
+ factory(SetAssigneeOp.Factory.class);
factory(SetHashtagsOp.Factory.class);
factory(ChangeResource.Factory.class);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index 2139ec4..954392c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -33,8 +33,6 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -94,7 +92,7 @@
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
- u.addOp(req.getChange().getId(), new Op(control, input));
+ u.addOp(req.getChange().getId(), new Op(input));
u.execute();
}
@@ -103,14 +101,12 @@
private class Op extends BatchUpdate.Op {
private final MoveInput input;
- private final IdentifiedUser caller;
private Change change;
private Branch.NameKey newDestKey;
- Op(ChangeControl ctl, MoveInput input) {
+ Op(MoveInput input) {
this.input = input;
- this.caller = ctl.getUser().asIdentifiedUser();
}
@Override
@@ -179,11 +175,8 @@
msgBuf.append("\n\n");
msgBuf.append(input.message);
}
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- caller.getAccountId(), ctx.getWhen(), change.currentPatchSetId());
- cmsg.setMessage(msgBuf.toString());
+ ChangeMessage cmsg =
+ ChangeMessagesUtil.newMessage(ctx, msgBuf.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
return true;
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 5bc3a36..9a6455c 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
@@ -30,12 +30,10 @@
import com.google.gerrit.server.ApprovalCopier;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.events.CommitReceivedEvent;
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 +45,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 +87,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 +139,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 +160,8 @@
return this;
}
- public PatchSetInserter setSendMail(boolean sendMail) {
- this.sendMail = sendMail;
+ public PatchSetInserter setNotify(NotifyHandling notify) {
+ this.notify = notify;
return this;
}
@@ -198,7 +188,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,14 +219,13 @@
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());
}
if (message != null) {
- changeMessage = new ChangeMessage(
- new ChangeMessage.Key(ctl.getId(), ChangeUtil.messageUUID(db)),
- ctx.getAccountId(), ctx.getWhen(), patchSet.getId());
+ changeMessage = ChangeMessagesUtil.newMessage(
+ db, patchSet.getId(), ctx.getUser(), ctx.getWhen(), message);
changeMessage.setMessage(message);
}
@@ -257,7 +245,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 +254,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 +262,21 @@
}
}
- NotifyHandling notify = sendMail
- ? NotifyHandling.ALL
- : NotifyHandling.NONE;
if (fireRevisionCreated) {
revisionCreated.fire(change, patchSet, ctx.getAccount(),
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 +289,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..9210d2b 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
@@ -14,12 +14,11 @@
package com.google.gerrit.server.change;
-import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
-import static com.google.gerrit.server.change.PutDraftComment.side;
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.auto.value.AutoValue;
@@ -44,10 +43,12 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.client.Side;
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.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -55,18 +56,20 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CommentsUtil;
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;
@@ -75,6 +78,7 @@
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.query.change.ChangeData;
@@ -93,7 +97,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -109,13 +112,14 @@
private final ChangeData.Factory changeDataFactory;
private final ApprovalsUtil approvalsUtil;
private final ChangeMessagesUtil cmUtil;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final PatchListCache patchListCache;
private final AccountsCollection accounts;
private final EmailReviewComments.Factory email;
private final CommentAdded commentAdded;
private final PostReviewers postReviewers;
+ private final NotesMigration migration;
@Inject
PostReview(Provider<ReviewDb> db,
@@ -124,18 +128,19 @@
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
AccountsCollection accounts,
EmailReviewComments.Factory email,
CommentAdded commentAdded,
- PostReviewers postReviewers) {
+ PostReviewers postReviewers,
+ NotesMigration migration) {
this.db = db;
this.batchUpdateFactory = batchUpdateFactory;
this.changes = changes;
this.changeDataFactory = changeDataFactory;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.approvalsUtil = approvalsUtil;
@@ -144,6 +149,7 @@
this.email = email;
this.commentAdded = commentAdded;
this.postReviewers = postReviewers;
+ this.migration = migration;
}
@Override
@@ -162,6 +168,8 @@
}
if (input.onBehalfOf != null) {
revision = onBehalfOf(revision, input);
+ } else if (input.drafts == null) {
+ input.drafts = DraftHandling.DELETE;
}
if (input.labels != null) {
checkLabels(revision, input.strictLabels, input.labels);
@@ -169,6 +177,12 @@
if (input.comments != null) {
checkComments(revision, input.comments);
}
+ if (input.robotComments != null) {
+ if (!migration.readChanges()) {
+ throw new MethodNotAllowedException("robot comments not supported");
+ }
+ checkRobotComments(revision, input.robotComments);
+ }
if (input.notify == null) {
log.warn("notify = null; assuming notify = NONE");
input.notify = NotifyHandling.NONE;
@@ -212,7 +226,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) {
@@ -231,6 +245,13 @@
"label required to post review on behalf of \"%s\"",
in.onBehalfOf));
}
+ if (in.drafts == null) {
+ in.drafts = DraftHandling.KEEP;
+ }
+ if (in.drafts != DraftHandling.KEEP) {
+ throw new AuthException("not allowed to modify other user's drafts");
+ }
+
ChangeControl caller = rev.getControl();
Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
@@ -258,7 +279,13 @@
in.onBehalfOf));
}
- ChangeControl target = caller.forUser(accounts.parse(in.onBehalfOf));
+ ChangeControl target = caller.forUser(
+ accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
+ if (!target.getRefControl().isVisible()) {
+ throw new UnprocessableEntityException(String.format(
+ "on_behalf_of account %s cannot see destination ref",
+ target.getUser().getAccountId()));
+ }
return new RevisionResource(changes.parse(target), rev.getPatchSet());
}
@@ -312,32 +339,40 @@
}
}
- private void checkComments(RevisionResource revision, Map<String, List<CommentInput>> in)
- throws BadRequestException, OrmException {
- Iterator<Map.Entry<String, List<CommentInput>>> mapItr =
- in.entrySet().iterator();
+ private <T extends CommentInput> void checkComments(RevisionResource revision,
+ Map<String, List<T>> in) throws BadRequestException, OrmException {
+ Iterator<? extends Map.Entry<String, List<T>>> mapItr =
+ in.entrySet().iterator();
Set<String> filePaths =
Sets.newHashSet(changeDataFactory.create(
db.get(), revision.getControl()).filePaths(
revision.getPatchSet()));
while (mapItr.hasNext()) {
- Map.Entry<String, List<CommentInput>> ent = mapItr.next();
+ Map.Entry<String, List<T>> ent = mapItr.next();
String path = ent.getKey();
- if (!filePaths.contains(path) && !Patch.COMMIT_MSG.equals(path)) {
+ if (!filePaths.contains(path) && !Patch.isMagic(path)) {
throw new BadRequestException(String.format(
"file %s not found in revision %s",
path, revision.getChange().currentPatchSetId()));
}
- List<CommentInput> list = ent.getValue();
+ List<T> list = ent.getValue();
if (list == null) {
mapItr.remove();
continue;
}
+ if (Patch.isMagic(path)) {
+ for (T comment : list) {
+ if (comment.side == Side.PARENT && comment.parent == null) {
+ throw new BadRequestException(
+ String.format("cannot comment on %s on auto-merge", path));
+ }
+ }
+ }
- Iterator<CommentInput> listItr = list.iterator();
+ Iterator<T> listItr = list.iterator();
while (listItr.hasNext()) {
- CommentInput c = listItr.next();
+ T c = listItr.next();
if (c == null) {
listItr.remove();
continue;
@@ -358,48 +393,72 @@
}
}
+ private void checkRobotComments(RevisionResource revision,
+ Map<String, List<RobotCommentInput>> in)
+ throws BadRequestException, OrmException {
+ for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
+ String path = e.getKey();
+ for (RobotCommentInput c : e.getValue()) {
+ if (c.robotId == null) {
+ throw new BadRequestException(String
+ .format("robotId is missing for robot comment on %s", path));
+ }
+ if (c.robotRunId == null) {
+ throw new BadRequestException(String
+ .format("robotRunId is missing for robot comment on %s", path));
+ }
+ }
+ }
+ checkComments(revision, in);
+ }
+
/**
- * Used to compare PatchLineComments with CommentInput comments.
+ * Used to compare Comments with CommentInput comments.
*/
@AutoValue
abstract static class CommentSetEntry {
- private static CommentSetEntry create(Patch.Key key,
- Integer line, Side side, HashCode message, CommentRange range) {
- return new AutoValue_PostReview_CommentSetEntry(key, line, side, message,
- range);
+ private static CommentSetEntry create(String filename, int patchSetId,
+ Integer line, Side side, HashCode message, Comment.Range range) {
+ return new AutoValue_PostReview_CommentSetEntry(filename, patchSetId,
+ line, side, message, range);
}
- public static CommentSetEntry create(PatchLineComment comment) {
- return create(comment.getKey().getParentKey(),
- comment.getLine(),
- Side.fromShort(comment.getSide()),
- Hashing.sha1().hashString(comment.getMessage(), UTF_8),
- comment.getRange());
+ public static CommentSetEntry create(Comment comment) {
+ return create(comment.key.filename,
+ comment.key.patchSetId,
+ comment.lineNbr,
+ Side.fromShort(comment.side),
+ Hashing.sha1().hashString(comment.message, UTF_8),
+ comment.range);
}
- abstract Patch.Key key();
+ abstract String filename();
+ abstract int patchSetId();
@Nullable abstract Integer line();
abstract Side side();
abstract HashCode message();
- @Nullable abstract CommentRange range();
+ @Nullable abstract Comment.Range range();
}
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;
private PatchSet ps;
private ChangeMessage message;
- private List<PatchLineComment> comments = new ArrayList<>();
+ private List<Comment> comments = new ArrayList<>();
private List<String> labelDelta = new ArrayList<>();
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
@@ -410,6 +469,7 @@
ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
boolean dirty = false;
dirty |= insertComments(ctx);
+ dirty |= insertRobotComments(ctx);
dirty |= updateLabels(ctx);
dirty |= insertMessage(ctx);
return dirty;
@@ -440,7 +500,7 @@
map = Collections.emptyMap();
}
- Map<String, PatchLineComment> drafts = Collections.emptyMap();
+ Map<String, Comment> drafts = Collections.emptyMap();
if (!map.isEmpty() || in.drafts != DraftHandling.KEEP) {
if (in.drafts == DraftHandling.PUBLISH_ALL_REVISIONS) {
drafts = changeDrafts(ctx);
@@ -449,101 +509,127 @@
}
}
- List<PatchLineComment> del = new ArrayList<>();
- List<PatchLineComment> ups = new ArrayList<>();
+ List<Comment> toDel = new ArrayList<>();
+ List<Comment> toPublish = new ArrayList<>();
Set<CommentSetEntry> existingIds = in.omitDuplicateComments
? readExistingComments(ctx)
- : Collections.<CommentSetEntry>emptySet();
+ : Collections.emptySet();
for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
String path = ent.getKey();
for (CommentInput c : ent.getValue()) {
String parent = Url.decode(c.inReplyTo);
- PatchLineComment e = drafts.remove(Url.decode(c.id));
+ Comment e = drafts.remove(Url.decode(c.id));
if (e == null) {
- e = new PatchLineComment(
- new PatchLineComment.Key(new Patch.Key(psId, path), null),
- c.line != null ? c.line : 0,
- user.getAccountId(),
- parent, ctx.getWhen());
- } else if (parent != null) {
- e.setParentUuid(parent);
+ e = commentsUtil.newComment(ctx, path, psId, c.side(), c.message);
+ } else {
+ e.writtenOn = ctx.getWhen();
+ e.side = c.side();
+ e.message = c.message;
}
- e.setStatus(PatchLineComment.Status.PUBLISHED);
- e.setWrittenOn(ctx.getWhen());
- e.setSide(side(c));
+
+ if (parent != null) {
+ e.parentUuid = parent;
+ }
setCommentRevId(e, patchListCache, ctx.getChange(), ps);
- e.setMessage(c.message);
- e.setTag(in.tag);
- if (c.range != null) {
- e.setRange(new CommentRange(
- c.range.startLine,
- c.range.startCharacter,
- c.range.endLine,
- c.range.endCharacter));
- e.setLine(c.range.endLine);
- }
+ e.setLineNbrAndRange(c.line, c.range);
+ e.tag = in.tag;
+
if (existingIds.contains(CommentSetEntry.create(e))) {
continue;
}
- if (e.getKey().get() == null) {
- e.getKey().set(ChangeUtil.messageUUID(ctx.getDb()));
- }
- ups.add(e);
+ toPublish.add(e);
}
}
- switch (firstNonNull(in.drafts, DraftHandling.DELETE)) {
+ switch (in.drafts) {
case KEEP:
default:
break;
case DELETE:
- del.addAll(drafts.values());
+ toDel.addAll(drafts.values());
break;
case PUBLISH:
- for (PatchLineComment e : drafts.values()) {
- ups.add(publishComment(ctx, e, ps));
+ for (Comment e : drafts.values()) {
+ toPublish.add(publishComment(ctx, e, ps));
}
break;
case PUBLISH_ALL_REVISIONS:
- publishAllRevisions(ctx, drafts, ups);
+ publishAllRevisions(ctx, drafts, toPublish);
break;
}
ChangeUpdate u = ctx.getUpdate(psId);
- plcUtil.deleteComments(ctx.getDb(), u, del);
- plcUtil.putComments(ctx.getDb(), u, ups);
- comments.addAll(ups);
- return !del.isEmpty() || !ups.isEmpty();
+ commentsUtil.deleteComments(ctx.getDb(), u, toDel);
+ commentsUtil.putComments(ctx.getDb(), u, Status.PUBLISHED, toPublish);
+ comments.addAll(toPublish);
+ return !toDel.isEmpty() || !toPublish.isEmpty();
+ }
+
+ private boolean insertRobotComments(ChangeContext ctx) throws OrmException {
+ if (in.robotComments == null) {
+ return false;
+ }
+
+ List<RobotComment> toAdd = new ArrayList<>(in.robotComments.size());
+
+ Set<CommentSetEntry> existingIds = in.omitDuplicateComments
+ ? readExistingRobotComments(ctx)
+ : Collections.emptySet();
+
+ for (Map.Entry<String, List<RobotCommentInput>> ent : in.robotComments.entrySet()) {
+ String path = ent.getKey();
+ for (RobotCommentInput c : ent.getValue()) {
+ RobotComment e = commentsUtil.newRobotComment(
+ ctx, path, psId, c.side(), c.message, c.robotId, c.robotRunId);
+ e.parentUuid = Url.decode(c.inReplyTo);
+ e.url = c.url;
+ e.properties = c.properties;
+ e.setLineNbrAndRange(c.line, c.range);
+ e.tag = in.tag;
+ setCommentRevId(e, patchListCache, ctx.getChange(), ps);
+
+ if (existingIds.contains(CommentSetEntry.create(e))) {
+ continue;
+ }
+ toAdd.add(e);
+ }
+ }
+
+ commentsUtil.putRobotComments(ctx.getUpdate(psId), toAdd);
+ comments.addAll(toAdd);
+ return !toAdd.isEmpty();
}
private Set<CommentSetEntry> readExistingComments(ChangeContext ctx)
throws OrmException {
- Set<CommentSetEntry> r = new HashSet<>();
- for (PatchLineComment c : plcUtil.publishedByChange(ctx.getDb(),
- ctx.getNotes())) {
- r.add(CommentSetEntry.create(c));
- }
- return r;
+ return commentsUtil.publishedByChange(ctx.getDb(), ctx.getNotes())
+ .stream().map(CommentSetEntry::create).collect(toSet());
}
- private Map<String, PatchLineComment> changeDrafts(ChangeContext ctx)
+ private Set<CommentSetEntry> readExistingRobotComments(ChangeContext ctx)
throws OrmException {
- Map<String, PatchLineComment> drafts = new HashMap<>();
- for (PatchLineComment c : plcUtil.draftByChangeAuthor(
+ return commentsUtil.robotCommentsByChange(ctx.getNotes())
+ .stream().map(CommentSetEntry::create).collect(toSet());
+ }
+
+ private Map<String, Comment> changeDrafts(ChangeContext ctx)
+ throws OrmException {
+ Map<String, Comment> drafts = new HashMap<>();
+ for (Comment c : commentsUtil.draftByChangeAuthor(
ctx.getDb(), ctx.getNotes(), user.getAccountId())) {
- c.setTag(in.tag);
- drafts.put(c.getKey().get(), c);
+ c.tag = in.tag;
+ drafts.put(c.key.uuid, c);
}
return drafts;
}
- private Map<String, PatchLineComment> patchSetDrafts(ChangeContext ctx)
+ private Map<String, Comment> patchSetDrafts(ChangeContext ctx)
throws OrmException {
- Map<String, PatchLineComment> drafts = new HashMap<>();
- for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(ctx.getDb(),
+ Map<String, Comment> drafts = new HashMap<>();
+ for (Comment c : commentsUtil.draftByPatchSetAuthor(ctx.getDb(),
psId, user.getAccountId(), ctx.getNotes())) {
- drafts.put(c.getKey().get(), c);
+ drafts.put(c.key.uuid, c);
}
return drafts;
}
@@ -557,21 +643,24 @@
return labels;
}
- private PatchLineComment publishComment(ChangeContext ctx,
- PatchLineComment c, PatchSet ps) throws OrmException {
- c.setStatus(PatchLineComment.Status.PUBLISHED);
- c.setWrittenOn(ctx.getWhen());
- c.setTag(in.tag);
+ private Comment publishComment(ChangeContext ctx,
+ Comment c, PatchSet ps) throws OrmException {
+ c.writtenOn = ctx.getWhen();
+ c.tag = in.tag;
+ // Draft may have been created by a different real user; copy the current
+ // real user. (Only applies to X-Gerrit-RunAs, since modifying drafts via
+ // on_behalf_of is not allowed.)
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps));
return c;
}
private void publishAllRevisions(ChangeContext ctx,
- Map<String, PatchLineComment> drafts, List<PatchLineComment> ups)
+ Map<String, Comment> drafts, List<Comment> ups)
throws OrmException {
boolean needOtherPatchSets = false;
- for (PatchLineComment c : drafts.values()) {
- if (!c.getPatchSetId().equals(psId)) {
+ for (Comment c : drafts.values()) {
+ if (c.key.patchSetId != psId.get()) {
needOtherPatchSets = true;
break;
}
@@ -579,8 +668,9 @@
Map<PatchSet.Id, PatchSet> patchSets = needOtherPatchSets
? psUtil.byChangeAsMap(ctx.getDb(), ctx.getNotes())
: ImmutableMap.of(psId, ps);
- for (PatchLineComment e : drafts.values()) {
- ups.add(publishComment(ctx, e, patchSets.get(e.getPatchSetId())));
+ for (Comment e : drafts.values()) {
+ ups.add(publishComment(ctx, e, patchSets
+ .get(new PatchSet.Id(ctx.getChange().getId(), e.key.patchSetId))));
}
}
@@ -615,6 +705,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,
@@ -659,6 +771,7 @@
c.setValue(ent.getValue());
c.setGranted(ctx.getWhen());
c.setTag(in.tag);
+ ctx.getUser().updateRealAccountId(c::setRealAccountId);
ups.add(c);
addLabelDelta(normName, c.getValue());
oldApprovals.put(normName, previous.get(normName));
@@ -669,11 +782,8 @@
oldApprovals.put(normName, null);
approvals.put(normName, c.getValue());
} else if (c == null) {
- c = new PatchSetApproval(new PatchSetApproval.Key(
- psId,
- user.getAccountId(),
- lt.getLabelId()),
- ent.getValue(), ctx.getWhen());
+ c = ApprovalsUtil.newApproval(
+ psId, user, lt.getLabelId(), ent.getValue(), ctx.getWhen());
c.setTag(in.tag);
c.setGranted(ctx.getWhen());
ups.add(c);
@@ -689,6 +799,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);
@@ -703,12 +821,10 @@
if (del.isEmpty()) {
// If no existing label is being set to 0, hack in the caller
// as a reviewer by picking the first server-wide LabelType.
- PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
- psId,
- user.getAccountId(),
- ctx.getControl().getLabelTypes().getLabelTypes().get(0)
- .getLabelId()),
- (short) 0, ctx.getWhen());
+ LabelId labelId = ctx.getControl().getLabelTypes().getLabelTypes()
+ .get(0).getLabelId();
+ PatchSetApproval c = ApprovalsUtil.newApproval(
+ psId, user, labelId, 0, ctx.getWhen());
c.setTag(in.tag);
c.setGranted(ctx.getWhen());
ups.add(c);
@@ -767,17 +883,10 @@
return false;
}
- message = new ChangeMessage(
- new ChangeMessage.Key(
- psId.getParentKey(), ChangeUtil.messageUUID(ctx.getDb())),
- user.getAccountId(),
- ctx.getWhen(),
- psId);
+ message = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, user, ctx.getWhen(),
+ "Patch Set " + psId.get() + ":" + buf);
message.setTag(in.tag);
- message.setMessage(String.format(
- "Patch Set %d:%s",
- psId.get(),
- buf.toString()));
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), message);
return true;
}
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..ee771f1 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
@@ -25,6 +25,7 @@
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -96,10 +97,10 @@
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;
+ private final AccountCache accountCache;
@Inject
PostReviewers(AccountsCollection accounts,
@@ -115,10 +116,10 @@
Provider<IdentifiedUser> user,
IdentifiedUser.GenericFactory identifiedUserFactory,
@GerritServerConfig Config cfg,
- AccountCache accountCache,
ReviewerJson json,
ReviewerAdded reviewerAdded,
- NotesMigration migration) {
+ NotesMigration migration,
+ AccountCache accountCache) {
this.accounts = accounts;
this.reviewerFactory = reviewerFactory;
this.approvalsUtil = approvalsUtil;
@@ -132,10 +133,10 @@
this.user = user;
this.identifiedUserFactory = identifiedUserFactory;
this.cfg = cfg;
- this.accountCache = accountCache;
this.json = json;
this.reviewerAdded = reviewerAdded;
this.migration = migration;
+ this.accountCache = accountCache;
}
@Override
@@ -173,18 +174,24 @@
}
}
return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId),
- input.state());
+ input.state(), input.notify);
}
private Addition putAccount(String reviewer, ReviewerResource rsrc,
- ReviewerState state) throws UnprocessableEntityException {
+ ReviewerState state, NotifyHandling notify)
+ throws UnprocessableEntityException {
Account member = rsrc.getReviewerUser().getAccount();
ChangeControl control = rsrc.getReviewerControl();
if (isValidReviewer(member, control)) {
return new Addition(reviewer, rsrc.getChangeResource(),
- ImmutableMap.of(member.getId(), control), state);
+ ImmutableMap.of(member.getId(), control), state, notify);
}
- 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)
@@ -234,7 +241,8 @@
}
}
- return new Addition(input.reviewer, rsrc, reviewers, input.state());
+ return new Addition(input.reviewer, rsrc, reviewers, input.state(),
+ input.notify);
}
private boolean isValidReviewer(Account member, ChangeControl control) {
@@ -258,18 +266,19 @@
return addition;
}
- class Addition {
+ public class Addition {
final AddReviewerResult result;
final Op op;
private final Map<Account.Id, ChangeControl> reviewers;
protected Addition(String reviewer) {
- this(reviewer, null, null, REVIEWER);
+ this(reviewer, null, null, REVIEWER, null);
}
protected Addition(String reviewer, ChangeResource rsrc,
- Map<Account.Id, ChangeControl> reviewers, ReviewerState state) {
+ Map<Account.Id, ChangeControl> reviewers, ReviewerState state,
+ NotifyHandling notify) {
result = new AddReviewerResult(reviewer);
if (reviewers == null) {
this.reviewers = ImmutableMap.of();
@@ -277,7 +286,7 @@
return;
}
this.reviewers = reviewers;
- op = new Op(rsrc, reviewers, state);
+ op = new Op(rsrc, reviewers, state, notify);
}
void gatherResults() throws OrmException {
@@ -304,9 +313,10 @@
}
}
- class Op extends BatchUpdate.Op {
+ public class Op extends BatchUpdate.Op {
final Map<Account.Id, ChangeControl> reviewers;
final ReviewerState state;
+ final NotifyHandling notify;
List<PatchSetApproval> addedReviewers;
Collection<Account.Id> addedCCs;
@@ -314,10 +324,11 @@
private PatchSet patchSet;
Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers,
- ReviewerState state) {
+ ReviewerState state, NotifyHandling notify) {
this.rsrc = rsrc;
this.reviewers = reviewers;
this.state = state;
+ this.notify = notify;
}
@Override
@@ -353,20 +364,19 @@
if (addedCCs == null) {
addedCCs = new ArrayList<>();
}
- emailReviewers(rsrc.getChange(), addedReviewers, addedCCs);
+ emailReviewers(rsrc.getChange(), addedReviewers, addedCCs, notify);
if (!addedReviewers.isEmpty()) {
- for (PatchSetApproval psa : addedReviewers) {
- Account account = accountCache.get(psa.getAccountId()).getAccount();
- reviewerAdded.fire(rsrc.getChange(), patchSet, account,
+ List<Account> reviewers = Lists.transform(addedReviewers,
+ psa -> accountCache.get(psa.getAccountId()).getAccount());
+ reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers,
ctx.getAccount(), ctx.getWhen());
- }
}
}
}
}
private void emailReviewers(Change change, List<PatchSetApproval> added,
- Collection<Account.Id> copied) {
+ Collection<Account.Id> copied, NotifyHandling notify) {
if (added.isEmpty() && copied.isEmpty()) {
return;
}
@@ -393,7 +403,7 @@
try {
AddReviewerSender cm = addReviewerSenderFactory
- .create(change.getProject(), change.getId());
+ .create(change.getProject(), change.getId(), notify);
cm.setFrom(userId);
cm.addReviewers(toMail);
cm.addExtraCC(toCopy);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
new file mode 100644
index 0000000..435832a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -0,0 +1,149 @@
+// 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.base.Strings;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeOpRepoManager;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.transport.BundleWriter;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Set;
+
+@Singleton
+public class PreviewSubmit implements RestReadView<RevisionResource> {
+ private final Provider<ReviewDb> dbProvider;
+ private final Provider<MergeOp> mergeOpProvider;
+ private final AllowedFormats allowedFormats;
+
+ private String format;
+
+ @Option(name = "--format")
+ public void setFormat(String f) {
+ this.format = f;
+ }
+
+ @Inject
+ PreviewSubmit(Provider<ReviewDb> dbProvider,
+ Provider<MergeOp> mergeOpProvider,
+ AllowedFormats allowedFormats) {
+ this.dbProvider = dbProvider;
+ this.mergeOpProvider = mergeOpProvider;
+ this.allowedFormats = allowedFormats;
+ }
+
+ @Override
+ public BinaryResult apply(RevisionResource rsrc) throws RestApiException {
+ if (Strings.isNullOrEmpty(format)) {
+ throw new BadRequestException("format is not specified");
+ }
+ ArchiveFormat f = allowedFormats.extensions.get("." + format);
+ if (f == null && format.equals("tgz")) {
+ // Always allow tgz, even when the allowedFormats doesn't contain it.
+ // Then we allow at least one format even if the list of allowed
+ // formats is empty.
+ f = ArchiveFormat.TGZ;
+ }
+ if (f == null) {
+ throw new BadRequestException("unknown archive format");
+ }
+
+ Change change = rsrc.getChange();
+ if (!change.getStatus().isOpen()) {
+ throw new PreconditionFailedException("change is " + Submit.status(change));
+ }
+ ChangeControl control = rsrc.getControl();
+ if (!control.getUser().isIdentifiedUser()) {
+ throw new MethodNotAllowedException("Anonymous users cannot submit");
+ }
+ try (BinaryResult b = getBundles(rsrc, f)) {
+ b.disableGzip()
+ .setContentType(f.getMimeType())
+ .setAttachmentName("submit-preview-"
+ + change.getChangeId() + "." + format);
+ return b;
+ } catch (OrmException | IOException e) {
+ throw new RestApiException("Error generating submit preview");
+ }
+ }
+
+ private BinaryResult getBundles(RevisionResource rsrc, final ArchiveFormat f)
+ throws OrmException, RestApiException {
+ ReviewDb db = dbProvider.get();
+ ChangeControl control = rsrc.getControl();
+ IdentifiedUser caller = control.getUser().asIdentifiedUser();
+ Change change = rsrc.getChange();
+
+ BinaryResult bin;
+ try (MergeOp op = mergeOpProvider.get()) {
+ op.merge(db, change, caller, false, new SubmitInput(), true);
+ final MergeOpRepoManager orm = op.getMergeOpRepoManager();
+ final Set<Project.NameKey> projects = op.getAllProjects();
+
+ bin = new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ ArchiveOutputStream aos = f.createArchiveOutputStream(out);
+
+ for (Project.NameKey p : projects) {
+ OpenRepo or = orm.getRepo(p);
+ BundleWriter bw = new BundleWriter(or.getRepo());
+ bw.setObjectCountCallback(null);
+ bw.setPackConfig(null);
+ Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates();
+ for (ReceiveCommand r : refs) {
+ bw.include(r.getRefName(), r.getNewId());
+ if (!r.getOldId().equals(ObjectId.zeroId())) {
+ bw.assume(or.getCodeReviewRevWalk().parseCommit(r.getOldId()));
+ }
+ }
+ // This naming scheme cannot produce directory/file conflicts
+ // as no projects contains ".git/":
+ aos.putArchiveEntry(f.prepareArchiveEntry(p.get() + ".git"));
+ bw.writeBundle(NullProgressMonitor.INSTANCE, aos);
+ aos.closeArchiveEntry();
+ }
+ aos.finish();
+ }
+ };
+ }
+ return bin;
+ }
+}
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/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index d17d69b..1dcbf85 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -34,7 +34,7 @@
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
@@ -252,15 +252,12 @@
private void sendReplacePatchSet(Context ctx)
throws EmailException, OrmException {
- Account.Id accountId = ctx.getAccountId();
- ChangeMessage msg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())), accountId,
- ctx.getWhen(), psId);
- msg.setMessage("Uploaded patch set " + psId.get() + ".");
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(),
+ "Uploaded patch set " + psId.get() + ".");
ReplacePatchSetSender cm =
replacePatchSetFactory.create(ctx.getProject(), change.getId());
- cm.setFrom(accountId);
+ cm.setFrom(ctx.getAccountId());
cm.setPatchSet(patchSet, patchSetInfo);
cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
cm.addReviewers(recipients.getReviewers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
new file mode 100644
index 0000000..4298937
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.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.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountJson;
+import com.google.gerrit.server.change.PostReviewers.Addition;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class PutAssignee implements
+ RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> {
+
+ private final SetAssigneeOp.Factory assigneeFactory;
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final Provider<ReviewDb> db;
+ private final PostReviewers postReviewers;
+
+ @Inject
+ PutAssignee(SetAssigneeOp.Factory assigneeFactory,
+ BatchUpdate.Factory batchUpdateFactory,
+ Provider<ReviewDb> db,
+ PostReviewers postReviewers) {
+ this.assigneeFactory = assigneeFactory;
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.db = db;
+ this.postReviewers = postReviewers;
+ }
+
+ @Override
+ public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input)
+ throws RestApiException, UpdateException, OrmException, IOException {
+ try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+ rsrc.getChange().getProject(), rsrc.getControl().getUser(),
+ TimeUtil.nowTs())) {
+ SetAssigneeOp op = assigneeFactory.create(input);
+ bu.addOp(rsrc.getId(), op);
+
+ PostReviewers.Addition reviewersAddition =
+ addAssigneeAsCC(rsrc, input.assignee);
+ bu.addOp(rsrc.getId(), reviewersAddition.op);
+
+ bu.execute();
+ reviewersAddition.gatherResults();
+ return Response.ok(AccountJson.toAccountInfo(op.getNewAssignee()));
+ }
+ }
+
+ private Addition addAssigneeAsCC(ChangeResource rsrc, String assignee)
+ throws OrmException, RestApiException, IOException {
+ AddReviewerInput reviewerInput = new AddReviewerInput();
+ reviewerInput.reviewer = assignee;
+ reviewerInput.state = ReviewerState.CC;
+ reviewerInput.confirmed = true;
+ reviewerInput.notify = NotifyHandling.NONE;
+ return postReviewers.prepareApplication(rsrc, reviewerInput);
+ }
+
+ @Override
+ public UiAction.Description getDescription(ChangeResource resource) {
+ return new UiAction.Description()
+ .setLabel("Edit Assignee")
+ .setVisible(resource.getControl().canEditAssignee());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 655e07d..91ad849 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -14,13 +14,11 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import com.google.common.base.Optional;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.DraftInput;
-import com.google.gerrit.extensions.client.Comment;
-import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -28,11 +26,11 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -52,7 +50,7 @@
private final Provider<ReviewDb> db;
private final DeleteDraftComment delete;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final BatchUpdate.Factory updateFactory;
private final Provider<CommentJson> commentJson;
@@ -61,14 +59,14 @@
@Inject
PutDraftComment(Provider<ReviewDb> db,
DeleteDraftComment delete,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
BatchUpdate.Factory updateFactory,
Provider<CommentJson> commentJson,
PatchListCache patchListCache) {
this.db = db;
this.delete = delete;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.updateFactory = updateFactory;
this.commentJson = commentJson;
@@ -91,21 +89,22 @@
try (BatchUpdate bu = updateFactory.create(
db.get(), rsrc.getChange().getProject(), rsrc.getControl().getUser(),
TimeUtil.nowTs())) {
- Op op = new Op(rsrc.getComment().getKey(), in);
+ Op op = new Op(rsrc.getComment().key, in);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
- return Response.ok(
- commentJson.get().setFillAccounts(false).format(op.comment));
+ return Response.ok(commentJson.get()
+ .setFillAccounts(false)
+ .newCommentFormatter().format(op.comment));
}
}
private class Op extends BatchUpdate.Op {
- private final PatchLineComment.Key key;
+ private final Comment.Key key;
private final DraftInput in;
- private PatchLineComment comment;
+ private Comment comment;
- private Op(PatchLineComment.Key key, DraftInput in) {
+ private Op(Comment.Key key, DraftInput in) {
this.key = key;
this.in = in;
}
@@ -113,17 +112,21 @@
@Override
public boolean updateChange(ChangeContext ctx)
throws ResourceNotFoundException, OrmException {
- Optional<PatchLineComment> maybeComment =
- plcUtil.get(ctx.getDb(), ctx.getNotes(), key);
+ Optional<Comment> maybeComment =
+ commentsUtil.get(ctx.getDb(), ctx.getNotes(), key);
if (!maybeComment.isPresent()) {
// Disappeared out from under us. Can't easily fall back to insert,
// because the input might be missing required fields. Just give up.
throw new ResourceNotFoundException("comment not found: " + key);
}
- PatchLineComment origComment = maybeComment.get();
- comment = new PatchLineComment(origComment);
+ Comment origComment = maybeComment.get();
+ comment = new Comment(origComment);
+ // Copy constructor preserved old real author; replace with current real
+ // user.
+ ctx.getUser().updateRealAccountId(comment::setRealAuthor);
- PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
+ PatchSet.Id psId =
+ new PatchSet.Id(ctx.getChange().getId(), origComment.key.patchSetId);
ChangeUpdate update = ctx.getUpdate(psId);
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
@@ -131,61 +134,36 @@
throw new ResourceNotFoundException("patch set not found: " + psId);
}
if (in.path != null
- && !in.path.equals(comment.getKey().getParentKey().getFileName())) {
+ && !in.path.equals(origComment.key.filename)) {
// Updating the path alters the primary key, which isn't possible.
// Delete then recreate the comment instead of an update.
- plcUtil.deleteComments(
+ commentsUtil.deleteComments(
ctx.getDb(), update, Collections.singleton(origComment));
- comment = new PatchLineComment(
- new PatchLineComment.Key(
- new Patch.Key(psId, in.path),
- comment.getKey().get()),
- comment.getLine(),
- ctx.getAccountId(),
- comment.getParentUuid(), ctx.getWhen());
- comment.setTag(origComment.getTag());
- setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
- plcUtil.putComments(ctx.getDb(), update,
- Collections.singleton(update(comment, in, ctx.getWhen())));
- } else {
- if (comment.getRevId() == null) {
- setCommentRevId(
- comment, patchListCache, ctx.getChange(), ps);
- }
- plcUtil.putComments(ctx.getDb(), update,
- Collections.singleton(update(comment, in, ctx.getWhen())));
+ comment.key.filename = in.path;
}
+ setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
+ commentsUtil.putComments(ctx.getDb(), update, Status.DRAFT,
+ Collections.singleton(update(comment, in, ctx.getWhen())));
ctx.bumpLastUpdatedOn(false);
return true;
}
}
- private static PatchLineComment update(PatchLineComment e, DraftInput in,
- Timestamp when) {
+ private static Comment update(Comment e, DraftInput in, Timestamp when) {
if (in.side != null) {
- e.setSide(side(in));
+ e.side = in.side();
}
if (in.inReplyTo != null) {
- e.setParentUuid(Url.decode(in.inReplyTo));
+ e.parentUuid = Url.decode(in.inReplyTo);
}
- e.setMessage(in.message.trim());
- if (in.range != null || in.line != null) {
- e.setRange(in.range);
- e.setLine(in.range != null ? in.range.endLine : in.line);
- }
- e.setWrittenOn(when);
+ e.setLineNbrAndRange(in.line, in.range);
+ e.message = in.message.trim();
+ e.writtenOn = when;
if (in.tag != null) {
// TODO(dborowitz): Can we support changing tags via PUT?
- e.setTag(in.tag);
+ e.tag = in.tag;
}
return e;
}
-
- static short side(Comment c) {
- if (c.side == Side.PARENT) {
- return (short) (c.parent == null ? 0 : -c.parent.shortValue());
- }
- return 1;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 31ae892..c0b3517 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -26,7 +26,6 @@
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.PutTopic.Input;
import com.google.gerrit.server.extensions.events.TopicEdited;
import com.google.gerrit.server.git.BatchUpdate;
@@ -115,13 +114,7 @@
change.setTopic(Strings.emptyToNull(newTopicName));
update.setTopic(change.getTopic());
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- change.currentPatchSetId());
- cmsg.setMessage(summary);
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, summary);
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
return true;
}
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/Rebuild.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java
index 5fe0e0b..c27fa53 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java
@@ -20,8 +20,8 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.Rebuild.Input;
-import com.google.gerrit.server.notedb.ChangeRebuilder;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 9c4c6d9..f317ef2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -29,7 +29,6 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeRestored;
import com.google.gerrit.server.git.BatchUpdate;
@@ -131,16 +130,7 @@
msg.append("\n\n");
msg.append(input.message.trim());
}
-
- ChangeMessage message = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(),
- ctx.getWhen(),
- change.currentPatchSetId());
- message.setMessage(msg.toString());
- return message;
+ return ChangeMessagesUtil.newMessage(ctx, msg.toString());
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 3ca496a..c42b257 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -34,7 +34,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.PatchSetUtil;
@@ -274,14 +273,8 @@
public boolean updateChange(ChangeContext ctx) throws Exception {
Change change = ctx.getChange();
PatchSet.Id patchSetId = change.currentPatchSetId();
- ChangeMessage changeMessage = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db.get())),
- ctx.getAccountId(), ctx.getWhen(), patchSetId);
- StringBuilder msgBuf = new StringBuilder();
- msgBuf.append("Created a revert of this change as ")
- .append("I").append(computedChangeId.name());
- changeMessage.setMessage(msgBuf.toString());
+ ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(ctx,
+ "Created a revert of this change as I" + computedChangeId.name());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(patchSetId),
changeMessage);
return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotCommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotCommentResource.java
new file mode 100644
index 0000000..856c777
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotCommentResource.java
@@ -0,0 +1,51 @@
+// 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.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.inject.TypeLiteral;
+
+public class RobotCommentResource implements RestResource {
+ public static final TypeLiteral<RestView<RobotCommentResource>> ROBOT_COMMENT_KIND =
+ new TypeLiteral<RestView<RobotCommentResource>>() {};
+
+ private final RevisionResource rev;
+ private final RobotComment comment;
+
+ public RobotCommentResource(RevisionResource rev, RobotComment c) {
+ this.rev = rev;
+ this.comment = c;
+ }
+
+ public PatchSet getPatchSet() {
+ return rev.getPatchSet();
+ }
+
+ RobotComment getComment() {
+ return comment;
+ }
+
+ String getId() {
+ return comment.key.uuid;
+ }
+
+ Account.Id getAuthorId() {
+ return comment.author.getId();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotComments.java
new file mode 100644
index 0000000..886af1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RobotComments.java
@@ -0,0 +1,69 @@
+// 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.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class RobotComments
+ implements ChildCollection<RevisionResource, RobotCommentResource> {
+ private final DynamicMap<RestView<RobotCommentResource>> views;
+ private final ListRobotComments list;
+ private final CommentsUtil commentsUtil;
+
+ @Inject
+ RobotComments(DynamicMap<RestView<RobotCommentResource>> views,
+ ListRobotComments list,
+ CommentsUtil commentsUtil) {
+ this.views = views;
+ this.list = list;
+ this.commentsUtil = commentsUtil;
+ }
+
+ @Override
+ public DynamicMap<RestView<RobotCommentResource>> views() {
+ return views;
+ }
+
+ @Override
+ public ListRobotComments list() {
+ return list;
+ }
+
+ @Override
+ public RobotCommentResource parse(RevisionResource rev, IdString id)
+ throws ResourceNotFoundException, OrmException {
+ String uuid = id.get();
+ ChangeNotes notes = rev.getNotes();
+
+ for (RobotComment c : commentsUtil.robotCommentsByPatchSet(
+ notes, rev.getPatchSet().getId())) {
+ if (uuid.equals(c.key.uuid)) {
+ return new RobotCommentResource(rev, c);
+ }
+ }
+ throw new ResourceNotFoundException(id);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
new file mode 100644
index 0000000..dba5358
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -0,0 +1,153 @@
+// 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.base.Optional;
+import com.google.gerrit.extensions.api.changes.AssigneeInput;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.extensions.events.AssigneeChanged;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.validators.AssigneeValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class SetAssigneeOp extends BatchUpdate.Op {
+ public interface Factory {
+ SetAssigneeOp create(AssigneeInput input);
+ }
+
+ private final AccountsCollection accounts;
+ private final ChangeMessagesUtil cmUtil;
+ private final AccountInfoCacheFactory.Factory accountInfosFactory;
+ private final DynamicSet<AssigneeValidationListener> validationListeners;
+ private final AssigneeInput input;
+ private final String anonymousCowardName;
+ private final AssigneeChanged assigneeChanged;
+
+ private Change change;
+ private Account newAssignee;
+ private Account oldAssignee;
+
+ @AssistedInject
+ SetAssigneeOp(AccountsCollection accounts,
+ ChangeMessagesUtil cmUtil,
+ AccountInfoCacheFactory.Factory accountInfosFactory,
+ DynamicSet<AssigneeValidationListener> validationListeners,
+ AssigneeChanged assigneeChanged,
+ @AnonymousCowardName String anonymousCowardName,
+ @Assisted AssigneeInput input) {
+ this.accounts = accounts;
+ this.cmUtil = cmUtil;
+ this.accountInfosFactory = accountInfosFactory;
+ this.validationListeners = validationListeners;
+ this.assigneeChanged = assigneeChanged;
+ this.anonymousCowardName = anonymousCowardName;
+ this.input = input;
+ }
+
+ @Override
+ public boolean updateChange(BatchUpdate.ChangeContext ctx)
+ throws OrmException, RestApiException {
+ if (!ctx.getControl().canEditAssignee()) {
+ throw new AuthException("Changing Assignee not permitted");
+ }
+ change = ctx.getChange();
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+ Optional<Account.Id> oldAssigneeId =
+ Optional.fromNullable(change.getAssignee());
+ if (input.assignee == null) {
+ if (oldAssigneeId.isPresent()) {
+ throw new BadRequestException("Cannot set Assignee to empty");
+ }
+ return false;
+ }
+ oldAssignee = null;
+ if (oldAssigneeId.isPresent()) {
+ oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get());
+ }
+ IdentifiedUser newAssigneeUser = accounts.parse(input.assignee);
+ if (oldAssigneeId.isPresent() &&
+ oldAssigneeId.get().equals(newAssigneeUser.getAccountId())) {
+ newAssignee = oldAssignee;
+ return false;
+ }
+ if (!newAssigneeUser.getAccount().isActive()) {
+ throw new UnprocessableEntityException(String.format(
+ "Account of %s is not active", input.assignee));
+ }
+ if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) {
+ throw new AuthException(String.format(
+ "Change %s is not visible to %s.",
+ change.getChangeId(),
+ input.assignee));
+ }
+ try {
+ for (AssigneeValidationListener validator : validationListeners) {
+ validator.validateAssignee(change, newAssigneeUser.getAccount());
+ }
+ } catch (ValidationException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ // notedb
+ update.setAssignee(newAssigneeUser.getAccountId());
+ // reviewdb
+ change.setAssignee(newAssigneeUser.getAccountId());
+ this.newAssignee = newAssigneeUser.getAccount();
+ addMessage(ctx, update, oldAssignee);
+ return true;
+ }
+
+ private void addMessage(BatchUpdate.ChangeContext ctx, ChangeUpdate update,
+ Account previousAssignee) throws OrmException {
+ StringBuilder msg = new StringBuilder();
+ msg.append("Assignee ");
+ if (previousAssignee == null) {
+ msg.append("added: ");
+ msg.append(newAssignee.getName(anonymousCowardName));
+ } else {
+ msg.append("changed from: ");
+ msg.append(previousAssignee.getName(anonymousCowardName));
+ msg.append(" to: ");
+ msg.append(newAssignee.getName(anonymousCowardName));
+ }
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, msg.toString());
+ cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws OrmException {
+ assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen());
+ }
+
+ public Account getNewAssignee() {
+ return newAssignee;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 50f6e74..cf6d307 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -28,7 +28,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.extensions.events.HashtagsEdited;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -129,18 +128,12 @@
return true;
}
- private void addMessage(Context ctx, ChangeUpdate update)
+ private void addMessage(ChangeContext ctx, ChangeUpdate update)
throws OrmException {
StringBuilder msg = new StringBuilder();
appendHashtagMessage(msg, "added", toAdd);
appendHashtagMessage(msg, "removed", toRemove);
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- change.currentPatchSetId());
- cmsg.setMessage(msg.toString());
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
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..e80e758 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
@@ -14,15 +14,13 @@
package com.google.gerrit.server.change;
+import static java.util.stream.Collectors.joining;
+
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
-import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.ParameterizedString;
@@ -134,7 +132,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 +152,7 @@
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory changeNotesFactory,
Provider<MergeOp> mergeOpProvider,
- MergeSuperSet mergeSuperSet,
+ Provider<MergeSuperSet> mergeSuperSet,
AccountsCollection accounts,
ChangesCollection changes,
@GerritServerConfig Config cfg,
@@ -222,7 +220,7 @@
try (MergeOp op = mergeOpProvider.get()) {
ReviewDb db = dbProvider.get();
- op.merge(db, change, caller, true, input);
+ op.merge(db, change, caller, true, input, false);
try {
change = changeNotesFactory
.createChecked(db, change.getProject(), change.getId()).getChange();
@@ -282,14 +280,10 @@
return CHANGE_UNMERGEABLE;
}
}
- return CHANGES_NOT_MERGEABLE + Joiner.on(", ").join(
- Iterables.transform(unmergeable,
- new Function<ChangeData, String>() {
- @Override
- public String apply(ChangeData cd) {
- return String.valueOf(cd.getId().get());
- }
- }));
+ return CHANGES_NOT_MERGEABLE +
+ unmergeable.stream()
+ .map(c -> c.getId().toString())
+ .collect(joining(", "));
}
} catch (ResourceConflictException e) {
return BLOCKED_SUBMIT_TOOLTIP;
@@ -300,22 +294,6 @@
return null;
}
- /**
- * Check if there are any problems with the given change. It doesn't take
- * any problems of related changes into account.
- * <p>
- * @param cd the change to check for submittability
- * @return if the change has any problems for submission
- */
- public static boolean submittable(ChangeData cd) {
- try {
- MergeOp.checkSubmitRule(cd);
- return true;
- } catch (ResourceConflictException | OrmException e) {
- return false;
- }
- }
-
@Override
public UiAction.Description getDescription(RevisionResource resource) {
PatchSet.Id current = resource.getChange().currentPatchSetId();
@@ -345,7 +323,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 " +
@@ -421,14 +399,10 @@
*/
public ChangeMessage getConflictMessage(RevisionResource rsrc)
throws OrmException {
- return FluentIterable.from(cmUtil.byPatchSet(dbProvider.get(), rsrc.getNotes(),
- rsrc.getPatchSet().getId()))
- .filter(new Predicate<ChangeMessage>() {
- @Override
- public boolean apply(ChangeMessage input) {
- return input.getAuthor() == null;
- }
- })
+ return FluentIterable.from(
+ cmUtil.byPatchSet(
+ dbProvider.get(), rsrc.getNotes(), rsrc.getPatchSet().getId()))
+ .filter(cm -> cm.getAuthor() == null)
.last()
.orNull();
}
@@ -511,16 +485,12 @@
if (!caller.canSubmitAs()) {
throw new AuthException("submit on behalf of not permitted");
}
- IdentifiedUser targetUser = accounts.parseId(in.onBehalfOf);
- if (targetUser == null) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", in.onBehalfOf));
- }
- ChangeControl target = caller.forUser(targetUser);
+ ChangeControl target = caller.forUser(
+ accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
if (!target.getRefControl().isVisible()) {
throw new UnprocessableEntityException(String.format(
"on_behalf_of account %s cannot see destination ref",
- targetUser.getAccountId()));
+ target.getUser().getAccountId()));
}
return new RevisionResource(changes.parse(target), rsrc.getPatchSet());
}
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..23e7d8b 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
@@ -14,6 +14,8 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
import com.google.gerrit.extensions.client.ChangeStatus;
@@ -32,7 +34,6 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.Singleton;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -44,29 +45,48 @@
import java.util.EnumSet;
import java.util.List;
-@Singleton
public class SubmittedTogether implements RestReadView<ChangeResource> {
private static final Logger log = LoggerFactory.getLogger(
SubmittedTogether.class);
private final EnumSet<SubmittedTogetherOption> options =
EnumSet.noneOf(SubmittedTogetherOption.class);
+
+ private final EnumSet<ListChangesOption> jsonOpt = EnumSet.of(
+ ListChangesOption.CURRENT_REVISION,
+ ListChangesOption.CURRENT_COMMIT,
+ ListChangesOption.SUBMITTABLE);
+
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")
- void addOption(SubmittedTogetherOption o) {
- options.add(o);
+ void addOption(String option) {
+ for (ListChangesOption o : ListChangesOption.values()) {
+ if (o.name().equalsIgnoreCase(option)) {
+ jsonOpt.add(o);
+ return;
+ }
+ }
+
+ for (SubmittedTogetherOption o : SubmittedTogetherOption.values()) {
+ if (o.name().equalsIgnoreCase(option)) {
+ options.add(o);
+ return;
+ }
+ }
+
+ throw new IllegalArgumentException("option not recognized: " + option);
}
@Inject
SubmittedTogether(ChangeJson.Factory json,
Provider<ReviewDb> dbProvider,
Provider<InternalChangeQuery> queryProvider,
- MergeSuperSet mergeSuperSet,
+ Provider<MergeSuperSet> mergeSuperSet,
Provider<WalkSorter> sorter) {
this.json = json;
this.dbProvider = dbProvider;
@@ -75,19 +95,29 @@
this.sorter = sorter;
}
+ public SubmittedTogether addListChangesOption(EnumSet<ListChangesOption> o) {
+ jsonOpt.addAll(o);
+ return this;
+ }
+
+ public SubmittedTogether addSubmittedTogetherOption(
+ EnumSet<SubmittedTogetherOption> o) {
+ options.addAll(o);
+ return this;
+ }
+
@Override
public Object apply(ChangeResource resource)
throws AuthException, BadRequestException,
ResourceConflictException, IOException, OrmException {
- SubmittedTogetherInfo info = apply(resource, options);
+ SubmittedTogetherInfo info = applyInfo(resource);
if (options.isEmpty()) {
return info.changes;
}
return info;
}
- public SubmittedTogetherInfo apply(ChangeResource resource,
- EnumSet<SubmittedTogetherOption> options)
+ public SubmittedTogetherInfo applyInfo(ChangeResource resource)
throws AuthException, IOException, OrmException {
Change c = resource.getChange();
try {
@@ -96,7 +126,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();
@@ -109,7 +139,7 @@
}
if (hidden != 0
- && !options.contains(SubmittedTogetherOption.NON_VISIBLE_CHANGES)) {
+ && !options.contains(NON_VISIBLE_CHANGES)) {
throw new AuthException(
"change would be submitted with a change that you cannot see");
}
@@ -123,10 +153,7 @@
}
SubmittedTogetherInfo info = new SubmittedTogetherInfo();
- info.changes = json.create(EnumSet.of(
- ListChangesOption.CURRENT_REVISION,
- ListChangesOption.CURRENT_COMMIT))
- .formatChangeDatas(cds);
+ info.changes = json.create(jsonOpt).formatChangeDatas(cds);
info.nonVisibleChanges = hidden;
return info;
} catch (OrmException | IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 02d3afe..a1d53e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -30,12 +30,18 @@
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.util.List;
public class SuggestChangeReviewers extends SuggestReviewers
implements RestReadView<ChangeResource> {
+
+ @Option(name = "--exclude-groups", aliases = {"-e"},
+ usage = "exclude groups from query")
+ boolean excludeGroups;
+
@Inject
SuggestChangeReviewers(AccountVisibility av,
GenericFactory identifiedUserFactory,
@@ -49,7 +55,7 @@
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
throws BadRequestException, OrmException, IOException {
return reviewersUtil.suggestReviewers(this,
- rsrc.getControl().getProjectControl(), getVisibility(rsrc));
+ rsrc.getControl().getProjectControl(), getVisibility(rsrc), excludeGroups);
}
private VisibilityControl getVisibility(final ChangeResource rsrc) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
index d31805d..e0959f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WalkSorter.java
@@ -18,7 +18,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -72,21 +71,17 @@
LoggerFactory.getLogger(WalkSorter.class);
private static final Ordering<List<PatchSetData>> PROJECT_LIST_SORTER =
- Ordering.natural().nullsFirst()
- .onResultOf(
- new Function<List<PatchSetData>, Project.NameKey>() {
- @Override
- public Project.NameKey apply(List<PatchSetData> in) {
- if (in == null || in.isEmpty()) {
- return null;
- }
- try {
- return in.get(0).data().change().getProject();
- } catch (OrmException e) {
- throw new IllegalStateException(e);
- }
- }
- });
+ Ordering.natural().nullsFirst().onResultOf(
+ (List<PatchSetData> in) -> {
+ if (in == null || in.isEmpty()) {
+ return null;
+ }
+ try {
+ return in.get(0).data().change().getProject();
+ } catch (OrmException e) {
+ throw new IllegalStateException(e);
+ }
+ });
private final GitRepositoryManager repoManager;
private final Set<PatchSet.Id> includePatchSets;
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..11a34f7 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,9 @@
package com.google.gerrit.server.config;
+import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
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;
@@ -62,6 +63,7 @@
private final boolean cookieSecure;
private final SignedToken emailReg;
private final boolean allowRegisterNewEmail;
+ private GitBasicAuthPolicy gitBasicAuthPolicy;
@Inject
AuthConfig(@GerritServerConfig final Config cfg)
@@ -90,6 +92,7 @@
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
+ gitBasicAuthPolicy = getBasicAuthPolicy(cfg);
useContributorAgreements =
cfg.getBoolean("auth", "contributoragreements", false);
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
@@ -124,6 +127,12 @@
return cfg.getEnum("auth", null, "type", AuthType.OPENID);
}
+ private GitBasicAuthPolicy getBasicAuthPolicy(Config cfg) {
+ GitBasicAuthPolicy defaultAuthPolicy =
+ isLdapAuthType() ? GitBasicAuthPolicy.LDAP : GitBasicAuthPolicy.HTTP;
+ return cfg.getEnum("auth", null, "gitBasicAuthPolicy", defaultAuthPolicy);
+ }
+
/** Type of user authentication used by this Gerrit server. */
public AuthType getAuthType() {
return authType;
@@ -218,6 +227,10 @@
return gitBasicAuth;
}
+ public GitBasicAuthPolicy getGitBasicAuthPolicy() {
+ return gitBasicAuthPolicy;
+ }
+
/** Whether contributor agreements are used. */
public boolean isUseContributorAgreements() {
return useContributorAgreements;
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..fecb156 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.
@@ -30,6 +30,7 @@
import com.google.gerrit.extensions.config.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.events.AgreementSignupListener;
+import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
@@ -116,7 +117,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 +134,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;
@@ -161,6 +163,7 @@
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gerrit.server.validators.AssigneeValidationListener;
import com.google.gerrit.server.validators.GroupCreationValidationListener;
import com.google.gerrit.server.validators.HashtagValidationListener;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
@@ -170,6 +173,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 +279,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)
@@ -300,6 +307,7 @@
DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+ DynamicSet.setOf(binder(), AssigneeChangedListener.class);
DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
DynamicSet.setOf(binder(), CommentAddedListener.class);
DynamicSet.setOf(binder(), DraftPublishedListener.class);
@@ -357,6 +365,7 @@
DynamicSet.setOf(binder(), AccountExternalIdCreator.class);
DynamicSet.setOf(binder(), WebUiPlugin.class);
DynamicItem.itemOf(binder(), AccountPatchReviewStore.class);
+ DynamicSet.setOf(binder(), AssigneeValidationListener.class);
factory(UploadValidators.Factory.class);
DynamicSet.setOf(binder(), UploadValidationListener.class);
@@ -366,7 +375,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/GerritOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java
new file mode 100644
index 0000000..c181f79
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritOptions.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.extensions.client.UiType;
+
+import org.eclipse.jgit.lib.Config;
+
+public class GerritOptions {
+ private final boolean headless;
+ private final boolean slave;
+ private final boolean enablePolyGerrit;
+ private final boolean enableGwtUi;
+ private final boolean forcePolyGerritDev;
+ private final UiType defaultUi;
+
+ public GerritOptions(Config cfg, boolean headless, boolean slave,
+ boolean forcePolyGerritDev) {
+ this.slave = slave;
+ this.enablePolyGerrit = forcePolyGerritDev
+ || cfg.getBoolean("gerrit", null, "enablePolyGerrit", false);
+ this.enableGwtUi = cfg.getBoolean("gerrit", null, "enableGwtUi", true);
+ this.forcePolyGerritDev = forcePolyGerritDev;
+ this.headless = headless || (!enableGwtUi && !enablePolyGerrit);
+
+ UiType defaultUi = enablePolyGerrit && !enableGwtUi
+ ? UiType.POLYGERRIT
+ : UiType.GWT;
+ String uiStr = firstNonNull(
+ cfg.getString("gerrit", null, "ui"),
+ defaultUi.name());
+ this.defaultUi = firstNonNull(UiType.parse(uiStr), UiType.NONE);
+
+ switch (defaultUi) {
+ case GWT:
+ checkArgument(enableGwtUi,
+ "gerrit.ui = %s but GWT UI is disabled", defaultUi);
+ break;
+ case POLYGERRIT:
+ checkArgument(enablePolyGerrit,
+ "gerrit.ui = %s but PolyGerrit is disabled", defaultUi);
+ break;
+ case NONE:
+ default:
+ throw new IllegalArgumentException("invalid gerrit.ui: " + uiStr);
+ }
+ }
+
+ public boolean headless() {
+ return headless;
+ }
+
+ public boolean enableGwtUi() {
+ return !headless && enableGwtUi;
+ }
+
+ public boolean enableMasterFeatures() {
+ return !slave;
+ }
+
+ public boolean enablePolyGerrit() {
+ return !headless && enablePolyGerrit;
+ }
+
+ public boolean forcePolyGerritDev() {
+ return !headless && forcePolyGerritDev;
+ }
+
+ public UiType defaultUi() {
+ return defaultUi;
+ }
+}
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..447d1be 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
@@ -14,12 +14,25 @@
package com.google.gerrit.server.config;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.base.CharMatcher;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
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.client.UiType;
+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,24 +41,26 @@
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;
+import com.google.gerrit.server.change.AllowedFormats;
import com.google.gerrit.server.change.ArchiveFormat;
-import com.google.gerrit.server.change.GetArchive;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
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.EnumSet;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -61,7 +76,7 @@
private final DynamicMap<DownloadCommand> downloadCommands;
private final DynamicMap<CloneCommand> cloneCommands;
private final DynamicSet<WebUiPlugin> plugins;
- private final GetArchive.AllowedFormats archiveFormats;
+ private final AllowedFormats archiveFormats;
private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
private final String anonymousCowardName;
@@ -69,6 +84,10 @@
private final boolean enableSignedPush;
private final QueryDocumentationExecutor docSearcher;
private final NotesMigration migration;
+ private final ProjectCache projectCache;
+ private final AgreementJson agreementJson;
+ private final GerritOptions gerritOptions;
+ private final ChangeIndexCollection indexes;
@Inject
public GetServerInfo(
@@ -79,14 +98,18 @@
DynamicMap<DownloadCommand> downloadCommands,
DynamicMap<CloneCommand> cloneCommands,
DynamicSet<WebUiPlugin> webUiPlugins,
- GetArchive.AllowedFormats archiveFormats,
+ AllowedFormats archiveFormats,
AllProjectsName allProjectsName,
AllUsersName allUsersName,
@AnonymousCowardName String anonymousCowardName,
DynamicItem<AvatarProvider> avatar,
@EnableSignedPush boolean enableSignedPush,
QueryDocumentationExecutor docSearcher,
- NotesMigration migration) {
+ NotesMigration migration,
+ ProjectCache projectCache,
+ AgreementJson agreementJson,
+ GerritOptions gerritOptions,
+ ChangeIndexCollection indexes) {
this.config = config;
this.authConfig = authConfig;
this.realm = realm;
@@ -102,6 +125,10 @@
this.enableSignedPush = enableSignedPush;
this.docSearcher = docSearcher;
this.migration = migration;
+ this.projectCache = projectCache;
+ this.agreementJson = agreementJson;
+ this.gerritOptions = gerritOptions;
+ this.indexes = indexes;
}
@Override
@@ -133,6 +160,19 @@
info.editableAccountFields = new ArrayList<>(realm.getEditableFields());
info.switchAccountUrl = cfg.getSwitchAccountUrl();
info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
+ info.gitBasicAuthPolicy = cfg.getGitBasicAuthPolicy();
+
+ 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:
@@ -169,6 +209,10 @@
ChangeConfigInfo info = new ChangeConfigInfo();
info.allowBlame = toBoolean(cfg.getBoolean("change", "allowBlame", true));
info.allowDrafts = toBoolean(cfg.getBoolean("change", "allowDrafts", true));
+ info.showAssignee = toBoolean(
+ cfg.getBoolean("change", "showAssignee", true)
+ && indexes.getSearchIndex().getSchema()
+ .hasField(ChangeField.ASSIGNEE));
info.largeChange = cfg.getInt("change", "largeChange", 500);
info.replyTooltip =
Optional.fromNullable(cfg.getString("change", null, "replyTooltip"))
@@ -186,7 +230,7 @@
DynamicMap<DownloadScheme> downloadSchemes,
DynamicMap<DownloadCommand> downloadCommands,
DynamicMap<CloneCommand> cloneCommands,
- GetArchive.AllowedFormats archiveFormats) {
+ AllowedFormats archiveFormats) {
DownloadInfo info = new DownloadInfo();
info.schemes = new HashMap<>();
for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
@@ -196,14 +240,8 @@
getDownloadSchemeInfo(scheme, downloadCommands, cloneCommands));
}
}
- info.archives = Lists.newArrayList(Iterables.transform(
- archiveFormats.getAllowed(),
- new Function<ArchiveFormat, String>() {
- @Override
- public String apply(ArchiveFormat in) {
- return in.getShortName();
- }
- }));
+ info.archives = archiveFormats.getAllowed().stream()
+ .map(ArchiveFormat::getShortName).collect(toList());
return info;
}
@@ -251,6 +289,13 @@
info.docSearch = docSearcher.isAvailable();
info.editGpgKeys = toBoolean(enableSignedPush
&& cfg.getBoolean("gerrit", null, "editGpgKeys", true));
+ info.webUis = EnumSet.noneOf(UiType.class);
+ if (gerritOptions.enableGwtUi()) {
+ info.webUis.add(UiType.GWT);
+ }
+ if (gerritOptions.enablePolyGerrit()) {
+ info.webUis.add(UiType.POLYGERRIT);
+ }
return info;
}
@@ -322,85 +367,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/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index cc7857c..7d11ff4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.config;
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
+import static java.util.stream.Collectors.toList;
+
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.extensions.api.projects.ConfigValue;
import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
@@ -137,14 +137,9 @@
T defaultValue, Class<T> permittedValues, boolean inheritable,
String description) {
this(displayName, defaultValue.name(), ProjectConfigEntryType.LIST,
- Lists.transform(
- Arrays.asList(permittedValues.getEnumConstants()),
- new Function<Enum<?>, String>() {
- @Override
- public String apply(Enum<?> e) {
- return e.name();
- }
- }), inheritable, description);
+ Arrays.stream(permittedValues.getEnumConstants())
+ .map(Enum::name).collect(toList()),
+ inheritable, description);
}
public ProjectConfigEntry(String displayName, String defaultValue,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 8c18514..8f6035a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -26,6 +26,7 @@
public String number;
public String subject;
public AccountAttribute owner;
+ public AccountAttribute assignee;
public String url;
public String commitMessage;
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..8fca453 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,13 +35,12 @@
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;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -69,7 +69,6 @@
private final PatchSetInserter.Factory patchSetInserterFactory;
private final ChangeControl.GenericFactory changeControlFactory;
private final ChangeIndexer indexer;
- private final ProjectCache projectCache;
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> user;
private final ChangeKindCache changeKindCache;
@@ -81,7 +80,6 @@
PatchSetInserter.Factory patchSetInserterFactory,
ChangeControl.GenericFactory changeControlFactory,
ChangeIndexer indexer,
- ProjectCache projectCache,
Provider<ReviewDb> db,
Provider<CurrentUser> user,
ChangeKindCache changeKindCache,
@@ -91,7 +89,6 @@
this.patchSetInserterFactory = patchSetInserterFactory;
this.changeControlFactory = changeControlFactory;
this.indexer = indexer;
- this.projectCache = projectCache;
this.db = db;
this.user = user;
this.changeKindCache = changeKindCache;
@@ -168,24 +165,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(": ");
+
+ // Previously checked that the base patch set is the current patch set.
+ ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+ ChangeKind kind = changeKindCache.getChangeKind(
+ change.getProject(), repo, prior, squashed);
+ 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 +272,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/AssigneeChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
new file mode 100644
index 0000000..60a0935
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/AssigneeChangedEvent.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.common.base.Supplier;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.data.AccountAttribute;
+
+public class AssigneeChangedEvent extends ChangeEvent {
+ static final String TYPE = "assignee-changed";
+ public Supplier<AccountAttribute> changer;
+ public Supplier<AccountAttribute> oldAssignee;
+
+ public AssigneeChangedEvent(Change change) {
+ super(TYPE, change);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 56daccc..fbeb835 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -15,11 +15,10 @@
package com.google.gerrit.server.events;
import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Comparator.comparing;
-import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
-import com.google.common.collect.Ordering;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
@@ -28,8 +27,8 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.UserIdentity;
@@ -159,6 +158,7 @@
}
a.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner());
+ a.assignee = asAccountAttribute(change.getAssignee());
a.status = change.getStatus();
return a;
}
@@ -298,22 +298,21 @@
}
}
// Sort by original parent order.
- Collections.sort(ca.dependsOn, Ordering.natural().onResultOf(
- new Function<DependencyAttribute, Integer>() {
- @Override
- public Integer apply(DependencyAttribute d) {
- for (int i = 0; i < parentNames.size(); i++) {
- if (parentNames.get(i).equals(d.revision)) {
- return i;
+ Collections.sort(
+ ca.dependsOn,
+ comparing(
+ (DependencyAttribute d) -> {
+ for (int i = 0; i < parentNames.size(); i++) {
+ if (parentNames.get(i).equals(d.revision)) {
+ return i;
+ }
}
- }
- return parentNames.size() + 1;
- }
- }));
+ return parentNames.size() + 1;
+ }));
}
- private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change,
- PatchSet currentPs) throws OrmException, IOException {
+ private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs)
+ throws OrmException, IOException {
if (currentPs.getGroups().isEmpty()) {
return;
}
@@ -400,10 +399,10 @@
}
public void addPatchSetComments(PatchSetAttribute patchSetAttribute,
- Collection<PatchLineComment> patchLineComments) {
- for (PatchLineComment comment : patchLineComments) {
- if (comment.getKey().getParentKey().getParentKey().get()
- == Integer.parseInt(patchSetAttribute.number)) {
+ Collection<Comment> comments) {
+ for (Comment comment : comments) {
+ if (comment.key.patchSetId ==
+ Integer.parseInt(patchSetAttribute.number)) {
if (patchSetAttribute.comments == null) {
patchSetAttribute.comments = new ArrayList<>();
}
@@ -500,7 +499,7 @@
List<Patch> list =
patchListCache.get(change, patchSet).toPatchList(pId);
for (Patch pe : list) {
- if (!Patch.COMMIT_MSG.equals(pe.getFileName())) {
+ if (!Patch.isMagic(pe.getFileName())) {
p.sizeDeletions -= pe.getDeletions();
p.sizeInsertions += pe.getInsertions();
}
@@ -639,12 +638,12 @@
return a;
}
- public PatchSetCommentAttribute asPatchSetLineAttribute(PatchLineComment c) {
+ public PatchSetCommentAttribute asPatchSetLineAttribute(Comment c) {
PatchSetCommentAttribute a = new PatchSetCommentAttribute();
- a.reviewer = asAccountAttribute(c.getAuthor());
- a.file = c.getKey().getParentKey().get();
- a.line = c.getLine();
- a.message = c.getMessage();
+ a.reviewer = asAccountAttribute(c.author.getId());
+ a.file = c.key.filename;
+ a.line = c.lineNbr;
+ a.message = c.message;
return a;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
index 447e8b2..cd6e2f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -22,6 +22,7 @@
private static final Map<String, Class<?>> typesByString = new HashMap<>();
static {
+ register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class);
register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class);
register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class);
register(ChangeRestoredEvent.TYPE, ChangeRestoredEvent.class);
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 5294391..c867d26 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
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
@@ -73,6 +74,7 @@
@Singleton
public class StreamEventsApiListener implements
+ AssigneeChangedListener,
ChangeAbandonedListener,
ChangeMergedListener,
ChangeRestoredListener,
@@ -91,6 +93,8 @@
public static class Module extends AbstractModule {
@Override
protected void configure() {
+ DynamicSet.bind(binder(), AssigneeChangedListener.class)
+ .to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeAbandonedListener.class)
.to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeMergedListener.class)
@@ -177,8 +181,8 @@
new Supplier<AccountAttribute>() {
@Override
public AccountAttribute get() {
- return eventFactory.asAccountAttribute(
- new Account.Id(account._accountId));
+ return account != null ? eventFactory.asAccountAttribute(
+ new Account.Id(account._accountId)) : null;
}
});
}
@@ -266,6 +270,22 @@
}
@Override
+ public void onAssigneeChanged(AssigneeChangedListener.Event ev) {
+ try {
+ Change change = getChange(ev.getChange());
+ AssigneeChangedEvent event = new AssigneeChangedEvent(change);
+
+ event.change = changeAttributeSupplier(change);
+ event.changer = accountAttributeSupplier(ev.getWho());
+ event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
+
+ dispatcher.get().postEvent(change, event);
+ } catch (OrmException e) {
+ log.error("Failed to dispatch event", e);
+ }
+ }
+
+ @Override
public void onTopicEdited(TopicEditedListener.Event ev) {
try {
Change change = getChange(ev.getChange());
@@ -321,7 +341,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 +350,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/AssigneeChanged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
new file mode 100644
index 0000000..2234556
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -0,0 +1,91 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.AssigneeChangedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+
+public class AssigneeChanged {
+ private static final Logger log =
+ LoggerFactory.getLogger(AssigneeChanged.class);
+
+ private final DynamicSet<AssigneeChangedListener> listeners;
+ private final EventUtil util;
+
+ @Inject
+ AssigneeChanged(DynamicSet<AssigneeChangedListener> listeners,
+ EventUtil util) {
+ this.listeners = listeners;
+ this.util = util;
+ }
+
+ public void fire(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
+ Timestamp when) {
+ if (!listeners.iterator().hasNext()) {
+ return;
+ }
+ Event event = new Event(change, editor, oldAssignee, when);
+ for (AssigneeChangedListener l : listeners) {
+ try {
+ l.onAssigneeChanged(event);
+ } catch (Exception e) {
+ log.warn("Error in event listener", e);
+ }
+ }
+ }
+
+ public void fire(Change change, Account account, Account oldAssignee,
+ Timestamp when) {
+ if (!listeners.iterator().hasNext()) {
+ return;
+ }
+ try {
+ fire(util.changeInfo(change),
+ util.accountInfo(account),
+ util.accountInfo(oldAssignee),
+ when);
+ } catch (OrmException e) {
+ log.error("Couldn't fire event", e);
+ }
+ }
+
+ private static class Event extends AbstractChangeEvent
+ implements AssigneeChangedListener.Event {
+ private final AccountInfo oldAssignee;
+
+ Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
+ Timestamp when) {
+ super(change, editor, when, NotifyHandling.ALL);
+ this.oldAssignee = oldAssignee;
+ }
+
+ @Override
+ public AccountInfo getOldAssignee() {
+ return oldAssignee;
+ }
+ }
+}
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 821db6c..ad5bb98 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
@@ -74,22 +74,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 ec3a987..ebfe7d6 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
@@ -74,22 +74,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 d8081cf..6087070 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
@@ -75,22 +75,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 d7fe249..35f2c7d 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
@@ -80,7 +80,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;
@@ -89,18 +88,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 1f8f41c..b5dba77 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
@@ -71,17 +71,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/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 2a223b8..c5eaa0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -25,6 +25,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GpgException;
+import com.google.gerrit.server.account.AccountJson;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -80,18 +81,14 @@
if (a == null || a.getId() == null) {
return null;
}
- AccountInfo ai = new AccountInfo(a.getId().get());
- ai.email = a.getPreferredEmail();
- ai.name = a.getFullName();
- ai.username = a.getUserName();
- return ai;
+ return AccountJson.toAccountInfo(a);
}
public Map<String, ApprovalInfo> approvals(Account a,
Map<String, Short> approvals, Timestamp ts) {
Map<String, ApprovalInfo> result = new HashMap<>();
for (Map.Entry<String, Short> e : approvals.entrySet()) {
- Integer value = e.getValue() != null ? new Integer(e.getValue()) : null;
+ Integer value = e.getValue() != null ? Integer.valueOf(e.getValue()) : null;
result.put(e.getKey(),
ChangeJson.getApprovalInfo(a.getId(), value, null, ts));
}
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 3eba465..c9cf5f1 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
@@ -73,7 +73,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;
@@ -81,18 +80,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 611597c..337982f 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,7 @@
package com.google.gerrit.server.extensions.events;
+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 +34,7 @@
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.List;
public class ReviewerAdded {
private static final Logger log =
@@ -48,21 +50,22 @@
this.util = util;
}
- public void fire(Change change, PatchSet patchSet, Account account,
+ public void fire(Change change, PatchSet patchSet, List<Account> reviewers,
Account adder, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
+ if (!listeners.iterator().hasNext() || reviewers.isEmpty()) {
return;
}
+
try {
Event event = new Event(
util.changeInfo(change),
util.revisionInfo(change.getProject(), patchSet),
- util.accountInfo(account),
+ Lists.transform(reviewers, util::accountInfo),
util.accountInfo(adder),
when);
for (ReviewerAddedListener l : listeners) {
try {
- l.onReviewerAdded(event);
+ l.onReviewersAdded(event);
} catch (Exception e) {
util.logEventListenerError(log, e);
}
@@ -75,17 +78,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 6b4a9ef..00950b7 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
@@ -51,9 +51,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;
}
@@ -66,6 +65,7 @@
message,
util.approvals(reviewer, newApprovals, when),
util.approvals(reviewer, oldApprovals, when),
+ notify,
when);
for (ReviewerDeletedListener listener : listeners) {
try {
@@ -91,8 +91,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 7a65fe9..c7d1ef6 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
@@ -74,17 +74,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 9e6269a..837d730 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
@@ -68,22 +68,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/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
index 601bcc6..3dbd1e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -14,11 +14,8 @@
package com.google.gerrit.server.extensions.webui;
-import com.google.common.base.Function;
import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.common.Nullable;
+import com.google.common.collect.FluentIterable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestCollection;
@@ -33,16 +30,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Objects;
+
public class UiActions {
private static final Logger log = LoggerFactory.getLogger(UiActions.class);
public static Predicate<UiAction.Description> enabled() {
- return new Predicate<UiAction.Description>() {
- @Override
- public boolean apply(UiAction.Description input) {
- return input.isEnabled();
- }
- };
+ return UiAction.Description::isEnabled;
}
public static <R extends RestResource> Iterable<UiAction.Description> from(
@@ -56,58 +50,52 @@
DynamicMap<RestView<R>> views,
final R resource,
final Provider<CurrentUser> userProvider) {
- return Iterables.filter(
- Iterables.transform(
- views,
- new Function<DynamicMap.Entry<RestView<R>>, UiAction.Description> () {
- @Override
- @Nullable
- public UiAction.Description apply(DynamicMap.Entry<RestView<R>> e) {
- int d = e.getExportName().indexOf('.');
- if (d < 0) {
- return null;
- }
+ return FluentIterable.from(views)
+ .transform((DynamicMap.Entry<RestView<R>> e) -> {
+ int d = e.getExportName().indexOf('.');
+ if (d < 0) {
+ return null;
+ }
- RestView<R> view;
- try {
- view = e.getProvider().get();
- } catch (RuntimeException err) {
- log.error(String.format(
- "error creating view %s.%s",
- e.getPluginName(), e.getExportName()), err);
- return null;
- }
+ RestView<R> view;
+ try {
+ view = e.getProvider().get();
+ } catch (RuntimeException err) {
+ log.error(String.format(
+ "error creating view %s.%s",
+ e.getPluginName(), e.getExportName()), err);
+ return null;
+ }
- if (!(view instanceof UiAction)) {
- return null;
- }
+ if (!(view instanceof UiAction)) {
+ return null;
+ }
- try {
- CapabilityUtils.checkRequiresCapability(userProvider,
- e.getPluginName(), view.getClass());
- } catch (AuthException exc) {
- return null;
- }
+ try {
+ CapabilityUtils.checkRequiresCapability(userProvider,
+ e.getPluginName(), view.getClass());
+ } catch (AuthException exc) {
+ return null;
+ }
- UiAction.Description dsc =
- ((UiAction<R>) view).getDescription(resource);
- if (dsc == null || !dsc.isVisible()) {
- return null;
- }
+ UiAction.Description dsc =
+ ((UiAction<R>) view).getDescription(resource);
+ if (dsc == null || !dsc.isVisible()) {
+ return null;
+ }
- String name = e.getExportName().substring(d + 1);
- PrivateInternals_UiActionDescription.setMethod(
- dsc,
- e.getExportName().substring(0, d));
- PrivateInternals_UiActionDescription.setId(
- dsc,
- "gerrit".equals(e.getPluginName())
- ? name
- : e.getPluginName() + '~' + name);
- return dsc;
- }
- }),
- Predicates.notNull());
+ String name = e.getExportName().substring(d + 1);
+ PrivateInternals_UiActionDescription.setMethod(
+ dsc,
+ e.getExportName().substring(0, d));
+ PrivateInternals_UiActionDescription.setId(
+ dsc,
+ "gerrit".equals(e.getPluginName())
+ ? name
+ : e.getPluginName() + '~' + name);
+ return dsc;
+ })
+ .filter(Objects::nonNull);
}
private UiActions() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 370d9a6..49221c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static java.util.Comparator.comparing;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -226,7 +227,7 @@
this.dbWrapper = dbWrapper;
this.threadLocalRepo = repo;
this.threadLocalRevWalk = rw;
- updates = new TreeMap<>(ReviewDbUtil.intKeyOrdering());
+ updates = new TreeMap<>(comparing(PatchSet.Id::get));
}
@Override
@@ -379,7 +380,8 @@
}
static void execute(Collection<BatchUpdate> updates, Listener listener,
- @Nullable RequestId requestId) throws UpdateException, RestApiException {
+ @Nullable RequestId requestId, boolean dryrun)
+ throws UpdateException, RestApiException {
if (updates.isEmpty()) {
return;
}
@@ -401,17 +403,17 @@
}
listener.afterUpdateRepos();
for (BatchUpdate u : updates) {
- u.executeRefUpdates();
+ u.executeRefUpdates(dryrun);
}
listener.afterRefUpdates();
for (BatchUpdate u : updates) {
- u.executeChangeOps(updateChangesInParallel);
+ u.executeChangeOps(updateChangesInParallel, dryrun);
}
listener.afterUpdateChanges();
break;
case DB_BEFORE_REPO:
for (BatchUpdate u : updates) {
- u.executeChangeOps(updateChangesInParallel);
+ u.executeChangeOps(updateChangesInParallel, dryrun);
}
listener.afterUpdateChanges();
for (BatchUpdate u : updates) {
@@ -419,7 +421,7 @@
}
listener.afterUpdateRepos();
for (BatchUpdate u : updates) {
- u.executeRefUpdates();
+ u.executeRefUpdates(dryrun);
}
listener.afterRefUpdates();
break;
@@ -447,9 +449,10 @@
: null);
}
}
-
- for (BatchUpdate u : updates) {
- u.executePostOps();
+ if (!dryrun) {
+ for (BatchUpdate u : updates) {
+ u.executePostOps();
+ }
}
} catch (UpdateException | RestApiException e) {
// Propagate REST API exceptions thrown by operations; they commonly throw
@@ -640,13 +643,17 @@
return this;
}
+ public Collection<ReceiveCommand> getRefUpdates() {
+ return commands.getCommands().values();
+ }
+
public void execute() throws UpdateException, RestApiException {
execute(Listener.NONE);
}
public void execute(Listener listener)
throws UpdateException, RestApiException {
- execute(ImmutableList.of(this), listener, requestId);
+ execute(ImmutableList.of(this), listener, requestId, false);
}
private void executeUpdateRepo() throws UpdateException, RestApiException {
@@ -676,7 +683,8 @@
}
}
- private void executeRefUpdates() throws IOException, UpdateException {
+ private void executeRefUpdates(boolean dryrun)
+ throws IOException, RestApiException {
if (commands == null || commands.isEmpty()) {
logDebug("No ref updates to execute");
return;
@@ -687,6 +695,10 @@
commands.addTo(batchRefUpdate);
logDebug("Executing batch of {} ref updates",
batchRefUpdate.getCommands().size());
+ if (dryrun) {
+ return;
+ }
+
batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
boolean ok = true;
for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
@@ -696,12 +708,13 @@
}
}
if (!ok) {
- throw new UpdateException("BatchRefUpdate failed: " + batchRefUpdate);
+ throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate);
}
}
- private void executeChangeOps(boolean parallel)
- throws UpdateException, RestApiException {
+ private void executeChangeOps(boolean parallel,
+ boolean dryrun) throws UpdateException,
+ RestApiException {
logDebug("Executing change ops (parallel? {})", parallel);
ListeningExecutorService executor = parallel
? changeUpdateExector
@@ -724,7 +737,8 @@
List<ListenableFuture<?>> futures = new ArrayList<>(ops.keySet().size());
for (Map.Entry<Change.Id, Collection<Op>> e : ops.asMap().entrySet()) {
ChangeTask task =
- new ChangeTask(e.getKey(), e.getValue(), Thread.currentThread());
+ new ChangeTask(e.getKey(), e.getValue(), Thread.currentThread(),
+ dryrun);
tasks.add(task);
if (!parallel) {
logDebug("Direct execution of task for ops: {}", ops);
@@ -742,7 +756,9 @@
if (notesMigration.commitChangeWrites()) {
startNanos = System.nanoTime();
- executeNoteDbUpdates(tasks);
+ if (!dryrun) {
+ executeNoteDbUpdates(tasks);
+ }
maybeLogSlowUpdate(startNanos, "NoteDb");
}
} catch (ExecutionException | InterruptedException e) {
@@ -874,6 +890,7 @@
final Change.Id id;
private final Collection<Op> changeOps;
private final Thread mainThread;
+ private final boolean dryrun;
NoteDbUpdateManager.StagedResult noteDbResult;
boolean dirty;
@@ -881,10 +898,11 @@
private String taskId;
private ChangeTask(Change.Id id, Collection<Op> changeOps,
- Thread mainThread) {
+ Thread mainThread, boolean dryrun) {
this.id = id;
this.changeOps = changeOps;
this.mainThread = mainThread;
+ this.dryrun = dryrun;
}
@Override
@@ -953,7 +971,9 @@
logDebug("Updating change");
db.changes().update(cs);
}
- db.commit();
+ if (!dryrun) {
+ db.commit();
+ }
} finally {
db.rollback();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
index 857cbea..16e4bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -15,17 +15,14 @@
package com.google.gerrit.server.git;
import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
-import com.google.common.collect.SetMultimap;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
@@ -85,16 +82,6 @@
return changeData;
}
- public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject()
- throws OrmException {
- SetMultimap<Project.NameKey, Branch.NameKey> ret =
- HashMultimap.create();
- for (ChangeData cd : changeData.values()) {
- ret.put(cd.change().getProject(), cd.change().getDest());
- }
- return ret;
- }
-
public Multimap<Branch.NameKey, ChangeData> changesByBranch()
throws OrmException {
ListMultimap<Branch.NameKey, ChangeData> ret =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index f07b922..27767c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -16,7 +16,6 @@
import static com.google.common.base.Preconditions.checkArgument;
-import com.google.common.base.Function;
import com.google.common.collect.Ordering;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -46,14 +45,11 @@
* AnyObjectId} and only orders on SHA-1.
*/
public static final Ordering<CodeReviewCommit> ORDER = Ordering.natural()
- .onResultOf(new Function<CodeReviewCommit, Integer>() {
- @Override
- public Integer apply(CodeReviewCommit in) {
- return in.getPatchsetId() != null
- ? in.getPatchsetId().getParentKey().get()
- : null;
- }
- }).nullsFirst();
+ .onResultOf((CodeReviewCommit c) ->
+ c.getPatchsetId() != null
+ ? c.getPatchsetId().getParentKey().get()
+ : null)
+ .nullsFirst();
public static CodeReviewRevWalk newRevWalk(Repository repo) {
return new CodeReviewRevWalk(repo);
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/GroupCollector.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
index d832260..795a838 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupCollector.java
@@ -18,7 +18,6 @@
import static org.eclipse.jgit.revwalk.RevFlag.UNINTERESTING;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
@@ -158,13 +157,7 @@
private static Multimap<ObjectId, PatchSet.Id> transformRefs(
Multimap<ObjectId, Ref> refs) {
return Multimaps.transformValues(
- refs,
- new Function<Ref, PatchSet.Id>() {
- @Override
- public PatchSet.Id apply(Ref in) {
- return PatchSet.Id.fromRef(in.getName());
- }
- });
+ refs, r -> PatchSet.Id.fromRef(r.getName()));
}
@VisibleForTesting
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..71b29a1 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
@@ -17,12 +17,11 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static java.util.Comparator.comparing;
import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
@@ -31,7 +30,6 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
@@ -49,7 +47,6 @@
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;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -122,13 +119,10 @@
}
byBranch = bb.build();
commits = new HashMap<>();
- problems = MultimapBuilder.treeKeys(
- Ordering.natural().onResultOf(new Function<Change.Id, Integer>() {
- @Override
- public Integer apply(Change.Id in) {
- return in.get();
- }
- })).arrayListValues(1).build();
+ problems = MultimapBuilder
+ .treeKeys(comparing(Change.Id::get))
+ .arrayListValues(1)
+ .build();
}
public ImmutableSet<Change.Id> getChangeIds() {
@@ -230,6 +224,8 @@
private CommitStatus commits;
private ReviewDb db;
private SubmitInput submitInput;
+ private Set<Project.NameKey> allProjects;
+ private boolean dryrun;
@Inject
MergeOp(ChangeMessagesUtil cmUtil,
@@ -262,12 +258,7 @@
if (in == null) {
return Optional.absent();
}
- return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
- @Override
- public boolean apply(SubmitRecord input) {
- return input.status == SubmitRecord.Status.OK;
- }
- });
+ return Iterables.tryFind(in, r -> r.status == SubmitRecord.Status.OK);
}
public static void checkSubmitRule(ChangeData cd)
@@ -400,10 +391,26 @@
}
}
+ /**
+ * Merges the given change.
+ *
+ * Depending on the server configuration, more changes may be affected, e.g.
+ * by submission of a topic or via superproject subscriptions. All affected
+ * changes are integrated using the projects integration strategy.
+ *
+ * @param db the review database.
+ * @param change the change to be merged.
+ * @param caller the identity of the caller
+ * @param checkSubmitRules whether the prolog submit rules should be evaluated
+ * @param submitInput parameters regarding the merge
+ * @throws OrmException an error occurred reading or writing the database.
+ * @throws RestApiException if an error occurred.
+ */
public void merge(ReviewDb db, Change change, IdentifiedUser caller,
- boolean checkSubmitRules, SubmitInput submitInput)
+ boolean checkSubmitRules, SubmitInput submitInput, boolean dryrun)
throws OrmException, RestApiException {
this.submitInput = submitInput;
+ this.dryrun = dryrun;
this.caller = caller;
this.ts = TimeUtil.nowTs();
submissionId = RequestId.forChange(change);
@@ -412,7 +419,8 @@
logDebug("Beginning integration of {}", change);
try {
- ChangeSet cs = mergeSuperSet.completeChangeSet(db, change, caller);
+ ChangeSet cs = mergeSuperSet.setMergeOpRepoManager(orm)
+ .completeChangeSet(db, change, caller);
checkState(cs.ids().contains(change.getId()),
"change %s missing from %s", change.getId(), cs);
if (cs.furtherHiddenChanges()) {
@@ -447,38 +455,30 @@
"cannot integrate hidden changes into history");
logDebug("Beginning merge attempt on {}", cs);
Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
- logDebug("Perform the merges");
- Multimap<Project.NameKey, Branch.NameKey> br;
Multimap<Branch.NameKey, ChangeData> cbb;
try {
- br = cs.branchesByProject();
cbb = cs.changesByBranch();
} catch (OrmException e) {
throw new IntegrationException("Error reading changes to submit", e);
}
- Set<Project.NameKey> projects = br.keySet();
Set<Branch.NameKey> branches = cbb.keySet();
- openRepos(projects);
-
for (Branch.NameKey branch : branches) {
- OpenRepo or = orm.getRepo(branch.getParentKey());
- toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
+ OpenRepo or = openRepo(branch.getParentKey());
+ if (or != null) {
+ toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
+ }
}
// Done checks that don't involve running submit strategies.
commits.maybeFailVerbose();
SubmoduleOp submoduleOp = subOpFactory.create(branches, orm);
try {
- List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp);
- Set<Project.NameKey> allProjects = submoduleOp.getProjectsInOrder();
- // in case superproject subscription is disabled, allProjects would be null
- if (allProjects == null) {
- allProjects = projects;
- }
- BatchUpdate.execute(
- orm.batchUpdates(allProjects),
+ List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit,
+ submoduleOp, dryrun);
+ this.allProjects = submoduleOp.getProjectsInOrder();
+ BatchUpdate.execute(orm.batchUpdates(allProjects),
new SubmitStrategyListener(submitInput, strategies, commits),
- submissionId);
+ submissionId, dryrun);
} catch (UpdateException | SubmoduleException e) {
// BatchUpdate may have inadvertently wrapped an IntegrationException
// thrown by some legacy SubmitStrategyOp code that intended the error
@@ -498,16 +498,19 @@
}
}
+ public Set<Project.NameKey> getAllProjects() {
+ return allProjects;
+ }
+
+ public MergeOpRepoManager getMergeOpRepoManager() {
+ return orm;
+ }
+
private List<SubmitStrategy> getSubmitStrategies(
- Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp)
- throws IntegrationException {
+ Map<Branch.NameKey, BranchBatch> toSubmit, SubmoduleOp submoduleOp,
+ boolean dryrun) throws IntegrationException {
List<SubmitStrategy> strategies = new ArrayList<>();
Set<Branch.NameKey> allBranches = submoduleOp.getBranchesInOrder();
- // in case superproject subscription is disabled, allBranches would be null
- if (allBranches == null) {
- allBranches = toSubmit.keySet();
- }
-
for (Branch.NameKey branch : allBranches) {
OpenRepo or = orm.getRepo(branch.getParentKey());
if (toSubmit.containsKey(branch)) {
@@ -519,9 +522,13 @@
Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes());
ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit);
SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch,
- submitting.submitType(), ob.oldTip, submoduleOp);
+ submitting.submitType(), ob.oldTip, submoduleOp, dryrun);
strategies.add(strategy);
strategy.addOps(or.getUpdate(), commitsToSubmit);
+ if (submitting.submitType().equals(SubmitType.FAST_FORWARD_ONLY) &&
+ submoduleOp.hasSubscription(branch)) {
+ submoduleOp.addOp(or.getUpdate(), branch);
+ }
} else {
// no open change for this branch
// add submodule triggered op into BatchUpdate
@@ -545,10 +552,12 @@
private SubmitStrategy createStrategy(OpenRepo or,
MergeTip mergeTip, Branch.NameKey destBranch, SubmitType submitType,
- CodeReviewCommit branchTip, SubmoduleOp submoduleOp) throws IntegrationException {
+ CodeReviewCommit branchTip, SubmoduleOp submoduleOp, boolean dryrun)
+ throws IntegrationException {
return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller,
- mergeTip, commits, submissionId, submitInput.notify, submoduleOp);
+ mergeTip, commits, submissionId, submitInput.notify, submoduleOp,
+ dryrun);
}
private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
@@ -723,19 +732,18 @@
}
}
- private void openRepos(Collection<Project.NameKey> projects)
+ private OpenRepo openRepo(Project.NameKey project)
throws IntegrationException {
- for (Project.NameKey project : projects) {
- try {
- orm.openRepo(project, true);
- } catch (NoSuchProjectException noProject) {
- logWarn("Project " + noProject.project() + " no longer exists, "
- + "abandoning open changes");
- abandonAllOpenChangeForDeletedProject(noProject.project());
- } catch (IOException e) {
- throw new IntegrationException("Error opening project " + project, e);
- }
+ try {
+ return orm.openRepo(project);
+ } catch (NoSuchProjectException noProject) {
+ logWarn("Project " + noProject.project() + " no longer exists, "
+ + "abandoning open changes");
+ abandonAllOpenChangeForDeletedProject(noProject.project());
+ } catch (IOException e) {
+ throw new IntegrationException("Error opening project " + project, e);
}
+ return null;
}
private void abandonAllOpenChangeForDeletedProject(
@@ -755,11 +763,10 @@
change.setStatus(Change.Status.ABANDONED);
- ChangeMessage msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- null, change.getLastUpdatedOn(), change.currentPatchSetId());
- msg.setMessage("Project was deleted.");
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), change.currentPatchSetId(),
+ internalUserFactory.create(), change.getLastUpdatedOn(),
+ "Project was deleted.");
cmUtil.addChangeMessage(ctx.getDb(),
ctx.getUpdate(change.currentPatchSetId()), msg);
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..5196ebe 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
@@ -90,11 +90,19 @@
return ob;
}
+ public Repository getRepo() {
+ return repo;
+ }
+
Project.NameKey getProjectName() {
return project.getProject().getNameKey();
}
- BatchUpdate getUpdate() {
+ public CodeReviewRevWalk getCodeReviewRevWalk() {
+ return rw;
+ }
+
+ public BatchUpdate getUpdate() {
checkState(db != null, "call setContext before getUpdate");
if (update == null) {
update = batchUpdateFactory.create(db, getProjectName(), caller, ts)
@@ -187,13 +195,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..108572f 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
@@ -14,13 +14,16 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Multimap;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
@@ -31,31 +34,33 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
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;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -68,7 +73,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,46 +81,69 @@
for (ChangeData cd : cs.changes()) {
cd.reloadChange();
cd.setPatchSets(null);
+ cd.setMergeable(null);
}
}
+ @AutoValue
+ abstract static class QueryKey {
+ private static QueryKey create(
+ Branch.NameKey branch, Iterable<String> hashes) {
+ return new AutoValue_MergeSuperSet_QueryKey(
+ branch, ImmutableSet.copyOf(hashes));
+ }
+
+ abstract Branch.NameKey branch();
+ abstract ImmutableSet<String> hashes();
+ }
+
private final ChangeData.Factory changeDataFactory;
private final Provider<InternalChangeQuery> queryProvider;
- private final GitRepositoryManager repoManager;
+ private final Provider<MergeOpRepoManager> repoManagerProvider;
private final Config cfg;
+ private final Map<QueryKey, List<ChangeData>> queryCache;
+ private final Map<Branch.NameKey, Optional<RevCommit>> heads;
+
+ private MergeOpRepoManager orm;
+ private boolean closeOrm;
@Inject
MergeSuperSet(@GerritServerConfig Config cfg,
ChangeData.Factory changeDataFactory,
Provider<InternalChangeQuery> queryProvider,
- GitRepositoryManager repoManager) {
+ Provider<MergeOpRepoManager> repoManagerProvider) {
this.cfg = cfg;
this.changeDataFactory = changeDataFactory;
this.queryProvider = queryProvider;
- this.repoManager = repoManager;
+ this.repoManagerProvider = repoManagerProvider;
+ queryCache = new HashMap<>();
+ heads = new HashMap<>();
}
- public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
- throws MissingObjectException, IncorrectObjectTypeException, IOException,
- OrmException {
- ChangeData cd =
- changeDataFactory.create(db, change.getProject(), change.getId());
- cd.changeControl(user);
- ChangeSet cs = new ChangeSet(cd, cd.changeControl().isVisible(db, cd));
- if (Submit.wholeTopicEnabled(cfg)) {
- return completeChangeSetIncludingTopics(db, cs, user);
- }
- return completeChangeSetWithoutTopic(db, cs, user);
+ public MergeSuperSet setMergeOpRepoManager(MergeOpRepoManager orm) {
+ checkState(this.orm == null);
+ this.orm = checkNotNull(orm);
+ closeOrm = false;
+ return this;
}
- private static ImmutableListMultimap<Project.NameKey, ChangeData>
- byProject(Iterable<ChangeData> changes) throws OrmException {
- ImmutableListMultimap.Builder<Project.NameKey, ChangeData> builder =
- new ImmutableListMultimap.Builder<>();
- for (ChangeData cd : changes) {
- builder.put(cd.change().getProject(), cd);
+ public ChangeSet completeChangeSet(ReviewDb db, Change change,
+ CurrentUser user) throws IOException, OrmException {
+ try {
+ ChangeData cd =
+ changeDataFactory.create(db, change.getProject(), change.getId());
+ cd.changeControl(user);
+ ChangeSet cs = new ChangeSet(cd, cd.changeControl().isVisible(db, cd));
+ if (Submit.wholeTopicEnabled(cfg)) {
+ return completeChangeSetIncludingTopics(db, cs, user);
+ }
+ return completeChangeSetWithoutTopic(db, cs, user);
+ } finally {
+ if (closeOrm && orm != null) {
+ orm.close();
+ orm = null;
+ }
}
- return builder.build();
}
private SubmitType submitType(ChangeData cd, PatchSet ps, boolean visible)
@@ -146,94 +173,175 @@
return str.type;
}
- private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes,
- CurrentUser user) throws MissingObjectException,
- IncorrectObjectTypeException, IOException, OrmException {
- List<ChangeData> visibleChanges = new ArrayList<>();
- List<ChangeData> nonVisibleChanges = new ArrayList<>();
+ private static ImmutableListMultimap<Branch.NameKey, ChangeData>
+ byBranch(Iterable<ChangeData> changes) throws OrmException {
+ ImmutableListMultimap.Builder<Branch.NameKey, ChangeData> builder =
+ ImmutableListMultimap.builder();
+ for (ChangeData cd : changes) {
+ builder.put(cd.change().getDest(), cd);
+ }
+ return builder.build();
+ }
- Multimap<Project.NameKey, ChangeData> pc =
- byProject(
- Iterables.concat(changes.changes(), changes.nonVisibleChanges()));
- for (Project.NameKey project : pc.keySet()) {
- try (Repository repo = repoManager.openRepository(project);
- RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
- for (ChangeData cd : pc.get(project)) {
- checkState(cd.hasChangeControl(),
- "completeChangeSet forgot to set changeControl for current user"
- + " at ChangeData creation time");
- boolean visible = changes.ids().contains(cd.getId());
- if (visible && !cd.changeControl().isVisible(db, cd)) {
- // We thought the change was visible, but it isn't.
- // This can happen if the ACL changes during the
- // completeChangeSet computation, for example.
- visible = false;
- }
- List<ChangeData> dest = visible ? visibleChanges : nonVisibleChanges;
+ private Set<String> walkChangesByHashes(Collection<RevCommit> sourceCommits,
+ Set<String> ignoreHashes, OpenRepo or, Branch.NameKey b)
+ throws IOException {
+ Set<String> destHashes = new HashSet<>();
+ or.rw.reset();
+ markHeadUninteresting(or, b);
+ for (RevCommit c : sourceCommits) {
+ String name = c.name();
+ if (ignoreHashes.contains(name)) {
+ continue;
+ }
+ destHashes.add(name);
+ or.rw.markStart(c);
+ }
+ for (RevCommit c : or.rw) {
+ String name = c.name();
+ if (ignoreHashes.contains(name)) {
+ continue;
+ }
+ destHashes.add(name);
+ }
- // Pick a revision to use for traversal. If any of the patch sets
- // is visible, we use the most recent one. Otherwise, use the current
- // patch set.
- PatchSet ps = cd.currentPatchSet();
- boolean visiblePatchSet = visible;
- if (!cd.changeControl().isPatchVisible(ps, cd)) {
- Iterable<PatchSet> visiblePatchSets = cd.visiblePatchSets();
- if (Iterables.isEmpty(visiblePatchSets)) {
- visiblePatchSet = false;
- } else {
- ps = Iterables.getLast(visiblePatchSets);
- }
- }
+ return destHashes;
+ }
- if (submitType(cd, ps, visiblePatchSet) == SubmitType.CHERRY_PICK) {
- dest.add(cd);
- continue;
- }
+ private ChangeSet completeChangeSetWithoutTopic(ReviewDb db,
+ ChangeSet changes, CurrentUser user) throws IOException, OrmException {
+ Collection<ChangeData> visibleChanges = new ArrayList<>();
+ Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
- // Get the underlying git commit object
- String objIdStr = ps.getRevision().get();
- RevCommit commit = rw.parseCommit(ObjectId.fromString(objIdStr));
+ // For each target branch we run a separate rev walk to find open changes
+ // reachable from changes already in the merge super set.
+ ImmutableListMultimap<Branch.NameKey, ChangeData> bc = byBranch(
+ Iterables.concat(changes.changes(), changes.nonVisibleChanges()));
+ for (Branch.NameKey b : bc.keySet()) {
+ OpenRepo or = getRepo(b.getParentKey());
+ List<RevCommit> visibleCommits = new ArrayList<>();
+ List<RevCommit> nonVisibleCommits = new ArrayList<>();
+ for (ChangeData cd : bc.get(b)) {
+ checkState(cd.hasChangeControl(),
+ "completeChangeSet forgot to set changeControl for current user"
+ + " at ChangeData creation time");
- // Collect unmerged ancestors
- Branch.NameKey destBranch = cd.change().getDest();
- repo.getRefDatabase().refresh();
- Ref ref = repo.getRefDatabase().getRef(destBranch.get());
+ boolean visible = changes.ids().contains(cd.getId());
+ if (visible && !cd.changeControl().isVisible(db, cd)) {
+ // We thought the change was visible, but it isn't.
+ // This can happen if the ACL changes during the
+ // completeChangeSet computation, for example.
+ visible = false;
+ }
+ Collection<RevCommit> toWalk = visible ?
+ visibleCommits : nonVisibleCommits;
- rw.reset();
- rw.sort(RevSort.TOPO);
- rw.markStart(commit);
- if (ref != null) {
- RevCommit head = rw.parseCommit(ref.getObjectId());
- rw.markUninteresting(head);
- }
-
- List<String> hashes = new ArrayList<>();
- // Always include the input, even if merged. This allows
- // SubmitStrategyOp to correct the situation later, assuming it gets
- // returned by byCommitsOnBranchNotMerged below.
- hashes.add(objIdStr);
- for (RevCommit c : rw) {
- if (!c.equals(commit)) {
- hashes.add(c.name());
- }
- }
-
- if (!hashes.isEmpty()) {
- Iterable<ChangeData> destChanges = query()
- .byCommitsOnBranchNotMerged(
- repo, db, cd.change().getDest(), hashes);
- for (ChangeData chd : destChanges) {
- chd.changeControl(user);
- dest.add(chd);
- }
+ // Pick a revision to use for traversal. If any of the patch sets
+ // is visible, we use the most recent one. Otherwise, use the current
+ // patch set.
+ PatchSet ps = cd.currentPatchSet();
+ boolean visiblePatchSet = visible;
+ if (!cd.changeControl().isPatchVisible(ps, cd)) {
+ Iterable<PatchSet> visiblePatchSets = cd.visiblePatchSets();
+ if (Iterables.isEmpty(visiblePatchSets)) {
+ visiblePatchSet = false;
+ } else {
+ ps = Iterables.getLast(visiblePatchSets);
}
}
+
+ if (submitType(cd, ps, visiblePatchSet) == SubmitType.CHERRY_PICK) {
+ if (visible) {
+ visibleChanges.add(cd);
+ } else {
+ nonVisibleChanges.add(cd);
+ }
+
+ continue;
+ }
+
+ // Get the underlying git commit object
+ String objIdStr = ps.getRevision().get();
+ RevCommit commit = or.rw.parseCommit(ObjectId.fromString(objIdStr));
+
+ // Always include the input, even if merged. This allows
+ // SubmitStrategyOp to correct the situation later, assuming it gets
+ // returned by byCommitsOnBranchNotMerged below.
+ toWalk.add(commit);
}
+
+ Set<String> emptySet = Collections.emptySet();
+ Set<String> visibleHashes =
+ walkChangesByHashes(visibleCommits, emptySet, or, b);
+
+ List<ChangeData> cds =
+ byCommitsOnBranchNotMerged(or, db, user, b, visibleHashes);
+ for (ChangeData chd : cds) {
+ chd.changeControl(user);
+ visibleChanges.add(chd);
+ }
+
+ Set<String> nonVisibleHashes =
+ walkChangesByHashes(nonVisibleCommits, visibleHashes, or, b);
+ Iterables.addAll(nonVisibleChanges,
+ byCommitsOnBranchNotMerged(or, db, user, b, nonVisibleHashes));
}
return new ChangeSet(visibleChanges, nonVisibleChanges);
}
+ private OpenRepo getRepo(Project.NameKey project) throws IOException {
+ if (orm == null) {
+ orm = repoManagerProvider.get();
+ closeOrm = true;
+ }
+ try {
+ OpenRepo or = orm.openRepo(project);
+ checkState(or.rw.hasRevSort(RevSort.TOPO));
+ return or;
+ } catch (NoSuchProjectException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void markHeadUninteresting(OpenRepo or, Branch.NameKey b)
+ throws IOException {
+ Optional<RevCommit> head = heads.get(b);
+ if (head == null) {
+ Ref ref = or.repo.getRefDatabase().exactRef(b.get());
+ head = ref != null
+ ? Optional.<RevCommit>of(or.rw.parseCommit(ref.getObjectId()))
+ : Optional.<RevCommit>absent();
+ heads.put(b, head);
+ }
+ if (head.isPresent()) {
+ or.rw.markUninteresting(head.get());
+ }
+ }
+
+ private List<ChangeData> byCommitsOnBranchNotMerged(OpenRepo or, ReviewDb db,
+ CurrentUser user, Branch.NameKey branch, Set<String> hashes)
+ throws OrmException, IOException {
+ if (hashes.isEmpty()) {
+ return ImmutableList.of();
+ }
+ QueryKey k = QueryKey.create(branch, hashes);
+ List<ChangeData> cached = queryCache.get(k);
+ if (cached != null) {
+ return cached;
+ }
+
+ List<ChangeData> result = new ArrayList<>();
+ Iterable<ChangeData> destChanges = query()
+ .byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
+ for (ChangeData chd : destChanges) {
+ chd.changeControl(user);
+ result.add(chd);
+ }
+ queryCache.put(k, result);
+ return result;
+ }
+
/**
* Completes {@code cs} with any additional changes from its topics
* <p>
@@ -261,11 +369,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);
@@ -288,8 +404,7 @@
private ChangeSet completeChangeSetIncludingTopics(
ReviewDb db, ChangeSet changes, CurrentUser user)
- throws MissingObjectException, IncorrectObjectTypeException, IOException,
- OrmException {
+ throws IOException, OrmException {
Set<String> topicsSeen = new HashSet<>();
Set<String> visibleTopicsSeen = new HashSet<>();
int oldSeen;
@@ -307,13 +422,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/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 89ec1d6..0667e14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -16,9 +16,9 @@
import static com.google.common.base.Preconditions.checkArgument;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
@@ -213,7 +213,8 @@
PersonIdent committerIndent, String commitMsg, RevWalk rw)
throws IOException, MergeIdenticalTreeException, MergeConflictException {
- if (rw.isMergedInto(originalCommit, mergeTip)) {
+ if (!MergeStrategy.THEIRS.getName().equals(mergeStrategy) &&
+ rw.isMergedInto(originalCommit, mergeTip)) {
throw new ChangeAlreadyMergedException(
"'" + originalCommit.getName() + "' has already been merged");
}
@@ -598,14 +599,10 @@
Joiner.on("', '").join(topics));
} else {
return String.format("Merge changes %s%s",
- Joiner.on(',').join(Iterables.transform(
- Iterables.limit(merged, 5),
- new Function<CodeReviewCommit, String>() {
- @Override
- public String apply(CodeReviewCommit in) {
- return in.change().getKey().abbreviate();
- }
- })),
+ FluentIterable.from(merged)
+ .limit(5)
+ .transform(c -> c.change().getKey().abbreviate())
+ .join(Joiner.on(',')),
merged.size() > 5 ? ", ..." : "");
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
index 2ccc849..6da1335 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -22,8 +22,8 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeMerged;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -149,19 +149,13 @@
}
}
msgBuf.append(".");
- ChangeMessage msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), psId);
- msg.setMessage(msgBuf.toString());
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), msgBuf.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, msg);
- PatchSetApproval submitter = new PatchSetApproval(
- new PatchSetApproval.Key(
- change.currentPatchSetId(),
- ctx.getAccountId(),
- LabelId.legacySubmit()),
- (short) 1, ctx.getWhen());
+ PatchSetApproval submitter = ApprovalsUtil.newApproval(
+ change.currentPatchSetId(), ctx.getUser(), LabelId.legacySubmit(),
+ 1, ctx.getWhen());
update.putApproval(submitter.getLabel(), submitter.getValue());
ctx.getDb().patchSetApprovals().upsert(
Collections.singleton(submitter));
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 9b1c3f7..801f259 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..4139fc5 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
@@ -21,6 +21,9 @@
import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+import static java.util.Comparator.comparingInt;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static org.eclipse.jgit.lib.RefDatabase.ALL;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
@@ -30,24 +33,20 @@
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
import com.google.common.base.Function;
-import com.google.common.base.Joiner;
import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.BiMap;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
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;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.SortedSetMultimap;
@@ -315,6 +314,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 +491,7 @@
advHooks.add(new HackPushNegotiateHook());
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
rp.setPostReceiveHook(lazyPostReceive.get());
+ rp.setAllowPushOptions(true);
}
public void init() {
@@ -677,14 +679,9 @@
}
private void reportMessages() {
- Iterable<CreateRequest> created =
- Iterables.filter(newChanges, new Predicate<CreateRequest>() {
- @Override
- public boolean apply(CreateRequest input) {
- return input.change != null;
- }
- });
- if (!Iterables.isEmpty(created)) {
+ List<CreateRequest> created =
+ newChanges.stream().filter(r -> r.change != null).collect(toList());
+ if (!created.isEmpty()) {
addMessage("");
addMessage("New Changes:");
for (CreateRequest c : created) {
@@ -695,21 +692,10 @@
addMessage("");
}
- List<ReplaceRequest> updated = FluentIterable
- .from(replaceByChange.values())
- .filter(new Predicate<ReplaceRequest>() {
- @Override
- public boolean apply(ReplaceRequest input) {
- return !input.skip && input.inputCommand.getResult() == OK;
- }
- })
- .toSortedList(Ordering.natural().onResultOf(
- new Function<ReplaceRequest, Integer>() {
- @Override
- public Integer apply(ReplaceRequest in) {
- return in.notes.getChangeId().get();
- }
- }));
+ List<ReplaceRequest> updated = replaceByChange.values().stream()
+ .filter(r -> !r.skip && r.inputCommand.getResult() == OK)
+ .sorted(comparingInt(r -> r.notes.getChangeId().get()))
+ .collect(toList());
if (!updated.isEmpty()) {
addMessage("");
addMessage("Updated Changes:");
@@ -827,7 +813,7 @@
// One or more new references failed to create. Assume the
// system isn't working correctly anymore and abort.
reject(magicBranch.cmd, "Unable to create changes: "
- + Joiner.on(' ').join(lastCreateChangeErrors));
+ + lastCreateChangeErrors.stream().collect(joining(" ")));
logError(String.format(
"Only %d of %d new change refs created in %s; aborting",
okToInsert, replaceCount + newChanges.size(), project.getName()));
@@ -915,6 +901,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) {
@@ -1040,11 +1038,12 @@
.getPluginConfig(e.getPluginName())
.getString(e.getExportName());
if (configEntry.getType() == ProjectConfigEntryType.ARRAY) {
- List<String> l =
- Arrays.asList(projectControl.getProjectState()
- .getConfig().getPluginConfig(e.getPluginName())
- .getStringList(e.getExportName()));
- oldValue = Joiner.on("\n").join(l);
+ oldValue =
+ Arrays.stream(
+ projectControl.getProjectState()
+ .getConfig().getPluginConfig(e.getPluginName())
+ .getStringList(e.getExportName()))
+ .collect(joining("\n"));
}
if ((value == null ? oldValue != null : !value.equals(oldValue)) &&
@@ -1238,6 +1237,9 @@
@Option(name = "--submit", usage = "immediately submit the change")
boolean submit;
+ @Option(name = "--merged", usage = "create single change for a merged commit")
+ boolean merged;
+
@Option(name = "--notify",
usage = "Notify handling that defines to whom email notifications "
+ "should be sent. Allowed values are NONE, OWNER, "
@@ -1305,14 +1307,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 +1323,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 +1352,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 +1380,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");
@@ -1408,7 +1428,8 @@
errors.put(Error.CODE_REVIEW, ref);
reject(cmd, "draft workflow is disabled");
return;
- } else if (projectControl.controlForRef("refs/drafts/" + ref)
+ } else if (projectControl
+ .controlForRef(MagicBranch.NEW_DRAFT_CHANGE + ref)
.isBlocked(Permission.PUSH)) {
errors.put(Error.CODE_REVIEW, ref);
reject(cmd, "cannot upload drafts");
@@ -1428,7 +1449,7 @@
}
if (magicBranch.submit && !projectControl.controlForRef(
- MagicBranch.NEW_CHANGE + ref).canSubmit()) {
+ MagicBranch.NEW_CHANGE + ref).canSubmit(true)) {
reject(cmd, "submit not allowed");
return;
}
@@ -1444,56 +1465,71 @@
return;
}
- // If tip is a merge commit, or the root commit or
- // if %base was specified, ignore newChangeForAllNotInTarget
- if (tip.getParentCount() > 1
- || magicBranch.base != null
- || tip.getParentCount() == 0) {
- logDebug("Forcing newChangeForAllNotInTarget = false");
- newChangeForAllNotInTarget = false;
- }
-
- if (magicBranch.base != null) {
- logDebug("Handling %base: {}", magicBranch.base);
- magicBranch.baseCommit = Lists.newArrayListWithCapacity(
- magicBranch.base.size());
- for (ObjectId id : magicBranch.base) {
- try {
- magicBranch.baseCommit.add(walk.parseCommit(id));
- } catch (IncorrectObjectTypeException notCommit) {
- reject(cmd, "base must be a commit");
+ String destBranch = magicBranch.dest.get();
+ try {
+ if (magicBranch.merged) {
+ if (magicBranch.draft) {
+ reject(cmd, "cannot be draft & merged");
return;
- } catch (MissingObjectException e) {
- reject(cmd, "base not found");
+ }
+ if (magicBranch.base != null) {
+ reject(cmd, "cannot use merged with base");
return;
- } catch (IOException e) {
- logWarn(String.format(
- "Project %s cannot read %s",
- project.getName(), id.name()), e);
- reject(cmd, "internal server error");
+ }
+ RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
+ if (branchTip == null) {
+ return; // readBranchTip already rejected cmd.
+ }
+ if (!walk.isMergedInto(tip, branchTip)) {
+ reject(cmd, "not merged into branch");
return;
}
}
- } else if (newChangeForAllNotInTarget) {
- logDebug("Handling newChangeForAllNotInTarget");
- String destBranch = magicBranch.dest.get();
- try {
- Ref r = repo.getRefDatabase().exactRef(destBranch);
- if (r == null) {
- reject(cmd, destBranch + " not found");
- return;
- }
- ObjectId baseHead = r.getObjectId();
- magicBranch.baseCommit =
- Collections.singletonList(walk.parseCommit(baseHead));
+ // If tip is a merge commit, or the root commit or
+ // if %base or %merged was specified, ignore newChangeForAllNotInTarget.
+ if (tip.getParentCount() > 1
+ || magicBranch.base != null
+ || magicBranch.merged
+ || tip.getParentCount() == 0) {
+ logDebug("Forcing newChangeForAllNotInTarget = false");
+ newChangeForAllNotInTarget = false;
+ }
+
+ if (magicBranch.base != null) {
+ logDebug("Handling %base: {}", magicBranch.base);
+ magicBranch.baseCommit = Lists.newArrayListWithCapacity(
+ magicBranch.base.size());
+ for (ObjectId id : magicBranch.base) {
+ try {
+ magicBranch.baseCommit.add(walk.parseCommit(id));
+ } catch (IncorrectObjectTypeException notCommit) {
+ reject(cmd, "base must be a commit");
+ return;
+ } catch (MissingObjectException e) {
+ reject(cmd, "base not found");
+ return;
+ } catch (IOException e) {
+ logWarn(String.format(
+ "Project %s cannot read %s",
+ project.getName(), id.name()), e);
+ reject(cmd, "internal server error");
+ return;
+ }
+ }
+ } else if (newChangeForAllNotInTarget) {
+ RevCommit branchTip = readBranchTip(cmd, magicBranch.dest);
+ if (branchTip == null) {
+ return; // readBranchTip already rejected cmd.
+ }
+ magicBranch.baseCommit = Collections.singletonList(branchTip);
logDebug("Set baseCommit = {}", magicBranch.baseCommit.get(0).name());
- } catch (IOException ex) {
- logWarn(String.format("Project %s cannot read %s", project.getName(),
- destBranch), ex);
- reject(cmd, "internal server error");
- return;
}
+ } catch (IOException ex) {
+ logWarn(String.format("Error walking to %s in project %s",
+ destBranch, project.getName()), ex);
+ reject(cmd, "internal server error");
+ return;
}
// Validate that the new commits are connected with the target
@@ -1540,6 +1576,16 @@
}
}
+ private RevCommit readBranchTip(ReceiveCommand cmd, Branch.NameKey branch)
+ throws IOException {
+ Ref r = allRefs.get(branch.get());
+ if (r == null) {
+ reject(cmd, branch.get() + " not found");
+ return null;
+ }
+ return rp.getRevWalk().parseCommit(r.getObjectId());
+ }
+
private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
logDebug("Parsing replace command");
if (cmd.getType() != ReceiveCommand.Type.CREATE) {
@@ -1604,29 +1650,10 @@
GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil,
notesFactory, project.getNameKey());
- rp.getRevWalk().reset();
- rp.getRevWalk().sort(RevSort.TOPO);
- rp.getRevWalk().sort(RevSort.REVERSE, true);
try {
- RevCommit start = rp.getRevWalk().parseCommit(magicBranch.cmd.getNewId());
- rp.getRevWalk().markStart(start);
- if (magicBranch.baseCommit != null) {
- logDebug("Marking {} base commits uninteresting",
- magicBranch.baseCommit.size());
- for (RevCommit c : magicBranch.baseCommit) {
- rp.getRevWalk().markUninteresting(c);
- }
- Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
- if (targetRef != null) {
- logDebug("Marking target ref {} ({}) uninteresting",
- magicBranch.ctl.getRefName(), targetRef.getObjectId().name());
- rp.getRevWalk().markUninteresting(
- rp.getRevWalk().parseCommit(targetRef.getObjectId()));
- }
- } else {
- markHeadsAsUninteresting(
- rp.getRevWalk(),
- magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+ RevCommit start = setUpWalkForSelectingChanges();
+ if (start == null) {
+ return;
}
List<ChangeLookup> pending = new ArrayList<>();
@@ -1636,7 +1663,11 @@
int total = 0;
int alreadyTracked = 0;
boolean rejectImplicitMerges = start.getParentCount() == 1
- && projectCache.get(project.getNameKey()).isRejectImplicitMerges();
+ && projectCache.get(project.getNameKey()).isRejectImplicitMerges()
+ // Don't worry about implicit merges when creating changes for
+ // already-merged commits; they're already in history, so it's too
+ // late.
+ && !magicBranch.merged;
Set<RevCommit> mergedParents;
if (rejectImplicitMerges) {
mergedParents = new HashSet<>();
@@ -1655,9 +1686,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);
}
@@ -1750,14 +1779,10 @@
List<ChangeData> changes = p.destChanges;
if (changes.size() > 1) {
logDebug("Multiple changes in project with Change-Id {}: {}",
- p.changeKey, Lists.transform(
- changes,
- new Function<ChangeData, String>() {
- @Override
- public String apply(ChangeData in) {
- return in.getId().toString();
- }
- }));
+ p.changeKey,
+ changes.stream()
+ .map(cd -> cd.getId().toString())
+ .collect(joining()));
// WTF, multiple changes in this project have the same key?
// Since the commit is new, the user should recreate it with
// a different Change-Id. In practice, we should never see
@@ -1854,8 +1879,46 @@
}
}
+ private RevCommit setUpWalkForSelectingChanges() throws IOException {
+ RevWalk rw = rp.getRevWalk();
+ RevCommit start = rw.parseCommit(magicBranch.cmd.getNewId());
+
+ rw.reset();
+ rw.sort(RevSort.TOPO);
+ rw.sort(RevSort.REVERSE, true);
+ rp.getRevWalk().markStart(start);
+ if (magicBranch.baseCommit != null) {
+ markExplicitBasesUninteresting();
+ } else if (magicBranch.merged) {
+ logDebug(
+ "Marking parents of merged commit {} uninteresting", start.name());
+ for (RevCommit c : start.getParents()) {
+ rw.markUninteresting(c);
+ }
+ } else {
+ markHeadsAsUninteresting(
+ rw, magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
+ }
+ return start;
+ }
+
+ private void markExplicitBasesUninteresting() throws IOException {
+ logDebug("Marking {} base commits uninteresting",
+ magicBranch.baseCommit.size());
+ for (RevCommit c : magicBranch.baseCommit) {
+ rp.getRevWalk().markUninteresting(c);
+ }
+ Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
+ if (targetRef != null) {
+ logDebug("Marking target ref {} ({}) uninteresting",
+ magicBranch.ctl.getRefName(), targetRef.getObjectId().name());
+ rp.getRevWalk().markUninteresting(
+ rp.getRevWalk().parseCommit(targetRef.getObjectId()));
+ }
+ }
+
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) {
@@ -1938,13 +2001,17 @@
private void setChangeId(int id) {
changeId = new Change.Id(id);
ins = changeInserterFactory.create(changeId, commit, refName)
- .setDraft(magicBranch.draft)
.setTopic(magicBranch.topic)
// Changes already validated in validateNewCommits.
.setValidatePolicy(CommitValidators.Policy.NONE);
+
+ if (magicBranch.draft) {
+ ins.setDraft(magicBranch.draft);
+ } else if (magicBranch.merged) {
+ ins.setStatus(Change.Status.MERGED);
+ }
cmd = new ReceiveCommand(ObjectId.zeroId(), commit,
ins.getPatchSetId().toRefName());
- ins.setUpdateRefCommand(cmd);
if (rp.getPushCertificate() != null) {
ins.setPushCertificate(rp.getPushCertificate().toTextWithSignature());
}
@@ -1983,7 +2050,7 @@
.setNotify(magicBranch.notify)
.setRequestScopePropagator(requestScopePropagator)
.setSendMail(true)
- .setUpdateRef(true));
+ .setUpdateRef(false));
if (!magicBranch.hashtags.isEmpty()) {
bu.addOp(
changeId,
@@ -2035,7 +2102,7 @@
logDebug("Processing submit with tip change {} ({})",
tipChange.getId(), magicBranch.cmd.getNewId());
try (MergeOp op = mergeOpProvider.get()) {
- op.merge(db, tipChange, user, false, new SubmitInput());
+ op.merge(db, tipChange, user, false, new SubmitInput(), false);
}
}
@@ -2099,14 +2166,8 @@
Collection<ChangeNotes> allNotes =
notesFactory.create(
db,
- Collections2.transform(
- replaceByChange.values(),
- new Function<ReplaceRequest, Change.Id>() {
- @Override
- public Change.Id apply(ReplaceRequest in) {
- return in.ontoChange;
- }
- }));
+ replaceByChange.values().stream()
+ .map(r -> r.ontoChange).collect(toList()));
for (ChangeNotes notes : allNotes) {
replaceByChange.get(notes.getChangeId()).notes = notes;
}
@@ -2556,12 +2617,20 @@
rw.parseBody(c);
CommitReceivedEvent receiveEvent =
new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user);
- CommitValidators commitValidators =
- commitValidatorsFactory.create(ctl, sshInfo, repo);
+
+ CommitValidators.Policy policy;
+ if (magicBranch != null
+ && cmd.getRefName().equals(magicBranch.cmd.getRefName())
+ && magicBranch.merged) {
+ policy = CommitValidators.Policy.MERGED;
+ } else {
+ policy = CommitValidators.Policy.RECEIVE_COMMITS;
+ }
try {
- messages.addAll(commitValidators.validateForReceiveCommits(
- receiveEvent, rejectCommits));
+ messages.addAll(
+ commitValidatorsFactory.create(policy, ctl, sshInfo, repo)
+ .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 7754813..a9dcd4b 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
@@ -34,7 +34,6 @@
import com.google.gerrit.server.ApprovalCopier;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeKindCache;
@@ -193,7 +192,8 @@
@Override
public void updateRepo(RepoContext ctx) throws Exception {
- changeKind = changeKindCache.getChangeKind(projectControl.getProjectState(),
+ changeKind = changeKindCache.getChangeKind(
+ projectControl.getProject().getNameKey(),
ctx.getRepository(), priorCommit, commit);
if (checkMergedInto) {
@@ -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.addApprovalsForNewPatchSet(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(
@@ -276,11 +278,8 @@
if (!Strings.isNullOrEmpty(reviewMessage)) {
message.append("\n").append(reviewMessage);
}
- msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), patchSetId);
- msg.setMessage(message.toString());
+ msg = ChangeMessagesUtil.newMessage(ctx.getDb(), patchSetId, ctx.getUser(),
+ ctx.getWhen(), message.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, msg);
if (mergedByPushOp == null) {
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..1421556 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
@@ -105,6 +105,7 @@
private final boolean enableSuperProjectSubscriptions;
private final Multimap<Branch.NameKey, SubmoduleSubscription> targets;
private final Set<Branch.NameKey> updatedBranches;
+ private final Set<Branch.NameKey> affectedBranches;
private final MergeOpRepoManager orm;
private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
private final Map<Branch.NameKey, GitModules> branchGitModules;
@@ -131,6 +132,7 @@
this.orm = orm;
this.updatedBranches = updatedBranches;
this.targets = HashMultimap.create();
+ this.affectedBranches = new HashSet<>();
this.branchTips = new HashMap<>();
this.branchGitModules = new HashMap<>();
this.sortedBranches = calculateSubscriptionMap();
@@ -154,8 +156,11 @@
allVisited);
}
- // Since the searchForSuperprojects will add the superprojects before one
- // submodule in sortedBranches, need reverse the order of it
+ // Since the searchForSuperprojects will add all branches (related or
+ // unrelated) and ensure the superproject's branches get added first before
+ // a submodule branch. Need remove all unrelated branches and reverse
+ // the order.
+ allVisited.retainAll(affectedBranches);
reverse(allVisited);
return ImmutableSet.copyOf(allVisited);
}
@@ -184,6 +189,8 @@
Branch.NameKey superProject = sub.getSuperProject();
searchForSuperprojects(superProject, currentVisited, allVisited);
targets.put(superProject, sub);
+ affectedBranches.add(superProject);
+ affectedBranches.add(sub.getSubmodule());
}
} catch (IOException e) {
throw new SubmoduleException("Cannot find superprojects for " + current,
@@ -251,7 +258,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 +297,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,14 +334,14 @@
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);
}
}
}
BatchUpdate.execute(orm.batchUpdates(superProjects), Listener.NONE,
- orm.getSubmissionId());
+ orm.getSubmissionId(), false);
} catch (RestApiException | UpdateException | IOException |
NoSuchProjectException e) {
throw new SubmoduleException("Cannot update gitlinks", e);
@@ -348,18 +355,22 @@
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);
}
CodeReviewCommit currentCommit;
- Ref r = or.repo.exactRef(subscriber.get());
- if (r == null) {
- throw new SubmoduleException(
- "The branch was probably deleted from the subscriber repository");
+ if (branchTips.containsKey(subscriber)) {
+ currentCommit = branchTips.get(subscriber);
+ } else {
+ Ref r = or.repo.exactRef(subscriber.get());
+ if (r == null) {
+ throw new SubmoduleException(
+ "The branch was probably deleted from the subscriber repository");
+ }
+ currentCommit = or.rw.parseCommit(r.getObjectId());
}
- currentCommit = or.rw.parseCommit(r.getObjectId());
StringBuilder msgbuf = new StringBuilder("");
PersonIdent author = null;
@@ -404,7 +415,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);
}
@@ -436,7 +447,9 @@
commit.setAuthor(currentCommit.getAuthorIdent());
commit.setCommitter(myIdent);
ObjectId id = or.ins.insert(commit);
- return or.rw.parseCommit(id);
+ CodeReviewCommit newCommit = or.rw.parseCommit(id);
+ newCommit.copyFrom(currentCommit);
+ return newCommit;
}
private RevCommit updateSubmodule(DirCache dc, DirCacheEditor ed,
@@ -444,7 +457,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);
}
@@ -543,30 +556,36 @@
public ImmutableSet<Project.NameKey> getProjectsInOrder()
throws SubmoduleException {
- if (sortedBranches == null) {
- return null;
- }
-
LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
- Project.NameKey prev = null;
- for (Branch.NameKey branch : sortedBranches) {
- Project.NameKey project = branch.getParentKey();
- if (!project.equals(prev)) {
- if (projects.contains(project)) {
- throw new SubmoduleException(
- "Project level circular subscriptions detected: " +
- printCircularPath(projects, project));
+ if (sortedBranches != null) {
+ Project.NameKey prev = null;
+ for (Branch.NameKey branch : sortedBranches) {
+ Project.NameKey project = branch.getParentKey();
+ if (!project.equals(prev)) {
+ if (projects.contains(project)) {
+ throw new SubmoduleException(
+ "Project level circular subscriptions detected: " +
+ printCircularPath(projects, project));
+ }
+ projects.add(project);
}
- projects.add(project);
+ prev = project;
}
- prev = project;
}
+ for (Branch.NameKey branch : updatedBranches) {
+ projects.add(branch.getParentKey());
+ }
return ImmutableSet.copyOf(projects);
}
public ImmutableSet<Branch.NameKey> getBranchesInOrder() {
- return sortedBranches;
+ LinkedHashSet<Branch.NameKey> branches = new LinkedHashSet<>();
+ if (sortedBranches != null) {
+ branches.addAll(sortedBranches);
+ }
+ branches.addAll(updatedBranches);
+ return ImmutableSet.copyOf(branches);
}
public boolean hasSubscription(Branch.NameKey branch) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index 5260aab..ad650c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.git;
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
+import static java.util.stream.Collectors.toList;
+
import com.google.gerrit.reviewdb.client.Project;
import org.eclipse.jgit.lib.Ref;
@@ -45,12 +45,7 @@
}
TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
- include = FluentIterable.from(include).filter(new Predicate<Ref>() {
- @Override
- public boolean apply(Ref ref) {
- return !TagSet.skip(ref);
- }
- }).toList();
+ include = include.stream().filter(r -> !TagSet.skip(r)).collect(toList());
TagSet tags = this.tags;
if (tags == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 4a5e94d..31da05c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -48,14 +48,14 @@
@Override
public List<SubmitStrategyOp> buildOps(
- Collection<CodeReviewCommit> toMerge) {
+ Collection<CodeReviewCommit> toMerge) throws IntegrationException {
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
boolean first = true;
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
if (first && args.mergeTip.getInitialTip() == null) {
- ops.add(new CherryPickUnbornRootOp(n));
+ ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new CherryPickRootOp(n));
} else if (n.getParentCount() == 1) {
@@ -68,21 +68,6 @@
return ops;
}
- private class CherryPickUnbornRootOp extends SubmitStrategyOp {
- private CherryPickUnbornRootOp(CodeReviewCommit toMerge) {
- super(CherryPick.this.args, toMerge);
- }
-
- @Override
- protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
- // The branch is unborn. Take fast-forward resolution to create the
- // branch.
- CodeReviewCommit newCommit = amendGitlink(toMerge);
- args.mergeTip.moveTipTo(newCommit, toMerge);
- newCommit.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- }
- }
-
private class CherryPickRootOp extends SubmitStrategyOp {
private CherryPickRootOp(CodeReviewCommit toMerge) {
super(CherryPick.this.args, toMerge);
@@ -191,8 +176,9 @@
// different first parent. So instead behave as though MERGE_IF_NECESSARY
// was configured.
MergeTip mergeTip = args.mergeTip;
- if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
- mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
+ if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge) &&
+ !args.submoduleOp.hasSubscription(args.destBranch)) {
+ mergeTip.moveTipTo(toMerge, toMerge);
} else {
PersonIdent myIdent = new PersonIdent(args.serverIdent, ctx.getWhen());
CodeReviewCommit result = args.mergeUtil.mergeOneCommit(myIdent,
@@ -200,9 +186,9 @@
mergeTip.getCurrentTip(), toMerge);
result = amendGitlink(result);
mergeTip.moveTipTo(result, toMerge);
+ args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+ mergeTip.getCurrentTip(), args.alreadyAccepted);
}
- args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
- mergeTip.getCurrentTip(), args.alreadyAccepted);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
index 0e69128..bb58540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOp.java
@@ -25,6 +25,6 @@
@Override
protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
- args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
+ args.mergeTip.moveTipTo(toMerge, toMerge);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index 0e2cbd7..5b2e213 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -32,11 +32,15 @@
List<CodeReviewCommit> sorted =
args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
- CodeReviewCommit firstFastForward = args.mergeUtil.getFirstFastForward(
+
+ if (args.mergeTip.getInitialTip() == null || !args.submoduleOp
+ .hasSubscription(args.destBranch)) {
+ CodeReviewCommit firstFastForward = args.mergeUtil.getFirstFastForward(
args.mergeTip.getInitialTip(), args.rw, sorted);
- if (firstFastForward != null &&
- !firstFastForward.equals(args.mergeTip.getInitialTip())) {
- ops.add(new FastForwardOp(args, firstFastForward));
+ if (firstFastForward != null &&
+ !firstFastForward.equals(args.mergeTip.getInitialTip())) {
+ ops.add(new FastForwardOp(args, firstFastForward));
+ }
}
// For every other commit do a pair-wise merge.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index f183772d..3270fc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -63,7 +63,7 @@
while (!sorted.isEmpty()) {
CodeReviewCommit n = sorted.remove(0);
if (first && args.mergeTip.getInitialTip() == null) {
- ops.add(new RebaseUnbornRootOp(n));
+ ops.add(new FastForwardOp(args, n));
} else if (n.getParentCount() == 0) {
ops.add(new RebaseRootOp(n));
} else if (n.getParentCount() == 1) {
@@ -76,22 +76,6 @@
return ops;
}
- private class RebaseUnbornRootOp extends SubmitStrategyOp {
- private RebaseUnbornRootOp(CodeReviewCommit toMerge) {
- super(RebaseIfNecessary.this.args, toMerge);
- }
-
- @Override
- public void updateRepoImpl(RepoContext ctx) throws IntegrationException {
- // The branch is unborn. Take fast-forward resolution to create the
- // branch.
- toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- CodeReviewCommit newCommit = amendGitlink(toMerge);
- args.mergeTip.moveTipTo(newCommit, toMerge);
- acceptMergeTip(args.mergeTip);
- }
- }
-
private class RebaseRootOp extends SubmitStrategyOp {
private RebaseRootOp(CodeReviewCommit toMerge) {
super(RebaseIfNecessary.this.args, toMerge);
@@ -120,9 +104,11 @@
// TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
// When hoisting BatchUpdate into MergeOp, we will need to teach
// BatchUpdate how to produce CodeReviewRevWalks.
- if (args.mergeUtil.canFastForward(args.mergeSorter,
- args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
+ if (args.mergeUtil
+ .canFastForward(args.mergeSorter, args.mergeTip.getCurrentTip(),
+ args.rw, toMerge)) {
args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
+ toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
acceptMergeTip(args.mergeTip);
return;
}
@@ -193,9 +179,9 @@
// first parent. So instead behave as though MERGE_IF_NECESSARY was
// configured.
MergeTip mergeTip = args.mergeTip;
- if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
- mergeTip.moveTipTo(amendGitlink(toMerge), toMerge);
- acceptMergeTip(mergeTip);
+ if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge) &&
+ !args.submoduleOp.hasSubscription(args.destBranch)) {
+ mergeTip.moveTipTo(toMerge, toMerge);
} else {
CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
args.serverIdent, args.serverIdent, args.repo, args.rw,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index c784379..e60b947 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.git.strategy;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
@@ -70,12 +69,7 @@
return FluentIterable
.from(repo.getRefDatabase().getRefs(Constants.R_HEADS).values())
.append(repo.getRefDatabase().getRefs(Constants.R_TAGS).values())
- .transform(new Function<Ref, ObjectId>() {
- @Override
- public ObjectId apply(Ref r) {
- return r.getObjectId();
- }
- });
+ .transform(Ref::getObjectId);
}
public static Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index 36de70e..28e170f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -96,7 +96,8 @@
Set<RevCommit> alreadyAccepted,
RequestId submissionId,
NotifyHandling notifyHandling,
- SubmoduleOp submoduleOp);
+ SubmoduleOp submoduleOp,
+ boolean dryrun);
}
final AccountCache accountCache;
@@ -133,6 +134,7 @@
final ProjectState project;
final MergeSorter mergeSorter;
final MergeUtil mergeUtil;
+ final boolean dryrun;
@AssistedInject
Arguments(
@@ -165,7 +167,8 @@
@Assisted RequestId submissionId,
@Assisted SubmitType submitType,
@Assisted NotifyHandling notifyHandling,
- @Assisted SubmoduleOp submoduleOp) {
+ @Assisted SubmoduleOp submoduleOp,
+ @Assisted boolean dryrun) {
this.accountCache = accountCache;
this.approvalsUtil = approvalsUtil;
this.batchUpdateFactory = batchUpdateFactory;
@@ -196,6 +199,7 @@
this.submitType = submitType;
this.notifyHandling = notifyHandling;
this.submoduleOp = submoduleOp;
+ this.dryrun = dryrun;
this.project = checkNotNull(projectCache.get(destBranch.getParentKey()),
"project not found: %s", destBranch.getParentKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 6bb6fa6..146eda1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -54,12 +54,12 @@
Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
Branch.NameKey destBranch, IdentifiedUser caller, MergeTip mergeTip,
- CommitStatus commits, RequestId submissionId, NotifyHandling notifyHandling,
- SubmoduleOp submoduleOp)
+ CommitStatus commits, RequestId submissionId,
+ NotifyHandling notifyHandling, SubmoduleOp submoduleOp, boolean dryrun)
throws IntegrationException {
SubmitStrategy.Arguments args = argsFactory.create(submitType, destBranch,
commits, rw, caller, mergeTip, inserter, repo, canMergeFlag, db,
- alreadyAccepted, submissionId, notifyHandling, submoduleOp);
+ alreadyAccepted, submissionId, notifyHandling, submoduleOp, dryrun);
switch (submitType) {
case CHERRY_PICK:
return new CherryPick(args);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index 72bfedf..7dacc6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -33,7 +33,8 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -182,13 +183,7 @@
}
}
Collections.sort(commits, ReviewDbUtil.intKeyOrdering().reverse()
- .onResultOf(
- new Function<CodeReviewCommit, PatchSet.Id>() {
- @Override
- public PatchSet.Id apply(CodeReviewCommit in) {
- return in.getPatchsetId();
- }
- }));
+ .onResultOf(c -> c.getPatchsetId()));
CodeReviewCommit result = MergeUtil.findAnyMergedInto(rw, commits, tip);
if (result == null) {
return null;
@@ -335,15 +330,9 @@
byKey.put(psa.getKey(), psa);
}
- submitter = new PatchSetApproval(
- new PatchSetApproval.Key(
- psId,
- ctx.getAccountId(),
- LabelId.legacySubmit()),
- (short) 1, ctx.getWhen());
+ submitter = ApprovalsUtil.newApproval(
+ psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
byKey.put(submitter.getKey(), submitter);
- submitter.setValue((short) 1);
- submitter.setGranted(ctx.getWhen());
// Flatten out existing approvals for this patch set based upon the current
// permissions. Once the change is closed the approvals are not updated at
@@ -385,14 +374,11 @@
private static Function<PatchSetApproval, PatchSetApproval>
convertPatchSet(final PatchSet.Id psId) {
- return new Function<PatchSetApproval, PatchSetApproval>() {
- @Override
- public PatchSetApproval apply(PatchSetApproval in) {
- if (in.getPatchSetId().equals(psId)) {
- return in;
- }
- return new PatchSetApproval(psId, in);
+ return psa -> {
+ if (psa.getPatchSetId().equals(psId)) {
+ return psa;
}
+ return new PatchSetApproval(psId, psa);
};
}
@@ -403,14 +389,12 @@
private static Iterable<PatchSetApproval> zero(
Iterable<PatchSetApproval> approvals) {
- return Iterables.transform(approvals,
- new Function<PatchSetApproval, PatchSetApproval>() {
- @Override
- public PatchSetApproval apply(PatchSetApproval in) {
- PatchSetApproval copy = new PatchSetApproval(in.getPatchSetId(), in);
- copy.setValue((short) 0);
- return copy;
- }
+ return Iterables.transform(
+ approvals,
+ a -> {
+ PatchSetApproval copy = new PatchSetApproval(a.getPatchSetId(), a);
+ copy.setValue((short) 0);
+ return copy;
});
}
@@ -426,7 +410,7 @@
}
private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit,
- CommitMergeStatus s) {
+ CommitMergeStatus s) throws OrmException {
checkNotNull(s, "CommitMergeStatus may not be null");
String txt = s.getMessage();
if (s == CommitMergeStatus.CLEAN_MERGE) {
@@ -463,19 +447,9 @@
}
private ChangeMessage message(ChangeContext ctx, PatchSet.Id psId,
- String body) {
- checkNotNull(psId);
- String uuid;
- try {
- uuid = ChangeUtil.messageUUID(ctx.getDb());
- } catch (OrmException e) {
- return null;
- }
- ChangeMessage m = new ChangeMessage(
- new ChangeMessage.Key(psId.getParentKey(), uuid),
- ctx.getAccountId(), ctx.getWhen(), psId);
- m.setMessage(body);
- return m;
+ String body) throws OrmException {
+ return ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), body);
}
private void setMerged(ChangeContext ctx, ChangeMessage msg)
@@ -524,7 +498,7 @@
} catch (Exception e) {
log.error("Cannot email merged notification for " + getId(), e);
}
- if (mergeResultRev != null) {
+ if (mergeResultRev != null && !args.dryrun) {
args.changeMerged.fire(
updatedChange,
mergedPatchSet,
@@ -564,26 +538,18 @@
*/
protected CodeReviewCommit amendGitlink(CodeReviewCommit commit)
throws IntegrationException {
- CodeReviewCommit newCommit = commit;
- // Modify the commit with gitlink update
- if (args.submoduleOp.hasSubscription(args.destBranch)) {
- try {
- newCommit =
- args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
- newCommit.copyFrom(commit);
- if (commit.equals(toMerge)) {
- newCommit.setPatchsetId(ChangeUtil.nextPatchSetId(
- args.repo, toMerge.change().currentPatchSetId()));
- args.commits.put(newCommit);
- }
- } catch (SubmoduleException | IOException e) {
- throw new IntegrationException(
- "cannot update gitlink for the commit at branch: "
- + args.destBranch);
- }
+ if (!args.submoduleOp.hasSubscription(args.destBranch)) {
+ return commit;
}
- return newCommit;
+ // Modify the commit with gitlink update
+ try {
+ return args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
+ } catch (SubmoduleException | IOException e) {
+ throw new IntegrationException(
+ "cannot update gitlink for the commit at branch: "
+ + args.destBranch);
+ }
}
protected final void logDebug(String msg, Object... args) {
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..9cb09cc 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,124 @@
.getLogger(CommitValidators.class);
public enum Policy {
- /** Use {@link #validateForGerritCommits}. */
+ /** Use {@link Factory#forGerritCommits}. */
GERRIT,
- /** Use {@link #validateForReceiveCommits}. */
+ /** Use {@link Factory#forReceiveCommits}. */
RECEIVE_COMMITS,
+ /** Use {@link Factory#forMergedCommits}. */
+ MERGED,
+
/** 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 MERGED:
+ return forMergedCommits(refControl);
+ 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 forMergedCommits(RefControl refControl) {
+ // Generally only include validators that are based on permissions of the
+ // user creating a change for a merged commit; generally exclude
+ // validators that would require amending the change in order to correct.
+ //
+ // Examples:
+ // - Change-Id and Signed-off-by can't be added to an already-merged
+ // commit.
+ // - If the commit is banned, we can't ban it here. In fact, creating a
+ // review of a previously merged and recently-banned commit is a use
+ // case for post-commit code review: so reviewers have a place to
+ // discuss what to do about it.
+ // - Plugin validators may do things like require certain commit message
+ // formats, so we play it safe and exclude them.
+ return new CommitValidators(ImmutableList.of(
+ new UploadMergesPermissionValidator(refControl),
+ new AuthorUploaderValidator(refControl, canonicalWebUrl),
+ new CommitterUploaderValidator(refControl, canonicalWebUrl)));
+ }
+
+ private CommitValidators none() {
+ return new CommitValidators(ImmutableList.<CommitValidationListener>of());
+ }
}
- 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 +241,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 +278,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/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index d5d90d3..9e0be86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,7 +17,6 @@
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
@@ -56,7 +55,7 @@
*/
public class IndexModule extends LifecycleModule {
public enum IndexType {
- LUCENE
+ LUCENE, ELASTICSEARCH
}
public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS =
@@ -110,19 +109,11 @@
accounts,
changes);
Set<String> expected = FluentIterable.from(ALL_SCHEMA_DEFS)
- .transform(new Function<SchemaDefinitions<?>, String>() {
- @Override
- public String apply(SchemaDefinitions<?> in) {
- return in.getName();
- }
- }).toSet();
+ .transform(SchemaDefinitions::getName)
+ .toSet();
Set<String> actual = FluentIterable.from(result)
- .transform(new Function<IndexDefinition<?, ?, ?>, String>() {
- @Override
- public String apply(IndexDefinition<?, ?, ?> in) {
- return in.getName();
- }
- }).toSet();
+ .transform(IndexDefinition::getName)
+ .toSet();
if (!expected.equals(actual)) {
throw new ProvisionException(
"need index definitions for all schemas: "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
index 824739e..afe3f70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -14,14 +14,12 @@
package com.google.gerrit.server.index.account;
-import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
@@ -47,13 +45,7 @@
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return Iterables.transform(
- input.getExternalIds(),
- new Function<AccountExternalId, String>() {
- @Override
- public String apply(AccountExternalId in) {
- return in.getKey().get();
- }
- });
+ input.getExternalIds(), id -> id.getKey().get());
}
};
@@ -68,12 +60,7 @@
fullName,
Iterables.transform(
input.getExternalIds(),
- new Function<AccountExternalId, String>() {
- @Override
- public String apply(AccountExternalId in) {
- return in.getEmailAddress();
- }
- }));
+ AccountExternalId::getEmailAddress));
// Additional values not currently added by getPersonParts.
// TODO(dborowitz): Move to getPersonParts and remove this hack.
@@ -108,23 +95,11 @@
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getExternalIds())
- .transform(
- new Function<AccountExternalId, String>() {
- @Override
- public String apply(AccountExternalId in) {
- return in.getEmailAddress();
- }
- })
+ .transform(AccountExternalId::getEmailAddress)
.append(
Collections.singleton(input.getAccount().getPreferredEmail()))
.filter(Predicates.notNull())
- .transform(
- new Function<String, String>() {
- @Override
- public String apply(String in) {
- return in.toLowerCase();
- }
- })
+ .transform(String::toLowerCase)
.toSet();
}
};
@@ -153,12 +128,8 @@
@Override
public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getProjectWatches().keySet())
- .transform(new Function<ProjectWatchKey, String>() {
- @Override
- public String apply(ProjectWatchKey in) {
- return in.project().get();
- }
- }).toSet();
+ .transform(k -> k.project().get())
+ .toSet();
}
};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index d659215..22416ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -222,6 +222,10 @@
try (Repository repo = repoManager.openRepository(project);
ReviewDb db = schemaFactory.open()) {
Map<String, Ref> refs = repo.getRefDatabase().getRefs(ALL);
+ // TODO(dborowitz): Pre-loading all notes is almost certainly a
+ // terrible idea for performance. If we can get rid of walking by
+ // commit (see note below), then all we need to discover here is the
+ // change IDs.
for (ChangeNotes cn : notesFactory.scan(repo, db, project)) {
Ref r = refs.get(cn.getChange().currentPatchSetId().toRefName());
if (r != null) {
@@ -290,6 +294,9 @@
}
}
+ // TODO(dborowitz): This is basically pointless; it computes
+ // currentFilePaths faster than going through PatchListCache, but we
+ // still need to go through PatchListCache for changedLines.
RevCommit bCommit;
while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
if (byId.containsKey(bCommit)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index fe448c6..c683513 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -16,9 +16,9 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
@@ -31,10 +31,9 @@
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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.index.FieldDef;
@@ -75,6 +74,8 @@
* characters.
*/
public class ChangeField {
+ public static final int NO_ASSIGNEE = -1;
+
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
new FieldDef.Single<ChangeData, Integer>("legacy_id",
@@ -247,13 +248,9 @@
@Override
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
- return ImmutableSet.copyOf(Iterables.transform(input.hashtags(),
- new Function<String, String>() {
- @Override
- public String apply(String input) {
- return input.toLowerCase();
- }
- }));
+ return input.hashtags().stream()
+ .map(String::toLowerCase)
+ .collect(toSet());
}
};
@@ -264,13 +261,9 @@
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
- return ImmutableSet.copyOf(Iterables.transform(input.hashtags(),
- new Function<String, byte[]>() {
- @Override
- public byte[] apply(String hashtag) {
- return hashtag.getBytes(UTF_8);
- }
- }));
+ return input.hashtags().stream()
+ .map(t -> t.getBytes(UTF_8))
+ .collect(toSet());
}
};
@@ -300,26 +293,15 @@
}
};
- /** Reviewer(s) associated with the change. */
- @Deprecated
- public static final FieldDef<ChangeData, Iterable<Integer>> LEGACY_REVIEWER =
- new FieldDef.Repeatable<ChangeData, Integer>(
- ChangeQueryBuilder.FIELD_REVIEWER, FieldType.INTEGER, false) {
+ /** The user assigned to the change. */
+ public static final FieldDef<ChangeData, Integer> ASSIGNEE =
+ new FieldDef.Single<ChangeData, Integer>(
+ ChangeQueryBuilder.FIELD_ASSIGNEE, FieldType.INTEGER, false) {
@Override
- public Iterable<Integer> get(ChangeData input, FillArgs args)
+ public Integer get(ChangeData input, FillArgs args)
throws OrmException {
- Change c = input.change();
- if (c == null) {
- return ImmutableSet.of();
- }
- Set<Integer> r = new HashSet<>();
- if (!args.allowsDrafts && c.getStatus() == Change.Status.DRAFT) {
- return r;
- }
- for (PatchSetApproval a : input.approvals().values()) {
- r.add(a.getAccountId().get());
- }
- return r;
+ Account.Id id = input.change().getAssignee();
+ return id != null ? id.get() : NO_ASSIGNEE;
}
};
@@ -424,26 +406,47 @@
};
/** List of labels on the current patch set. */
+ @Deprecated
public static final FieldDef<ChangeData, Iterable<String>> LABEL =
new FieldDef.Repeatable<ChangeData, String>(
ChangeQueryBuilder.FIELD_LABEL, FieldType.EXACT, false) {
@Override
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
- Set<String> allApprovals = new HashSet<>();
- Set<String> distinctApprovals = new HashSet<>();
- for (PatchSetApproval a : input.currentApprovals()) {
- if (a.getValue() != 0 && !a.isLegacySubmit()) {
- allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
- a.getAccountId()));
- distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
- }
- }
- allApprovals.addAll(distinctApprovals);
- return allApprovals;
+ return getLabels(input, false);
}
};
+ /** List of labels on the current patch set including change owner votes. */
+ public static final FieldDef<ChangeData, Iterable<String>> LABEL2 =
+ new FieldDef.Repeatable<ChangeData, String>(
+ "label2", FieldType.EXACT, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return getLabels(input, true);
+ }
+ };
+
+ private static Iterable<String> getLabels(ChangeData input, boolean owners)
+ throws OrmException {
+ Set<String> allApprovals = new HashSet<>();
+ Set<String> distinctApprovals = new HashSet<>();
+ for (PatchSetApproval a : input.currentApprovals()) {
+ if (a.getValue() != 0 && !a.isLegacySubmit()) {
+ allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
+ a.getAccountId()));
+ if (owners && input.change().getOwner().equals(a.getAccountId())) {
+ allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
+ ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ }
+ distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
+ }
+ }
+ allApprovals.addAll(distinctApprovals);
+ return allApprovals;
+ }
+
public static Set<String> getAuthorParts(ChangeData cd) throws OrmException {
try {
return SchemaUtil.getPersonParts(cd.getAuthor());
@@ -539,7 +542,14 @@
public static String formatLabel(String label, int value, Account.Id accountId) {
return label.toLowerCase() + (value >= 0 ? "+" : "") + value
- + (accountId != null ? "," + accountId.get() : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "");
+ }
+
+ private static String formatAccount(Account.Id accountId) {
+ if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) {
+ return ChangeQueryBuilder.ARG_ID_OWNER;
+ }
+ return Integer.toString(accountId.get());
}
/** Commit message of the current patch set. */
@@ -564,8 +574,8 @@
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
Set<String> r = new HashSet<>();
- for (PatchLineComment c : input.publishedComments()) {
- r.add(c.getMessage());
+ for (Comment c : input.publishedComments()) {
+ r.add(c.message);
}
for (ChangeMessage m : input.messages()) {
r.add(m.getMessage());
@@ -642,31 +652,13 @@
r.add(m.getAuthor().get());
}
}
- for (PatchLineComment c : input.publishedComments()) {
- r.add(c.getAuthor().get());
+ for (Comment c : input.publishedComments()) {
+ r.add(c.author.getId().get());
}
return r;
}
};
- /** Users who have starred this change. */
- @Deprecated
- public static final FieldDef<ChangeData, Iterable<Integer>> STARREDBY =
- new FieldDef.Repeatable<ChangeData, Integer>(
- ChangeQueryBuilder.FIELD_STARREDBY, FieldType.INTEGER, true) {
- @Override
- public Iterable<Integer> get(ChangeData input, FillArgs args)
- throws OrmException {
- return Iterables.transform(input.starredBy(),
- new Function<Account.Id, Integer>() {
- @Override
- public Integer apply(Account.Id accountId) {
- return accountId.get();
- }
- });
- }
- };
-
/**
* Star labels on this change in the format: <account-id>:<label>
*/
@@ -676,14 +668,12 @@
@Override
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
- return Iterables.transform(input.stars().entries(),
- new Function<Map.Entry<Account.Id, String>, String>() {
- @Override
- public String apply(Map.Entry<Account.Id, String> e) {
- return StarredChangesUtil.StarField.create(
- e.getKey(), e.getValue()).toString();
- }
- });
+ return Iterables.transform(
+ input.stars().entries(),
+ (Map.Entry<Account.Id, String> e) -> {
+ return StarredChangesUtil.StarField.create(
+ e.getKey(), e.getValue()).toString();
+ });
}
};
@@ -694,8 +684,7 @@
@Override
public Iterable<Integer> get(ChangeData input, FillArgs args)
throws OrmException {
- return Iterables.transform(input.stars().keySet(),
- ReviewDbUtil.INT_KEY_FUNCTION);
+ return Iterables.transform(input.stars().keySet(), Account.Id::get);
}
};
@@ -740,13 +729,9 @@
@Override
public Iterable<Integer> get(ChangeData input, FillArgs args)
throws OrmException {
- return ImmutableSet.copyOf(Iterables.transform(input.editsByUser(),
- new Function<Account.Id, Integer>() {
- @Override
- public Integer apply(Account.Id account) {
- return account.get();
- }
- }));
+ return input.editsByUser().stream()
+ .map(Account.Id::get)
+ .collect(toSet());
}
};
@@ -758,13 +743,9 @@
@Override
public Iterable<Integer> get(ChangeData input, FillArgs args)
throws OrmException {
- return ImmutableSet.copyOf(Iterables.transform(input.draftsByUser(),
- new Function<Account.Id, Integer>() {
- @Override
- public Integer apply(Account.Id account) {
- return account.get();
- }
- }));
+ return input.draftsByUser().stream()
+ .map(Account.Id::get)
+ .collect(toSet());
}
};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index c98d311..3c0ff0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -22,7 +22,7 @@
public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
@Deprecated
- static final Schema<ChangeData> V25 = schema(
+ static final Schema<ChangeData> V32 = schema(
ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
@@ -35,7 +35,6 @@
ChangeField.FILE_PART,
ChangeField.PATH,
ChangeField.OWNER,
- ChangeField.LEGACY_REVIEWER,
ChangeField.COMMIT,
ChangeField.TR,
ChangeField.LABEL,
@@ -56,36 +55,22 @@
ChangeField.REVIEWEDBY,
ChangeField.EXACT_COMMIT,
ChangeField.AUTHOR,
- ChangeField.COMMITTER);
+ ChangeField.COMMITTER,
+ ChangeField.DRAFTBY,
+ ChangeField.HASHTAG_CASE_AWARE,
+ ChangeField.STAR,
+ ChangeField.STARBY,
+ ChangeField.REVIEWER);
@Deprecated
- static final Schema<ChangeData> V26 = schema(V25, ChangeField.DRAFTBY);
-
- @Deprecated
- static final Schema<ChangeData> V27 = schema(V26.getFields().values());
-
- @Deprecated
- static final Schema<ChangeData> V28 = schema(V27, ChangeField.STARREDBY);
-
- @Deprecated
- static final Schema<ChangeData> V29 =
- schema(V28, ChangeField.HASHTAG_CASE_AWARE);
-
- @Deprecated
- static final Schema<ChangeData> V30 =
- schema(V29, ChangeField.STAR, ChangeField.STARBY);
-
- @Deprecated
- static final Schema<ChangeData> V31 = new Schema.Builder<ChangeData>()
- .add(V30)
- .remove(ChangeField.STARREDBY)
- .build();
+ static final Schema<ChangeData> V33 =
+ schema(V32, ChangeField.ASSIGNEE);
@SuppressWarnings("deprecation")
- static final Schema<ChangeData> V32 = new Schema.Builder<ChangeData>()
- .add(V31)
- .remove(ChangeField.LEGACY_REVIEWER)
- .add(ChangeField.REVIEWER)
+ static final Schema<ChangeData> V34 = new Schema.Builder<ChangeData>()
+ .add(V33)
+ .remove(ChangeField.LABEL)
+ .add(ChangeField.LABEL2)
.build();
public static final String NAME = "changes";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 996caa7..3e0678d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -19,7 +19,6 @@
import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.Change;
@@ -94,12 +93,9 @@
public Iterator<ChangeData> iterator() {
return Iterables.transform(
rs,
- new Function<ChangeData, ChangeData>() {
- @Override
- public ChangeData apply(ChangeData cd) {
- fromSource.put(cd, currSource);
- return cd;
- }
+ cd -> {
+ fromSource.put(cd, currSource);
+ return cd;
}).iterator();
}
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..f1e609e 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,14 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("Abandoned.vm"));
+ appendText(textTemplate("Abandoned"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("AbandonedHtml"));
+ }
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
}
}
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..40492bd 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,10 @@
@Override
protected void format() throws EmailException {
- appendText(velocifyFile("AddKey.vm"));
+ appendText(textTemplate("AddKey"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("AddKeyHtml"));
+ }
}
public String getEmail() {
@@ -110,4 +113,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 supportsHtml() {
+ return true;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index c9e42ad..5f61353 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.mail;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtorm.server.OrmException;
@@ -24,15 +26,20 @@
/** Asks a user to review a change. */
public class AddReviewerSender extends NewChangeSender {
public interface Factory {
- AddReviewerSender create(Project.NameKey project, Change.Id id);
+ AddReviewerSender create(Project.NameKey project, Change.Id id,
+ NotifyHandling notify);
}
@Inject
public AddReviewerSender(EmailArguments ea,
@Assisted Project.NameKey project,
- @Assisted Change.Id id)
+ @Assisted Change.Id id,
+ @Assisted @Nullable NotifyHandling notify)
throws OrmException {
super(ea, newChangeData(ea, project, id));
+ if (notify != null) {
+ setNotify(notify);
+ }
}
@Override
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..df0f018 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,10 @@
@Override
protected void format() throws EmailException {
formatChange();
- appendText(velocifyFile("ChangeFooter.vm"));
+ appendText(textTemplate("ChangeFooter"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("ChangeFooterHtml"));
+ }
try {
TreeSet<String> names = new TreeSet<>();
for (Account.Id who : changeData.reviewers().all()) {
@@ -199,7 +203,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. */
@@ -255,7 +259,7 @@
detail.append("---\n");
PatchList patchList = getPatchList();
for (PatchListEntry p : patchList.getPatches()) {
- if (Patch.COMMIT_MSG.equals(p.getNewName())) {
+ if (Patch.isMagic(p.getNewName())) {
continue;
}
detail.append(p.getChangeType().getCode())
@@ -435,11 +439,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..dd8bbaa 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
@@ -14,21 +14,19 @@
package com.google.gerrit.server.mail;
-import static com.google.gerrit.server.PatchLineCommentsUtil.getCommentPsId;
-
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.patch.PatchFile;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -47,7 +45,8 @@
import java.util.List;
import java.util.Set;
-/** Send comments, after the author of them hit used Publish Comments in the UI. */
+/** Send comments, after the author of them hit used Publish Comments in the UI.
+ */
public class CommentSender extends ReplyToChangeSender {
private static final Logger log = LoggerFactory
.getLogger(CommentSender.class);
@@ -56,27 +55,25 @@
CommentSender create(Project.NameKey project, Change.Id id);
}
- private List<PatchLineComment> inlineComments = Collections.emptyList();
- private final PatchLineCommentsUtil plcUtil;
+ private List<Comment> inlineComments = Collections.emptyList();
+ private final CommentsUtil commentsUtil;
@Inject
public CommentSender(EmailArguments ea,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
@Assisted Project.NameKey project,
@Assisted Change.Id id) throws OrmException {
super(ea, "comment", newChangeData(ea, project, id));
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
- public void setPatchLineComments(final List<PatchLineComment> plc)
- throws OrmException {
- inlineComments = plc;
+ public void setComments(List<Comment> comments) throws OrmException {
+ inlineComments = comments;
Set<String> paths = new HashSet<>();
- for (PatchLineComment c : plc) {
- Patch.Key p = c.getKey().getParentKey();
- if (!Patch.COMMIT_MSG.equals(p.getFileName())) {
- paths.add(p.getFileName());
+ for (Comment c : comments) {
+ if (!Patch.isMagic(c.key.filename)) {
+ paths.add(c.key.filename);
}
}
changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths));
@@ -97,12 +94,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() {
@@ -125,31 +122,34 @@
}
}
- Patch.Key currentFileKey = null;
+ String currentFileName = null;
+ int currentPatchSetId = -1;
PatchFile currentFileData = null;
- for (final PatchLineComment c : inlineComments) {
- final Patch.Key pk = c.getKey().getParentKey();
-
- if (!pk.equals(currentFileKey)) {
- String link = makeLink(pk);
+ for (Comment c : inlineComments) {
+ if (!c.key.filename.equals(currentFileName)
+ || c.key.patchSetId != currentPatchSetId) {
+ String link = makeLink(change.getId(), c.key);
if (link != null) {
cmts.append(link).append('\n');
}
- if (Patch.COMMIT_MSG.equals(pk.get())) {
+ if (Patch.COMMIT_MSG.equals(c.key.filename)) {
cmts.append("Commit Message:\n\n");
+ } else if (Patch.MERGE_LIST.equals(c.key.filename)) {
+ cmts.append("Merge List:\n\n");
} else {
- cmts.append("File ").append(pk.get()).append(":\n\n");
+ cmts.append("File ").append(c.key.filename).append(":\n\n");
}
- currentFileKey = pk;
+ currentFileName = c.key.filename;
+ currentPatchSetId = c.key.patchSetId;
if (patchList != null) {
try {
currentFileData =
- new PatchFile(repo, patchList, pk.get());
+ new PatchFile(repo, patchList, c.key.filename);
} catch (IOException e) {
log.warn(String.format(
"Cannot load %s from %s in %s",
- pk.getFileName(),
+ c.key.filename,
patchList.getNewId().name(),
projectState.getProject().getName()), e);
currentFileData = null;
@@ -167,42 +167,54 @@
}
private void appendComment(StringBuilder out, int contextLines,
- PatchFile currentFileData, PatchLineComment comment) {
- short side = comment.getSide();
- CommentRange range = comment.getRange();
+ PatchFile currentFileData, Comment comment) {
+ if (comment instanceof RobotComment) {
+ RobotComment robotComment = (RobotComment) comment;
+ out.append("Robot Comment from ")
+ .append(robotComment.robotId)
+ .append(" (run ID ")
+ .append(robotComment.robotRunId)
+ .append("):\n");
+ }
+ short side = comment.side;
+ Comment.Range range = comment.range;
if (range != null) {
- String prefix = "PS" + getCommentPsId(comment).get()
- + ", Line " + range.getStartLine() + ": ";
- for (int n = range.getStartLine(); n <= range.getEndLine(); n++) {
- out.append(n == range.getStartLine()
+ String prefix = "PS" + comment.key.patchSetId
+ + ", Line " + range.startLine + ": ";
+ for (int n = range.startLine; n <= range.endLine; n++) {
+ out.append(n == range.startLine
? prefix
: Strings.padStart(": ", prefix.length(), ' '));
- try {
- String s = currentFileData.getLine(side, n);
- if (n == range.getStartLine() && n == range.getEndLine()) {
- s = s.substring(
- Math.min(range.getStartCharacter(), s.length()),
- Math.min(range.getEndCharacter(), s.length()));
- } else if (n == range.getStartLine()) {
- s = s.substring(Math.min(range.getStartCharacter(), s.length()));
- } else if (n == range.getEndLine()) {
- s = s.substring(0, Math.min(range.getEndCharacter(), s.length()));
- }
- out.append(s);
- } catch (Throwable e) {
- // Don't quote the line if we can't safely convert it.
+ String s = getLine(currentFileData, side, n);
+ if (n == range.startLine && n == range.endLine) {
+ s = s.substring(
+ Math.min(range.startChar, s.length()),
+ Math.min(range.endChar, s.length()));
+ } else if (n == range.startLine) {
+ s = s.substring(Math.min(range.startChar, s.length()));
+ } else if (n == range.endLine) {
+ s = s.substring(0, Math.min(range.endChar, s.length()));
}
- out.append('\n');
+ out.append(s).append('\n');
}
appendQuotedParent(out, comment);
- out.append(comment.getMessage().trim()).append('\n');
+ out.append(comment.message.trim()).append('\n');
} else {
- int lineNbr = comment.getLine();
- int maxLines;
+ int lineNbr = comment.lineNbr;
+
+ // Initialize maxLines to the known line number.
+ int maxLines = lineNbr;
+
try {
maxLines = currentFileData.getLineCount(side);
- } catch (Throwable e) {
- maxLines = lineNbr;
+ } catch (IOException err) {
+ // The file could not be read, leave the max as is.
+ log.warn(String.format("Failed to read file %s on side %d",
+ comment.key.filename, side), err);
+ } catch (NoSuchEntityException err) {
+ // The file could not be read, leave the max as is.
+ log.warn(String.format("Side %d of file %s didn't exist",
+ side, comment.key.filename), err);
}
final int startLine = Math.max(1, lineNbr - contextLines + 1);
@@ -212,7 +224,7 @@
appendFileLine(out, currentFileData, side, line);
}
appendQuotedParent(out, comment);
- out.append(comment.getMessage().trim()).append('\n');
+ out.append(comment.message.trim()).append('\n');
for (int line = lineNbr + 1; line < stopLine; ++line) {
appendFileLine(out, currentFileData, side, line);
@@ -220,33 +232,30 @@
}
}
- private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
- cmts.append("Line " + line);
- try {
- final String lineStr = fileData.getLine(side, line);
- cmts.append(": ");
- cmts.append(lineStr);
- } catch (Throwable e) {
- // Don't quote the line if we can't safely convert it.
- }
- cmts.append("\n");
+ private void appendFileLine(StringBuilder cmts, PatchFile fileData,
+ short side, int line) {
+ String lineStr = getLine(fileData, side, line);
+ cmts.append("Line ")
+ .append(line)
+ .append(": ")
+ .append(lineStr)
+ .append("\n");
}
- private void appendQuotedParent(StringBuilder out, PatchLineComment child) {
- if (child.getParentUuid() != null) {
- Optional<PatchLineComment> parent;
- PatchLineComment.Key key = new PatchLineComment.Key(
- child.getKey().getParentKey(),
- child.getParentUuid());
+ private void appendQuotedParent(StringBuilder out, Comment child) {
+ if (child.parentUuid != null) {
+ Optional<Comment> parent;
+ Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename,
+ child.key.patchSetId);
try {
- parent = plcUtil.get(args.db.get(), changeData.notes(), key);
+ parent = commentsUtil.get(args.db.get(), changeData.notes(), key);
} catch (OrmException e) {
log.warn("Could not find the parent of this comment: "
+ child.toString());
parent = Optional.absent();
}
if (parent.isPresent()) {
- String msg = parent.get().getMessage().trim();
+ String msg = parent.get().message.trim();
if (msg.length() > 75) {
msg = msg.substring(0, 75);
}
@@ -260,19 +269,17 @@
}
// Makes a link back to the given patch set and file.
- private String makeLink(Patch.Key patch) {
+ private String makeLink(Change.Id changeId, Comment.Key key) {
String url = getGerritUrl();
if (url == null) {
return null;
}
- PatchSet.Id ps = patch.getParentKey();
- Change.Id c = ps.getParentKey();
return new StringBuilder()
.append(url)
- .append("#/c/").append(c)
- .append('/').append(ps.get())
- .append('/').append(KeyUtil.encode(patch.get()))
+ .append("#/c/").append(changeId)
+ .append('/').append(key.patchSetId)
+ .append('/').append(KeyUtil.encode(key.filename))
.toString();
}
@@ -283,4 +290,31 @@
return null;
}
}
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+ soyContextEmailData.put("inlineComments", getInlineComments());
+ soyContextEmailData.put("hasInlineComments", hasInlineComments());
+ }
+
+ private String getLine(PatchFile fileInfo, short side, int lineNbr) {
+ try {
+ return fileInfo.getLine(side, lineNbr);
+ } catch (IOException err) {
+ // Default to the empty string if the file cannot be safely read.
+ log.warn(String.format("Failed to read file on side %d", side), err);
+ return "";
+ } catch (IndexOutOfBoundsException err) {
+ // Default to the empty string if the given line number does not appear
+ // in the file.
+ log.warn(String.format("Failed to get line number of file on side %d",
+ side), err);
+ return "";
+ } catch (NoSuchEntityException err) {
+ // Default to the empty string if the side cannot be found.
+ log.warn(String.format("Side %d of file didn't exist", side), err);
+ return "";
+ }
+ }
}
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..ec4a728 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,10 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("DeleteReviewer.vm"));
+ appendText(textTemplate("DeleteReviewer"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("DeleteReviewerHtml"));
+ }
}
public List<String> getReviewerNames() {
@@ -78,4 +81,15 @@
}
return names;
}
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+ soyContextEmailData.put("reviewerNames", getReviewerNames());
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
}
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..6257deb 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,14 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("DeleteVote.vm"));
+ appendText(textTemplate("DeleteVote"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("DeleteVoteHtml"));
+ }
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
}
}
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/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
index a7a1028..d36225a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.EmailException;
import java.util.Collection;
@@ -32,7 +33,36 @@
boolean canEmail(String address);
/**
- * Sends an email message.
+ * Sends an email message. Messages always contain a text body, but messages
+ * can optionally include an additional HTML body. If both body types are
+ * present, {@code send} should construct a {@code multipart/alternative}
+ * message with an appropriately-selected boundary.
+ *
+ * @param from who the message is from.
+ * @param rcpt one or more address where the message will be delivered to.
+ * This list overrides any To or CC headers in {@code headers}.
+ * @param headers message headers.
+ * @param textBody text to appear in the {@code text/plain} body of the
+ * message.
+ * @param htmlBody optional HTML code to appear in the {@code text/html} body
+ * of the message.
+ * @throws EmailException the message cannot be sent.
+ */
+ default void send(Address from, Collection<Address> rcpt,
+ Map<String, EmailHeader> headers, String textBody,
+ @Nullable String htmlBody) throws EmailException {
+ send(from, rcpt, headers, textBody);
+ }
+
+ /**
+ * Sends an email message with a text body only (i.e. not HTML or multipart).
+ *
+ * Authors of new implementations of this interface should not use this method
+ * to send a message because this method does not accept the HTML body.
+ * Instead, authors should use the above signature of {@code send}.
+ *
+ * This version of the method is preserved for support of legacy
+ * implementations.
*
* @param from who the message is from.
* @param rcpt one or more address where the message will be delivered to.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
index 3c14f2f..2f2fd8c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -22,11 +22,13 @@
@Singleton
public class EmailSettings {
+ public final boolean html;
public final boolean includeDiff;
public final int maximumDiffSize;
@Inject
EmailSettings(@GerritServerConfig Config cfg) {
+ html = cfg.getBoolean("sendemail", "html", true);
includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
}
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..26bd99e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.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.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",
+ "AbandonedHtml.soy",
+ "AddKey.soy",
+ "AddKeyHtml.soy",
+ "ChangeFooter.soy",
+ "ChangeFooterHtml.soy",
+ "ChangeSubject.soy",
+ "Comment.soy",
+ "CommentFooter.soy",
+ "DeleteReviewer.soy",
+ "DeleteReviewerHtml.soy",
+ "DeleteVote.soy",
+ "DeleteVoteHtml.soy",
+ "Footer.soy",
+ "FooterHtml.soy",
+ "HeaderHtml.soy",
+ "Merged.soy",
+ "MergedHtml.soy",
+ "NewChange.soy",
+ "NewChangeHtml.soy",
+ "RegisterNewEmail.soy",
+ "ReplacePatchSet.soy",
+ "ReplacePatchSetHtml.soy",
+ "Restored.soy",
+ "RestoredHtml.soy",
+ "Reverted.soy",
+ "RevertedHtml.soy",
+ "ViewChangeButton.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..17a0854 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,11 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("Merged.vm"));
+ appendText(textTemplate("Merged"));
+
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("MergedHtml"));
+ }
}
public String getApprovals() {
@@ -123,4 +127,15 @@
txt.append('\n');
return txt.toString();
}
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+ soyContextEmailData.put("approvals", getApprovals());
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
}
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..05a709d 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,10 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("NewChange.vm"));
+ appendText(textTemplate("NewChange"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("NewChangeHtml"));
+ }
}
public List<String> getReviewerNames() {
@@ -80,4 +83,15 @@
}
return names;
}
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+ soyContextEmailData.put("reviewerNames", getReviewerNames());
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
}
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..1565567 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.
@@ -28,6 +28,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 +44,11 @@
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.Date;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -63,9 +67,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;
@@ -101,8 +107,14 @@
}
init();
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("HeaderHtml"));
+ }
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 +148,20 @@
}
}
+ 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();
+
+ va.body = textPart;
+ if (useHtml()) {
+ va.htmlBody = htmlBody.toString();
+ } else {
+ va.htmlBody = null;
+ }
+
for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
try {
validator.validateOutgoingEmail(va);
@@ -150,7 +170,8 @@
}
}
- args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body);
+ args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers,
+ va.body, va.htmlBody);
}
}
@@ -164,6 +185,7 @@
*/
protected void init() throws EmailException {
setupVelocityContext();
+ setupSoyContext();
smtpFromAddress = args.fromAddressGenerator.from(fromId);
setHeader("Date", new Date());
@@ -185,7 +207,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 +283,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 +364,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 +458,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 +505,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 +577,13 @@
private static String safeToString(Object obj) {
return obj != null ? obj.toString() : "";
}
+
+ protected final boolean useHtml() {
+ return args.settings.html && supportsHtml();
+ }
+
+ /** Override this method to enable HTML in a subclass. */
+ protected boolean supportsHtml() {
+ 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..3f1e356 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,34 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("ReplacePatchSet.vm"));
+ appendText(textTemplate("ReplacePatchSet"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("ReplacePatchSetHtml"));
+ }
}
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());
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
}
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..45a45c7 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,14 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("Restored.vm"));
+ appendText(textTemplate("Restored"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("RestoredHtml"));
+ }
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
}
}
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..0734a3c 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,14 @@
@Override
protected void formatChange() throws EmailException {
- appendText(velocifyFile("Reverted.vm"));
+ appendText(textTemplate("Reverted"));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("RevertedHtml"));
+ }
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index e263c6a..3b4fcfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -16,7 +16,9 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.Version;
import com.google.gerrit.common.errors.EmailException;
@@ -42,6 +44,7 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/** Sends email via a nearby SMTP server. */
@@ -146,8 +149,15 @@
@Override
public void send(final Address from, Collection<Address> rcpt,
- final Map<String, EmailHeader> callerHeaders, final String body)
+ final Map<String, EmailHeader> callerHeaders, String body)
throws EmailException {
+ send(from, rcpt, callerHeaders, body, null);
+ }
+
+ @Override
+ public void send(final Address from, Collection<Address> rcpt,
+ final Map<String, EmailHeader> callerHeaders, String textBody,
+ @Nullable String htmlBody) throws EmailException {
if (!isEnabled()) {
throw new EmailException("Sending email is disabled");
}
@@ -155,7 +165,6 @@
final Map<String, EmailHeader> hdrs =
new LinkedHashMap<>(callerHeaders);
setMissingHeader(hdrs, "MIME-Version", "1.0");
- setMissingHeader(hdrs, "Content-Type", "text/plain; charset=UTF-8");
setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit");
setMissingHeader(hdrs, "Content-Disposition", "inline");
setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion());
@@ -169,6 +178,18 @@
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
}
+ String encodedBody;
+ if (htmlBody == null) {
+ setMissingHeader(hdrs, "Content-Type", "text/plain; charset=UTF-8");
+ encodedBody = textBody;
+ } else {
+ String boundary = generateMultipartBoundary(textBody, htmlBody);
+ setMissingHeader(hdrs, "Content-Type", "multipart/alternative; "
+ + "boundary=\"" + boundary + "\"; "
+ + "charset=UTF-8");
+ encodedBody = buildMultipartBody(boundary, textBody, htmlBody);
+ }
+
StringBuffer rejected = new StringBuffer();
try {
final SMTPClient client = open();
@@ -214,7 +235,7 @@
}
w.write("\r\n");
- w.write(body);
+ w.write(encodedBody);
w.flush();
}
@@ -235,6 +256,49 @@
}
}
+ public static 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");
+ }
+
+ 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";
+ }
+
private void setMissingHeader(final Map<String, EmailHeader> hdrs,
final String name, final String value) {
if (!hdrs.containsKey(name) || hdrs.get(name).isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 679a9de..3c669f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 70a5f4f..fa23b80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -20,6 +20,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
@@ -45,6 +46,7 @@
protected final ChangeNoteUtil noteUtil;
protected final String anonymousCowardName;
protected final Account.Id accountId;
+ protected final Account.Id realAccountId;
protected final PersonIdent authorIdent;
protected final Date when;
@@ -69,6 +71,9 @@
this.notes = ctl.getNotes();
this.change = notes.getChange();
this.accountId = accountId(ctl.getUser());
+ Account.Id realAccountId = accountId(ctl.getUser().getRealUser());
+ this.realAccountId =
+ realAccountId != null ? realAccountId : accountId;
this.authorIdent =
ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when);
this.when = when;
@@ -82,6 +87,7 @@
@Nullable ChangeNotes notes,
@Nullable Change change,
Account.Id accountId,
+ Account.Id realAccountId,
PersonIdent authorIdent,
Date when) {
checkArgument(
@@ -95,6 +101,7 @@
this.notes = notes;
this.change = change != null ? change : notes.getChange();
this.accountId = accountId;
+ this.realAccountId = realAccountId;
this.authorIdent = authorIdent;
this.when = when;
}
@@ -255,4 +262,18 @@
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
return ins.insert(Constants.OBJ_TREE, new byte[] {});
}
+
+ protected void verifyComment(Comment c) {
+ checkArgument(c.revId != null, "RevId required for comment: %s", c);
+ checkArgument(
+ c.author.getId().equals(getAccountId()),
+ "The author for the following comment does not match the author of"
+ + " this %s (%s): %s",
+ getClass().getSimpleName(), getAccountId(), c);
+ checkArgument(
+ c.getRealAuthor().getId().equals(realAccountId),
+ "The real author for the following comment does not match the real"
+ + " author of this %s (%s): %s",
+ getClass().getSimpleName(), realAccountId, c);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index e15af9d..3322776 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -50,9 +50,9 @@
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.server.OrmException;
@@ -83,35 +83,18 @@
REVIEW_DB, NOTE_DB;
}
- public static ChangeBundle fromReviewDb(ReviewDb db, Change.Id id)
- throws OrmException {
- db.changes().beginTransaction(id);
- try {
- List<PatchSetApproval> approvals =
- db.patchSetApprovals().byChange(id).toList();
- return new ChangeBundle(
- db.changes().get(id),
- db.changeMessages().byChange(id),
- db.patchSets().byChange(id),
- approvals,
- db.patchComments().byChange(id),
- ReviewerSet.fromApprovals(approvals),
- Source.REVIEW_DB);
- } finally {
- db.rollback();
- }
- }
-
- public static ChangeBundle fromNotes(PatchLineCommentsUtil plcUtil,
+ public static ChangeBundle fromNotes(CommentsUtil commentsUtil,
ChangeNotes notes) throws OrmException {
return new ChangeBundle(
notes.getChange(),
notes.getChangeMessages(),
notes.getPatchSets().values(),
notes.getApprovals().values(),
- Iterables.concat(
- plcUtil.draftByChange(null, notes),
- plcUtil.publishedByChange(null, notes)),
+ Iterables.concat(CommentsUtil.toPatchLineComments(notes.getChangeId(),
+ PatchLineComment.Status.DRAFT, commentsUtil.draftByChange(null, notes)),
+ CommentsUtil.toPatchLineComments(notes.getChangeId(),
+ PatchLineComment.Status.PUBLISHED,
+ commentsUtil.publishedByChange(null, notes))),
notes.getReviewers(),
Source.NOTE_DB);
}
@@ -241,18 +224,15 @@
checkColumns(Change.Id.class, 1);
checkColumns(Change.class,
- 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18,
- // TODO(dborowitz): It's potentially possible to compare noteDbState in
- // the Change with the state implied by a ChangeNotes.
- 101);
+ 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 101);
checkColumns(ChangeMessage.Key.class, 1, 2);
- checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6);
+ checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7);
checkColumns(PatchSet.Id.class, 1, 2);
checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
- checkColumns(PatchSetApproval.class, 1, 2, 3, 6);
+ checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7);
checkColumns(PatchLineComment.Key.class, 1, 2);
- checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+ checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
}
private final Change change;
@@ -367,54 +347,36 @@
private Map<PatchSetApproval.Key, PatchSetApproval>
filterPatchSetApprovals() {
- return limitToValidPatchSets(patchSetApprovals,
- new Function<PatchSetApproval.Key, PatchSet.Id>() {
- @Override
- public PatchSet.Id apply(PatchSetApproval.Key in) {
- return in.getParentKey();
- }
- });
+ return limitToValidPatchSets(
+ patchSetApprovals, PatchSetApproval.Key::getParentKey);
}
private Map<PatchLineComment.Key, PatchLineComment>
filterPatchLineComments() {
- return limitToValidPatchSets(patchLineComments,
- new Function<PatchLineComment.Key, PatchSet.Id>() {
- @Override
- public PatchSet.Id apply(PatchLineComment.Key in) {
- return in.getParentKey().getParentKey();
- }
- });
+ return limitToValidPatchSets(
+ patchLineComments,
+ k -> k.getParentKey().getParentKey());
}
private <K, V> Map<K, V> limitToValidPatchSets(Map<K, V> in,
- final Function<K, PatchSet.Id> func) {
+ Function<K, PatchSet.Id> func) {
return Maps.filterKeys(
in, Predicates.compose(validPatchSetPredicate(), func));
}
private Predicate<PatchSet.Id> validPatchSetPredicate() {
- final Predicate<PatchSet.Id> upToCurrent = upToCurrentPredicate();
- return new Predicate<PatchSet.Id>() {
- @Override
- public boolean apply(PatchSet.Id in) {
- return upToCurrent.apply(in) && patchSets.containsKey(in);
- }
- };
+ Predicate<PatchSet.Id> upToCurrent = upToCurrentPredicate();
+ return p -> upToCurrent.apply(p) && patchSets.containsKey(p);
}
private Collection<ChangeMessage> filterChangeMessages() {
final Predicate<PatchSet.Id> validPatchSet = validPatchSetPredicate();
- return Collections2.filter(changeMessages,
- new Predicate<ChangeMessage>() {
- @Override
- public boolean apply(ChangeMessage in) {
- PatchSet.Id psId = in.getPatchSetId();
- if (psId == null) {
- return true;
- }
- return validPatchSet.apply(psId);
+ return Collections2.filter(changeMessages, m -> {
+ PatchSet.Id psId = m.getPatchSetId();
+ if (psId == null) {
+ return true;
}
+ return validPatchSet.apply(psId);
});
}
@@ -423,13 +385,8 @@
if (current == null) {
return Predicates.alwaysFalse();
}
- final int max = current.get();
- return new Predicate<PatchSet.Id>() {
- @Override
- public boolean apply(PatchSet.Id in) {
- return in.get() <= max;
- }
- };
+ int max = current.get();
+ return p -> p.get() <= max;
}
private Map<PatchSet.Id, PatchSet> filterPatchSets() {
@@ -669,17 +626,31 @@
List<String> tempDiffs = new ArrayList<>();
String temp = "temp";
+ // ReviewDb allows timestamps before patch set was created, but NoteDb
+ // truncates this to the patch set creation timestamp.
+ Timestamp ta = a.getWrittenOn();
+ Timestamp tb = b.getWrittenOn();
+ PatchSet psa = bundleA.patchSets.get(a.getPatchSetId());
+ PatchSet psb = bundleB.patchSets.get(b.getPatchSetId());
boolean excludePatchSet = false;
+ boolean excludeWrittenOn = false;
if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
excludePatchSet = a.getPatchSetId() == null;
+ excludeWrittenOn = psa != null && psb != null
+ && ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn());
} else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
excludePatchSet = b.getPatchSetId() == null;
+ excludeWrittenOn = psa != null && psb != null
+ && tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn());
}
List<String> exclude = Lists.newArrayList("key");
if (excludePatchSet) {
exclude.add("patchset");
}
+ if (excludeWrittenOn) {
+ exclude.add("writtenOn");
+ }
diffColumnsExcluding(
tempDiffs, ChangeMessage.class, temp, bundleA, a, bundleB, b, exclude);
@@ -718,7 +689,28 @@
PatchSetApproval a = as.get(k);
PatchSetApproval b = bs.get(k);
String desc = describe(k);
- diffColumns(diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b);
+
+ // ReviewDb allows timestamps before patch set was created, but NoteDb
+ // truncates this to the patch set creation timestamp.
+ Timestamp ta = a.getGranted();
+ Timestamp tb = b.getGranted();
+ PatchSet psa = checkNotNull(bundleA.patchSets.get(a.getPatchSetId()));
+ PatchSet psb = checkNotNull(bundleB.patchSets.get(b.getPatchSetId()));
+ boolean excludeGranted = false;
+ List<String> exclude = new ArrayList<>(1);
+ if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) {
+ excludeGranted =
+ ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn());
+ } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) {
+ excludeGranted =
+ tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn());
+ }
+ if (excludeGranted) {
+ exclude.add("granted");
+ }
+
+ diffColumnsExcluding(
+ diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b, exclude);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java
new file mode 100644
index 0000000..9e7a1fe1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.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.server.notedb;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+
+public interface ChangeBundleReader {
+ ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 7b59a47..57d5dce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -15,13 +15,13 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.auto.value.AutoValue;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -61,25 +61,34 @@
*/
public class ChangeDraftUpdate extends AbstractChangeUpdate {
public interface Factory {
- ChangeDraftUpdate create(ChangeNotes notes, Account.Id accountId,
- PersonIdent authorIdent, Date when);
- ChangeDraftUpdate create(Change change, Account.Id accountId,
- PersonIdent authorIdent, Date when);
+ ChangeDraftUpdate create(
+ ChangeNotes notes,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
+
+ ChangeDraftUpdate create(
+ Change change,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
}
@AutoValue
abstract static class Key {
- abstract RevId revId();
- abstract PatchLineComment.Key key();
+ abstract String revId();
+ abstract Comment.Key key();
}
- private static Key key(PatchLineComment c) {
- return new AutoValue_ChangeDraftUpdate_Key(c.getRevId(), c.getKey());
+ private static Key key(Comment c) {
+ return new AutoValue_ChangeDraftUpdate_Key(c.revId, c.key);
}
private final AllUsersName draftsProject;
- private List<PatchLineComment> put = new ArrayList<>();
+ private List<Comment> put = new ArrayList<>();
private Set<Key> delete = new HashSet<>();
@AssistedInject
@@ -90,11 +99,12 @@
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@Assisted ChangeNotes notes,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
@@ -106,51 +116,44 @@
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@Assisted Change change,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
- public void putComment(PatchLineComment c) {
+ public void putComment(Comment c) {
verifyComment(c);
- checkArgument(c.getStatus() == PatchLineComment.Status.DRAFT,
- "Cannot insert a published comment into a ChangeDraftUpdate");
put.add(c);
}
- public void deleteComment(PatchLineComment c) {
+ public void deleteComment(Comment c) {
verifyComment(c);
delete.add(key(c));
}
- public void deleteComment(RevId revId, PatchLineComment.Key key) {
+ public void deleteComment(String revId, Comment.Key key) {
delete.add(new AutoValue_ChangeDraftUpdate_Key(revId, key));
}
- private void verifyComment(PatchLineComment comment) {
- checkArgument(comment.getAuthor().equals(accountId),
- "The author for the following comment does not match the author of"
- + " this ChangeDraftUpdate (%s): %s", accountId, comment);
- }
-
private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
ObjectId curr, CommitBuilder cb)
throws ConfigInvalidException, OrmException, IOException {
- RevisionNoteMap rnm = getRevisionNoteMap(rw, curr);
+ RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
Set<RevId> updatedRevs =
Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
- for (PatchLineComment c : put) {
+ for (Comment c : put) {
if (!delete.contains(key(c))) {
- cache.get(c.getRevId()).putComment(c);
+ cache.get(new RevId(c.revId)).putComment(c);
}
}
for (Key k : delete) {
- cache.get(k.revId()).deleteComment(k.key());
+ cache.get(new RevId(k.revId())).deleteComment(k.key());
}
Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
@@ -159,7 +162,7 @@
for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
updatedRevs.add(e.getKey());
ObjectId id = ObjectId.fromString(e.getKey().get());
- byte[] data = e.getValue().build(noteUtil);
+ byte[] data = e.getValue().build(noteUtil, noteUtil.getWriteJson());
if (!Arrays.equals(data, e.getValue().baseRaw)) {
touchedAnyRevs = true;
}
@@ -190,8 +193,8 @@
return cb;
}
- private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
- throws ConfigInvalidException, OrmException, IOException {
+ private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw,
+ ObjectId curr) throws ConfigInvalidException, OrmException, IOException {
if (migration.readChanges()) {
// If reading from changes is enabled, then the old DraftCommentNotes
// already parsed the revision notes. We can reuse them as long as the ref
@@ -203,7 +206,8 @@
if (draftNotes != null) {
ObjectId idFromNotes =
firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
- RevisionNoteMap rnm = draftNotes.getRevisionNoteMap();
+ RevisionNoteMap<ChangeRevisionNote> rnm =
+ draftNotes.getRevisionNoteMap();
if (idFromNotes.equals(curr) && rnm != null) {
return rnm;
}
@@ -219,7 +223,10 @@
// Even though reading from changes might not be enabled, we need to
// parse any existing revision notes so we can merge them.
return RevisionNoteMap.parse(
- noteUtil, getId(), rw.getObjectReader(), noteMap, true);
+ noteUtil, getId(),
+ rw.getObjectReader(),
+ noteMap,
+ PatchLineComment.Status.DRAFT);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 4c1a734..239b54e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
+import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -25,20 +25,21 @@
import com.google.common.primitives.Ints;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.util.GitDateFormatter;
@@ -54,6 +55,7 @@
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -61,20 +63,23 @@
import java.util.Set;
public class ChangeNoteUtil {
- static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
- static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
- static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
- static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
- static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
- static final FooterKey FOOTER_LABEL = new FooterKey("Label");
- static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
- static final FooterKey FOOTER_STATUS = new FooterKey("Status");
- static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
- static final FooterKey FOOTER_SUBMISSION_ID = new FooterKey("Submission-id");
- static final FooterKey FOOTER_SUBMITTED_WITH =
+ public static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
+ public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
+ public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
+ public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
+ public static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
+ public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
+ public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
+ public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
+ public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
+ public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
+ public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
+ public static final FooterKey FOOTER_SUBMISSION_ID =
+ new FooterKey("Submission-id");
+ public static final FooterKey FOOTER_SUBMITTED_WITH =
new FooterKey("Submitted-with");
- static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
- static final FooterKey FOOTER_TAG = new FooterKey("Tag");
+ public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
+ public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
private static final String AUTHOR = "Author";
private static final String BASE_PATCH_SET = "Base-for-patch-set";
@@ -84,6 +89,7 @@
private static final String PARENT = "Parent";
private static final String PARENT_NUMBER = "Parent-number";
private static final String PATCH_SET = "Patch-set";
+ private static final String REAL_AUTHOR = "Real-author";
private static final String REVISION = "Revision";
private static final String UUID = "UUID";
private static final String TAG = FOOTER_TAG.getName();
@@ -99,16 +105,20 @@
private final PersonIdent serverIdent;
private final String anonymousCowardName;
private final String serverId;
+ private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ private final boolean writeJson;
@Inject
public ChangeNoteUtil(AccountCache accountCache,
@GerritPersonIdent PersonIdent serverIdent,
@AnonymousCowardName String anonymousCowardName,
- @GerritServerId String serverId) {
+ @GerritServerId String serverId,
+ @GerritServerConfig Config config) {
this.accountCache = accountCache;
this.serverIdent = serverIdent;
this.anonymousCowardName = anonymousCowardName;
this.serverId = serverId;
+ this.writeJson = config.getBoolean("notedb", "writeJson", false);
}
@VisibleForTesting
@@ -120,6 +130,18 @@
when, serverIdent.getTimeZone());
}
+ public boolean getWriteJson() {
+ return writeJson;
+ }
+
+ public Gson getGson() {
+ return gson;
+ }
+
+ public String getServerId() {
+ return serverId;
+ }
+
public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
throws ConfigInvalidException {
String email = ident.getEmailAddress();
@@ -142,13 +164,13 @@
return m == p.value + expected.length;
}
- public List<PatchLineComment> parseNote(byte[] note, MutableInteger p,
- Change.Id changeId, Status status) throws ConfigInvalidException {
+ public List<Comment> parseNote(byte[] note, MutableInteger p,
+ Change.Id changeId) throws ConfigInvalidException {
if (p.value >= note.length) {
return ImmutableList.of();
}
- Set<PatchLineComment.Key> seen = new HashSet<>();
- List<PatchLineComment> result = new ArrayList<>();
+ Set<Comment.Key> seen = new HashSet<>();
+ List<Comment> result = new ArrayList<>();
int sizeOfNote = note.length;
byte[] psb = PATCH_SET.getBytes(UTF_8);
byte[] bpsb = BASE_PATCH_SET.getBytes(UTF_8);
@@ -179,21 +201,21 @@
PATCH_SET, BASE_PATCH_SET);
}
- PatchLineComment c = parseComment(
- note, p, fileName, psId, revId, isForBase, parentNumber, status);
- fileName = c.getKey().getParentKey().getFileName();
- if (!seen.add(c.getKey())) {
+ Comment c = parseComment(
+ note, p, fileName, psId, revId, isForBase, parentNumber);
+ fileName = c.key.filename;
+ if (!seen.add(c.key)) {
throw parseException(
- changeId, "multiple comments for %s in note", c.getKey());
+ changeId, "multiple comments for %s in note", c.key);
}
result.add(c);
}
return result;
}
- private PatchLineComment parseComment(byte[] note, MutableInteger curr,
+ private Comment parseComment(byte[] note, MutableInteger curr,
String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
- Integer parentNumber, Status status) throws ConfigInvalidException {
+ Integer parentNumber) throws ConfigInvalidException {
Change.Id changeId = psId.getParentKey();
// Check if there is a new file.
@@ -212,7 +234,14 @@
}
Timestamp commentTime = parseTimestamp(note, curr, changeId);
- Account.Id aId = parseAuthor(note, curr, changeId);
+ Account.Id aId = parseAuthor(note, curr, changeId, AUTHOR);
+ boolean hasRealAuthor =
+ (RawParseUtils.match(note, curr.value, REAL_AUTHOR.getBytes(UTF_8)))
+ != -1;
+ Account.Id raId = null;
+ if (hasRealAuthor) {
+ raId = parseAuthor(note, curr, changeId, REAL_AUTHOR);
+ }
boolean hasParent =
(RawParseUtils.match(note, curr.value, PARENT.getBytes(UTF_8))) != -1;
@@ -236,27 +265,30 @@
UTF_8, note, curr.value, curr.value + commentLength);
checkResult(message, "message contents", changeId);
- PatchLineComment plc = new PatchLineComment(
- new PatchLineComment.Key(new Patch.Key(psId, currentFileName), uuid),
- range.getEndLine(), aId, parentUUID, commentTime);
- plc.setMessage(message);
- plc.setTag(tag);
-
- if (isForBase) {
- plc.setSide((short) (parentNumber == null ? 0 : -parentNumber));
- } else {
- plc.setSide((short) 1);
+ Comment c = new Comment(
+ new Comment.Key(uuid, currentFileName, psId.get()),
+ aId,
+ commentTime,
+ isForBase
+ ? (short) (parentNumber == null ? 0 : -parentNumber)
+ : (short) 1,
+ message,
+ serverId);
+ c.lineNbr = range.getEndLine();
+ c.parentUuid = parentUUID;
+ c.tag = tag;
+ c.setRevId(revId);
+ if (raId != null) {
+ c.setRealAuthor(raId);
}
if (range.getStartCharacter() != -1) {
- plc.setRange(range);
+ c.setRange(range);
}
- plc.setRevId(revId);
- plc.setStatus(status);
curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
curr.value = RawParseUtils.nextLF(note, curr.value);
- return plc;
+ return c;
}
private static String parseStringField(byte[] note, MutableInteger curr,
@@ -391,15 +423,15 @@
}
private Account.Id parseAuthor(byte[] note, MutableInteger curr,
- Change.Id changeId) throws ConfigInvalidException {
- checkHeaderLineFormat(note, curr, AUTHOR, changeId);
+ Change.Id changeId, String fieldName) throws ConfigInvalidException {
+ checkHeaderLineFormat(note, curr, fieldName, changeId);
int startOfAccountId =
RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
PersonIdent ident =
RawParseUtils.parsePersonIdent(note, startOfAccountId);
Account.Id aId = parseIdent(ident, changeId);
curr.value = RawParseUtils.nextLF(note, curr.value);
- return checkResult(aId, "comment author", changeId);
+ return checkResult(aId, fieldName, changeId);
}
private static int parseCommentLength(byte[] note, MutableInteger curr,
@@ -470,47 +502,45 @@
* side.
* @param out output stream to write to.
*/
- void buildNote(Multimap<PatchSet.Id, PatchLineComment> comments,
+ void buildNote(Multimap<Integer, Comment> comments,
OutputStream out) {
if (comments.isEmpty()) {
return;
}
- List<PatchSet.Id> psIds =
- ReviewDbUtil.intKeyOrdering().sortedCopy(comments.keySet());
+ List<Integer> psIds = new ArrayList<>(comments.keySet());
+ Collections.sort(psIds);
OutputStreamWriter streamWriter = new OutputStreamWriter(out, UTF_8);
try (PrintWriter writer = new PrintWriter(streamWriter)) {
- RevId revId = comments.values().iterator().next().getRevId();
- appendHeaderField(writer, REVISION, revId.get());
+ String revId = comments.values().iterator().next().revId;
+ appendHeaderField(writer, REVISION, revId);
- for (PatchSet.Id psId : psIds) {
- List<PatchLineComment> psComments =
- PLC_ORDER.sortedCopy(comments.get(psId));
- PatchLineComment first = psComments.get(0);
+ for (int psId : psIds) {
+ List<Comment> psComments = COMMENT_ORDER.sortedCopy(comments.get(psId));
+ Comment first = psComments.get(0);
- short side = first.getSide();
+ short side = first.side;
appendHeaderField(writer, side <= 0
? BASE_PATCH_SET
: PATCH_SET,
- Integer.toString(psId.get()));
+ Integer.toString(psId));
if (side < 0) {
appendHeaderField(writer, PARENT_NUMBER, Integer.toString(-side));
}
String currentFilename = null;
- for (PatchLineComment c : psComments) {
- checkArgument(revId.equals(c.getRevId()),
+ for (Comment c : psComments) {
+ checkArgument(revId.equals(c.revId),
"All comments being added must have all the same RevId. The "
+ "comment below does not have the same RevId as the others "
+ "(%s).\n%s", revId, c);
- checkArgument(side == c.getSide(),
+ checkArgument(side == c.side,
"All comments being added must all have the same side. The "
+ "comment below does not have the same side as the others "
+ "(%s).\n%s", side, c);
- String commentFilename = QuotedString.GIT_PATH.quote(
- c.getKey().getParentKey().getFileName());
+ String commentFilename = QuotedString.GIT_PATH.quote(c.key.filename);
if (!commentFilename.equals(currentFilename)) {
currentFilename = commentFilename;
@@ -525,53 +555,61 @@
}
}
- private void appendOneComment(PrintWriter writer, PatchLineComment c) {
+ private void appendOneComment(PrintWriter writer, Comment c) {
// The CommentRange field for a comment is allowed to be null. If it is
// null, then in the first line, we simply use the line number field for a
// comment instead. If it isn't null, we write the comment range itself.
- CommentRange range = c.getRange();
+ Comment.Range range = c.range;
if (range != null) {
- writer.print(range.getStartLine());
+ writer.print(range.startLine);
writer.print(':');
- writer.print(range.getStartCharacter());
+ writer.print(range.startChar);
writer.print('-');
- writer.print(range.getEndLine());
+ writer.print(range.endLine);
writer.print(':');
- writer.print(range.getEndCharacter());
+ writer.print(range.endChar);
} else {
- writer.print(c.getLine());
+ writer.print(c.lineNbr);
}
writer.print("\n");
- writer.print(formatTime(serverIdent, c.getWrittenOn()));
+ writer.print(formatTime(serverIdent, c.writtenOn));
writer.print("\n");
+ appendIdent(writer, AUTHOR, c.author.getId(), c.writtenOn);
+ if (!c.getRealAuthor().equals(c.author)) {
+ appendIdent(writer, REAL_AUTHOR, c.getRealAuthor().getId(), c.writtenOn);
+ }
+
+ String parent = c.parentUuid;
+ if (parent != null) {
+ appendHeaderField(writer, PARENT, parent);
+ }
+
+ appendHeaderField(writer, UUID, c.key.uuid);
+
+ if (c.tag != null) {
+ appendHeaderField(writer, TAG, c.tag);
+ }
+
+ byte[] messageBytes = c.message.getBytes(UTF_8);
+ appendHeaderField(writer, LENGTH,
+ Integer.toString(messageBytes.length));
+
+ writer.print(c.message);
+ writer.print("\n\n");
+ }
+
+ private void appendIdent(PrintWriter writer, String header, Account.Id id,
+ Timestamp ts) {
PersonIdent ident = newIdent(
- accountCache.get(c.getAuthor()).getAccount(),
- c.getWrittenOn(), serverIdent, anonymousCowardName);
+ accountCache.get(id).getAccount(),
+ ts, serverIdent, anonymousCowardName);
StringBuilder name = new StringBuilder();
PersonIdent.appendSanitized(name, ident.getName());
name.append(" <");
PersonIdent.appendSanitized(name, ident.getEmailAddress());
name.append('>');
- appendHeaderField(writer, AUTHOR, name.toString());
-
- String parent = c.getParentUuid();
- if (parent != null) {
- appendHeaderField(writer, PARENT, parent);
- }
-
- appendHeaderField(writer, UUID, c.getKey().get());
-
- if (c.getTag() != null) {
- appendHeaderField(writer, TAG, c.getTag());
- }
-
- byte[] messageBytes = c.getMessage().getBytes(UTF_8);
- appendHeaderField(writer, LENGTH,
- Integer.toString(messageBytes.length));
-
- writer.print(c.getMessage());
- writer.print("\n\n");
+ appendHeaderField(writer, header, name.toString());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 6327682..360785f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -19,10 +19,9 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
+import static java.util.Comparator.comparing;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -40,18 +39,20 @@
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.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.git.RefCache;
import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
@@ -69,7 +70,6 @@
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -77,28 +77,17 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
/** View of a single {@link Change} based on the log of its notes branch. */
public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
private static final Logger log = LoggerFactory.getLogger(ChangeNotes.class);
static final Ordering<PatchSetApproval> PSA_BY_TIME =
- Ordering.natural().onResultOf(
- new Function<PatchSetApproval, Timestamp>() {
- @Override
- public Timestamp apply(PatchSetApproval input) {
- return input.getGranted();
- }
- });
+ Ordering.from(comparing(PatchSetApproval::getGranted));
public static final Ordering<ChangeMessage> MESSAGE_BY_TIME =
- Ordering.natural().onResultOf(
- new Function<ChangeMessage, Timestamp>() {
- @Override
- public Timestamp apply(ChangeMessage input) {
- return input.getWrittenOn();
- }
- });
+ Ordering.from(comparing(ChangeMessage::getWrittenOn));
public static ConfigInvalidException parseException(Change.Id changeId,
String fmt, Object... args) {
@@ -154,6 +143,7 @@
private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
Change.Id changeId) throws OrmException {
Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+ checkArgument(project != null, "project is required");
checkNotNull(change,
"change %s not found in ReviewDb", changeId);
checkArgument(change.getProject().equals(project),
@@ -193,17 +183,6 @@
return new ChangeNotes(args, change, false, null).load();
}
- // TODO(dborowitz): Remove when deleting index schemas <27.
- public ChangeNotes createFromIdOnlyWhenNoteDbDisabled(
- ReviewDb db, Change.Id changeId) throws OrmException {
- checkState(!args.migration.readChanges(), "do not call"
- + " createFromIdOnlyWhenNoteDbDisabled when NoteDb is enabled");
- Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
- checkNotNull(change,
- "change %s not found in ReviewDb", changeId);
- return new ChangeNotes(args, change).load();
- }
-
public ChangeNotes createWithAutoRebuildingDisabled(Change change,
RefCache refs) throws OrmException {
return new ChangeNotes(args, change, false, refs).load();
@@ -248,7 +227,7 @@
if (args.migration.enabled()) {
for (Change.Id cid : changeIds) {
ChangeNotes cn = create(db, project, cid);
- if (cn.getChange() != null && predicate.apply(cn)) {
+ if (cn.getChange() != null && predicate.test(cn)) {
notes.add(cn);
}
}
@@ -258,7 +237,7 @@
for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) {
if (c != null && project.equals(c.getDest().getParentKey())) {
ChangeNotes cn = createFromChangeOnlyWhenNoteDbDisabled(c);
- if (predicate.apply(cn)) {
+ if (predicate.test(cn)) {
notes.add(cn);
}
}
@@ -274,7 +253,7 @@
try (Repository repo = args.repoManager.openRepository(project)) {
List<ChangeNotes> changes = scanNoteDb(repo, db, project);
for (ChangeNotes cn : changes) {
- if (predicate.apply(cn)) {
+ if (predicate.test(cn)) {
m.put(project, cn);
}
}
@@ -283,7 +262,7 @@
} else {
for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
- if (predicate.apply(notes)) {
+ if (predicate.test(notes)) {
m.put(change.getProject(), notes);
}
}
@@ -361,10 +340,11 @@
// Parsed note map state, used by ChangeUpdate to make in-place editing of
// notes easier.
- RevisionNoteMap revisionNoteMap;
+ RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
private NoteDbUpdateManager.Result rebuildResult;
private DraftCommentNotes draftCommentNotes;
+ private RobotCommentNotes robotCommentNotes;
@VisibleForTesting
public ChangeNotes(Args args, Change change) {
@@ -399,8 +379,16 @@
}
/**
- *
- * @return a ImmutableSet of all hashtags for this change sorted in alphabetical order.
+ * @return an ImmutableSet of Account.Ids of all users that have been assigned
+ * to this change.
+ */
+ public ImmutableSet<Account.Id> getPastAssignees() {
+ return state.pastAssignees();
+ }
+
+ /**
+ * @return a ImmutableSet of all hashtags for this change sorted in
+ * alphabetical order.
*/
public ImmutableSet<String> getHashtags() {
return ImmutableSortedSet.copyOf(state.hashtags());
@@ -436,35 +424,38 @@
}
/** @return inline comments on each revision. */
- public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+ public ImmutableListMultimap<RevId, Comment> getComments() {
return state.publishedComments();
}
- public ImmutableListMultimap<RevId, PatchLineComment> getDraftComments(
+ public ImmutableListMultimap<RevId, Comment> getDraftComments(
Account.Id author) throws OrmException {
loadDraftComments(author);
- final Multimap<RevId, PatchLineComment> published =
+ final Multimap<RevId, Comment> published =
state.publishedComments();
// Filter out any draft comments that also exist in the published map, in
// case the update to All-Users to delete them during the publish operation
// failed.
- Multimap<RevId, PatchLineComment> filtered = Multimaps.filterEntries(
+ Multimap<RevId, Comment> filtered = Multimaps.filterEntries(
draftCommentNotes.getComments(),
- new Predicate<Map.Entry<RevId, PatchLineComment>>() {
- @Override
- public boolean apply(Map.Entry<RevId, PatchLineComment> in) {
- for (PatchLineComment c : published.get(in.getKey())) {
- if (c.getKey().equals(in.getValue().getKey())) {
+ (Map.Entry<RevId, Comment> e) -> {
+ for (Comment c : published.get(e.getKey())) {
+ if (c.key.equals(e.getValue().key)) {
return false;
}
}
return true;
- }
});
return ImmutableListMultimap.copyOf(
filtered);
}
+ public ImmutableListMultimap<RevId, RobotComment> getRobotComments()
+ throws OrmException {
+ loadRobotComments();
+ return robotCommentNotes.getComments();
+ }
+
/**
* If draft comments have already been loaded for this author, then they will
* not be reloaded. However, this method will load the comments if no draft
@@ -481,22 +472,33 @@
}
}
+ private void loadRobotComments() throws OrmException {
+ if (robotCommentNotes == null) {
+ robotCommentNotes = new RobotCommentNotes(args, change);
+ robotCommentNotes.load();
+ }
+ }
+
@VisibleForTesting
DraftCommentNotes getDraftCommentNotes() {
return draftCommentNotes;
}
- public boolean containsComment(PatchLineComment c) throws OrmException {
+ RobotCommentNotes getRobotCommentNotes() {
+ return robotCommentNotes;
+ }
+
+ public boolean containsComment(Comment c) throws OrmException {
if (containsCommentPublished(c)) {
return true;
}
- loadDraftComments(c.getAuthor());
+ loadDraftComments(c.author.getId());
return draftCommentNotes.containsComment(c);
}
- public boolean containsCommentPublished(PatchLineComment c) {
- for (PatchLineComment l : getComments().values()) {
- if (c.getKey().equals(l.getKey())) {
+ public boolean containsCommentPublished(Comment c) {
+ for (Comment l : getComments().values()) {
+ if (c.key.equals(l.key)) {
return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index a8f85a4..198eb2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -33,6 +33,9 @@
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -49,7 +52,8 @@
cache(CACHE_NAME,
Key.class,
ChangeNotesState.class)
- .maximumWeight(1000);
+ .weigher(Weigher.class)
+ .maximumWeight(10 << 20);
}
};
}
@@ -61,6 +65,144 @@
abstract ObjectId id();
}
+ public static class Weigher
+ implements com.google.common.cache.Weigher<Key, ChangeNotesState> {
+ // Single object overhead.
+ private static final int O = 16;
+
+ // Single pointer overhead.
+ private static final int P = 8;
+
+ // Single IntKey overhead.
+ private static final int K = O + 4;
+
+ // Single Timestamp overhead.
+ private static final int T = O + 8;
+
+ @Override
+ public int weigh(Key key, ChangeNotesState state) {
+ // Take all columns and all collection sizes into account, but use
+ // estimated average element sizes rather than iterating over collections.
+ // Numbers are largely hand-wavy based on
+ // http://stackoverflow.com/questions/258120/what-is-the-memory-consumption-of-an-object-in-java
+ return
+ K // changeId
+ + str(40) // changeKey
+ + T // createdOn
+ + T // lastUpdatedOn
+ + P + K // owner
+ + P + str(state.columns().branch())
+ + P + patchSetId() // currentPatchSetId
+ + P + str(state.columns().subject())
+ + P + str(state.columns().topic())
+ + P + str(state.columns().originalSubject())
+ + P + str(state.columns().submissionId())
+ + ptr(state.columns().assignee(), K) // assignee
+ + P // status
+ + P + set(state.pastAssignees(), K)
+ + P + set(state.hashtags(), str(10))
+ + P + map(state.patchSets(), patchSet())
+ + P + list(state.reviewerUpdates(), 4 * O + K + K + P)
+ + P + list(state.submitRecords(), P + list(2, str(4) + P + K) + P)
+ + P + list(state.allChangeMessages(), changeMessage())
+ // Just key overhead for map, already counted messages in previous.
+ + P + map(state.changeMessagesByPatchSet().asMap(), patchSetId())
+ + P + map(state.publishedComments().asMap(), comment());
+ }
+
+ private static int ptr(Object o, int size) {
+ return o != null ? P + size : P;
+ }
+
+ private static int str(String s) {
+ if (s == null) {
+ return P;
+ }
+ return str(s.length());
+ }
+
+ private static int str(int n) {
+ return 8 + 24 + 2 * n;
+ }
+
+ private static int patchSetId() {
+ return O + 4 + O + 4;
+ }
+
+ private static int set(Set<?> set, int elemSize) {
+ if (set == null) {
+ return P;
+ }
+ return hashtable(set.size(), elemSize);
+ }
+
+ private static int map(Map<?, ?> map, int elemSize) {
+ if (map == null) {
+ return P;
+ }
+ return hashtable(map.size(), elemSize);
+ }
+
+ private static int hashtable(int n, int elemSize) {
+ // Made up numbers.
+ int overhead = 32;
+ int elemOverhead = O + 32;
+ return overhead + elemOverhead * n * elemSize;
+ }
+
+ private static int list(List<?> list, int elemSize) {
+ if (list == null) {
+ return P;
+ }
+ return list(list.size(), elemSize);
+ }
+
+ private static int list(int n, int elemSize) {
+ return O + O + n * (P + elemSize);
+ }
+
+ private static int patchSet() {
+ return O
+ + P + patchSetId()
+ + str(40) // revision
+ + P + K // uploader
+ + P + T // createdOn
+ + 1 // draft
+ + str(40) // groups
+ + P; // pushCertificate
+ }
+
+ private static int changeMessage() {
+ int key = K + str(20);
+ return O
+ + P + key
+ + P + K // author
+ + P + T // writtenON
+ + str(64) // message
+ + P + patchSetId()
+ + P
+ + P; // realAuthor
+ }
+
+ private static int comment() {
+ int key = P + str(20) + P + str(32) + 4;
+ int ident = O + 4;
+ return O
+ + P + key
+ + 4 // lineNbr
+ + P + ident // author
+ + P + ident //realAuthor
+ + P + T // writtenOn
+ + 2 // side
+ + str(32) // message
+ + str(10) // parentUuid
+ + (P + O + 4 + 4 + 4 + 4) / 2 // range on 50% of comments
+ + P // tag
+ + P + str(40) // revId
+ + P + str(36); // serverId
+ }
+ }
+
@AutoValue
abstract static class Value {
abstract ChangeNotesState state();
@@ -73,14 +215,14 @@
* used as an optimization; {@link ChangeNotes} is capable of lazily loading
* it as necessary.
*/
- @Nullable abstract RevisionNoteMap revisionNoteMap();
+ @Nullable abstract RevisionNoteMap<ChangeRevisionNote> revisionNoteMap();
}
private class Loader implements Callable<ChangeNotesState> {
private final Key key;
private final ChangeNotesRevWalk rw;
- private RevisionNoteMap revisionNoteMap;
+ private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
private Loader(Key key, ChangeNotesRevWalk rw) {
this.key = key;
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..a3ef2ce 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
@@ -14,6 +14,7 @@
package com.google.gerrit.server.notedb;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
@@ -21,6 +22,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
@@ -28,13 +30,13 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.joining;
+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,19 +48,20 @@
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;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -85,7 +88,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 +99,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;
@@ -110,12 +126,11 @@
private final List<Account.Id> allPastReviewers;
private final List<ReviewerStatusUpdate> reviewerUpdates;
private final List<SubmitRecord> submitRecords;
- private final Multimap<RevId, PatchLineComment> comments;
+ private final Multimap<RevId, Comment> comments;
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;
@@ -123,6 +138,8 @@
private String branch;
private Change.Status status;
private String topic;
+ private Optional<Account.Id> assignee;
+ private List<Account.Id> pastAssignees;
private Set<String> hashtags;
private Timestamp createdOn;
private Timestamp lastUpdatedOn;
@@ -133,7 +150,7 @@
private String submissionId;
private String tag;
private PatchSet.Id currentPatchSetId;
- private RevisionNoteMap revisionNoteMap;
+ private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
ChangeNotesParser(Change.Id changeId, ObjectId tip, ChangeNotesRevWalk walk,
ChangeNoteUtil noteUtil, NoteDbMetrics metrics) {
@@ -142,7 +159,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<>();
@@ -150,7 +167,7 @@
allChangeMessages = new ArrayList<>();
changeMessagesByPatchSet = LinkedListMultimap.create();
comments = ArrayListMultimap.create();
- patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering());
+ patchSets = Maps.newTreeMap(comparing(PatchSet.Id::get));
deletedPatchSets = new HashSet<>();
patchSetStates = new HashMap<>();
}
@@ -171,6 +188,7 @@
parseNotes();
allPastReviewers.addAll(reviewers.rowKeySet());
pruneReviewers();
+
updatePatchSetStates();
checkMandatoryFooters();
}
@@ -178,7 +196,7 @@
return buildState();
}
- RevisionNoteMap getRevisionNoteMap() {
+ RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() {
return revisionNoteMap;
}
@@ -195,8 +213,10 @@
topic,
originalSubject,
submissionId,
+ assignee != null ? assignee.orNull() : null,
status,
+ Sets.newLinkedHashSet(Lists.reverse(pastAssignees)),
hashtags,
patchSets,
buildApprovals(),
@@ -210,14 +230,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);
@@ -283,6 +304,7 @@
if (accountId != null) {
ownerId = accountId;
}
+ Account.Id realAccountId = parseRealAccountId(commit, accountId);
if (changeId == null) {
changeId = parseChangeId(commit);
@@ -296,13 +318,15 @@
originalSubject = currSubject;
}
- parseChangeMessage(psId, accountId, commit, ts);
+ parseChangeMessage(psId, accountId, realAccountId, commit, ts);
if (topic == null) {
topic = parseTopic(commit);
}
parseHashtags(commit);
+ parseAssignee(commit);
+
if (submissionId == null) {
submissionId = parseSubmissionId(commit);
}
@@ -320,7 +344,7 @@
}
for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
- parseApproval(psId, accountId, ts, line);
+ parseApproval(psId, accountId, realAccountId, ts, line);
}
for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
@@ -357,6 +381,16 @@
return parseOneFooter(commit, FOOTER_SUBJECT);
}
+ private Account.Id parseRealAccountId(ChangeNotesCommit commit,
+ Account.Id effectiveAccountId) throws ConfigInvalidException {
+ String realUser = parseOneFooter(commit, FOOTER_REAL_USER);
+ if (realUser == null) {
+ return effectiveAccountId;
+ }
+ PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
+ return noteUtil.parseIdent(ident, id);
+ }
+
private String parseTopic(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_TOPIC);
@@ -459,6 +493,30 @@
}
}
+ private void parseAssignee(ChangeNotesCommit commit)
+ throws ConfigInvalidException {
+ if (pastAssignees == null) {
+ pastAssignees = Lists.newArrayList();
+ }
+ String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
+ if (assigneeValue != null) {
+ Optional<Account.Id> parsedAssignee;
+ if (assigneeValue.equals("")) {
+ // Empty footer found, assignee deleted
+ parsedAssignee = Optional.absent();
+ } else {
+ PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue);
+ parsedAssignee = Optional.fromNullable(noteUtil.parseIdent(ident, id));
+ }
+ if (assignee == null) {
+ assignee = parsedAssignee;
+ }
+ if (parsedAssignee.isPresent()) {
+ pastAssignees.add(parsedAssignee.get());
+ }
+ }
+ }
+
private void parseTag(ChangeNotesCommit commit)
throws ConfigInvalidException {
tag = null;
@@ -519,7 +577,8 @@
}
private void parseChangeMessage(PatchSet.Id psId,
- Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
+ Account.Id accountId, Account.Id realAccountId,
+ ChangeNotesCommit commit, Timestamp ts) {
byte[] raw = commit.getRawBuffer();
int size = raw.length;
Charset enc = RawParseUtils.parseEncoding(raw);
@@ -568,11 +627,10 @@
changeMessageStart, changeMessageEnd + 1);
ChangeMessage changeMessage = new ChangeMessage(
new ChangeMessage.Key(psId.getParentKey(), commit.name()),
- accountId,
- ts,
- psId);
+ accountId, ts, psId);
changeMessage.setMessage(changeMsgString);
changeMessage.setTag(tag);
+ changeMessage.setRealAuthor(realAccountId);
changeMessagesByPatchSet.put(psId, changeMessage);
allChangeMessages.add(changeMessage);
}
@@ -582,49 +640,64 @@
ObjectReader reader = walk.getObjectReader();
ChangeNotesCommit tipCommit = walk.parseCommit(tip);
revisionNoteMap = RevisionNoteMap.parse(
- noteUtil, id, reader, NoteMap.read(reader, tipCommit), false);
- Map<RevId, RevisionNote> rns = revisionNoteMap.revisionNotes;
+ noteUtil, id, reader, NoteMap.read(reader, tipCommit),
+ PatchLineComment.Status.PUBLISHED);
+ Map<RevId, ChangeRevisionNote> rns = revisionNoteMap.revisionNotes;
- for (Map.Entry<RevId, RevisionNote> e : rns.entrySet()) {
- for (PatchLineComment plc : e.getValue().comments) {
- comments.put(e.getKey(), plc);
+ for (Map.Entry<RevId, ChangeRevisionNote> e : rns.entrySet()) {
+ for (Comment c : e.getValue().getComments()) {
+ comments.put(e.getKey(), c);
}
}
for (PatchSet ps : patchSets.values()) {
- RevisionNote rn = rns.get(ps.getRevision());
- if (rn != null && rn.pushCert != null) {
- ps.setPushCertificate(rn.pushCert);
+ ChangeRevisionNote rn = rns.get(ps.getRevision());
+ if (rn != null && rn.getPushCert() != null) {
+ ps.setPushCertificate(rn.getPushCert());
}
}
}
private void parseApproval(PatchSet.Id psId, Account.Id accountId,
- Timestamp ts, String line) throws ConfigInvalidException {
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
if (accountId == null) {
throw parseException(
"patch set %s requires an identified user as uploader", psId.get());
}
if (line.startsWith("-")) {
- parseRemoveApproval(psId, accountId, line);
+ parseRemoveApproval(psId, accountId, realAccountId, ts, line);
} else {
- parseAddApproval(psId, accountId, ts, line);
+ parseAddApproval(psId, accountId, realAccountId, ts, line);
}
}
private void parseAddApproval(PatchSet.Id psId, Account.Id committerId,
- Timestamp ts, String line) throws ConfigInvalidException {
- Account.Id accountId;
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
+ // There are potentially 3 accounts involved here:
+ // 1. The account from the commit, which is the effective IdentifiedUser
+ // that produced the update.
+ // 2. The account in the label footer itself, which is used during submit
+ // to copy other users' labels to a new patch set.
+ // 3. The account in the Real-user footer, indicating that the whole
+ // update operation was executed by this user on behalf of the effective
+ // user.
+ Account.Id effectiveAccountId;
String labelVoteStr;
int s = line.indexOf(' ');
if (s > 0) {
+ // Account in the label line (2) becomes the effective ID of the
+ // approval. If there is a real user (3) different from the commit user
+ // (2), we actually don't store that anywhere in this case; it's more
+ // important to record that the real user (3) actually initiated submit.
labelVoteStr = line.substring(0, s);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
- accountId = noteUtil.parseIdent(ident, id);
+ effectiveAccountId = noteUtil.parseIdent(ident, id);
} else {
labelVoteStr = line;
- accountId = committerId;
+ effectiveAccountId = committerId;
}
LabelVote l;
@@ -637,39 +710,43 @@
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,
+ effectiveAccountId,
+ new LabelId(l.label())),
+ l.value(),
+ ts);
+ psa.setTag(tag);
+ if (!Objects.equals(realAccountId, committerId)) {
+ psa.setRealAccountId(realAccountId);
+ }
+ ApprovalKey k =
+ ApprovalKey.create(psId, effectiveAccountId, l.label(), tag);
+ if (!approvals.containsKey(k)) {
+ approvals.put(k, psa);
}
}
private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
- String line) throws ConfigInvalidException {
- Account.Id accountId;
- Entry<String, String> label;
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
+ // See comments in parseAddApproval about the various users involved.
+ Account.Id effectiveAccountId;
+ 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);
+ effectiveAccountId = noteUtil.parseIdent(ident, id);
} else {
- label = Maps.immutableEntry(line.substring(1), tag);
- accountId = committerId;
+ label = line.substring(1);
+ effectiveAccountId = committerId;
}
try {
- LabelType.checkNameInternal(label.getKey());
+ LabelType.checkNameInternal(label);
} catch (IllegalArgumentException e) {
ConfigInvalidException pe =
parseException("invalid %s: %s", FOOTER_LABEL, line);
@@ -677,36 +754,26 @@
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,
+ effectiveAccountId,
+ new LabelId(label)),
+ (short) 0,
+ ts);
+ if (!Objects.equals(realAccountId, committerId)) {
+ remove.setRealAccountId(realAccountId);
}
- }
-
- 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);
+ ApprovalKey k = ApprovalKey.create(psId, effectiveAccountId, label, tag);
+ if (!approvals.containsKey(k)) {
+ approvals.put(k, remove);
}
- return curr;
}
private void parseSubmitRecords(List<String> lines)
@@ -787,9 +854,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 +896,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();
@@ -847,9 +912,9 @@
it.remove();
}
}
- for (Iterator<PatchLineComment> it = comments.values().iterator();
+ for (Iterator<Comment> it = comments.values().iterator();
it.hasNext();) {
- PatchSet.Id psId = it.next().getKey().getParentKey().getParentKey();
+ PatchSet.Id psId = new PatchSet.Id(id, it.next().key.patchSetId);
if (!all.contains(psId)) {
it.remove();
}
@@ -868,13 +933,8 @@
missing.add(FOOTER_SUBJECT);
}
if (!missing.isEmpty()) {
- throw parseException("Missing footers: " + Joiner.on(", ")
- .join(Lists.transform(missing, new Function<FooterKey, String>() {
- @Override
- public String apply(FooterKey input) {
- return input.getName();
- }
- })));
+ throw parseException("Missing footers: "
+ + missing.stream().map(FooterKey::getName).collect(joining(", ")));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 988184f..67eb328 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Comparator.comparing;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
@@ -29,11 +30,10 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
@@ -59,6 +59,7 @@
return new AutoValue_ChangeNotesState(
change.getId(),
null,
+ ImmutableSet.<Account.Id>of(),
ImmutableSet.<String>of(),
ImmutableSortedMap.<PatchSet.Id, PatchSet>of(),
ImmutableListMultimap.<PatchSet.Id, PatchSetApproval>of(),
@@ -68,7 +69,7 @@
ImmutableList.<SubmitRecord>of(),
ImmutableList.<ChangeMessage>of(),
ImmutableListMultimap.<PatchSet.Id, ChangeMessage>of(),
- ImmutableListMultimap.<RevId, PatchLineComment>of());
+ ImmutableListMultimap.<RevId, Comment>of());
}
static ChangeNotesState create(
@@ -83,7 +84,9 @@
@Nullable String topic,
@Nullable String originalSubject,
@Nullable String submissionId,
+ @Nullable Account.Id assignee,
@Nullable Change.Status status,
+ @Nullable Set<Account.Id> pastAssignees,
@Nullable Set<String> hashtags,
Map<PatchSet.Id, PatchSet> patchSets,
Multimap<PatchSet.Id, PatchSetApproval> approvals,
@@ -93,7 +96,7 @@
List<SubmitRecord> submitRecords,
List<ChangeMessage> allChangeMessages,
Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet,
- Multimap<RevId, PatchLineComment> publishedComments) {
+ Multimap<RevId, Comment> publishedComments) {
if (hashtags == null) {
hashtags = ImmutableSet.of();
}
@@ -110,9 +113,11 @@
topic,
originalSubject,
submissionId,
+ assignee,
status),
+ ImmutableSet.copyOf(pastAssignees),
ImmutableSet.copyOf(hashtags),
- ImmutableSortedMap.copyOf(patchSets, ReviewDbUtil.intKeyOrdering()),
+ ImmutableSortedMap.copyOf(patchSets, comparing(PatchSet.Id::get)),
ImmutableListMultimap.copyOf(approvals),
reviewers,
ImmutableList.copyOf(allPastReviewers),
@@ -144,6 +149,7 @@
@Nullable abstract String topic();
@Nullable abstract String originalSubject();
@Nullable abstract String submissionId();
+ @Nullable abstract Account.Id assignee();
// TODO(dborowitz): Use a sensible default other than null
@Nullable abstract Change.Status status();
}
@@ -153,6 +159,7 @@
@Nullable abstract ChangeColumns columns();
// Other related to this Change.
+ abstract ImmutableSet<Account.Id> pastAssignees();
abstract ImmutableSet<String> hashtags();
abstract ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets();
abstract ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals();
@@ -165,7 +172,7 @@
abstract ImmutableList<ChangeMessage> allChangeMessages();
abstract ImmutableListMultimap<PatchSet.Id, ChangeMessage>
changeMessagesByPatchSet();
- abstract ImmutableListMultimap<RevId, PatchLineComment> publishedComments();
+ abstract ImmutableListMultimap<RevId, Comment> publishedComments();
void copyColumnsTo(Change change) {
ChangeColumns c = checkNotNull(columns());
@@ -179,6 +186,7 @@
change.setLastUpdatedOn(c.lastUpdatedOn());
change.setOwner(c.owner());
change.setSubmissionId(c.submissionId());
+ change.setAssignee(c.assignee());
if (!patchSets().isEmpty()) {
change.setCurrentPatchSet(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
deleted file mode 100644
index 08acbad..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilderImpl.java
+++ /dev/null
@@ -1,1060 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.notedb;
-
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
-import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
-import com.google.common.base.Splitter;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Ordering;
-import com.google.common.collect.Sets;
-import com.google.common.collect.Table;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.common.FormatUtil;
-import com.google.gerrit.common.Nullable;
-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.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.PatchLineCommentsUtil;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.git.ChainedReceiveCommands;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class ChangeRebuilderImpl extends ChangeRebuilder {
- private static final Logger log =
- LoggerFactory.getLogger(ChangeRebuilderImpl.class);
-
- /**
- * The maximum amount of time between the ReviewDb timestamp of the first and
- * last events batched together into a single NoteDb update.
- * <p>
- * Used to account for the fact that different records with their own
- * timestamps (e.g. {@link PatchSetApproval} and {@link ChangeMessage})
- * historically didn't necessarily use the same timestamp, and tended to call
- * {@code System.currentTimeMillis()} independently.
- */
- static final long MAX_WINDOW_MS = SECONDS.toMillis(3);
-
- /**
- * The maximum amount of time between two consecutive events to consider them
- * to be in the same batch.
- */
- private static final long MAX_DELTA_MS = SECONDS.toMillis(1);
-
- private final AccountCache accountCache;
- private final ChangeDraftUpdate.Factory draftUpdateFactory;
- private final ChangeNoteUtil changeNoteUtil;
- private final ChangeUpdate.Factory updateFactory;
- private final NoteDbUpdateManager.Factory updateManagerFactory;
- private final NotesMigration migration;
- private final PatchListCache patchListCache;
- private final PersonIdent serverIdent;
- private final ProjectCache projectCache;
- private final String anonymousCowardName;
-
- @Inject
- ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
- AccountCache accountCache,
- ChangeDraftUpdate.Factory draftUpdateFactory,
- ChangeNoteUtil changeNoteUtil,
- ChangeUpdate.Factory updateFactory,
- NoteDbUpdateManager.Factory updateManagerFactory,
- NotesMigration migration,
- PatchListCache patchListCache,
- @GerritPersonIdent PersonIdent serverIdent,
- @Nullable ProjectCache projectCache,
- @AnonymousCowardName String anonymousCowardName) {
- super(schemaFactory);
- this.accountCache = accountCache;
- this.draftUpdateFactory = draftUpdateFactory;
- this.changeNoteUtil = changeNoteUtil;
- this.updateFactory = updateFactory;
- this.updateManagerFactory = updateManagerFactory;
- this.migration = migration;
- this.patchListCache = patchListCache;
- this.serverIdent = serverIdent;
- this.projectCache = projectCache;
- this.anonymousCowardName = anonymousCowardName;
- }
-
- @Override
- public Result rebuild(ReviewDb db, Change.Id changeId)
- throws NoSuchChangeException, IOException, OrmException,
- ConfigInvalidException {
- db = ReviewDbUtil.unwrapDb(db);
- Change change = db.changes().get(changeId);
- if (change == null) {
- throw new NoSuchChangeException(changeId);
- }
- try (NoteDbUpdateManager manager =
- updateManagerFactory.create(change.getProject())) {
- buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
- return execute(db, changeId, manager);
- }
- }
-
- private static class AbortUpdateException extends OrmRuntimeException {
- private static final long serialVersionUID = 1L;
-
- AbortUpdateException() {
- super("aborted");
- }
- }
-
- private static class ConflictingUpdateException extends OrmRuntimeException {
- private static final long serialVersionUID = 1L;
-
- ConflictingUpdateException(Change change, String expectedNoteDbState) {
- super(String.format(
- "Expected change %s to have noteDbState %s but was %s",
- change.getId(), expectedNoteDbState, change.getNoteDbState()));
- }
- }
-
- @Override
- public Result rebuild(NoteDbUpdateManager manager,
- ChangeBundle bundle) throws NoSuchChangeException, IOException,
- OrmException, ConfigInvalidException {
- Change change = new Change(bundle.getChange());
- buildUpdates(manager, bundle);
- return manager.stageAndApplyDelta(change);
- }
-
- @Override
- public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
- throws NoSuchChangeException, IOException, OrmException {
- db = ReviewDbUtil.unwrapDb(db);
- Change change = db.changes().get(changeId);
- if (change == null) {
- throw new NoSuchChangeException(changeId);
- }
- NoteDbUpdateManager manager =
- updateManagerFactory.create(change.getProject());
- buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
- manager.stage();
- return manager;
- }
-
- @Override
- public Result execute(ReviewDb db, Change.Id changeId,
- NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
- IOException {
- db = ReviewDbUtil.unwrapDb(db);
- Change change = db.changes().get(changeId);
- if (change == null) {
- throw new NoSuchChangeException(changeId);
- }
-
- final String oldNoteDbState = change.getNoteDbState();
- Result r = manager.stageAndApplyDelta(change);
- final String newNoteDbState = change.getNoteDbState();
- try {
- db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- String currNoteDbState = change.getNoteDbState();
- if (Objects.equals(currNoteDbState, newNoteDbState)) {
- // Another thread completed the same rebuild we were about to.
- throw new AbortUpdateException();
- } else if (!Objects.equals(oldNoteDbState, currNoteDbState)) {
- // Another thread updated the state to something else.
- throw new ConflictingUpdateException(change, oldNoteDbState);
- }
- change.setNoteDbState(newNoteDbState);
- return change;
- }
- });
- } catch (ConflictingUpdateException e) {
- // Rethrow as an OrmException so the caller knows to use staged results.
- // Strictly speaking they are not completely up to date, but result we
- // send to the caller is the same as if this rebuild had executed before
- // the other thread.
- throw new OrmException(e.getMessage());
- } catch (AbortUpdateException e) {
- if (NoteDbChangeState.parse(changeId, newNoteDbState).isUpToDate(
- manager.getChangeRepo().cmds.getRepoRefCache(),
- manager.getAllUsersRepo().cmds.getRepoRefCache())) {
- // If the state in ReviewDb matches NoteDb at this point, it means
- // another thread successfully completed this rebuild. It's ok to not
- // execute the update in this case, since the object referenced in the
- // Result was flushed to the repo by whatever thread won the race.
- return r;
- }
- // If the state doesn't match, that means another thread attempted this
- // rebuild, but failed. Fall through and try to update the ref again.
- }
- if (migration.failChangeWrites()) {
- // Don't even attempt to execute if read-only, it would fail anyway. But
- // do throw an exception to the caller so they know to use the staged
- // results instead of reading from the repo.
- throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
- }
- manager.execute();
- return r;
- }
-
- @Override
- public boolean rebuildProject(ReviewDb db,
- ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
- Project.NameKey project, Repository allUsersRepo)
- throws NoSuchChangeException, IOException, OrmException,
- ConfigInvalidException {
- checkArgument(allChanges.containsKey(project));
- boolean ok = true;
- ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
- pm.beginTask(
- FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
- try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
- ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
- RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
- manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
- new ChainedReceiveCommands(allUsersRepo));
- for (Change.Id changeId : allChanges.get(project)) {
- try {
- buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
- } catch (NoPatchSetsException e) {
- log.warn(e.getMessage());
- } catch (Throwable t) {
- log.error("Failed to rebuild change " + changeId, t);
- ok = false;
- }
- pm.update(1);
- }
- manager.execute();
- } finally {
- pm.endTask();
- }
- return ok;
- }
-
- private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
- throws IOException, OrmException {
- manager.setCheckExpectedState(false);
- Change change = new Change(bundle.getChange());
- if (bundle.getPatchSets().isEmpty()) {
- throw new NoPatchSetsException(change.getId());
- }
-
- PatchSet.Id currPsId = change.currentPatchSetId();
- // We will rebuild all events, except for draft comments, in buckets based
- // on author and timestamp.
- List<Event> events = new ArrayList<>();
- Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
- ArrayListMultimap.create();
-
- events.addAll(getHashtagsEvents(change, manager));
-
- // Delete ref only after hashtags have been read
- deleteChangeMetaRef(change, manager.getChangeRepo().cmds);
- deleteDraftRefs(change, manager.getAllUsersRepo());
-
- Integer minPsNum = getMinPatchSetNum(bundle);
- Set<PatchSet.Id> psIds =
- Sets.newHashSetWithExpectedSize(bundle.getPatchSets().size());
-
- for (PatchSet ps : bundle.getPatchSets()) {
- if (ps.getId().get() > currPsId.get()) {
- log.info(
- "Skipping patch set {}, which is higher than current patch set {}",
- ps.getId(), currPsId);
- continue;
- }
- psIds.add(ps.getId());
- events.add(new PatchSetEvent(
- change, ps, manager.getChangeRepo().rw));
- for (PatchLineComment c : getPatchLineComments(bundle, ps)) {
- PatchLineCommentEvent e =
- new PatchLineCommentEvent(c, change, ps, patchListCache);
- if (c.getStatus() == Status.PUBLISHED) {
- events.add(e);
- } else {
- draftCommentEvents.put(c.getAuthor(), e);
- }
- }
- }
-
- for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
- if (psIds.contains(psa.getPatchSetId())) {
- events.add(new ApprovalEvent(psa, change.getCreatedOn()));
- }
- }
-
- for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> r :
- bundle.getReviewers().asTable().cellSet()) {
- events.add(new ReviewerEvent(r, change.getCreatedOn()));
- }
-
- Change noteDbChange = new Change(null, null, null, null, null);
- for (ChangeMessage msg : bundle.getChangeMessages()) {
- if (msg.getPatchSetId() == null || psIds.contains(msg.getPatchSetId())) {
- events.add(
- new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
- }
- }
-
- sortAndFillEvents(change, noteDbChange, events, minPsNum);
-
- EventList<Event> el = new EventList<>();
- for (Event e : events) {
- if (!el.canAdd(e)) {
- flushEventsToUpdate(manager, el, change);
- checkState(el.canAdd(e));
- }
- el.add(e);
- }
- flushEventsToUpdate(manager, el, change);
-
- EventList<PatchLineCommentEvent> plcel = new EventList<>();
- for (Account.Id author : draftCommentEvents.keys()) {
- for (PatchLineCommentEvent e :
- EVENT_ORDER.sortedCopy(draftCommentEvents.get(author))) {
- if (!plcel.canAdd(e)) {
- flushEventsToDraftUpdate(manager, plcel, change);
- checkState(plcel.canAdd(e));
- }
- plcel.add(e);
- }
- flushEventsToDraftUpdate(manager, plcel, change);
- }
- }
-
- private static Integer getMinPatchSetNum(ChangeBundle bundle) {
- Integer minPsNum = null;
- for (PatchSet ps : bundle.getPatchSets()) {
- int n = ps.getId().get();
- if (minPsNum == null || n < minPsNum) {
- minPsNum = n;
- }
- }
- return minPsNum;
- }
-
- private static List<PatchLineComment> getPatchLineComments(ChangeBundle bundle,
- final PatchSet ps) {
- return FluentIterable.from(bundle.getPatchLineComments())
- .filter(new Predicate<PatchLineComment>() {
- @Override
- public boolean apply(PatchLineComment in) {
- return in.getPatchSetId().equals(ps.getId());
- }
- }).toSortedList(PatchLineCommentsUtil.PLC_ORDER);
- }
-
- private void sortAndFillEvents(Change change, Change noteDbChange,
- List<Event> events, Integer minPsNum) {
- Collections.sort(events, EVENT_ORDER);
- events.add(new FinalUpdatesEvent(change, noteDbChange));
-
- // Ensure the first event in the list creates the change, setting the author
- // and any required footers.
- Event first = events.get(0);
- if (first instanceof PatchSetEvent && change.getOwner().equals(first.who)) {
- ((PatchSetEvent) first).createChange = true;
- } else {
- events.add(0, new CreateChangeEvent(change, minPsNum));
- }
-
- // Fill in any missing patch set IDs using the latest patch set of the
- // change at the time of the event, because NoteDb can't represent actions
- // with no associated patch set ID. This workaround is as if a user added a
- // ChangeMessage on the change by replying from the latest patch set.
- //
- // Start with the first patch set that actually exists. If there are no
- // patch sets at all, minPsNum will be null, so just bail and use 1 as the
- // patch set ID. The corresponding patch set won't exist, but this change is
- // probably corrupt anyway, as deleting the last draft patch set should have
- // deleted the whole change.
- int ps = firstNonNull(minPsNum, 1);
- for (Event e : events) {
- if (e.psId == null) {
- e.psId = new PatchSet.Id(change.getId(), ps);
- } else {
- ps = Math.max(ps, e.psId.get());
- }
- }
- }
-
- private void flushEventsToUpdate(NoteDbUpdateManager manager,
- EventList<Event> events, Change change) throws OrmException, IOException {
- if (events.isEmpty()) {
- return;
- }
- Comparator<String> labelNameComparator;
- if (projectCache != null) {
- labelNameComparator = projectCache.get(change.getProject())
- .getLabelTypes().nameComparator();
- } else {
- // No project cache available, bail and use natural ordering; there's no
- // semantic difference anyway difference.
- labelNameComparator = Ordering.natural();
- }
- ChangeUpdate update = updateFactory.create(
- change,
- events.getAccountId(),
- events.newAuthorIdent(),
- events.getWhen(),
- labelNameComparator);
- update.setAllowWriteToNewRef(true);
- update.setPatchSetId(events.getPatchSetId());
- update.setTag(events.getTag());
- for (Event e : events) {
- e.apply(update);
- }
- manager.add(update);
- events.clear();
- }
-
- private void flushEventsToDraftUpdate(NoteDbUpdateManager manager,
- EventList<PatchLineCommentEvent> events, Change change)
- throws OrmException {
- if (events.isEmpty()) {
- return;
- }
- ChangeDraftUpdate update = draftUpdateFactory.create(
- change,
- events.getAccountId(),
- events.newAuthorIdent(),
- events.getWhen());
- update.setPatchSetId(events.getPatchSetId());
- for (PatchLineCommentEvent e : events) {
- e.applyDraft(update);
- }
- manager.add(update);
- events.clear();
- }
-
- private List<HashtagsEvent> getHashtagsEvents(Change change,
- NoteDbUpdateManager manager) throws IOException {
- String refName = changeMetaRef(change.getId());
- Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName);
- if (!old.isPresent()) {
- return Collections.emptyList();
- }
-
- RevWalk rw = manager.getChangeRepo().rw;
- List<HashtagsEvent> events = new ArrayList<>();
- rw.reset();
- rw.markStart(rw.parseCommit(old.get()));
- for (RevCommit commit : rw) {
- Account.Id authorId;
- try {
- authorId =
- changeNoteUtil.parseIdent(commit.getAuthorIdent(), change.getId());
- } catch (ConfigInvalidException e) {
- continue; // Corrupt data, no valid hashtags in this commit.
- }
- PatchSet.Id psId = parsePatchSetId(change, commit);
- Set<String> hashtags = parseHashtags(commit);
- if (authorId == null || psId == null || hashtags == null) {
- continue;
- }
-
- Timestamp commitTime =
- new Timestamp(commit.getCommitterIdent().getWhen().getTime());
- events.add(new HashtagsEvent(psId, authorId, commitTime, hashtags,
- change.getCreatedOn()));
- }
- return events;
- }
-
- private Set<String> parseHashtags(RevCommit commit) {
- List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
- if (hashtagsLines.isEmpty() || hashtagsLines.size() > 1) {
- return null;
- }
-
- if (hashtagsLines.get(0).isEmpty()) {
- return ImmutableSet.of();
- }
- return Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
- }
-
- private PatchSet.Id parsePatchSetId(Change change, RevCommit commit) {
- List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
- if (psIdLines.size() != 1) {
- return null;
- }
- Integer psId = Ints.tryParse(psIdLines.get(0));
- if (psId == null) {
- return null;
- }
- return new PatchSet.Id(change.getId(), psId);
- }
-
- private void deleteChangeMetaRef(Change change, ChainedReceiveCommands cmds)
- throws IOException {
- String refName = changeMetaRef(change.getId());
- Optional<ObjectId> old = cmds.get(refName);
- if (old.isPresent()) {
- cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName));
- }
- }
-
- private void deleteDraftRefs(Change change, OpenRepo allUsersRepo)
- throws IOException {
- for (Ref r : allUsersRepo.repo.getRefDatabase()
- .getRefs(RefNames.refsDraftCommentsPrefix(change.getId())).values()) {
- allUsersRepo.cmds.add(
- new ReceiveCommand(r.getObjectId(), ObjectId.zeroId(), r.getName()));
- }
- }
-
- private static final Ordering<Event> EVENT_ORDER = new Ordering<Event>() {
- @Override
- public int compare(Event a, Event b) {
- return ComparisonChain.start()
- .compare(a.when, b.when)
- .compareTrueFirst(isPatchSet(a), isPatchSet(b))
- .compareTrueFirst(a.predatesChange, b.predatesChange)
- .compare(a.who, b.who, ReviewDbUtil.intKeyOrdering())
- .compare(a.psId, b.psId, ReviewDbUtil.intKeyOrdering().nullsLast())
- .result();
- }
-
- private boolean isPatchSet(Event e) {
- return e instanceof PatchSetEvent;
- }
- };
-
- private abstract static class Event {
- // NOTE: EventList only supports direct subclasses, not an arbitrary
- // hierarchy.
-
- final Account.Id who;
- final Timestamp when;
- final String tag;
- final boolean predatesChange;
- PatchSet.Id psId;
-
- protected Event(PatchSet.Id psId, Account.Id who, Timestamp when,
- Timestamp changeCreatedOn, String tag) {
- this.psId = psId;
- this.who = who;
- this.tag = tag;
- // Truncate timestamps at the change's createdOn timestamp.
- predatesChange = when.before(changeCreatedOn);
- this.when = predatesChange ? changeCreatedOn : when;
- }
-
- protected void checkUpdate(AbstractChangeUpdate update) {
- checkState(Objects.equals(update.getPatchSetId(), psId),
- "cannot apply event for %s to update for %s",
- update.getPatchSetId(), psId);
- checkState(when.getTime() - update.getWhen().getTime() <= MAX_WINDOW_MS,
- "event at %s outside update window starting at %s",
- when, update.getWhen());
- checkState(Objects.equals(update.getNullableAccountId(), who),
- "cannot apply event by %s to update by %s",
- who, update.getNullableAccountId());
- }
-
- /**
- * @return whether this event type must be unique per {@link ChangeUpdate},
- * i.e. there may be at most one of this type.
- */
- abstract boolean uniquePerUpdate();
-
- abstract void apply(ChangeUpdate update) throws OrmException, IOException;
-
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this)
- .add("psId", psId)
- .add("who", who)
- .add("when", when)
- .toString();
- }
- }
-
- private class EventList<E extends Event> extends ArrayList<E> {
- private static final long serialVersionUID = 1L;
-
- private E getLast() {
- return get(size() - 1);
- }
-
- private long getLastTime() {
- return getLast().when.getTime();
- }
-
- private long getFirstTime() {
- return get(0).when.getTime();
- }
-
- boolean canAdd(E e) {
- if (isEmpty()) {
- return true;
- }
- if (e instanceof FinalUpdatesEvent) {
- return false; // FinalUpdatesEvent always gets its own update.
- }
-
- Event last = getLast();
- if (!Objects.equals(e.who, last.who)
- || !e.psId.equals(last.psId)
- || !Objects.equals(e.tag, last.tag)) {
- return false; // Different patch set, author, or tag.
- }
-
- long t = e.when.getTime();
- long tFirst = getFirstTime();
- long tLast = getLastTime();
- checkArgument(t >= tLast,
- "event %s is before previous event in list %s", e, last);
- if (t - tLast > MAX_DELTA_MS || t - tFirst > MAX_WINDOW_MS) {
- return false; // Too much time elapsed.
- }
-
- if (!e.uniquePerUpdate()) {
- return true;
- }
- for (Event o : this) {
- if (e.getClass() == o.getClass()) {
- return false; // Only one event of this type allowed per update.
- }
- }
-
- // TODO(dborowitz): Additional heuristics, like keeping events separate if
- // they affect overlapping fields within a single entity.
-
- return true;
- }
-
- Timestamp getWhen() {
- return get(0).when;
- }
-
- PatchSet.Id getPatchSetId() {
- PatchSet.Id id = checkNotNull(get(0).psId);
- for (int i = 1; i < size(); i++) {
- checkState(get(i).psId.equals(id),
- "mismatched patch sets in EventList: %s != %s", id, get(i).psId);
- }
- return id;
- }
-
- Account.Id getAccountId() {
- Account.Id id = get(0).who;
- for (int i = 1; i < size(); i++) {
- checkState(Objects.equals(id, get(i).who),
- "mismatched users in EventList: %s != %s", id, get(i).who);
- }
- return id;
- }
-
- PersonIdent newAuthorIdent() {
- Account.Id id = getAccountId();
- if (id == null) {
- return new PersonIdent(serverIdent, getWhen());
- }
- return changeNoteUtil.newIdent(
- accountCache.get(id).getAccount(), getWhen(), serverIdent,
- anonymousCowardName);
- }
-
- String getTag() {
- return getLast().tag;
- }
- }
-
- private static void createChange(ChangeUpdate update, Change change) {
- update.setSubjectForCommit("Create change");
- update.setChangeId(change.getKey().get());
- update.setBranch(change.getDest().get());
- update.setSubject(change.getOriginalSubject());
- }
-
- private static class CreateChangeEvent extends Event {
- private final Change change;
-
- private static PatchSet.Id psId(Change change, Integer minPsNum) {
- int n;
- if (minPsNum == null) {
- // There were no patch sets for the change at all, so something is very
- // wrong. Bail and use 1 as the patch set.
- n = 1;
- } else {
- n = minPsNum;
- }
- return new PatchSet.Id(change.getId(), n);
- }
-
- CreateChangeEvent(Change change, Integer minPsNum) {
- super(psId(change, minPsNum), change.getOwner(), change.getCreatedOn(),
- change.getCreatedOn(), null);
- this.change = change;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return true;
- }
-
- @Override
- void apply(ChangeUpdate update) throws IOException, OrmException {
- checkUpdate(update);
- createChange(update, change);
- }
- }
-
- private static class ApprovalEvent extends Event {
- private PatchSetApproval psa;
-
- ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
- super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted(),
- changeCreatedOn, psa.getTag());
- this.psa = psa;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return false;
- }
-
- @Override
- void apply(ChangeUpdate update) {
- checkUpdate(update);
- update.putApproval(psa.getLabel(), psa.getValue());
- }
- }
-
- private static class ReviewerEvent extends Event {
- private Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer;
-
- ReviewerEvent(
- Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer,
- Timestamp changeCreatedOn) {
- super(
- // Reviewers aren't generally associated with a particular patch set
- // (although as an implementation detail they were in ReviewDb). Just
- // use the latest patch set at the time of the event.
- null,
- reviewer.getColumnKey(), reviewer.getValue(), changeCreatedOn, null);
- this.reviewer = reviewer;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return false;
- }
-
- @Override
- void apply(ChangeUpdate update) throws IOException, OrmException {
- checkUpdate(update);
- update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
- }
- }
-
- private static class PatchSetEvent extends Event {
- private final Change change;
- private final PatchSet ps;
- private final RevWalk rw;
- private boolean createChange;
-
- PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
- super(ps.getId(), ps.getUploader(), ps.getCreatedOn(),
- change.getCreatedOn(), null);
- this.change = change;
- this.ps = ps;
- this.rw = rw;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return true;
- }
-
- @Override
- void apply(ChangeUpdate update) throws IOException, OrmException {
- checkUpdate(update);
- if (createChange) {
- createChange(update, change);
- } else {
- update.setSubject(change.getSubject());
- update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
- }
- setRevision(update, ps);
- List<String> groups = ps.getGroups();
- if (!groups.isEmpty()) {
- update.setGroups(ps.getGroups());
- }
- if (ps.isDraft()) {
- update.setPatchSetState(PatchSetState.DRAFT);
- }
- }
-
- private void setRevision(ChangeUpdate update, PatchSet ps)
- throws IOException {
- String rev = ps.getRevision().get();
- String cert = ps.getPushCertificate();
- ObjectId id;
- try {
- id = ObjectId.fromString(rev);
- } catch (InvalidObjectIdException e) {
- update.setRevisionForMissingCommit(rev, cert);
- return;
- }
- try {
- update.setCommit(rw, id, cert);
- } catch (MissingObjectException e) {
- update.setRevisionForMissingCommit(rev, cert);
- return;
- }
- }
- }
-
- private static class PatchLineCommentEvent extends Event {
- public final PatchLineComment c;
- private final Change change;
- private final PatchSet ps;
- private final PatchListCache cache;
-
- PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
- PatchListCache cache) {
- super(PatchLineCommentsUtil.getCommentPsId(c), c.getAuthor(),
- c.getWrittenOn(), change.getCreatedOn(), c.getTag());
- this.c = c;
- this.change = change;
- this.ps = ps;
- this.cache = cache;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return false;
- }
-
- @Override
- void apply(ChangeUpdate update) throws OrmException {
- checkUpdate(update);
- if (c.getRevId() == null) {
- setCommentRevId(c, cache, change, ps);
- }
- update.putComment(c);
- }
-
- void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
- if (c.getRevId() == null) {
- setCommentRevId(c, cache, change, ps);
- }
- draftUpdate.putComment(c);
- }
- }
-
- private static class HashtagsEvent extends Event {
- private final Set<String> hashtags;
-
- HashtagsEvent(PatchSet.Id psId, Account.Id who, Timestamp when,
- Set<String> hashtags, Timestamp changeCreatdOn) {
- super(psId, who, when, changeCreatdOn,
- // Somewhat confusingly, hashtags do not use the setTag method on
- // AbstractChangeUpdate, so pass null as the tag.
- null);
- this.hashtags = hashtags;
- }
-
- @Override
- boolean uniquePerUpdate() {
- // Since these are produced from existing commits in the old NoteDb graph,
- // we know that there must be one per commit in the rebuilt graph.
- return true;
- }
-
- @Override
- void apply(ChangeUpdate update) throws OrmException {
- update.setHashtags(hashtags);
- }
- }
-
- private static class ChangeMessageEvent extends Event {
- private static final Pattern TOPIC_SET_REGEXP =
- Pattern.compile("^Topic set to (.+)$");
- private static final Pattern TOPIC_CHANGED_REGEXP =
- Pattern.compile("^Topic changed from (.+) to (.+)$");
- private static final Pattern TOPIC_REMOVED_REGEXP =
- Pattern.compile("^Topic (.+) removed$");
-
- private static final Pattern STATUS_ABANDONED_REGEXP =
- Pattern.compile("^Abandoned(\n.*)*$");
- private static final Pattern STATUS_RESTORED_REGEXP =
- Pattern.compile("^Restored(\n.*)*$");
-
- private final ChangeMessage message;
- private final Change noteDbChange;
-
- ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
- Timestamp changeCreatedOn) {
- super(message.getPatchSetId(), message.getAuthor(),
- message.getWrittenOn(), changeCreatedOn, message.getTag());
- this.message = message;
- this.noteDbChange = noteDbChange;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return true;
- }
-
- @Override
- void apply(ChangeUpdate update) throws OrmException {
- checkUpdate(update);
- update.setChangeMessage(message.getMessage());
- setTopic(update);
- setStatus(update);
- }
-
- private void setTopic(ChangeUpdate update) {
- String msg = message.getMessage();
- if (msg == null) {
- return;
- }
- Matcher m = TOPIC_SET_REGEXP.matcher(msg);
- if (m.matches()) {
- String topic = m.group(1);
- update.setTopic(topic);
- noteDbChange.setTopic(topic);
- return;
- }
-
- m = TOPIC_CHANGED_REGEXP.matcher(msg);
- if (m.matches()) {
- String topic = m.group(2);
- update.setTopic(topic);
- noteDbChange.setTopic(topic);
- return;
- }
-
- if (TOPIC_REMOVED_REGEXP.matcher(msg).matches()) {
- update.setTopic(null);
- noteDbChange.setTopic(null);
- }
- }
-
- private void setStatus(ChangeUpdate update) {
- String msg = message.getMessage();
- if (msg == null) {
- return;
- }
- if (STATUS_ABANDONED_REGEXP.matcher(msg).matches()) {
- update.setStatus(Change.Status.ABANDONED);
- noteDbChange.setStatus(Change.Status.ABANDONED);
- return;
- }
-
- if (STATUS_RESTORED_REGEXP.matcher(msg).matches()) {
- update.setStatus(Change.Status.NEW);
- noteDbChange.setStatus(Change.Status.NEW);
- }
- }
- }
-
- private static class FinalUpdatesEvent extends Event {
- private final Change change;
- private final Change noteDbChange;
-
- FinalUpdatesEvent(Change change, Change noteDbChange) {
- super(change.currentPatchSetId(), change.getOwner(),
- change.getLastUpdatedOn(), change.getCreatedOn(), null);
- this.change = change;
- this.noteDbChange = noteDbChange;
- }
-
- @Override
- boolean uniquePerUpdate() {
- return true;
- }
-
- @SuppressWarnings("deprecation")
- @Override
- void apply(ChangeUpdate update) throws OrmException {
- if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
- update.setTopic(change.getTopic());
- }
- if (!Objects.equals(change.getStatus(), noteDbChange.getStatus())) {
- // TODO(dborowitz): Stamp approximate approvals at this time.
- update.fixStatus(change.getStatus());
- }
- if (change.getSubmissionId() != null) {
- update.setSubmissionId(change.getSubmissionId());
- }
- if (!update.isEmpty()) {
- update.setSubjectForCommit("Final NoteDb migration updates");
- }
- }
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
new file mode 100644
index 0000000..2bd61a7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.primitives.Bytes;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.util.MutableInteger;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+
+class ChangeRevisionNote extends RevisionNote<Comment> {
+ private static final byte[] CERT_HEADER =
+ "certificate version ".getBytes(UTF_8);
+ // See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
+ private static final byte[] END_SIGNATURE =
+ "-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
+
+ private final ChangeNoteUtil noteUtil;
+ private final Change.Id changeId;
+ private final PatchLineComment.Status status;
+ private String pushCert;
+
+ ChangeRevisionNote(ChangeNoteUtil noteUtil, Change.Id changeId,
+ ObjectReader reader, ObjectId noteId, PatchLineComment.Status status) {
+ super(reader, noteId);
+ this.noteUtil = noteUtil;
+ this.changeId = changeId;
+ this.status = status;
+ }
+
+ public String getPushCert() {
+ checkParsed();
+ return pushCert;
+ }
+
+ @Override
+ protected List<Comment> parse(byte[] raw, int offset)
+ throws IOException, ConfigInvalidException {
+ MutableInteger p = new MutableInteger();
+ p.value = offset;
+
+ if (isJson(raw, p.value)) {
+ RevisionNoteData data = parseJson(noteUtil, raw, p.value);
+ if (status == PatchLineComment.Status.PUBLISHED) {
+ pushCert = data.pushCert;
+ } else {
+ pushCert = null;
+ }
+ return data.comments;
+ }
+
+ if (status == PatchLineComment.Status.PUBLISHED) {
+ pushCert = parsePushCert(changeId, raw, p);
+ trimLeadingEmptyLines(raw, p);
+ } else {
+ pushCert = null;
+ }
+ return noteUtil.parseNote(raw, p, changeId);
+ }
+
+ private static boolean isJson(byte[] raw, int offset) {
+ return raw[offset] == '{' || raw[offset] == '[';
+ }
+
+ private RevisionNoteData parseJson(ChangeNoteUtil noteUtil, byte[] raw,
+ int offset) throws IOException {
+ try (InputStream is = new ByteArrayInputStream(
+ raw, offset, raw.length - offset);
+ Reader r = new InputStreamReader(is, UTF_8)) {
+ return noteUtil.getGson().fromJson(r, RevisionNoteData.class);
+ }
+ }
+
+ private static String parsePushCert(Change.Id changeId, byte[] bytes,
+ MutableInteger p) throws ConfigInvalidException {
+ if (RawParseUtils.match(bytes, p.value, CERT_HEADER) < 0) {
+ return null;
+ }
+ int end = Bytes.indexOf(bytes, END_SIGNATURE);
+ if (end < 0) {
+ throw ChangeNotes.parseException(
+ changeId, "invalid push certificate in note");
+ }
+ int start = p.value;
+ p.value = end + END_SIGNATURE.length;
+ return new String(bytes, start, p.value, UTF_8);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 77b8dc0..3642dff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -19,6 +19,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
@@ -26,12 +27,14 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
+import static java.util.Comparator.comparing;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.annotations.VisibleForTesting;
@@ -45,10 +48,11 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
@@ -56,6 +60,7 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.RequestId;
+import com.google.gwtorm.client.IntKey;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
@@ -78,6 +83,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -96,8 +102,13 @@
public interface Factory {
ChangeUpdate create(ChangeControl ctl);
ChangeUpdate create(ChangeControl ctl, Date when);
- ChangeUpdate create(Change change, @Nullable Account.Id accountId,
- PersonIdent authorIdent, Date when,
+
+ ChangeUpdate create(
+ Change change,
+ @Assisted("effective") @Nullable Account.Id accountId,
+ @Assisted("real") @Nullable Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when,
Comparator<String> labelNameComparator);
@VisibleForTesting
@@ -107,11 +118,12 @@
private final AccountCache accountCache;
private final ChangeDraftUpdate.Factory draftUpdateFactory;
+ private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
private final NoteDbUpdateManager.Factory updateManagerFactory;
private final Table<String, Account.Id, Optional<Short>> approvals;
private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
- private final List<PatchLineComment> comments = new ArrayList<>();
+ private final List<Comment> comments = new ArrayList<>();
private String commitSubject;
private String subject;
@@ -122,6 +134,7 @@
private String submissionId;
private String topic;
private String commit;
+ private Optional<Account.Id> assignee;
private Set<String> hashtags;
private String changeMessage;
private String tag;
@@ -131,6 +144,7 @@
private boolean isAllowWriteToNewtRef;
private ChangeDraftUpdate draftUpdate;
+ private RobotCommentUpdate robotCommentUpdate;
@AssistedInject
private ChangeUpdate(
@@ -140,11 +154,12 @@
AccountCache accountCache,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeDraftUpdate.Factory draftUpdateFactory,
+ RobotCommentUpdate.Factory robotCommentUpdateFactory,
ProjectCache projectCache,
@Assisted ChangeControl ctl,
ChangeNoteUtil noteUtil) {
this(serverIdent, anonymousCowardName, migration, accountCache,
- updateManagerFactory, draftUpdateFactory,
+ updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory,
projectCache, ctl, serverIdent.getWhen(), noteUtil);
}
@@ -156,13 +171,14 @@
AccountCache accountCache,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeDraftUpdate.Factory draftUpdateFactory,
+ RobotCommentUpdate.Factory robotCommentUpdateFactory,
ProjectCache projectCache,
@Assisted ChangeControl ctl,
@Assisted Date when,
ChangeNoteUtil noteUtil) {
this(serverIdent, anonymousCowardName, migration, accountCache,
- updateManagerFactory, draftUpdateFactory, ctl,
- when,
+ updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory,
+ ctl, when,
projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
noteUtil);
}
@@ -173,7 +189,7 @@
private static Table<String, Account.Id, Optional<Short>> approvals(
Comparator<String> nameComparator) {
- return TreeBasedTable.create(nameComparator, ReviewDbUtil.intKeyOrdering());
+ return TreeBasedTable.create(nameComparator, comparing(IntKey::get));
}
@AssistedInject
@@ -184,6 +200,7 @@
AccountCache accountCache,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeDraftUpdate.Factory draftUpdateFactory,
+ RobotCommentUpdate.Factory robotCommentUpdateFactory,
@Assisted ChangeControl ctl,
@Assisted Date when,
@Assisted Comparator<String> labelNameComparator,
@@ -192,6 +209,7 @@
anonymousCowardName, noteUtil, when);
this.accountCache = accountCache;
this.draftUpdateFactory = draftUpdateFactory;
+ this.robotCommentUpdateFactory = robotCommentUpdateFactory;
this.updateManagerFactory = updateManagerFactory;
this.approvals = approvals(labelNameComparator);
}
@@ -204,16 +222,19 @@
AccountCache accountCache,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeDraftUpdate.Factory draftUpdateFactory,
+ RobotCommentUpdate.Factory robotCommentUpdateFactory,
ChangeNoteUtil noteUtil,
@Assisted Change change,
- @Assisted @Nullable Account.Id accountId,
+ @Assisted("effective") @Nullable Account.Id accountId,
+ @Assisted("real") @Nullable Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when,
@Assisted Comparator<String> labelNameComparator) {
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.accountCache = accountCache;
this.draftUpdateFactory = draftUpdateFactory;
+ this.robotCommentUpdateFactory = robotCommentUpdateFactory;
this.updateManagerFactory = updateManagerFactory;
this.approvals = approvals(labelNameComparator);
}
@@ -284,7 +305,7 @@
this.commitSubject = commitSubject;
}
- void setSubject(String subject) {
+ public void setSubject(String subject) {
this.subject = subject;
}
@@ -301,10 +322,10 @@
this.tag = tag;
}
- public void putComment(PatchLineComment c) {
+ public void putComment(PatchLineComment.Status status, Comment c) {
verifyComment(c);
createDraftUpdateIfNull();
- if (c.getStatus() == PatchLineComment.Status.DRAFT) {
+ if (status == PatchLineComment.Status.DRAFT) {
draftUpdate.putComment(c);
} else {
comments.add(c);
@@ -316,14 +337,15 @@
}
}
- public void deleteComment(PatchLineComment c) {
+ public void putRobotComment(RobotComment c) {
verifyComment(c);
- if (c.getStatus() == PatchLineComment.Status.DRAFT) {
- createDraftUpdateIfNull().deleteComment(c);
- } else {
- throw new IllegalArgumentException(
- "Cannot delete published comment " + c);
- }
+ createRobotCommentUpdateIfNull();
+ robotCommentUpdate.putComment(c);
+ }
+
+ public void deleteComment(Comment c) {
+ verifyComment(c);
+ createDraftUpdateIfNull().deleteComment(c);
}
@VisibleForTesting
@@ -331,22 +353,29 @@
if (draftUpdate == null) {
ChangeNotes notes = getNotes();
if (notes != null) {
- draftUpdate =
- draftUpdateFactory.create(notes, accountId, authorIdent, when);
+ draftUpdate = draftUpdateFactory.create(
+ notes, accountId, realAccountId, authorIdent, when);
} else {
draftUpdate = draftUpdateFactory.create(
- getChange(), accountId, authorIdent, when);
+ getChange(), accountId, realAccountId, authorIdent, when);
}
}
return draftUpdate;
}
- private void verifyComment(PatchLineComment c) {
- checkArgument(c.getRevId() != null, "RevId required for comment: %s", c);
- checkArgument(c.getAuthor().equals(getAccountId()),
- "The author for the following comment does not match the author of"
- + " this ChangeDraftUpdate (%s): %s", getAccountId(), c);
-
+ @VisibleForTesting
+ RobotCommentUpdate createRobotCommentUpdateIfNull() {
+ if (robotCommentUpdate == null) {
+ ChangeNotes notes = getNotes();
+ if (notes != null) {
+ robotCommentUpdate = robotCommentUpdateFactory.create(
+ notes, accountId, realAccountId, authorIdent, when);
+ } else {
+ robotCommentUpdate = robotCommentUpdateFactory.create(
+ getChange(), accountId, realAccountId, authorIdent, when);
+ }
+ }
+ return robotCommentUpdate;
}
public void setTopic(String topic) {
@@ -379,6 +408,15 @@
this.hashtags = hashtags;
}
+ public void setAssignee(Account.Id assignee) {
+ checkArgument(assignee != null, "use removeAssignee");
+ this.assignee = Optional.of(assignee);
+ }
+
+ public void removeAssignee() {
+ this.assignee = Optional.absent();
+ }
+
public Map<Account.Id, ReviewerStateInternal> getReviewers() {
return reviewers;
}
@@ -407,12 +445,12 @@
if (comments.isEmpty() && pushCert == null) {
return null;
}
- RevisionNoteMap rnm = getRevisionNoteMap(rw, curr);
+ RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
- for (PatchLineComment c : comments) {
- c.setTag(tag);
- cache.get(c.getRevId()).putComment(c);
+ for (Comment c : comments) {
+ c.tag = tag;
+ cache.get(new RevId(c.revId)).putComment(c);
}
if (pushCert != null) {
checkState(commit != null);
@@ -423,15 +461,15 @@
for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
ObjectId data = inserter.insert(
- OBJ_BLOB, e.getValue().build(noteUtil));
+ OBJ_BLOB, e.getValue().build(noteUtil, noteUtil.getWriteJson()));
rnm.noteMap.set(ObjectId.fromString(e.getKey().get()), data);
}
return rnm.noteMap.writeTree(inserter);
}
- private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr)
- throws ConfigInvalidException, OrmException, IOException {
+ private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw,
+ ObjectId curr) throws ConfigInvalidException, OrmException, IOException {
if (curr.equals(ObjectId.zeroId())) {
return RevisionNoteMap.emptyMap();
}
@@ -452,16 +490,20 @@
// Even though reading from changes might not be enabled, we need to
// parse any existing revision notes so we can merge them.
return RevisionNoteMap.parse(
- noteUtil, getId(), rw.getObjectReader(), noteMap, false);
+ noteUtil,
+ getId(),
+ rw.getObjectReader(),
+ noteMap,
+ PatchLineComment.Status.PUBLISHED);
}
- private void checkComments(Map<RevId, RevisionNote> existingNotes,
+ private void checkComments(Map<RevId, ChangeRevisionNote> existingNotes,
Map<RevId, RevisionNoteBuilder> toUpdate) throws OrmException {
// Prohibit various kinds of illegal operations on comments.
- Set<PatchLineComment.Key> existing = new HashSet<>();
- for (RevisionNote rn : existingNotes.values()) {
- for (PatchLineComment c : rn.comments) {
- existing.add(c.getKey());
+ Set<Comment.Key> existing = new HashSet<>();
+ for (ChangeRevisionNote rn : existingNotes.values()) {
+ for (Comment c : rn.getComments()) {
+ existing.add(c.key);
if (draftUpdate != null) {
// Take advantage of an existing update on All-Users to prune any
// published comments from drafts. NoteDbUpdateManager takes care of
@@ -478,14 +520,14 @@
// separate commit. But note that we don't care much about the commit
// graph of the draft ref, particularly because the ref is completely
// deleted when all drafts are gone.
- draftUpdate.deleteComment(c.getRevId(), c.getKey());
+ draftUpdate.deleteComment(c.revId, c.key);
}
}
}
for (RevisionNoteBuilder b : toUpdate.values()) {
- for (PatchLineComment c : b.put.values()) {
- if (existing.contains(c.getKey())) {
+ for (Comment c : b.put.values()) {
+ if (existing.contains(c.key)) {
throw new OrmException(
"Cannot update existing published comment: " + c);
}
@@ -543,6 +585,15 @@
addFooter(msg, FOOTER_COMMIT, commit);
}
+ if (assignee != null) {
+ if (assignee.isPresent()) {
+ addFooter(msg, FOOTER_ASSIGNEE);
+ addIdent(msg, assignee.get()).append('\n');
+ } else {
+ addFooter(msg, FOOTER_ASSIGNEE).append('\n');
+ }
+ }
+
Joiner comma = Joiner.on(',');
if (hashtags != null) {
addFooter(msg, FOOTER_HASHTAGS, comma.join(hashtags));
@@ -595,10 +646,8 @@
addFooter(msg, FOOTER_SUBMITTED_WITH)
.append(label.status).append(": ").append(label.label);
if (label.appliedBy != null) {
- PersonIdent ident =
- newIdent(accountCache.get(label.appliedBy).getAccount(), when);
- msg.append(": ").append(ident.getName())
- .append(" <").append(ident.getEmailAddress()).append('>');
+ msg.append(": ");
+ addIdent(msg, label.appliedBy);
}
msg.append('\n');
}
@@ -606,6 +655,11 @@
}
}
+ if (!Objects.equals(accountId, realAccountId)) {
+ addFooter(msg, FOOTER_REAL_USER);
+ addIdent(msg, realAccountId).append('\n');
+ }
+
cb.setMessage(msg.toString());
try {
ObjectId treeId = storeRevisionNotes(rw, ins, curr);
@@ -643,6 +697,7 @@
&& status == null
&& submissionId == null
&& submitRecords == null
+ && assignee == null
&& hashtags == null
&& topic == null
&& commit == null
@@ -655,6 +710,10 @@
return draftUpdate;
}
+ RobotCommentUpdate getRobotCommentUpdate() {
+ return robotCommentUpdate;
+ }
+
public void setAllowWriteToNewRef(boolean allow) {
isAllowWriteToNewtRef = allow;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 08195e4..661112e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -24,6 +24,7 @@
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -31,6 +32,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.StagedResult;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
@@ -67,8 +69,8 @@
private final Account.Id author;
private final NoteDbUpdateManager.Result rebuildResult;
- private ImmutableListMultimap<RevId, PatchLineComment> comments;
- private RevisionNoteMap revisionNoteMap;
+ private ImmutableListMultimap<RevId, Comment> comments;
+ private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
@AssistedInject
DraftCommentNotes(
@@ -101,7 +103,7 @@
this.rebuildResult = rebuildResult;
}
- RevisionNoteMap getRevisionNoteMap() {
+ RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() {
return revisionNoteMap;
}
@@ -109,13 +111,13 @@
return author;
}
- public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+ public ImmutableListMultimap<RevId, Comment> getComments() {
return comments;
}
- public boolean containsComment(PatchLineComment c) {
- for (PatchLineComment existing : comments.values()) {
- if (c.getKey().equals(existing.getKey())) {
+ public boolean containsComment(Comment c) {
+ for (Comment existing : comments.values()) {
+ if (c.key.equals(existing.key)) {
return true;
}
}
@@ -140,11 +142,11 @@
ObjectReader reader = handle.walk().getObjectReader();
revisionNoteMap = RevisionNoteMap.parse(
args.noteUtil, getChangeId(), reader, NoteMap.read(reader, tipCommit),
- true);
- Multimap<RevId, PatchLineComment> cs = ArrayListMultimap.create();
- for (RevisionNote rn : revisionNoteMap.revisionNotes.values()) {
- for (PatchLineComment c : rn.comments) {
- cs.put(c.getRevId(), c);
+ PatchLineComment.Status.DRAFT);
+ Multimap<RevId, Comment> cs = ArrayListMultimap.create();
+ for (ChangeRevisionNote rn : revisionNoteMap.revisionNotes.values()) {
+ for (Comment c : rn.getComments()) {
+ cs.put(new RevId(c.revId), c);
}
}
comments = ImmutableListMultimap.copyOf(cs);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java
new file mode 100644
index 0000000..e401a52
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.notedb.ChangeBundle.Source;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.List;
+
+@Singleton
+public class GwtormChangeBundleReader implements ChangeBundleReader {
+ @Inject
+ GwtormChangeBundleReader() {
+ }
+
+ @Override
+ public ChangeBundle fromReviewDb(ReviewDb db, Change.Id id)
+ throws OrmException {
+ db.changes().beginTransaction(id);
+ try {
+ List<PatchSetApproval> approvals =
+ db.patchSetApprovals().byChange(id).toList();
+ return new ChangeBundle(
+ db.changes().get(id),
+ db.changeMessages().byChange(id),
+ db.patchSets().byChange(id),
+ approvals,
+ db.patchComments().byChange(id),
+ ReviewerSet.fromApprovals(approvals),
+ Source.REVIEW_DB);
+ } finally {
+ db.rollback();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index 4a7a781..4aed71e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+import static java.util.Comparator.comparing;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -30,7 +31,6 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.git.RefCache;
import org.eclipse.jgit.lib.ObjectId;
@@ -78,7 +78,7 @@
}
@VisibleForTesting
- static NoteDbChangeState parse(Change.Id id, String str) {
+ public static NoteDbChangeState parse(Change.Id id, String str) {
if (str == null) {
return null;
}
@@ -163,7 +163,7 @@
public static String toString(ObjectId changeMetaId,
Map<Account.Id, ObjectId> draftIds) {
List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet());
- Collections.sort(accountIds, ReviewDbUtil.intKeyOrdering());
+ Collections.sort(accountIds, comparing(Account.Id::get));
StringBuilder sb = new StringBuilder(changeMetaId.name());
for (Account.Id id : accountIds) {
sb.append(',')
@@ -204,7 +204,7 @@
return id.get().equals(draftIds.get(accountId));
}
- boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
+ public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
throws IOException {
if (!isChangeUpToDate(changeRepoRefs)) {
return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index ff3b4b8..72a1f1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -23,6 +23,8 @@
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
@@ -51,6 +53,8 @@
factory(ChangeUpdate.Factory.class);
factory(ChangeDraftUpdate.Factory.class);
factory(DraftCommentNotes.Factory.class);
+ factory(RobotCommentUpdate.Factory.class);
+ factory(RobotCommentNotes.Factory.class);
factory(NoteDbUpdateManager.Factory.class);
if (!useTestBindings) {
install(ChangeNotesCache.module());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index cad531f..18cac47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -70,7 +70,7 @@
* of updates, use {@link #stage()}.
*/
public class NoteDbUpdateManager implements AutoCloseable {
- public static String CHANGES_READ_ONLY = "NoteDb changes are read-only";
+ public static final String CHANGES_READ_ONLY = "NoteDb changes are read-only";
public interface Factory {
NoteDbUpdateManager create(Project.NameKey projectName);
@@ -78,8 +78,9 @@
@AutoValue
public abstract static class StagedResult {
- private static StagedResult create(Change.Id id, NoteDbChangeState.Delta delta,
- OpenRepo changeRepo, OpenRepo allUsersRepo) {
+ private static StagedResult create(Change.Id id,
+ NoteDbChangeState.Delta delta, OpenRepo changeRepo,
+ OpenRepo allUsersRepo) {
ImmutableList<ReceiveCommand> changeCommands = ImmutableList.of();
ImmutableList<InsertedObject> changeObjects = ImmutableList.of();
if (changeRepo != null) {
@@ -119,10 +120,10 @@
@Nullable abstract NoteDbUpdateManager.StagedResult staged();
}
- static class OpenRepo implements AutoCloseable {
- final Repository repo;
- final RevWalk rw;
- final ChainedReceiveCommands cmds;
+ public static class OpenRepo implements AutoCloseable {
+ public final Repository repo;
+ public final RevWalk rw;
+ public final ChainedReceiveCommands cmds;
private final InMemoryInserter tempIns;
@Nullable private final ObjectInserter finalIns;
@@ -143,7 +144,7 @@
this.close = close;
}
- Optional<ObjectId> getObjectId(String refName) throws IOException {
+ public Optional<ObjectId> getObjectId(String refName) throws IOException {
return cmds.get(refName);
}
@@ -179,6 +180,7 @@
private final Project.NameKey projectName;
private final ListMultimap<String, ChangeUpdate> changeUpdates;
private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
+ private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
private final Set<Change.Id> toDelete;
private OpenRepo changeRepo;
@@ -199,6 +201,7 @@
this.projectName = projectName;
changeUpdates = ArrayListMultimap.create();
draftUpdates = ArrayListMultimap.create();
+ robotCommentUpdates = ArrayListMultimap.create();
toDelete = new HashSet<>();
}
@@ -233,17 +236,17 @@
return this;
}
- NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) {
+ public NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) {
this.checkExpectedState = checkExpectedState;
return this;
}
- OpenRepo getChangeRepo() throws IOException {
+ public OpenRepo getChangeRepo() throws IOException {
initChangeRepo();
return changeRepo;
}
- OpenRepo getAllUsersRepo() throws IOException {
+ public OpenRepo getAllUsersRepo() throws IOException {
initAllUsersRepo();
return allUsersRepo;
}
@@ -273,6 +276,7 @@
}
return changeUpdates.isEmpty()
&& draftUpdates.isEmpty()
+ && robotCommentUpdates.isEmpty()
&& toDelete.isEmpty();
}
@@ -294,6 +298,10 @@
if (du != null) {
draftUpdates.put(du.getRefName(), du);
}
+ RobotCommentUpdate rcu = update.getRobotCommentUpdate();
+ if (rcu != null) {
+ robotCommentUpdates.put(rcu.getRefName(), rcu);
+ }
}
public void add(ChangeDraftUpdate draftUpdate) {
@@ -453,6 +461,9 @@
if (!draftUpdates.isEmpty()) {
addUpdates(draftUpdates, allUsersRepo);
}
+ if (!robotCommentUpdates.isEmpty()) {
+ addUpdates(robotCommentUpdates, changeRepo);
+ }
for (Change.Id id : toDelete) {
doDelete(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
index 73ad68e..46e6dc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -14,72 +14,66 @@
package com.google.gerrit.server.notedb;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static com.google.common.base.Preconditions.checkState;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.collect.ImmutableList;
-import com.google.common.primitives.Bytes;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.util.MutableInteger;
-import org.eclipse.jgit.util.RawParseUtils;
import java.io.IOException;
+import java.util.List;
-class RevisionNote {
+abstract class RevisionNote<T extends Comment> {
static final int MAX_NOTE_SZ = 25 << 20;
- private static final byte[] CERT_HEADER =
- "certificate version ".getBytes(UTF_8);
- // See org.eclipse.jgit.transport.PushCertificateParser.END_SIGNATURE
- private static final byte[] END_SIGNATURE =
- "-----END PGP SIGNATURE-----\n".getBytes(UTF_8);
-
- private static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
+ protected static void trimLeadingEmptyLines(byte[] bytes, MutableInteger p) {
while (p.value < bytes.length && bytes[p.value] == '\n') {
p.value++;
}
}
- private static String parsePushCert(Change.Id changeId, byte[] bytes,
- MutableInteger p) throws ConfigInvalidException {
- if (RawParseUtils.match(bytes, p.value, CERT_HEADER) < 0) {
- return null;
- }
- int end = Bytes.indexOf(bytes, END_SIGNATURE);
- if (end < 0) {
- throw ChangeNotes.parseException(
- changeId, "invalid push certificate in note");
- }
- int start = p.value;
- p.value = end + END_SIGNATURE.length;
- return new String(bytes, start, p.value);
+ private final ObjectReader reader;
+ private final ObjectId noteId;
+
+ private byte[] raw;
+ private ImmutableList<T> comments;
+
+ RevisionNote(ObjectReader reader, ObjectId noteId) {
+ this.reader = reader;
+ this.noteId = noteId;
}
- final byte[] raw;
- final ImmutableList<PatchLineComment> comments;
- final String pushCert;
+ public byte[] getRaw() {
+ checkParsed();
+ return raw;
+ }
- RevisionNote(ChangeNoteUtil noteUtil, Change.Id changeId,
- ObjectReader reader, ObjectId noteId, boolean draftsOnly)
- throws ConfigInvalidException, IOException {
+ public ImmutableList<T> getComments() {
+ checkParsed();
+ return comments;
+ }
+
+ public void parse() throws IOException, ConfigInvalidException {
raw = reader.open(noteId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
MutableInteger p = new MutableInteger();
trimLeadingEmptyLines(raw, p);
- if (!draftsOnly) {
- pushCert = parsePushCert(changeId, raw, p);
- trimLeadingEmptyLines(raw, p);
- } else {
- pushCert = null;
+ if (p.value >= raw.length) {
+ comments = null;
+ return;
}
- PatchLineComment.Status status = draftsOnly
- ? PatchLineComment.Status.DRAFT
- : PatchLineComment.Status.PUBLISHED;
- comments = ImmutableList.copyOf(
- noteUtil.parseNote(raw, p, changeId, status));
+
+ comments = ImmutableList.copyOf(parse(raw, p.value));
+ }
+
+ protected abstract List<T> parse(byte[] raw, int offset)
+ throws IOException, ConfigInvalidException;
+
+ protected void checkParsed() {
+ checkState(raw != null, "revision note not parsed yet");
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index c8364d3..8491f57 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -15,16 +15,19 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.RevId;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -34,10 +37,12 @@
class RevisionNoteBuilder {
static class Cache {
- private final RevisionNoteMap revisionNoteMap;
+ private final RevisionNoteMap<?
+ extends RevisionNote<? extends Comment>> revisionNoteMap;
private final Map<RevId, RevisionNoteBuilder> builders;
- Cache(RevisionNoteMap revisionNoteMap) {
+ Cache(RevisionNoteMap<?
+ extends RevisionNote<? extends Comment>> revisionNoteMap) {
this.revisionNoteMap = revisionNoteMap;
this.builders = new HashMap<>();
}
@@ -58,18 +63,20 @@
}
final byte[] baseRaw;
- final List<PatchLineComment> baseComments;
- final Map<PatchLineComment.Key, PatchLineComment> put;
- final Set<PatchLineComment.Key> delete;
+ final List<? extends Comment> baseComments;
+ final Map<Comment.Key, Comment> put;
+ final Set<Comment.Key> delete;
private String pushCert;
- RevisionNoteBuilder(RevisionNote base) {
+ RevisionNoteBuilder(RevisionNote<? extends Comment> base) {
if (base != null) {
- baseRaw = base.raw;
- baseComments = base.comments;
- put = Maps.newHashMapWithExpectedSize(base.comments.size());
- pushCert = base.pushCert;
+ baseRaw = base.getRaw();
+ baseComments = base.getComments();
+ put = Maps.newHashMapWithExpectedSize(baseComments.size());
+ if (base instanceof ChangeRevisionNote) {
+ pushCert = ((ChangeRevisionNote) base).getPushCert();
+ }
} else {
baseRaw = new byte[0];
baseComments = Collections.emptyList();
@@ -79,13 +86,24 @@
delete = new HashSet<>();
}
- void putComment(PatchLineComment comment) {
- checkArgument(!delete.contains(comment.getKey()),
- "cannot both delete and put %s", comment.getKey());
- put.put(comment.getKey(), comment);
+ public byte[] build(ChangeNoteUtil noteUtil, boolean writeJson)
+ throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ if (writeJson) {
+ buildNoteJson(noteUtil, out);
+ } else {
+ buildNoteLegacy(noteUtil, out);
+ }
+ return out.toByteArray();
}
- void deleteComment(PatchLineComment.Key key) {
+ void putComment(Comment comment) {
+ checkArgument(!delete.contains(comment.key),
+ "cannot both delete and put %s", comment.key);
+ put.put(comment.key, comment);
+ }
+
+ void deleteComment(Comment.Key key) {
checkArgument(!put.containsKey(key), "cannot both delete and put %s", key);
delete.add(key);
}
@@ -94,27 +112,46 @@
this.pushCert = pushCert;
}
- byte[] build(ChangeNoteUtil noteUtil) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
+ private Multimap<Integer, Comment> buildCommentMap() {
+ Multimap<Integer, Comment> all = ArrayListMultimap.create();
+
+ for (Comment c : baseComments) {
+ if (!delete.contains(c.key) && !put.containsKey(c.key)) {
+ all.put(c.key.patchSetId, c);
+ }
+ }
+ for (Comment c : put.values()) {
+ if (!delete.contains(c.key)) {
+ all.put(c.key.patchSetId, c);
+ }
+ }
+ return all;
+ }
+
+ private void buildNoteJson(ChangeNoteUtil noteUtil, OutputStream out)
+ throws IOException {
+ Multimap<Integer, Comment> comments = buildCommentMap();
+ if (comments.isEmpty() && pushCert == null) {
+ return;
+ }
+
+ RevisionNoteData data = new RevisionNoteData();
+ data.comments = COMMENT_ORDER.sortedCopy(comments.values());
+ data.pushCert = pushCert;
+
+ try (OutputStreamWriter osw = new OutputStreamWriter(out, UTF_8)) {
+ noteUtil.getGson().toJson(data, osw);
+ }
+ }
+
+ private void buildNoteLegacy(ChangeNoteUtil noteUtil, OutputStream out)
+ throws IOException {
if (pushCert != null) {
byte[] certBytes = pushCert.getBytes(UTF_8);
out.write(certBytes, 0, trimTrailingNewlines(certBytes));
out.write('\n');
}
-
- Multimap<PatchSet.Id, PatchLineComment> all = ArrayListMultimap.create();
- for (PatchLineComment c : baseComments) {
- if (!delete.contains(c.getKey()) && !put.containsKey(c.getKey())) {
- all.put(c.getPatchSetId(), c);
- }
- }
- for (PatchLineComment c : put.values()) {
- if (!delete.contains(c.getKey())) {
- all.put(c.getPatchSetId(), c);
- }
- }
- noteUtil.buildNote(all, out);
- return out.toByteArray();
+ noteUtil.buildNote(buildCommentMap(), out);
}
private static int trimTrailingNewlines(byte[] bytes) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java
new file mode 100644
index 0000000..e0ee934
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java
@@ -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.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.gerrit.reviewdb.client.Comment;
+
+import java.util.List;
+
+/**
+ * Holds the raw data of a RevisionNote.
+ * <p>It is intended for (de)serialization to JSON only.
+ */
+class RevisionNoteData {
+ String pushCert;
+ List<Comment> comments;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index cd70528..8a9f711 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -16,6 +16,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.RevId;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -27,29 +29,45 @@
import java.util.HashMap;
import java.util.Map;
-class RevisionNoteMap {
+class RevisionNoteMap<T extends RevisionNote<? extends Comment>> {
final NoteMap noteMap;
- final ImmutableMap<RevId, RevisionNote> revisionNotes;
+ final ImmutableMap<RevId, T> revisionNotes;
- static RevisionNoteMap parse(ChangeNoteUtil noteUtil,
+ static RevisionNoteMap<ChangeRevisionNote> parse(ChangeNoteUtil noteUtil,
Change.Id changeId, ObjectReader reader, NoteMap noteMap,
- boolean draftsOnly) throws ConfigInvalidException, IOException {
- Map<RevId, RevisionNote> result = new HashMap<>();
+ PatchLineComment.Status status)
+ throws ConfigInvalidException, IOException {
+ Map<RevId, ChangeRevisionNote> result = new HashMap<>();
for (Note note : noteMap) {
- RevisionNote rn = new RevisionNote(
- noteUtil, changeId, reader, note.getData(), draftsOnly);
+ ChangeRevisionNote rn = new ChangeRevisionNote(
+ noteUtil, changeId, reader, note.getData(), status);
+ rn.parse();
result.put(new RevId(note.name()), rn);
}
- return new RevisionNoteMap(noteMap, ImmutableMap.copyOf(result));
+ return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
}
- static RevisionNoteMap emptyMap() {
- return new RevisionNoteMap(NoteMap.newEmptyMap(),
- ImmutableMap.<RevId, RevisionNote> of());
+ static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments(
+ ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap)
+ throws ConfigInvalidException, IOException {
+ Map<RevId, RobotCommentsRevisionNote> result = new HashMap<>();
+ for (Note note : noteMap) {
+ RobotCommentsRevisionNote rn = new RobotCommentsRevisionNote(
+ noteUtil, reader, note.getData());
+ rn.parse();
+ result.put(new RevId(note.name()), rn);
+ }
+ return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result));
+ }
+
+ static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T>
+ emptyMap() {
+ return new RevisionNoteMap<>(NoteMap.newEmptyMap(),
+ ImmutableMap.<RevId, T> of());
}
private RevisionNoteMap(NoteMap noteMap,
- ImmutableMap<RevId, RevisionNote> revisionNotes) {
+ ImmutableMap<RevId, T> revisionNotes) {
this.noteMap = noteMap;
this.revisionNotes = revisionNotes;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
new file mode 100644
index 0000000..4a26d7e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -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 com.google.gerrit.server.notedb;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+
+public class RobotCommentNotes extends AbstractChangeNotes<RobotCommentNotes> {
+ public interface Factory {
+ RobotCommentNotes create(Change change);
+ }
+
+ private final Change change;
+
+ private ImmutableListMultimap<RevId, RobotComment> comments;
+ private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
+
+ @AssistedInject
+ RobotCommentNotes(
+ Args args,
+ @Assisted Change change) {
+ super(args, change.getId(), false);
+ this.change = change;
+ }
+
+ RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap() {
+ return revisionNoteMap;
+ }
+
+ public ImmutableListMultimap<RevId, RobotComment> getComments() {
+ return comments;
+ }
+
+ public boolean containsComment(RobotComment c) {
+ for (RobotComment existing : comments.values()) {
+ if (c.key.equals(existing.key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected String getRefName() {
+ return RefNames.robotCommentsRef(getChangeId());
+ }
+
+ @Override
+ protected void onLoad(LoadHandle handle)
+ throws IOException, ConfigInvalidException {
+ ObjectId rev = handle.id();
+ if (rev == null) {
+ loadDefaults();
+ return;
+ }
+
+ RevCommit tipCommit = handle.walk().parseCommit(rev);
+ ObjectReader reader = handle.walk().getObjectReader();
+ revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader,
+ NoteMap.read(reader, tipCommit));
+ Multimap<RevId, RobotComment> cs = ArrayListMultimap.create();
+ for (RobotCommentsRevisionNote rn :
+ revisionNoteMap.revisionNotes.values()) {
+ for (RobotComment c : rn.getComments()) {
+ cs.put(new RevId(c.revId), c);
+ }
+ }
+ comments = ImmutableListMultimap.copyOf(cs);
+ }
+
+ @Override
+ protected void loadDefaults() {
+ comments = ImmutableListMultimap.of();
+ }
+
+ @Override
+ public Project.NameKey getProjectName() {
+ return change.getProject();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
new file mode 100644
index 0000000..9744632
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -0,0 +1,226 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.reviewdb.client.RefNames.robotCommentsRef;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A single delta to apply atomically to a change.
+ * <p>
+ * This delta contains only robot comments on a single patch set of a change by
+ * a single author. This delta will become a single commit in the repository.
+ * <p>
+ * This class is not thread safe.
+ */
+public class RobotCommentUpdate extends AbstractChangeUpdate {
+ public interface Factory {
+ RobotCommentUpdate create(
+ ChangeNotes notes,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
+
+ RobotCommentUpdate create(
+ Change change,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
+ }
+
+ private List<RobotComment> put = new ArrayList<>();
+
+ @AssistedInject
+ private RobotCommentUpdate(
+ @GerritPersonIdent PersonIdent serverIdent,
+ @AnonymousCowardName String anonymousCowardName,
+ NotesMigration migration,
+ ChangeNoteUtil noteUtil,
+ @Assisted ChangeNotes notes,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ @Assisted PersonIdent authorIdent,
+ @Assisted Date when) {
+ super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
+ accountId, realAccountId, authorIdent, when);
+ }
+
+ @AssistedInject
+ private RobotCommentUpdate(
+ @GerritPersonIdent PersonIdent serverIdent,
+ @AnonymousCowardName String anonymousCowardName,
+ NotesMigration migration,
+ ChangeNoteUtil noteUtil,
+ @Assisted Change change,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ @Assisted PersonIdent authorIdent,
+ @Assisted Date when) {
+ super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
+ accountId, realAccountId, authorIdent, when);
+ }
+
+ public void putComment(RobotComment c) {
+ verifyComment(c);
+ put.add(c);
+ }
+
+ private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
+ ObjectId curr, CommitBuilder cb)
+ throws ConfigInvalidException, OrmException, IOException {
+ RevisionNoteMap<RobotCommentsRevisionNote> rnm =
+ getRevisionNoteMap(rw, curr);
+ Set<RevId> updatedRevs =
+ Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
+ RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
+
+ for (RobotComment c : put) {
+ cache.get(new RevId(c.revId)).putComment(c);
+ }
+
+ Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+ boolean touchedAnyRevs = false;
+ boolean hasComments = false;
+ for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) {
+ updatedRevs.add(e.getKey());
+ ObjectId id = ObjectId.fromString(e.getKey().get());
+ byte[] data = e.getValue().build(noteUtil, true);
+ if (!Arrays.equals(data, e.getValue().baseRaw)) {
+ touchedAnyRevs = true;
+ }
+ if (data.length == 0) {
+ rnm.noteMap.remove(id);
+ } else {
+ hasComments = true;
+ ObjectId dataBlob = ins.insert(OBJ_BLOB, data);
+ rnm.noteMap.set(id, dataBlob);
+ }
+ }
+
+ // If we didn't touch any notes, tell the caller this was a no-op update. We
+ // couldn't have done this in isEmpty() below because we hadn't read the old
+ // data yet.
+ if (!touchedAnyRevs) {
+ return NO_OP_UPDATE;
+ }
+
+ // If we touched every revision and there are no comments left, tell the
+ // caller to delete the entire ref.
+ boolean touchedAllRevs = updatedRevs.equals(rnm.revisionNotes.keySet());
+ if (touchedAllRevs && !hasComments) {
+ return null;
+ }
+
+ cb.setTreeId(rnm.noteMap.writeTree(ins));
+ return cb;
+ }
+
+ private RevisionNoteMap<RobotCommentsRevisionNote> getRevisionNoteMap(
+ RevWalk rw, ObjectId curr)
+ throws ConfigInvalidException, OrmException, IOException {
+ if (curr.equals(ObjectId.zeroId())) {
+ return RevisionNoteMap.emptyMap();
+ }
+ if (migration.readChanges()) {
+ // If reading from changes is enabled, then the old RobotCommentNotes
+ // already parsed the revision notes. We can reuse them as long as the ref
+ // hasn't advanced.
+ ChangeNotes changeNotes = getNotes();
+ if (changeNotes != null) {
+ RobotCommentNotes robotCommentNotes =
+ changeNotes.load().getRobotCommentNotes();
+ if (robotCommentNotes != null) {
+ ObjectId idFromNotes =
+ firstNonNull(robotCommentNotes.getRevision(), ObjectId.zeroId());
+ RevisionNoteMap<RobotCommentsRevisionNote> rnm =
+ robotCommentNotes.getRevisionNoteMap();
+ if (idFromNotes.equals(curr) && rnm != null) {
+ return rnm;
+ }
+ }
+ }
+ }
+ NoteMap noteMap;
+ if (!curr.equals(ObjectId.zeroId())) {
+ noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
+ } else {
+ noteMap = NoteMap.newEmptyMap();
+ }
+ // Even though reading from changes might not be enabled, we need to
+ // parse any existing revision notes so we can merge them.
+ return RevisionNoteMap.parseRobotComments(
+ noteUtil,
+ rw.getObjectReader(),
+ noteMap);
+ }
+
+ @Override
+ protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins,
+ ObjectId curr) throws OrmException, IOException {
+ CommitBuilder cb = new CommitBuilder();
+ cb.setMessage("Update robot comments");
+ try {
+ return storeCommentsInNotes(rw, ins, curr, cb);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ @Override
+ protected Project.NameKey getProjectName() {
+ return getNotes().getProjectName();
+ }
+
+ @Override
+ protected String getRefName() {
+ return robotCommentsRef(getId());
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return put.isEmpty();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
new file mode 100644
index 0000000..0dca408
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNote.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.reviewdb.client.RobotComment;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.List;
+
+public class RobotCommentsRevisionNote extends RevisionNote<RobotComment> {
+ private final ChangeNoteUtil noteUtil;
+
+ RobotCommentsRevisionNote(ChangeNoteUtil noteUtil, ObjectReader reader,
+ ObjectId noteId) {
+ super(reader, noteId);
+ this.noteUtil = noteUtil;
+ }
+
+ @Override
+ protected List<RobotComment> parse(byte[] raw, int offset)
+ throws IOException {
+ try (InputStream is = new ByteArrayInputStream(
+ raw, offset, raw.length - offset);
+ Reader r = new InputStreamReader(is, UTF_8)) {
+ return noteUtil.getGson().fromJson(r,
+ RobotCommentsRevisionNoteData.class).comments;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
new file mode 100644
index 0000000..ea3a149
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.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.server.notedb;
+
+import com.google.gerrit.reviewdb.client.RobotComment;
+
+import java.util.List;
+
+public class RobotCommentsRevisionNoteData {
+ List<RobotComment> comments;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
index c0bb8ab..7bfb20c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
@@ -20,6 +20,8 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java
new file mode 100644
index 0000000..0e6d3e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.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.notedb.rebuild;
+
+import com.google.gwtorm.server.OrmRuntimeException;
+
+class AbortUpdateException extends OrmRuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ AbortUpdateException() {
+ super("aborted");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
new file mode 100644
index 0000000..3aba7c5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+
+import java.sql.Timestamp;
+
+class ApprovalEvent extends Event {
+ private PatchSetApproval psa;
+
+ ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
+ super(psa.getPatchSetId(), psa.getAccountId(), psa.getRealAccountId(),
+ psa.getGranted(), changeCreatedOn, psa.getTag());
+ this.psa = psa;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return false;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) {
+ checkUpdate(update);
+ update.putApproval(psa.getLabel(), psa.getValue());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
new file mode 100644
index 0000000..ed5cd8b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.sql.Timestamp;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class ChangeMessageEvent extends Event {
+ private static final Pattern TOPIC_SET_REGEXP =
+ Pattern.compile("^Topic set to (.+)$");
+ private static final Pattern TOPIC_CHANGED_REGEXP =
+ Pattern.compile("^Topic changed from (.+) to (.+)$");
+ private static final Pattern TOPIC_REMOVED_REGEXP =
+ Pattern.compile("^Topic (.+) removed$");
+
+ private final ChangeMessage message;
+ private final Change noteDbChange;
+
+ ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
+ Timestamp changeCreatedOn) {
+ super(message.getPatchSetId(), message.getAuthor(), message.getRealAuthor(),
+ message.getWrittenOn(), changeCreatedOn, message.getTag());
+ this.message = message;
+ this.noteDbChange = noteDbChange;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ checkUpdate(update);
+ update.setChangeMessage(message.getMessage());
+ setTopic(update);
+ }
+
+ private void setTopic(ChangeUpdate update) {
+ String msg = message.getMessage();
+ if (msg == null) {
+ return;
+ }
+ Matcher m = TOPIC_SET_REGEXP.matcher(msg);
+ if (m.matches()) {
+ String topic = m.group(1);
+ update.setTopic(topic);
+ noteDbChange.setTopic(topic);
+ return;
+ }
+
+ m = TOPIC_CHANGED_REGEXP.matcher(msg);
+ if (m.matches()) {
+ String topic = m.group(2);
+ update.setTopic(topic);
+ noteDbChange.setTopic(topic);
+ return;
+ }
+
+ if (TOPIC_REMOVED_REGEXP.matcher(msg).matches()) {
+ update.setTopic(null);
+ noteDbChange.setTopic(null);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
index 679b5e2..ea96012 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.notedb;
+package com.google.gerrit.server.notedb.rebuild;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.util.concurrent.ListenableFuture;
@@ -20,6 +20,8 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
new file mode 100644
index 0000000..1916610
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -0,0 +1,622 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.FormatUtil;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.ChainedReceiveCommands;
+import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.ChangeDraftUpdate;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class ChangeRebuilderImpl extends ChangeRebuilder {
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeRebuilderImpl.class);
+
+ /**
+ * The maximum amount of time between the ReviewDb timestamp of the first and
+ * last events batched together into a single NoteDb update.
+ * <p>
+ * Used to account for the fact that different records with their own
+ * timestamps (e.g. {@link PatchSetApproval} and {@link ChangeMessage})
+ * historically didn't necessarily use the same timestamp, and tended to call
+ * {@code System.currentTimeMillis()} independently.
+ */
+ public static final long MAX_WINDOW_MS = SECONDS.toMillis(3);
+
+ /**
+ * The maximum amount of time between two consecutive events to consider them
+ * to be in the same batch.
+ */
+ static final long MAX_DELTA_MS = SECONDS.toMillis(1);
+
+ private final AccountCache accountCache;
+ private final ChangeBundleReader bundleReader;
+ private final ChangeDraftUpdate.Factory draftUpdateFactory;
+ private final ChangeNoteUtil changeNoteUtil;
+ private final ChangeUpdate.Factory updateFactory;
+ private final NoteDbUpdateManager.Factory updateManagerFactory;
+ private final NotesMigration migration;
+ private final PatchListCache patchListCache;
+ private final PersonIdent serverIdent;
+ private final ProjectCache projectCache;
+ private final String anonymousCowardName;
+ private final String serverId;
+
+ @Inject
+ ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
+ AccountCache accountCache,
+ ChangeBundleReader bundleReader,
+ ChangeDraftUpdate.Factory draftUpdateFactory,
+ ChangeNoteUtil changeNoteUtil,
+ ChangeUpdate.Factory updateFactory,
+ NoteDbUpdateManager.Factory updateManagerFactory,
+ NotesMigration migration,
+ PatchListCache patchListCache,
+ @GerritPersonIdent PersonIdent serverIdent,
+ @Nullable ProjectCache projectCache,
+ @AnonymousCowardName String anonymousCowardName,
+ @GerritServerId String serverId) {
+ super(schemaFactory);
+ this.accountCache = accountCache;
+ this.bundleReader = bundleReader;
+ this.draftUpdateFactory = draftUpdateFactory;
+ this.changeNoteUtil = changeNoteUtil;
+ this.updateFactory = updateFactory;
+ this.updateManagerFactory = updateManagerFactory;
+ this.migration = migration;
+ this.patchListCache = patchListCache;
+ this.serverIdent = serverIdent;
+ this.projectCache = projectCache;
+ this.anonymousCowardName = anonymousCowardName;
+ this.serverId = serverId;
+ }
+
+ @Override
+ public Result rebuild(ReviewDb db, Change.Id changeId)
+ throws NoSuchChangeException, IOException, OrmException,
+ ConfigInvalidException {
+ db = ReviewDbUtil.unwrapDb(db);
+ Change change = db.changes().get(changeId);
+ if (change == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+ try (NoteDbUpdateManager manager =
+ updateManagerFactory.create(change.getProject())) {
+ buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
+ return execute(db, changeId, manager);
+ }
+ }
+
+ @Override
+ public Result rebuild(NoteDbUpdateManager manager,
+ ChangeBundle bundle) throws NoSuchChangeException, IOException,
+ OrmException, ConfigInvalidException {
+ Change change = new Change(bundle.getChange());
+ buildUpdates(manager, bundle);
+ return manager.stageAndApplyDelta(change);
+ }
+
+ @Override
+ public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
+ throws NoSuchChangeException, IOException, OrmException {
+ db = ReviewDbUtil.unwrapDb(db);
+ Change change = db.changes().get(changeId);
+ if (change == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+ NoteDbUpdateManager manager =
+ updateManagerFactory.create(change.getProject());
+ buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
+ manager.stage();
+ return manager;
+ }
+
+ @Override
+ public Result execute(ReviewDb db, Change.Id changeId,
+ NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
+ IOException {
+ db = ReviewDbUtil.unwrapDb(db);
+ Change change = db.changes().get(changeId);
+ if (change == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ final String oldNoteDbState = change.getNoteDbState();
+ Result r = manager.stageAndApplyDelta(change);
+ final String newNoteDbState = change.getNoteDbState();
+ try {
+ db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ String currNoteDbState = change.getNoteDbState();
+ if (Objects.equals(currNoteDbState, newNoteDbState)) {
+ // Another thread completed the same rebuild we were about to.
+ throw new AbortUpdateException();
+ } else if (!Objects.equals(oldNoteDbState, currNoteDbState)) {
+ // Another thread updated the state to something else.
+ throw new ConflictingUpdateException(change, oldNoteDbState);
+ }
+ change.setNoteDbState(newNoteDbState);
+ return change;
+ }
+ });
+ } catch (ConflictingUpdateException e) {
+ // Rethrow as an OrmException so the caller knows to use staged results.
+ // Strictly speaking they are not completely up to date, but result we
+ // send to the caller is the same as if this rebuild had executed before
+ // the other thread.
+ throw new OrmException(e.getMessage());
+ } catch (AbortUpdateException e) {
+ if (NoteDbChangeState.parse(changeId, newNoteDbState).isUpToDate(
+ manager.getChangeRepo().cmds.getRepoRefCache(),
+ manager.getAllUsersRepo().cmds.getRepoRefCache())) {
+ // If the state in ReviewDb matches NoteDb at this point, it means
+ // another thread successfully completed this rebuild. It's ok to not
+ // execute the update in this case, since the object referenced in the
+ // Result was flushed to the repo by whatever thread won the race.
+ return r;
+ }
+ // If the state doesn't match, that means another thread attempted this
+ // rebuild, but failed. Fall through and try to update the ref again.
+ }
+ if (migration.failChangeWrites()) {
+ // Don't even attempt to execute if read-only, it would fail anyway. But
+ // do throw an exception to the caller so they know to use the staged
+ // results instead of reading from the repo.
+ throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY);
+ }
+ manager.execute();
+ return r;
+ }
+
+ @Override
+ public boolean rebuildProject(ReviewDb db,
+ ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
+ Project.NameKey project, Repository allUsersRepo)
+ throws NoSuchChangeException, IOException, OrmException,
+ ConfigInvalidException {
+ checkArgument(allChanges.containsKey(project));
+ boolean ok = true;
+ ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
+ pm.beginTask(
+ FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
+ try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
+ ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
+ RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
+ manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
+ new ChainedReceiveCommands(allUsersRepo));
+ for (Change.Id changeId : allChanges.get(project)) {
+ try {
+ buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
+ } catch (NoPatchSetsException e) {
+ log.warn(e.getMessage());
+ } catch (Throwable t) {
+ log.error("Failed to rebuild change " + changeId, t);
+ ok = false;
+ }
+ pm.update(1);
+ }
+ manager.execute();
+ } finally {
+ pm.endTask();
+ }
+ return ok;
+ }
+
+ private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
+ throws IOException, OrmException {
+ manager.setCheckExpectedState(false);
+ Change change = new Change(bundle.getChange());
+ if (bundle.getPatchSets().isEmpty()) {
+ throw new NoPatchSetsException(change.getId());
+ }
+
+ PatchSet.Id currPsId = change.currentPatchSetId();
+ // We will rebuild all events, except for draft comments, in buckets based
+ // on author and timestamp.
+ List<Event> events = new ArrayList<>();
+ Multimap<Account.Id, DraftCommentEvent> draftCommentEvents =
+ ArrayListMultimap.create();
+
+ events.addAll(getHashtagsEvents(change, manager));
+
+ // Delete ref only after hashtags have been read
+ deleteChangeMetaRef(change, manager.getChangeRepo().cmds);
+ deleteDraftRefs(change, manager.getAllUsersRepo());
+
+ Integer minPsNum = getMinPatchSetNum(bundle);
+ Map<PatchSet.Id, PatchSetEvent> patchSetEvents =
+ Maps.newHashMapWithExpectedSize(bundle.getPatchSets().size());
+
+ for (PatchSet ps : bundle.getPatchSets()) {
+ if (ps.getId().get() > currPsId.get()) {
+ log.info(
+ "Skipping patch set {}, which is higher than current patch set {}",
+ ps.getId(), currPsId);
+ continue;
+ }
+ PatchSetEvent pse =
+ new PatchSetEvent(change, ps, manager.getChangeRepo().rw);
+ patchSetEvents.put(ps.getId(), pse);
+ events.add(pse);
+ for (Comment c : getComments(bundle, serverId, Status.PUBLISHED, ps)) {
+ CommentEvent e =
+ new CommentEvent(c, change, ps, patchListCache);
+ events.add(e.addDep(pse));
+ }
+ for (Comment c : getComments(bundle, serverId, Status.DRAFT, ps)) {
+ DraftCommentEvent e =
+ new DraftCommentEvent(c, change, ps, patchListCache);
+ draftCommentEvents.put(c.author.getId(), e);
+ }
+ }
+
+ for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
+ PatchSetEvent pse = patchSetEvents.get(psa.getPatchSetId());
+ if (pse != null) {
+ events.add(new ApprovalEvent(psa, change.getCreatedOn()).addDep(pse));
+ }
+ }
+
+ for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> r :
+ bundle.getReviewers().asTable().cellSet()) {
+ events.add(new ReviewerEvent(r, change.getCreatedOn()));
+ }
+
+ Change noteDbChange = new Change(null, null, null, null, null);
+ for (ChangeMessage msg : bundle.getChangeMessages()) {
+ List<Event> msgEvents = parseChangeMessage(msg, change, noteDbChange);
+ if (msg.getPatchSetId() != null) {
+ PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId());
+ if (pse != null) {
+ for (Event e : msgEvents) {
+ e.addDep(pse);
+ }
+ }
+ }
+ events.addAll(msgEvents);
+ }
+
+ sortAndFillEvents(change, noteDbChange, events, minPsNum);
+
+ EventList<Event> el = new EventList<>();
+ for (Event e : events) {
+ if (!el.canAdd(e)) {
+ flushEventsToUpdate(manager, el, change);
+ checkState(el.canAdd(e));
+ }
+ el.add(e);
+ }
+ flushEventsToUpdate(manager, el, change);
+
+ EventList<DraftCommentEvent> plcel = new EventList<>();
+ for (Account.Id author : draftCommentEvents.keys()) {
+ for (DraftCommentEvent e :
+ Ordering.natural().sortedCopy(draftCommentEvents.get(author))) {
+ if (!plcel.canAdd(e)) {
+ flushEventsToDraftUpdate(manager, plcel, change);
+ checkState(plcel.canAdd(e));
+ }
+ plcel.add(e);
+ }
+ flushEventsToDraftUpdate(manager, plcel, change);
+ }
+ }
+
+ private List<Event> parseChangeMessage(ChangeMessage msg, Change change,
+ Change noteDbChange) {
+ List<Event> events = new ArrayList<>(2);
+ events.add(new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
+ Optional<StatusChangeEvent> sce =
+ StatusChangeEvent.parseFromMessage(msg, change, noteDbChange);
+ if (sce.isPresent()) {
+ events.add(sce.get());
+ }
+ return events;
+ }
+
+ private static Integer getMinPatchSetNum(ChangeBundle bundle) {
+ Integer minPsNum = null;
+ for (PatchSet ps : bundle.getPatchSets()) {
+ int n = ps.getId().get();
+ if (minPsNum == null || n < minPsNum) {
+ minPsNum = n;
+ }
+ }
+ return minPsNum;
+ }
+
+ private static List<Comment> getComments(ChangeBundle bundle, String serverId,
+ PatchLineComment.Status status, PatchSet ps) {
+ return bundle.getPatchLineComments().stream()
+ .filter(c -> c.getPatchSetId().equals(ps.getId())
+ && c.getStatus() == status)
+ .map(plc -> plc.asComment(serverId)).sorted(CommentsUtil.COMMENT_ORDER)
+ .collect(toList());
+ }
+
+ private void sortAndFillEvents(Change change, Change noteDbChange,
+ List<Event> events, Integer minPsNum) {
+ events.add(new FinalUpdatesEvent(change, noteDbChange));
+ new EventSorter(events).sort();
+
+ // Ensure the first event in the list creates the change, setting the author
+ // and any required footers.
+ Event first = events.get(0);
+ if (first instanceof PatchSetEvent
+ && change.getOwner().equals(first.user)) {
+ ((PatchSetEvent) first).createChange = true;
+ } else {
+ events.add(0, new CreateChangeEvent(change, minPsNum));
+ }
+
+ // Final pass to correct some inconsistencies.
+ //
+ // First, fill in any missing patch set IDs using the latest patch set of
+ // the change at the time of the event, because NoteDb can't represent
+ // actions with no associated patch set ID. This workaround is as if a user
+ // added a ChangeMessage on the change by replying from the latest patch
+ // set.
+ //
+ // Start with the first patch set that actually exists. If there are no
+ // patch sets at all, minPsNum will be null, so just bail and use 1 as the
+ // patch set ID. The corresponding patch set won't exist, but this change is
+ // probably corrupt anyway, as deleting the last draft patch set should have
+ // deleted the whole change.
+ //
+ // Second, ensure timestamps are nondecreasing, by copying the previous
+ // timestamp if this happens. This assumes that the only way this can happen
+ // is due to dependency constraints, and it is ok to give an event the same
+ // timestamp as one of its dependencies.
+ int ps = firstNonNull(minPsNum, 1);
+ for (int i = 0; i < events.size(); i++) {
+ Event e = events.get(i);
+ if (e.psId == null) {
+ e.psId = new PatchSet.Id(change.getId(), ps);
+ } else {
+ ps = Math.max(ps, e.psId.get());
+ }
+
+ if (i > 0) {
+ Event p = events.get(i - 1);
+ if (e.when.before(p.when)) {
+ e.when = p.when;
+ }
+ }
+ }
+ }
+
+ private void flushEventsToUpdate(NoteDbUpdateManager manager,
+ EventList<Event> events, Change change) throws OrmException, IOException {
+ if (events.isEmpty()) {
+ return;
+ }
+ Comparator<String> labelNameComparator;
+ if (projectCache != null) {
+ labelNameComparator = projectCache.get(change.getProject())
+ .getLabelTypes().nameComparator();
+ } else {
+ // No project cache available, bail and use natural ordering; there's no
+ // semantic difference anyway difference.
+ labelNameComparator = Ordering.natural();
+ }
+ ChangeUpdate update = updateFactory.create(
+ change,
+ events.getAccountId(),
+ events.getRealAccountId(),
+ newAuthorIdent(events),
+ events.getWhen(),
+ labelNameComparator);
+ update.setAllowWriteToNewRef(true);
+ update.setPatchSetId(events.getPatchSetId());
+ update.setTag(events.getTag());
+ for (Event e : events) {
+ e.apply(update);
+ }
+ manager.add(update);
+ events.clear();
+ }
+
+ private void flushEventsToDraftUpdate(NoteDbUpdateManager manager,
+ EventList<DraftCommentEvent> events, Change change)
+ throws OrmException {
+ if (events.isEmpty()) {
+ return;
+ }
+ ChangeDraftUpdate update = draftUpdateFactory.create(
+ change,
+ events.getAccountId(),
+ events.getRealAccountId(),
+ newAuthorIdent(events),
+ events.getWhen());
+ update.setPatchSetId(events.getPatchSetId());
+ for (DraftCommentEvent e : events) {
+ e.applyDraft(update);
+ }
+ manager.add(update);
+ events.clear();
+ }
+
+ private PersonIdent newAuthorIdent(EventList<?> events) {
+ Account.Id id = events.getAccountId();
+ if (id == null) {
+ return new PersonIdent(serverIdent, events.getWhen());
+ }
+ return changeNoteUtil.newIdent(
+ accountCache.get(id).getAccount(), events.getWhen(), serverIdent,
+ anonymousCowardName);
+ }
+
+ private List<HashtagsEvent> getHashtagsEvents(Change change,
+ NoteDbUpdateManager manager) throws IOException {
+ String refName = changeMetaRef(change.getId());
+ Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName);
+ if (!old.isPresent()) {
+ return Collections.emptyList();
+ }
+
+ RevWalk rw = manager.getChangeRepo().rw;
+ List<HashtagsEvent> events = new ArrayList<>();
+ rw.reset();
+ rw.markStart(rw.parseCommit(old.get()));
+ for (RevCommit commit : rw) {
+ Account.Id authorId;
+ try {
+ authorId =
+ changeNoteUtil.parseIdent(commit.getAuthorIdent(), change.getId());
+ } catch (ConfigInvalidException e) {
+ continue; // Corrupt data, no valid hashtags in this commit.
+ }
+ PatchSet.Id psId = parsePatchSetId(change, commit);
+ Set<String> hashtags = parseHashtags(commit);
+ if (authorId == null || psId == null || hashtags == null) {
+ continue;
+ }
+
+ Timestamp commitTime =
+ new Timestamp(commit.getCommitterIdent().getWhen().getTime());
+ events.add(new HashtagsEvent(psId, authorId, commitTime, hashtags,
+ change.getCreatedOn()));
+ }
+ return events;
+ }
+
+ private Set<String> parseHashtags(RevCommit commit) {
+ List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
+ if (hashtagsLines.isEmpty() || hashtagsLines.size() > 1) {
+ return null;
+ }
+
+ if (hashtagsLines.get(0).isEmpty()) {
+ return ImmutableSet.of();
+ }
+ return Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
+ }
+
+ private PatchSet.Id parsePatchSetId(Change change, RevCommit commit) {
+ List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
+ if (psIdLines.size() != 1) {
+ return null;
+ }
+ Integer psId = Ints.tryParse(psIdLines.get(0));
+ if (psId == null) {
+ return null;
+ }
+ return new PatchSet.Id(change.getId(), psId);
+ }
+
+ private void deleteChangeMetaRef(Change change, ChainedReceiveCommands cmds)
+ throws IOException {
+ String refName = changeMetaRef(change.getId());
+ Optional<ObjectId> old = cmds.get(refName);
+ if (old.isPresent()) {
+ cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName));
+ }
+ }
+
+ private void deleteDraftRefs(Change change, OpenRepo allUsersRepo)
+ throws IOException {
+ for (Ref r : allUsersRepo.repo.getRefDatabase()
+ .getRefs(RefNames.refsDraftCommentsPrefix(change.getId())).values()) {
+ allUsersRepo.cmds.add(
+ new ReceiveCommand(r.getObjectId(), ObjectId.zeroId(), r.getName()));
+ }
+ }
+
+ static void createChange(ChangeUpdate update, Change change) {
+ update.setSubjectForCommit("Create change");
+ update.setChangeId(change.getKey().get());
+ update.setBranch(change.getDest().get());
+ update.setSubject(change.getOriginalSubject());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
new file mode 100644
index 0000000..8f461a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+
+class CommentEvent extends Event {
+ public final Comment c;
+ private final Change change;
+ private final PatchSet ps;
+ private final PatchListCache cache;
+
+ CommentEvent(Comment c, Change change, PatchSet ps,
+ PatchListCache cache) {
+ super(CommentsUtil.getCommentPsId(change.getId(), c), c.author.getId(),
+ c.getRealAuthor().getId(), c.writtenOn, change.getCreatedOn(), c.tag);
+ this.c = c;
+ this.change = change;
+ this.ps = ps;
+ this.cache = cache;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return false;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ checkUpdate(update);
+ if (c.revId == null) {
+ setCommentRevId(c, cache, change, ps);
+ }
+ update.putComment(PatchLineComment.Status.PUBLISHED, c);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java
new file mode 100644
index 0000000..2098727
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java
@@ -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.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmRuntimeException;
+
+class ConflictingUpdateException extends OrmRuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ ConflictingUpdateException(Change change, String expectedNoteDbState) {
+ super(String.format(
+ "Expected change %s to have noteDbState %s but was %s",
+ change.getId(), expectedNoteDbState, change.getNoteDbState()));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
new file mode 100644
index 0000000..b020911
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.io.IOException;
+
+class CreateChangeEvent extends Event {
+ private final Change change;
+
+ private static PatchSet.Id psId(Change change, Integer minPsNum) {
+ int n;
+ if (minPsNum == null) {
+ // There were no patch sets for the change at all, so something is very
+ // wrong. Bail and use 1 as the patch set.
+ n = 1;
+ } else {
+ n = minPsNum;
+ }
+ return new PatchSet.Id(change.getId(), n);
+ }
+
+ CreateChangeEvent(Change change, Integer minPsNum) {
+ super(psId(change, minPsNum), change.getOwner(), change.getOwner(),
+ change.getCreatedOn(), change.getCreatedOn(), null);
+ this.change = change;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws IOException, OrmException {
+ checkUpdate(update);
+ ChangeRebuilderImpl.createChange(update, change);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
new file mode 100644
index 0000000..2938480
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeDraftUpdate;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+
+class DraftCommentEvent extends Event {
+ public final Comment c;
+ private final Change change;
+ private final PatchSet ps;
+ private final PatchListCache cache;
+
+ DraftCommentEvent(Comment c, Change change, PatchSet ps,
+ PatchListCache cache) {
+ super(CommentsUtil.getCommentPsId(change.getId(), c), c.author.getId(),
+ c.getRealAuthor().getId(), c.writtenOn, change.getCreatedOn(), c.tag);
+ this.c = c;
+ this.change = change;
+ this.ps = ps;
+ this.cache = cache;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return false;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) {
+ throw new UnsupportedOperationException();
+ }
+
+ void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
+ if (c.revId == null) {
+ setCommentRevId(c, cache, change, ps);
+ }
+ draftUpdate.putComment(c);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
new file mode 100644
index 0000000..78df2d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl.MAX_WINDOW_MS;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ComparisonChain;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.notedb.AbstractChangeUpdate;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+abstract class Event implements Comparable<Event> {
+ // NOTE: EventList only supports direct subclasses, not an arbitrary
+ // hierarchy.
+
+ final Account.Id user;
+ final Account.Id realUser;
+ final String tag;
+ final boolean predatesChange;
+ final List<Event> deps;
+ Timestamp when;
+ PatchSet.Id psId;
+
+ protected Event(
+ PatchSet.Id psId,
+ Account.Id effectiveUser,
+ Account.Id realUser,
+ Timestamp when,
+ Timestamp changeCreatedOn,
+ String tag) {
+ this.psId = psId;
+ this.user = effectiveUser;
+ this.realUser = realUser != null ? realUser : effectiveUser;
+ this.tag = tag;
+ // Truncate timestamps at the change's createdOn timestamp.
+ predatesChange = when.before(changeCreatedOn);
+ this.when = predatesChange ? changeCreatedOn : when;
+ deps = new ArrayList<>();
+ }
+
+ protected void checkUpdate(AbstractChangeUpdate update) {
+ checkState(Objects.equals(update.getPatchSetId(), psId),
+ "cannot apply event for %s to update for %s",
+ update.getPatchSetId(), psId);
+ checkState(when.getTime() - update.getWhen().getTime() <= MAX_WINDOW_MS,
+ "event at %s outside update window starting at %s",
+ when, update.getWhen());
+ checkState(Objects.equals(update.getNullableAccountId(), user),
+ "cannot apply event by %s to update by %s",
+ user, update.getNullableAccountId());
+ }
+
+ Event addDep(Event e) {
+ deps.add(e);
+ return this;
+ }
+
+ /**
+ * @return whether this event type must be unique per {@link ChangeUpdate},
+ * i.e. there may be at most one of this type.
+ */
+ abstract boolean uniquePerUpdate();
+
+ abstract void apply(ChangeUpdate update) throws OrmException, IOException;
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("psId", psId)
+ .add("effectiveUser", user)
+ .add("realUser", realUser)
+ .add("when", when)
+ .toString();
+ }
+
+ @Override
+ public int compareTo(Event other) {
+ return ComparisonChain.start()
+ .compareFalseFirst(this.isFinalUpdates(), other.isFinalUpdates())
+ .compare(this.when, other.when)
+ .compareTrueFirst(isPatchSet(), isPatchSet())
+ .compareTrueFirst(this.predatesChange, other.predatesChange)
+ .compare(this.user, other.user,
+ ReviewDbUtil.intKeyOrdering())
+ .compare(this.realUser, other.realUser, ReviewDbUtil.intKeyOrdering())
+ .compare(this.psId, other.psId,
+ ReviewDbUtil.intKeyOrdering().nullsLast())
+ .result();
+ }
+
+ private boolean isPatchSet() {
+ return this instanceof PatchSetEvent;
+ }
+
+ private boolean isFinalUpdates() {
+ return this instanceof FinalUpdatesEvent;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
new file mode 100644
index 0000000..4f6e2e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Objects;
+
+class EventList<E extends Event> extends ArrayList<E> {
+ private static final long serialVersionUID = 1L;
+
+ private E getLast() {
+ return get(size() - 1);
+ }
+
+ private long getLastTime() {
+ return getLast().when.getTime();
+ }
+
+ private long getFirstTime() {
+ return get(0).when.getTime();
+ }
+
+ boolean canAdd(E e) {
+ if (isEmpty()) {
+ return true;
+ }
+ if (e instanceof FinalUpdatesEvent) {
+ return false; // FinalUpdatesEvent always gets its own update.
+ }
+
+ Event last = getLast();
+ if (!Objects.equals(e.user, last.user)
+ || !Objects.equals(e.realUser, last.realUser)
+ || !e.psId.equals(last.psId)
+ || !Objects.equals(e.tag, last.tag)) {
+ return false; // Different patch set, author, or tag.
+ }
+
+ long t = e.when.getTime();
+ long tFirst = getFirstTime();
+ long tLast = getLastTime();
+ checkArgument(t >= tLast,
+ "event %s is before previous event in list %s", e, last);
+ if (t - tLast > ChangeRebuilderImpl.MAX_DELTA_MS || t - tFirst > ChangeRebuilderImpl.MAX_WINDOW_MS) {
+ return false; // Too much time elapsed.
+ }
+
+ if (!e.uniquePerUpdate()) {
+ return true;
+ }
+ for (Event o : this) {
+ if (e.getClass() == o.getClass()) {
+ return false; // Only one event of this type allowed per update.
+ }
+ }
+
+ // TODO(dborowitz): Additional heuristics, like keeping events separate if
+ // they affect overlapping fields within a single entity.
+
+ return true;
+ }
+
+ Timestamp getWhen() {
+ return get(0).when;
+ }
+
+ PatchSet.Id getPatchSetId() {
+ PatchSet.Id id = checkNotNull(get(0).psId);
+ for (int i = 1; i < size(); i++) {
+ checkState(get(i).psId.equals(id),
+ "mismatched patch sets in EventList: %s != %s", id, get(i).psId);
+ }
+ return id;
+ }
+
+ Account.Id getAccountId() {
+ Account.Id id = get(0).user;
+ for (int i = 1; i < size(); i++) {
+ checkState(Objects.equals(id, get(i).user),
+ "mismatched users in EventList: %s != %s", id, get(i).user);
+ }
+ return id;
+ }
+
+ Account.Id getRealAccountId() {
+ Account.Id id = get(0).realUser;
+ for (int i = 1; i < size(); i++) {
+ checkState(Objects.equals(id, get(i).realUser),
+ "mismatched real users in EventList: %s != %s", id, get(i).realUser);
+ }
+ return id;
+ }
+
+ String getTag() {
+ return getLast().tag;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java
new file mode 100644
index 0000000..2ab4c00
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.SetMultimap;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * Helper to sort a list of events.
+ * <p>
+ * Events are sorted in two passes:
+ * <ol>
+ * <li>Sort by natural order (timestamp, patch set, author, etc.)</li>
+ * <li>Postpone any events with dependencies to occur only after all of their
+ * dependencies, where this violates natural order.</li>
+ * </ol>
+ *
+ * {@link #sort()} modifies the event list in place (similar to {@link
+ * Collections#sort(List)}), but does not modify any event. In particular,
+ * events might end up out of order with respect to timestamp; callers are
+ * responsible for adjusting timestamps later if they prefer monotonicity.
+ */
+class EventSorter {
+ private final List<Event> out;
+ private final LinkedHashSet<Event> sorted;
+ private ListMultimap<Event, Event> waiting;
+ private SetMultimap<Event, Event> deps;
+
+ EventSorter(List<Event> events) {
+ LinkedHashSet<Event> all = new LinkedHashSet<>(events);
+ out = events;
+
+ for (Event e : events) {
+ for (Event d : e.deps) {
+ checkArgument(all.contains(d), "dep %s of %s not in input list", d, e);
+ }
+ }
+
+ all.clear();
+ sorted = all; // Presized.
+ }
+
+ void sort() {
+ // First pass: sort by natural order.
+ Collections.sort(out);
+
+ // Populate waiting map after initial sort to preserve natural order.
+ waiting = ArrayListMultimap.create();
+ deps = HashMultimap.create();
+ for (Event e : out) {
+ for (Event d : e.deps) {
+ deps.put(e, d);
+ waiting.put(d, e);
+ }
+ }
+
+ // Second pass: enforce dependencies.
+ int size = out.size();
+ for (Event e : out) {
+ process(e);
+ }
+ checkState(sorted.size() == size,
+ "event sort expected %s elements, got %s", size, sorted.size());
+
+ // Modify out in-place a la Collections#sort.
+ out.clear();
+ out.addAll(sorted);
+ }
+
+ void process(Event e) {
+ if (sorted.contains(e)) {
+ return;
+ }
+ // If all events that e depends on have been emitted:
+ // - e can be emitted.
+ // - remove e from the dependency set of all events waiting on e, and then
+ // re-process those events in case they can now be emitted.
+ if (deps.get(e).isEmpty()) {
+ sorted.add(e);
+ for (Event w : waiting.get(e)) {
+ deps.get(w).remove(e);
+ process(w);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
new file mode 100644
index 0000000..9babc28
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Objects;
+
+class FinalUpdatesEvent extends Event {
+ private final Change change;
+ private final Change noteDbChange;
+
+ FinalUpdatesEvent(Change change, Change noteDbChange) {
+ super(change.currentPatchSetId(), change.getOwner(), change.getOwner(),
+ change.getLastUpdatedOn(), change.getCreatedOn(), null);
+ this.change = change;
+ this.noteDbChange = noteDbChange;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
+ update.setTopic(change.getTopic());
+ }
+ if (!Objects.equals(change.getStatus(), noteDbChange.getStatus())) {
+ // TODO(dborowitz): Stamp approximate approvals at this time.
+ update.fixStatus(change.getStatus());
+ }
+ if (change.getSubmissionId() != null
+ && noteDbChange.getSubmissionId() == null) {
+ update.setSubmissionId(change.getSubmissionId());
+ }
+ if (!Objects.equals(change.getAssignee(), noteDbChange.getAssignee())) {
+ // TODO(dborowitz): Parse intermediate values out from messages.
+ update.setAssignee(change.getAssignee());
+ }
+ if (!update.isEmpty()) {
+ update.setSubjectForCommit("Final NoteDb migration updates");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
new file mode 100644
index 0000000..f5bea3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.sql.Timestamp;
+import java.util.Set;
+
+class HashtagsEvent extends Event {
+ private final Set<String> hashtags;
+
+ HashtagsEvent(PatchSet.Id psId, Account.Id who, Timestamp when,
+ Set<String> hashtags, Timestamp changeCreatdOn) {
+ super(psId, who, who, when, changeCreatdOn,
+ // Somewhat confusingly, hashtags do not use the setTag method on
+ // AbstractChangeUpdate, so pass null as the tag.
+ null);
+ this.hashtags = hashtags;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ // Since these are produced from existing commits in the old NoteDb graph,
+ // we know that there must be one per commit in the rebuilt graph.
+ return true;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ update.setHashtags(hashtags);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
new file mode 100644
index 0000000..abac1b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.PatchSetState;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.List;
+
+class PatchSetEvent extends Event {
+ private final Change change;
+ private final PatchSet ps;
+ private final RevWalk rw;
+ boolean createChange;
+
+ PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
+ super(ps.getId(), ps.getUploader(), ps.getUploader(), ps.getCreatedOn(),
+ change.getCreatedOn(), null);
+ this.change = change;
+ this.ps = ps;
+ this.rw = rw;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws IOException, OrmException {
+ checkUpdate(update);
+ if (createChange) {
+ ChangeRebuilderImpl.createChange(update, change);
+ } else {
+ update.setSubject(change.getSubject());
+ update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
+ }
+ setRevision(update, ps);
+ List<String> groups = ps.getGroups();
+ if (!groups.isEmpty()) {
+ update.setGroups(ps.getGroups());
+ }
+ if (ps.isDraft()) {
+ update.setPatchSetState(PatchSetState.DRAFT);
+ }
+ }
+
+ private void setRevision(ChangeUpdate update, PatchSet ps)
+ throws IOException {
+ String rev = ps.getRevision().get();
+ String cert = ps.getPushCertificate();
+ ObjectId id;
+ try {
+ id = ObjectId.fromString(rev);
+ } catch (InvalidObjectIdException e) {
+ update.setRevisionForMissingCommit(rev, cert);
+ return;
+ }
+ try {
+ update.setCommit(rw, id, cert);
+ } catch (MissingObjectException e) {
+ update.setRevisionForMissingCommit(rev, cert);
+ return;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
new file mode 100644
index 0000000..c82f108
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gwtorm.server.OrmException;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+
+class ReviewerEvent extends Event {
+ private Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer;
+
+ ReviewerEvent(
+ Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer,
+ Timestamp changeCreatedOn) {
+ super(
+ // Reviewers aren't generally associated with a particular patch set
+ // (although as an implementation detail they were in ReviewDb). Just
+ // use the latest patch set at the time of the event.
+ null,
+ reviewer.getColumnKey(),
+ // TODO(dborowitz): Real account ID shouldn't really matter for
+ // reviewers, but we might have to deal with this to avoid ChangeBundle
+ // diffs when run against real data.
+ reviewer.getColumnKey(),
+ reviewer.getValue(),
+ changeCreatedOn, null);
+ this.reviewer = reviewer;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return false;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) throws IOException, OrmException {
+ checkUpdate(update);
+ update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
new file mode 100644
index 0000000..c7632e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.sql.Timestamp;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+class StatusChangeEvent extends Event {
+ private static final ImmutableMap<Change.Status, Pattern> PATTERNS =
+ ImmutableMap.of(
+ Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
+ Change.Status.MERGED, Pattern.compile(
+ "^Change has been successfully"
+ + " (merged|cherry-picked|rebased|pushed).*$"),
+ Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
+
+ static Optional<StatusChangeEvent> parseFromMessage(ChangeMessage message,
+ Change change, Change noteDbChange) {
+ String msg = message.getMessage();
+ if (msg == null) {
+ return Optional.absent();
+ }
+ for (Map.Entry<Change.Status, Pattern> e : PATTERNS.entrySet()) {
+ if (e.getValue().matcher(msg).matches()) {
+ return Optional.of(new StatusChangeEvent(
+ message, change, noteDbChange, e.getKey()));
+ }
+ }
+ return Optional.absent();
+ }
+
+ private final Change change;
+ private final Change noteDbChange;
+ private final Change.Status status;
+
+ private StatusChangeEvent(ChangeMessage message, Change change,
+ Change noteDbChange, Change.Status status) {
+ this(message.getPatchSetId(), message.getAuthor(),
+ message.getWrittenOn(), change, noteDbChange, message.getTag(),
+ status);
+ }
+
+ private StatusChangeEvent(PatchSet.Id psId, Account.Id author,
+ Timestamp when, Change change, Change noteDbChange,
+ String tag, Change.Status status) {
+ super(psId, author, author, when, change.getCreatedOn(), tag);
+ this.change = change;
+ this.noteDbChange = noteDbChange;
+ this.status = status;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ checkUpdate(update);
+ update.fixStatus(status);
+ noteDbChange.setStatus(status);
+ if (status == Change.Status.MERGED) {
+ update.setSubmissionId(change.getSubmissionId());
+ noteDbChange.setSubmissionId(change.getSubmissionId());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/ComparisonType.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/ComparisonType.java
new file mode 100644
index 0000000..abbb680
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/ComparisonType.java
@@ -0,0 +1,77 @@
+// 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.patch;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class ComparisonType {
+
+ /** 1-based parent */
+ private final Integer parentNum;
+
+ private final boolean autoMerge;
+
+ public static ComparisonType againstOtherPatchSet() {
+ return new ComparisonType(null, false);
+ }
+
+ public static ComparisonType againstParent(int parentNum) {
+ return new ComparisonType(parentNum, false);
+ }
+
+ public static ComparisonType againstAutoMerge() {
+ return new ComparisonType(null, true);
+ }
+
+ private ComparisonType(Integer parentNum, boolean autoMerge) {
+ this.parentNum = parentNum;
+ this.autoMerge = autoMerge;
+ }
+
+ public boolean isAgainstParentOrAutoMerge() {
+ return isAgainstParent() || isAgainstAutoMerge();
+ }
+
+ public boolean isAgainstParent() {
+ return parentNum != null;
+ }
+
+ public boolean isAgainstAutoMerge() {
+ return autoMerge;
+ }
+
+ public int getParentNum() {
+ checkNotNull(parentNum);
+ return parentNum;
+ }
+
+ void writeTo(OutputStream out) throws IOException {
+ writeVarInt32(out, parentNum != null ? parentNum : 0);
+ writeVarInt32(out, autoMerge ? 1 : 0);
+ }
+
+ static ComparisonType readFrom(InputStream in) throws IOException {
+ int p = readVarInt32(in);
+ Integer parentNum = p > 0 ? p : null;
+ boolean autoMerge = readVarInt32(in) != 0;
+ return new ComparisonType(parentNum, autoMerge);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummary.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummary.java
new file mode 100644
index 0000000..ae4589f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummary.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+public class DiffSummary implements Serializable {
+ private static final long serialVersionUID = DiffSummaryKey.serialVersionUID;
+
+ private transient String[] paths;
+
+ public DiffSummary(String[] paths) {
+ this.paths = paths;
+ }
+
+ public List<String> getPaths() {
+ return Collections.unmodifiableList(Arrays.asList(paths));
+ }
+
+ private void writeObject(ObjectOutputStream output) throws IOException {
+ writeVarInt32(output, paths.length);
+ try (DeflaterOutputStream out = new DeflaterOutputStream(output)) {
+ for (String p : paths) {
+ writeString(out, p);
+ }
+ }
+ }
+
+ private void readObject(ObjectInputStream input) throws IOException {
+ paths = new String[readVarInt32(input)];
+ try (InflaterInputStream in = new InflaterInputStream(input)) {
+ for (int i = 0; i < paths.length; i++) {
+ paths[i] = readString(in);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
new file mode 100644
index 0000000..4c708c4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
@@ -0,0 +1,117 @@
+// 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.patch;
+
+import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Objects;
+
+public class DiffSummaryKey implements Serializable {
+ public static final long serialVersionUID = 1L;
+
+ /** see PatchListKey#oldId */
+ private transient ObjectId oldId;
+
+ /** see PatchListKey#parentNum */
+ private transient Integer parentNum;
+
+ private transient ObjectId newId;
+ private transient Whitespace whitespace;
+
+ public static DiffSummaryKey fromPatchListKey(PatchListKey plk) {
+ return new DiffSummaryKey(plk.getOldId(), plk.getParentNum(),
+ plk.getNewId(), plk.getWhitespace());
+ }
+
+ private DiffSummaryKey(ObjectId oldId, Integer parentNum, ObjectId newId,
+ Whitespace whitespace) {
+ this.oldId = oldId;
+ this.parentNum = parentNum;
+ this.newId = newId;
+ this.whitespace = whitespace;
+ }
+
+ PatchListKey toPatchListKey() {
+ return new PatchListKey(oldId, parentNum, newId, whitespace);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oldId, parentNum, newId, whitespace);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (o instanceof DiffSummaryKey) {
+ DiffSummaryKey k = (DiffSummaryKey) o;
+ return Objects.equals(oldId, k.oldId)
+ && Objects.equals(parentNum, k.parentNum)
+ && Objects.equals(newId, k.newId)
+ && whitespace == k.whitespace;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder n = new StringBuilder();
+ n.append("DiffSummaryKey[");
+ n.append(oldId != null ? oldId.name() : "BASE");
+ n.append("..");
+ n.append(newId.name());
+ n.append(" ");
+ if (parentNum != null) {
+ n.append(parentNum);
+ n.append(" ");
+ }
+ n.append(whitespace.name());
+ n.append("]");
+ return n.toString();
+ }
+
+ private void writeObject(final ObjectOutputStream out) throws IOException {
+ writeCanBeNull(out, oldId);
+ out.writeInt(parentNum == null ? 0 : parentNum);
+ writeNotNull(out, newId);
+ Character c = PatchListKey.WHITESPACE_TYPES.get(whitespace);
+ if (c == null) {
+ throw new IOException("Invalid whitespace type: " + whitespace);
+ }
+ out.writeChar(c);
+ }
+
+ private void readObject(final ObjectInputStream in) throws IOException {
+ oldId = readCanBeNull(in);
+ int n = in.readInt();
+ parentNum = n == 0 ? null : Integer.valueOf(n);
+ newId = readNotNull(in);
+ char t = in.readChar();
+ whitespace = PatchListKey.WHITESPACE_TYPES.inverse().get(t);
+ if (whitespace == null) {
+ throw new IOException("Invalid whitespace type code: " + t);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
new file mode 100644
index 0000000..43e2392
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+public class DiffSummaryLoader implements Callable<DiffSummary> {
+ static final Logger log = LoggerFactory.getLogger(DiffSummaryLoader.class);
+
+ public interface Factory {
+ DiffSummaryLoader create(DiffSummaryKey key, Project.NameKey project);
+ }
+
+ private final PatchListCache patchListCache;
+ private final DiffSummaryKey key;
+ private final Project.NameKey project;
+
+ @AssistedInject
+ DiffSummaryLoader(PatchListCache plc,
+ @Assisted DiffSummaryKey k,
+ @Assisted Project.NameKey p) {
+ patchListCache = plc;
+ key = k;
+ project = p;
+ }
+
+ @Override
+ public DiffSummary call() throws Exception {
+ PatchList patchList = patchListCache.get(key.toPatchListKey(), project);
+ return toDiffSummary(patchList);
+ }
+
+ static DiffSummary toDiffSummary(PatchList patchList) {
+ List<String> r = new ArrayList<>(patchList.getPatches().size());
+ for (PatchListEntry e : patchList.getPatches()) {
+ if (Patch.isMagic(e.getNewName())) {
+ continue;
+ }
+ switch (e.getChangeType()) {
+ case ADDED:
+ case MODIFIED:
+ case DELETED:
+ case COPIED:
+ case REWRITE:
+ r.add(e.getNewName());
+ break;
+
+ case RENAMED:
+ r.add(e.getOldName());
+ r.add(e.getNewName());
+ break;
+ }
+ }
+ Collections.sort(r);
+ return new DiffSummary(r.toArray(new String[r.size()]));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryWeigher.java
new file mode 100644
index 0000000..548f999
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryWeigher.java
@@ -0,0 +1,34 @@
+// 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.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Computes memory usage for {@link DiffSummary} in bytes of memory used. */
+public class DiffSummaryWeigher implements
+ Weigher<DiffSummaryKey, DiffSummary> {
+
+ @Override
+ public int weigh(DiffSummaryKey key, DiffSummary value) {
+ int size = 16 + 4 * 8 + 2 * 36 // Size of DiffSummaryKey, 64 bit JVM
+ + 16 + 8 // Size of DiffSummary
+ + 16 + 8; // String[]
+ for (String p : value.getPaths()) {
+ size += 16 + 8 + 4 * 4 // String
+ + 16 + 8 + p.length() * 2; // char[]
+ }
+ return size;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/MergeListBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/MergeListBuilder.java
new file mode 100644
index 0000000..8f54e48
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/MergeListBuilder.java
@@ -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.
+
+package com.google.gerrit.server.patch;
+
+import com.google.common.collect.ImmutableList;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MergeListBuilder {
+ public static List<RevCommit> build(RevWalk rw, RevCommit merge,
+ int uninterestingParent) throws IOException {
+ rw.reset();
+ rw.parseBody(merge);
+ if (merge.getParentCount() < 2) {
+ return ImmutableList.of();
+ }
+
+ for (int parent = 0; parent < merge.getParentCount(); parent++) {
+ RevCommit parentCommit = merge.getParent(parent);
+ rw.parseBody(parentCommit);
+ if (parent == uninterestingParent - 1) {
+ rw.markUninteresting(parentCommit);
+ } else {
+ rw.markStart(parentCommit);
+ }
+ }
+
+ List<RevCommit> result = new ArrayList<>();
+ RevCommit c;
+ while ((c = rw.next()) != null) {
+ result.add(c);
+ }
+ return result;
+ }
+}
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..d2a6d2b 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;
@@ -42,9 +44,8 @@
private Text a;
private Text b;
- public PatchFile(final Repository repo, final PatchList patchList,
- final String fileName) throws MissingObjectException,
- IncorrectObjectTypeException, IOException {
+ public PatchFile(Repository repo, PatchList patchList, String fileName)
+ throws MissingObjectException, IncorrectObjectTypeException, IOException {
this.repo = repo;
this.entry = patchList.get(fileName);
@@ -53,7 +54,7 @@
final RevCommit bCommit = rw.parseCommit(patchList.getNewId());
if (Patch.COMMIT_MSG.equals(fileName)) {
- if (patchList.isAgainstParent()) {
+ if (patchList.getComparisonType().isAgainstParentOrAutoMerge()) {
a = Text.EMPTY;
} else {
// For the initial commit, we have an empty tree on Side A
@@ -66,7 +67,16 @@
aTree = null;
bTree = null;
+ } else if (Patch.MERGE_LIST.equals(fileName)) {
+ // For the initial commit, we have an empty tree on Side A
+ RevObject object = rw.parseAny(patchList.getOldId());
+ a = object instanceof RevCommit
+ ? Text.forMergeList(patchList.getComparisonType(), reader, object)
+ : Text.EMPTY;
+ b = Text.forMergeList(patchList.getComparisonType(), reader, bCommit);
+ aTree = null;
+ bTree = null;
} else {
if (patchList.getOldId() != null) {
aTree = rw.parseTree(patchList.getOldId());
@@ -151,7 +161,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/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 2a4afb3..2cfd007 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -58,16 +58,19 @@
@Nullable
private transient ObjectId oldId;
private transient ObjectId newId;
- private transient boolean againstParent;
+ private transient boolean isMerge;
+ private transient ComparisonType comparisonType;
private transient int insertions;
private transient int deletions;
private transient PatchListEntry[] patches;
- public PatchList(@Nullable final AnyObjectId oldId, final AnyObjectId newId,
- final boolean againstParent, final PatchListEntry[] patches) {
+ public PatchList(@Nullable AnyObjectId oldId, AnyObjectId newId,
+ boolean isMerge, ComparisonType comparisonType,
+ PatchListEntry[] patches) {
this.oldId = oldId != null ? oldId.copy() : null;
this.newId = newId.copy();
- this.againstParent = againstParent;
+ this.isMerge = isMerge;
+ this.comparisonType = comparisonType;
// We assume index 0 contains the magic commit message entry.
if (patches.length > 1) {
@@ -97,9 +100,9 @@
return Collections.unmodifiableList(Arrays.asList(patches));
}
- /** @return true if {@link #getOldId} is {@link #getNewId}'s ancestor. */
- public boolean isAgainstParent() {
- return againstParent;
+ /** @return the comparison type */
+ public ComparisonType getComparisonType() {
+ return comparisonType;
}
/** @return total number of new lines added. */
@@ -144,9 +147,12 @@
if (Patch.COMMIT_MSG.equals(fileName)) {
return 0;
}
+ if (isMerge && Patch.MERGE_LIST.equals(fileName)) {
+ return 1;
+ }
int high = patches.length;
- int low = 1;
+ int low = isMerge ? 2 : 1;
while (low < high) {
final int mid = (low + high) >>> 1;
final int cmp = patches[mid].getNewName().compareTo(fileName);
@@ -166,7 +172,8 @@
try (DeflaterOutputStream out = new DeflaterOutputStream(buf)) {
writeCanBeNull(out, oldId);
writeNotNull(out, newId);
- writeVarInt32(out, againstParent ? 1 : 0);
+ writeVarInt32(out, isMerge ? 1 : 0);
+ comparisonType.writeTo(out);
writeVarInt32(out, insertions);
writeVarInt32(out, deletions);
writeVarInt32(out, patches.length);
@@ -182,7 +189,8 @@
try (InflaterInputStream in = new InflaterInputStream(buf)) {
oldId = readCanBeNull(in);
newId = readNotNull(in);
- againstParent = readVarInt32(in) != 0;
+ isMerge = readVarInt32(in) != 0;
+ comparisonType = ComparisonType.readFrom(in);
insertions = readVarInt32(in);
deletions = readVarInt32(in);
final int cnt = readVarInt32(in);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 8a2403f..848b78f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -33,4 +33,7 @@
IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
IntraLineDiffArgs args);
+
+ DiffSummary getDiffSummary(Change change, PatchSet patchSet)
+ throws PatchListNotAvailableException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index abafad7..f1490f6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,6 +15,8 @@
package com.google.gerrit.server.patch;
+import static com.google.gerrit.server.patch.DiffSummaryLoader.toDiffSummary;
+
import com.google.common.cache.Cache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -39,6 +41,7 @@
public class PatchListCacheImpl implements PatchListCache {
static final String FILE_NAME = "diff";
static final String INTRA_NAME = "diff_intraline";
+ static final String DIFF_SUMMARY = "diff_summary";
public static Module module() {
return new CacheModule() {
@@ -54,6 +57,12 @@
.maximumWeight(10 << 20)
.weigher(IntraLineWeigher.class);
+ factory(DiffSummaryLoader.Factory.class);
+ persist(DIFF_SUMMARY, DiffSummaryKey.class, DiffSummary.class)
+ .maximumWeight(10 << 20)
+ .weigher(DiffSummaryWeigher.class)
+ .diskLimit(1 << 30);
+
bind(PatchListCacheImpl.class);
bind(PatchListCache.class).to(PatchListCacheImpl.class);
}
@@ -62,21 +71,27 @@
private final Cache<PatchListKey, PatchList> fileCache;
private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
+ private final Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
private final PatchListLoader.Factory fileLoaderFactory;
private final IntraLineLoader.Factory intraLoaderFactory;
+ private final DiffSummaryLoader.Factory diffSummaryLoaderFactory;
private final boolean computeIntraline;
@Inject
PatchListCacheImpl(
@Named(FILE_NAME) Cache<PatchListKey, PatchList> fileCache,
@Named(INTRA_NAME) Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
+ @Named(DIFF_SUMMARY) Cache<DiffSummaryKey, DiffSummary> diffSummaryCache,
PatchListLoader.Factory fileLoaderFactory,
IntraLineLoader.Factory intraLoaderFactory,
+ DiffSummaryLoader.Factory diffSummaryLoaderFactory,
@GerritServerConfig Config cfg) {
this.fileCache = fileCache;
this.intraCache = intraCache;
+ this.diffSummaryCache = diffSummaryCache;
this.fileLoaderFactory = fileLoaderFactory;
this.intraLoaderFactory = intraLoaderFactory;
+ this.diffSummaryLoaderFactory = diffSummaryLoaderFactory;
this.computeIntraline =
cfg.getBoolean("cache", INTRA_NAME, "enabled",
@@ -87,7 +102,11 @@
public PatchList get(PatchListKey key, Project.NameKey project)
throws PatchListNotAvailableException {
try {
- return fileCache.get(key, fileLoaderFactory.create(key, project));
+ PatchList pl = fileCache.get(key, fileLoaderFactory.create(key, project));
+ diffSummaryCache.put(
+ DiffSummaryKey.fromPatchListKey(key),
+ toDiffSummary(pl));
+ return pl;
} catch (ExecutionException e) {
PatchListLoader.log.warn("Error computing " + key, e);
throw new PatchListNotAvailableException(e);
@@ -140,4 +159,33 @@
}
return new IntraLineDiff(IntraLineDiff.Status.DISABLED);
}
+
+ @Override
+ public DiffSummary getDiffSummary(Change change, PatchSet patchSet)
+ throws PatchListNotAvailableException {
+ Project.NameKey project = change.getProject();
+ ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+ Whitespace ws = Whitespace.IGNORE_NONE;
+ return getDiffSummary(
+ DiffSummaryKey.fromPatchListKey(
+ PatchListKey.againstDefaultBase(b, ws)),
+ project);
+ }
+
+ private DiffSummary getDiffSummary(DiffSummaryKey key,
+ Project.NameKey project) throws PatchListNotAvailableException {
+ try {
+ return diffSummaryCache.get(key,
+ diffSummaryLoaderFactory.create(key, project));
+ } catch (ExecutionException e) {
+ PatchListLoader.log.warn("Error computing " + key, e);
+ throw new PatchListNotAvailableException(e);
+ } catch (UncheckedExecutionException e) {
+ if (e.getCause() instanceof LargeObjectException) {
+ PatchListLoader.log.warn("Error computing " + key, e);
+ throw new PatchListNotAvailableException(e);
+ }
+ throw e;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 43e3dce..6bb32a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -35,7 +35,7 @@
import java.util.Objects;
public class PatchListKey implements Serializable {
- public static final long serialVersionUID = 22L;
+ public static final long serialVersionUID = 24L;
public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
Whitespace.IGNORE_NONE, 'N',
@@ -92,6 +92,15 @@
whitespace = ws;
}
+ /** For use only by DiffSummaryKey. */
+ PatchListKey(ObjectId oldId, Integer parentNum, ObjectId newId,
+ Whitespace whitespace) {
+ this.oldId = oldId;
+ this.parentNum = parentNum;
+ this.newId = newId;
+ this.whitespace = whitespace;
+ }
+
/** Old side commit, or null to assume ancestor or combined merge. */
@Nullable
public ObjectId getOldId() {
@@ -138,6 +147,10 @@
n.append("..");
n.append(newId.name());
n.append(" ");
+ if (parentNum != null) {
+ n.append(parentNum);
+ n.append(" ");
+ }
n.append(whitespace.name());
n.append("]");
return n.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 2fa43bb..9616fc8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -17,11 +17,10 @@
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import com.google.common.base.Function;
import com.google.common.base.Throwables;
-import com.google.common.collect.FluentIterable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Project;
@@ -70,6 +69,7 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.stream.Stream;
public class PatchListLoader implements Callable<PatchList> {
static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
@@ -155,14 +155,19 @@
if (a == null) {
// TODO(sop) Remove this case.
- // This is a merge commit, compared to its ancestor.
+ // This is an octopus merge commit which should be compared against the
+ // auto-merge. However since we don't support computing the auto-merge
+ // for octopus merge commits, we fall back to diffing against the first
+ // parent, even though this wasn't what was requested.
//
- PatchListEntry[] entries = new PatchListEntry[1];
+ ComparisonType comparisonType = ComparisonType.againstParent(1);
+ PatchListEntry[] entries = new PatchListEntry[2];
entries[0] = newCommitMessage(cmp, reader, null, b);
- return new PatchList(a, b, true, entries);
+ entries[1] = newMergeList(cmp, reader, null, b, comparisonType);
+ return new PatchList(a, b, true, comparisonType, entries);
}
- boolean againstParent = isAgainstParent(a, b);
+ ComparisonType comparisonType = getComparisonType(a, b);
RevCommit aCommit = a instanceof RevCommit ? (RevCommit) a : null;
RevTree aTree = rw.parseTree(a);
@@ -179,22 +184,23 @@
key.getNewId(), key.getWhitespace());
PatchListKey oldKey = PatchListKey.againstDefaultBase(
key.getOldId(), key.getWhitespace());
- paths = FluentIterable
- .from(patchListCache.get(newKey, project).getPatches())
- .append(patchListCache.get(oldKey, project).getPatches())
- .transform(new Function<PatchListEntry, String>() {
- @Override
- public String apply(PatchListEntry entry) {
- return entry.getNewName();
- }
- })
- .toSet();
+ paths = Stream.concat(
+ patchListCache.get(newKey, project).getPatches().stream(),
+ patchListCache.get(oldKey, project).getPatches().stream())
+ .map(PatchListEntry::getNewName)
+ .collect(toSet());
}
int cnt = diffEntries.size();
List<PatchListEntry> entries = new ArrayList<>();
entries.add(newCommitMessage(cmp, reader,
- againstParent ? null : aCommit, b));
+ comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit, b));
+ boolean isMerge = b.getParentCount() > 1;
+ if (isMerge) {
+ entries.add(newMergeList(cmp, reader,
+ comparisonType.isAgainstParentOrAutoMerge() ? null : aCommit, b,
+ comparisonType));
+ }
for (int i = 0; i < cnt; i++) {
DiffEntry e = diffEntries.get(i);
if (paths == null || paths.contains(e.getNewPath())
@@ -208,19 +214,23 @@
entries.add(newEntry(aTree, fh, newSize, newSize - oldSize));
}
}
- return new PatchList(a, b, againstParent,
+ return new PatchList(a, b, isMerge, comparisonType,
entries.toArray(new PatchListEntry[entries.size()]));
}
}
- private boolean isAgainstParent(RevObject a, RevCommit b) {
+ private ComparisonType getComparisonType(RevObject a, RevCommit b) {
for (int i = 0; i < b.getParentCount(); i++) {
if (b.getParent(i).equals(a)) {
- return true;
+ return ComparisonType.againstParent(i + 1);
}
}
- return false;
+ if (key.getOldId() == null && b.getParentCount() > 0) {
+ return ComparisonType.againstAutoMerge();
+ }
+
+ return ComparisonType.againstOtherPatchSet();
}
private static long getFileSize(ObjectReader reader,
@@ -282,32 +292,30 @@
return diffFormatter.toFileHeader(diffEntry);
}
- private PatchListEntry newCommitMessage(final RawTextComparator cmp,
- final ObjectReader reader,
- final RevCommit aCommit, final RevCommit bCommit) throws IOException {
- StringBuilder hdr = new StringBuilder();
-
- hdr.append("diff --git");
- if (aCommit != null) {
- hdr.append(" a/").append(Patch.COMMIT_MSG);
- } else {
- hdr.append(" ").append(FileHeader.DEV_NULL);
- }
- hdr.append(" b/").append(Patch.COMMIT_MSG);
- hdr.append("\n");
-
- if (aCommit != null) {
- hdr.append("--- a/").append(Patch.COMMIT_MSG).append("\n");
- } else {
- hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
- }
- hdr.append("+++ b/").append(Patch.COMMIT_MSG).append("\n");
-
- Text aText =
- aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
+ private PatchListEntry newCommitMessage(RawTextComparator cmp,
+ ObjectReader reader, RevCommit aCommit, RevCommit bCommit)
+ throws IOException {
+ Text aText = aCommit != null
+ ? Text.forCommit(reader, aCommit)
+ : Text.EMPTY;
Text bText = Text.forCommit(reader, bCommit);
+ return createPatchListEntry(cmp, aCommit, aText, bText, Patch.COMMIT_MSG);
+ }
- byte[] rawHdr = hdr.toString().getBytes(UTF_8);
+ private PatchListEntry newMergeList(RawTextComparator cmp,
+ ObjectReader reader, RevCommit aCommit, RevCommit bCommit,
+ ComparisonType comparisonType) throws IOException {
+ Text aText = aCommit != null
+ ? Text.forMergeList(comparisonType, reader, aCommit)
+ : Text.EMPTY;
+ Text bText =
+ Text.forMergeList(comparisonType, reader, bCommit);
+ return createPatchListEntry(cmp, aCommit, aText, bText, Patch.MERGE_LIST);
+ }
+
+ private static PatchListEntry createPatchListEntry(RawTextComparator cmp,
+ RevCommit aCommit, Text aText, Text bText, String fileName) {
+ byte[] rawHdr = getRawHeader(aCommit != null, fileName);
byte[] aContent = aText.getContent();
byte[] bContent = bText.getContent();
long size = bContent.length;
@@ -319,6 +327,26 @@
return new PatchListEntry(fh, edits, size, sizeDelta);
}
+ private static byte[] getRawHeader(boolean hasA, String fileName) {
+ StringBuilder hdr = new StringBuilder();
+ hdr.append("diff --git");
+ if (hasA) {
+ hdr.append(" a/").append(fileName);
+ } else {
+ hdr.append(" ").append(FileHeader.DEV_NULL);
+ }
+ hdr.append(" b/").append(fileName);
+ hdr.append("\n");
+
+ if (hasA) {
+ hdr.append("--- a/").append(fileName).append("\n");
+ } else {
+ hdr.append("--- ").append(FileHeader.DEV_NULL).append("\n");
+ }
+ hdr.append("+++ b/").append(fileName).append("\n");
+ return hdr.toString().getBytes(UTF_8);
+ }
+
private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader,
long size, long sizeDelta) {
if (aTree == null // want combined diff
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/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index e09d26f..246d7a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -22,8 +22,8 @@
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.inject.Inject;
@@ -66,7 +66,7 @@
private ObjectReader reader;
private Change change;
private DiffPreferencesInfo diffPrefs;
- private boolean againstParent;
+ private ComparisonType comparisonType;
private ObjectId aId;
private ObjectId bId;
@@ -79,7 +79,8 @@
private int context;
@Inject
- PatchScriptBuilder(final FileTypeRegistry ftr, final PatchListCache plc) {
+ PatchScriptBuilder(FileTypeRegistry ftr,
+ PatchListCache plc) {
a = new Side();
b = new Side();
registry = ftr;
@@ -106,8 +107,8 @@
}
}
- void setTrees(final boolean ap, final ObjectId a, final ObjectId b) {
- againstParent = ap;
+ void setTrees(final ComparisonType ct, final ObjectId a, final ObjectId b) {
+ comparisonType = ct;
aId = a;
bId = b;
}
@@ -282,8 +283,8 @@
int lastLine;
lastLine = -1;
- for (PatchLineComment plc : comments.getCommentsA()) {
- final int a = plc.getLine();
+ for (Comment c : comments.getCommentsA()) {
+ final int a = c.lineNbr;
if (lastLine != a) {
final int b = mapA2B(a - 1);
if (0 <= b) {
@@ -294,8 +295,8 @@
}
lastLine = -1;
- for (PatchLineComment plc : comments.getCommentsB()) {
- final int b = plc.getLine();
+ for (Comment c : comments.getCommentsB()) {
+ int b = c.lineNbr;
if (lastLine != b) {
final int a = mapB2A(b - 1);
if (0 <= a) {
@@ -435,7 +436,8 @@
try {
final boolean reuse;
if (Patch.COMMIT_MSG.equals(path)) {
- if (againstParent && (aId == within || within.equals(aId))) {
+ if (comparisonType.isAgainstParentOrAutoMerge()
+ && (aId == within || within.equals(aId))) {
id = ObjectId.zeroId();
src = Text.EMPTY;
srcContent = Text.NO_BYTES;
@@ -453,7 +455,26 @@
}
}
reuse = false;
-
+ } else if (Patch.MERGE_LIST.equals(path)) {
+ if (comparisonType.isAgainstParentOrAutoMerge()
+ && (aId == within || within.equals(aId))) {
+ id = ObjectId.zeroId();
+ src = Text.EMPTY;
+ srcContent = Text.NO_BYTES;
+ mode = FileMode.MISSING;
+ displayMethod = DisplayMethod.NONE;
+ } else {
+ id = within;
+ src = Text.forMergeList(comparisonType, reader, within);
+ srcContent = src.getContent();
+ if (src == Text.EMPTY) {
+ mode = FileMode.MISSING;
+ displayMethod = DisplayMethod.NONE;
+ } else {
+ mode = FileMode.REGULAR_FILE;
+ }
+ }
+ reuse = false;
} else {
final TreeWalk tw = find(within);
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..7db206f 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;
@@ -26,14 +25,14 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
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.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.edit.ChangeEdit;
@@ -89,7 +88,7 @@
private final PatchListCache patchListCache;
private final ReviewDb db;
private final AccountInfoCacheFactory.Factory aicFactory;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final String fileName;
@Nullable
@@ -119,7 +118,7 @@
PatchListCache patchListCache,
ReviewDb db,
AccountInfoCacheFactory.Factory aicFactory,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
ChangeEditUtil editReader,
@Assisted ChangeControl control,
@Assisted final String fileName,
@@ -133,7 +132,7 @@
this.db = db;
this.control = control;
this.aicFactory = aicFactory;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.editReader = editReader;
this.fileName = fileName;
@@ -152,7 +151,7 @@
PatchListCache patchListCache,
ReviewDb db,
AccountInfoCacheFactory.Factory aicFactory,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
ChangeEditUtil editReader,
@Assisted ChangeControl control,
@Assisted String fileName,
@@ -166,7 +165,7 @@
this.db = db;
this.control = control;
this.aicFactory = aicFactory;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.editReader = editReader;
this.fileName = fileName;
@@ -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)
@@ -260,7 +260,7 @@
b.setRepository(git, project);
b.setChange(change);
b.setDiffPrefs(diffPrefs);
- b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
+ b.setTrees(list.getComparisonType(), list.getOldId(), list.getNewId());
return b;
}
@@ -402,13 +402,14 @@
private void loadPublished(final Map<Patch.Key, Patch> byKey,
final AccountInfoCacheFactory aic, final String file) throws OrmException {
ChangeNotes notes = control.getNotes();
- for (PatchLineComment c : plcUtil.publishedByChangeFile(db, notes, changeId, file)) {
- if (comments.include(c)) {
- aic.want(c.getAuthor());
+ for (Comment c : commentsUtil.publishedByChangeFile(db, notes, changeId, file)) {
+ if (comments.include(change.getId(), c)) {
+ aic.want(c.author.getId());
}
- final Patch.Key pKey = c.getKey().getParentKey();
- final Patch p = byKey.get(pKey);
+ PatchSet.Id psId = new PatchSet.Id(change.getId(), c.key.patchSetId);
+ Patch.Key pKey = new Patch.Key(psId, c.key.filename);
+ Patch p = byKey.get(pKey);
if (p != null) {
p.setCommentCount(p.getCommentCount() + 1);
}
@@ -418,14 +419,15 @@
private void loadDrafts(final Map<Patch.Key, Patch> byKey,
final AccountInfoCacheFactory aic, final Account.Id me, final String file)
throws OrmException {
- for (PatchLineComment c :
- plcUtil.draftByChangeFileAuthor(db, control.getNotes(), file, me)) {
- if (comments.include(c)) {
+ for (Comment c :
+ commentsUtil.draftByChangeFileAuthor(db, control.getNotes(), file, me)) {
+ if (comments.include(change.getId(), c)) {
aic.want(me);
}
- final Patch.Key pKey = c.getKey().getParentKey();
- final Patch p = byKey.get(pKey);
+ PatchSet.Id psId = new PatchSet.Id(change.getId(), c.key.patchSetId);
+ Patch.Key pKey = new Patch.Key(psId, c.key.filename);
+ Patch p = byKey.get(pKey);
if (p != null) {
p.setDraftCount(p.getDraftCount() + 1);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index 7982479..a84dd92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -87,6 +87,36 @@
}
}
+ public static Text forMergeList(ComparisonType comparisonType,
+ ObjectReader reader, AnyObjectId commitId) throws IOException {
+ try (RevWalk rw = new RevWalk(reader)) {
+ RevCommit c = rw.parseCommit(commitId);
+ StringBuilder b = new StringBuilder();
+ switch (c.getParentCount()) {
+ case 0:
+ break;
+ case 1: {
+ break;
+ }
+ default:
+ int uniterestingParent = comparisonType.isAgainstParent()
+ ? comparisonType.getParentNum()
+ : 1;
+
+ b.append("Merge List:\n\n");
+ for (RevCommit commit : MergeListBuilder.build(rw, c,
+ uniterestingParent)) {
+ b.append("* ");
+ b.append(reader.abbreviate(commit, 8).name());
+ b.append(" ");
+ b.append(commit.getShortMessage());
+ b.append("\n");
+ }
+ }
+ return new Text(b.toString().getBytes(UTF_8));
+ }
+ }
+
private static void appendPersonIdent(StringBuilder b, String field,
PersonIdent person) {
if (person != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 1f612a3..73d7b1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -17,7 +17,6 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.Iterables.transform;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
@@ -59,15 +58,6 @@
public class JarScanner implements PluginContentScanner {
private static final int SKIP_ALL = ClassReader.SKIP_CODE
| ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
- private static final Function<ClassData, ExtensionMetaData> CLASS_DATA_TO_EXTENSION_META_DATA =
- new Function<ClassData, ExtensionMetaData>() {
- @Override
- public ExtensionMetaData apply(ClassData classData) {
- return new ExtensionMetaData(classData.className,
- classData.annotationValue);
- }
- };
-
private final JarFile jarFile;
public JarScanner(Path src) throws IOException {
@@ -128,8 +118,11 @@
Collection<ClassData> values =
firstNonNull(discoverdData, Collections.<ClassData> emptySet());
- result.put(annotoation,
- transform(values, CLASS_DATA_TO_EXTENSION_META_DATA));
+ result.put(
+ annotoation,
+ transform(
+ values,
+ cd -> new ExtensionMetaData(cd.className, cd.annotationValue)));
}
return result.build();
@@ -202,7 +195,7 @@
Iterable<String> exports;
private ClassData(Iterable<String> exports) {
- super(Opcodes.ASM4);
+ super(Opcodes.ASM5);
this.exports = exports;
}
@@ -271,7 +264,7 @@
private abstract static class AbstractAnnotationVisitor extends
AnnotationVisitor {
AbstractAnnotationVisitor() {
- super(Opcodes.ASM4);
+ super(Opcodes.ASM5);
}
@Override
@@ -307,15 +300,12 @@
public Enumeration<PluginEntry> entries() {
return Collections.enumeration(Lists.transform(
Collections.list(jarFile.entries()),
- new Function<JarEntry, PluginEntry>() {
- @Override
- public PluginEntry apply(JarEntry jarEntry) {
- try {
- return resourceOf(jarEntry);
- } catch (IOException e) {
- throw new IllegalArgumentException("Cannot convert jar entry "
- + jarEntry + " to a resource", e);
- }
+ jarEntry -> {
+ try {
+ return resourceOf(jarEntry);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot convert jar entry "
+ + jarEntry + " to a resource", e);
}
}));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
index cf38310..e89eb7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
@@ -14,11 +14,10 @@
package com.google.gerrit.server.plugins;
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
-import com.google.common.collect.Iterables;
+import static java.util.stream.Collectors.joining;
import java.nio.file.Path;
+import java.util.stream.StreamSupport;
class MultipleProvidersForPluginException extends IllegalArgumentException {
private static final long serialVersionUID = 1L;
@@ -32,14 +31,8 @@
private static String providersListToString(
Iterable<ServerPluginProvider> providersHandlers) {
- Iterable<String> providerNames =
- Iterables.transform(providersHandlers,
- new Function<ServerPluginProvider, String>() {
- @Override
- public String apply(ServerPluginProvider provider) {
- return provider.getProviderPluginName();
- }
- });
- return Joiner.on(", ").join(providerNames);
+ return StreamSupport.stream(providersHandlers.spliterator(), false)
+ .map(ServerPluginProvider::getProviderPluginName)
+ .collect(joining(", "));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index e170510..5667003 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -17,7 +17,6 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
-import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
@@ -720,12 +719,9 @@
private static Iterable<Path> filterDisabledPlugins(
Collection<Path> paths) {
- return Iterables.filter(paths, new Predicate<Path>() {
- @Override
- public boolean apply(Path p) {
- return !p.getFileName().toString().endsWith(".disabled");
- }
- });
+ return Iterables.filter(
+ paths,
+ p -> !p.getFileName().toString().endsWith(".disabled"));
}
public String getGerritPluginName(Path srcPath) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index f0c2b78..ce97a83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.project;
-import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -91,14 +90,7 @@
if (commits == null || commits.isEmpty()) {
return null;
}
-
- return Lists.transform(commits,
- new Function<ObjectId, String>() {
- @Override
- public String apply(ObjectId id) {
- return id.getName();
- }
- });
+ return Lists.transform(commits, ObjectId::getName);
}
public static class BanResultInfo {
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..5cc461f 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);
}
@@ -352,6 +352,16 @@
return false;
}
+ /** Is this user assigned to this change? */
+ public boolean isAssignee() {
+ Account.Id currentAssignee = notes.getChange().getAssignee();
+ if (currentAssignee != null && getUser().isIdentifiedUser()) {
+ Account.Id id = getUser().getAccountId();
+ return id.equals(currentAssignee.get());
+ }
+ return false;
+ }
+
/** Is this user a reviewer for the change? */
public boolean isReviewer(ReviewDb db) throws OrmException {
return isReviewer(db, null);
@@ -414,6 +424,13 @@
return getRefControl().canForceEditTopicName();
}
+ public boolean canEditAssignee() {
+ return isOwner()
+ || getProjectControl().isOwner()
+ || getRefControl().canEditAssignee()
+ || isAssignee();
+ }
+
/** Can this user edit the hashtag name? */
public boolean canEditHashtags() {
return isOwner() // owner (aka creator) of the change can edit hashtags
@@ -424,7 +441,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..0f44a48 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.isMagic(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/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index b957ba1..8718a9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.project;
-import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -89,8 +88,8 @@
limit > 0 ? r.getReverseEntries(limit) : r.getReverseEntries();
} else {
entries = limit > 0
- ? new ArrayList<ReflogEntry>(limit)
- : new ArrayList<ReflogEntry>();
+ ? new ArrayList<>(limit)
+ : new ArrayList<>();
for (ReflogEntry e : r.getReverseEntries()) {
Timestamp timestamp = new Timestamp(e.getWho().getWhen().getTime());
if ((from == null || from.before(timestamp)) &&
@@ -102,12 +101,7 @@
}
}
}
- return Lists.transform(entries, new Function<ReflogEntry, ReflogEntryInfo>() {
- @Override
- public ReflogEntryInfo apply(ReflogEntry e) {
- return new ReflogEntryInfo(e);
- }
- });
+ return Lists.transform(entries, ReflogEntryInfo::new);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index bf17a37..1ea0c62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -16,7 +16,6 @@
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
@@ -445,13 +444,8 @@
} else if (matchSubstring != null) {
checkMatchOptions(matchPrefix == null && matchRegex == null);
return Iterables.filter(projectCache.all(),
- new Predicate<Project.NameKey>() {
- @Override
- public boolean apply(Project.NameKey in) {
- return in.get().toLowerCase(Locale.US)
- .contains(matchSubstring.toLowerCase(Locale.US));
- }
- });
+ p -> p.get().toLowerCase(Locale.US)
+ .contains(matchSubstring.toLowerCase(Locale.US)));
} else if (matchRegex != null) {
checkMatchOptions(matchPrefix == null && matchSubstring == null);
RegexListSearcher<Project.NameKey> searcher;
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/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 68d236e..f4ef129 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -17,7 +17,7 @@
import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -72,6 +72,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
/** Cached information on a project. */
public class ProjectState {
@@ -365,8 +366,8 @@
* from the immediate parent of this project and progresses up the
* hierarchy to All-Projects.
*/
- public Iterable<ProjectState> parents() {
- return Iterables.skip(tree(), 1);
+ public FluentIterable<ProjectState> parents() {
+ return FluentIterable.from(tree()).skip(1);
}
public boolean isAllProjects() {
@@ -378,75 +379,35 @@
}
public boolean isUseContributorAgreements() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getUseContributorAgreements();
- }
- });
+ return getInheritableBoolean(Project::getUseContributorAgreements);
}
public boolean isUseContentMerge() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getUseContentMerge();
- }
- });
+ return getInheritableBoolean(Project::getUseContentMerge);
}
public boolean isUseSignedOffBy() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getUseSignedOffBy();
- }
- });
+ return getInheritableBoolean(Project::getUseSignedOffBy);
}
public boolean isRequireChangeID() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getRequireChangeID();
- }
- });
+ return getInheritableBoolean(Project::getRequireChangeID);
}
public boolean isCreateNewChangeForAllNotInTarget() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getCreateNewChangeForAllNotInTarget();
- }
- });
+ return getInheritableBoolean(Project::getCreateNewChangeForAllNotInTarget);
}
public boolean isEnableSignedPush() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getEnableSignedPush();
- }
- });
+ return getInheritableBoolean(Project::getEnableSignedPush);
}
public boolean isRequireSignedPush() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getRequireSignedPush();
- }
- });
+ return getInheritableBoolean(Project::getRequireSignedPush);
}
public boolean isRejectImplicitMerges() {
- return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
- @Override
- public InheritableBoolean apply(Project input) {
- return input.getRejectImplicitMerges();
- }
- });
+ return getInheritableBoolean(Project::getRejectImplicitMerges);
}
public LabelTypes getLabelTypes() {
@@ -551,7 +512,8 @@
return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
}
- private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
+ private boolean getInheritableBoolean(
+ Function<Project, InheritableBoolean> func) {
for (ProjectState s : tree()) {
switch (func.apply(s.getProject())) {
case TRUE:
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..3314309 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);
}
}
@@ -429,6 +447,10 @@
return canPerform(Permission.EDIT_HASHTAGS);
}
+ public boolean canEditAssignee() {
+ return canPerform(Permission.EDIT_ASSIGNEE);
+ }
+
/** @return true if this user can force edit topic names. */
public boolean canForceEditTopicName() {
return canForcePerform(Permission.EDIT_TOPIC_NAME);
@@ -531,16 +553,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/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 01aacfb..cc215d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -17,7 +17,6 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects;
-import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -124,13 +123,10 @@
+ " not found");
}
- if (Iterables.tryFind(parent.tree(), new Predicate<ProjectState>() {
- @Override
- public boolean apply(ProjectState input) {
- return input.getProject().getNameKey()
- .equals(ctl.getProject().getNameKey());
- }
- }).isPresent()) {
+ if (Iterables.tryFind(parent.tree(), p -> {
+ return p.getProject().getNameKey()
+ .equals(ctl.getProject().getNameKey());
+ }).isPresent()) {
throw new ResourceConflictException("cycle exists between "
+ ctl.getProject().getName() + " and "
+ parent.getProject().getName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 5d0f4f1..b0c521b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -121,8 +121,9 @@
}
/**
- * @param fast if true, infer label information from rules rather than reading
- * from project config.
+ * @param fast if true assume reviewers are permitted to use label values
+ * currently stored on the change. Fast mode bypasses some reviewer
+ * permission checks.
* @return this
*/
public SubmitRuleEvaluator setFastEvalLabels(boolean fast) {
@@ -201,7 +202,7 @@
initPatchSet();
} catch (OrmException e) {
return ruleError("Error looking up patch set "
- + control.getChange().currentPatchSetId());
+ + control.getChange().currentPatchSetId(), e);
}
if (patchSet.isDraft()) {
return cannotSubmitDraft();
@@ -372,7 +373,7 @@
initPatchSet();
} catch (OrmException e) {
return typeError("Error looking up patch set "
- + control.getChange().currentPatchSetId());
+ + control.getChange().currentPatchSetId(), e);
}
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
index 168be5d..9f0bf89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
@@ -16,7 +16,6 @@
import static com.google.common.base.Preconditions.checkArgument;
-import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
@@ -157,12 +156,7 @@
private Iterable<T> buffer(ResultSet<T> scanner) {
return FluentIterable.from(Iterables.partition(scanner, 50))
- .transformAndConcat(new Function<List<T>, List<T>>() {
- @Override
- public List<T> apply(List<T> buffer) {
- return transformBuffer(buffer);
- }
- });
+ .transformAndConcat(this::transformBuffer);
}
protected List<T> transformBuffer(List<T> buffer) throws OrmRuntimeException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
index 36e5792..e98211e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/InternalQuery.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.query;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import com.google.gerrit.server.index.Index;
import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexConfig;
@@ -74,6 +75,25 @@
}
}
+ /**
+ * Run multiple queries in parallel.
+ * <p>
+ * If a limit was specified using {@link #setLimit(int)}, that limit is
+ * applied to each query independently.
+ *
+ * @param queries list of queries.
+ * @return results of the queries, one list of results per input query, in the
+ * same order as the input.
+ */
+ public List<List<T>> query(List<Predicate<T>> queries) throws OrmException {
+ try {
+ return Lists.transform(
+ queryProcessor.query(queries), QueryResult::entities);
+ } catch (QueryParseException e) {
+ throw new OrmException(e);
+ }
+ }
+
protected Schema<T> schema() {
Index<?, T> index = indexes != null ? indexes.getSearchIndex() : null;
return index != null ? index.getSchema() : null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 3a21ce4..644ed63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -353,6 +353,9 @@
} catch (RuntimeException | IllegalAccessException e) {
throw error("Error in operator " + name + ":" + value, e);
} catch (InvocationTargetException e) {
+ if (e.getCause() instanceof QueryParseException) {
+ throw (QueryParseException) e.getCause();
+ }
throw error("Error in operator " + name + ":" + value, e.getCause());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index 8373d4d..ae88887 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -122,16 +122,12 @@
return query(ImmutableList.of(query)).get(0);
}
- /*
- * Perform multiple queries over a list of query strings.
- * <p>
- * If a limit was specified using {@link #setLimit(int)} this method may
- * return up to {@code limit + 1} results, allowing the caller to determine if
- * there are more than {@code limit} matches and suggest to its own caller
- * that the query could be retried with {@link #setStart(int)}.
+ /**
+ * Perform multiple queries in parallel.
*
- * @param queries the queries.
- * @return results of the queries, one list per input query.
+ * @param queries list of queries.
+ * @return results of the queries, one QueryResult per input query, in the
+ * same order as the input.
*/
public List<QueryResult<T>> query(List<Predicate<T>> queries)
throws OrmException, QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 0288cb2..40fb3b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.query.account;
-import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
@@ -124,13 +123,9 @@
public Predicate<AccountState> defaultQuery(String query) {
return Predicate.and(
- Lists.transform(Splitter.on(' ').omitEmptyStrings().splitToList(query),
- new Function<String, Predicate<AccountState>>() {
- @Override
- public Predicate<AccountState> apply(String s) {
- return defaultField(s);
- }
- }));
+ Lists.transform(
+ Splitter.on(' ').omitEmptyStrings().splitToList(query),
+ this::defaultField));
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
similarity index 69%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
index cd93ed3..38622ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -18,22 +18,21 @@
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gwtorm.server.OrmException;
-@Deprecated
-class LegacyReviewerPredicate extends ChangeIndexPredicate {
+class AssigneePredicate extends ChangeIndexPredicate {
private final Account.Id id;
- LegacyReviewerPredicate(Account.Id id) {
- super(ChangeField.LEGACY_REVIEWER, id.toString());
+ AssigneePredicate(Account.Id id) {
+ super(ChangeField.ASSIGNEE, id.toString());
this.id = id;
}
- Account.Id getAccountId() {
- return id;
- }
-
@Override
- public boolean match(ChangeData object) throws OrmException {
- return object.reviewers().all().contains(id);
+ public boolean match(final ChangeData object) throws OrmException {
+ if (id.get() == ChangeField.NO_ASSIGNEE) {
+ Account.Id assignee = object.change().getAssignee();
+ return assignee == null;
+ }
+ return id.equals(object.change().getAssignee());
}
@Override
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..407d113 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
@@ -24,6 +24,7 @@
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
@@ -36,18 +37,18 @@
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.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
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.ReviewerStatusUpdate;
@@ -57,9 +58,9 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -307,6 +308,7 @@
return cd;
}
+ private boolean lazyLoad = true;
private final ReviewDb db;
private final GitRepositoryManager repoManager;
private final ChangeControl.GenericFactory changeControlFactory;
@@ -316,7 +318,7 @@
private final ChangeNotes.Factory notesFactory;
private final ApprovalsUtil approvalsUtil;
private final ChangeMessagesUtil cmUtil;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final PatchListCache patchListCache;
private final NotesMigration notesMigration;
@@ -334,7 +336,8 @@
private List<PatchSetApproval> currentApprovals;
private Map<Integer, List<String>> files;
private Map<Integer, Optional<PatchList>> patchLists;
- private Collection<PatchLineComment> publishedComments;
+ private Map<Integer, Optional<DiffSummary>> diffSummaries;
+ private Collection<Comment> publishedComments;
private CurrentUser visibleTo;
private ChangeControl changeControl;
private List<ChangeMessage> messages;
@@ -364,7 +367,7 @@
ChangeNotes.Factory notesFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
NotesMigration notesMigration,
@@ -382,7 +385,7 @@
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
@@ -402,7 +405,7 @@
ChangeNotes.Factory notesFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
NotesMigration notesMigration,
@@ -419,7 +422,7 @@
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
@@ -440,7 +443,7 @@
ChangeNotes.Factory notesFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
NotesMigration notesMigration,
@@ -457,7 +460,7 @@
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
@@ -479,7 +482,7 @@
ChangeNotes.Factory notesFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
NotesMigration notesMigration,
@@ -496,7 +499,7 @@
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
@@ -519,7 +522,7 @@
ChangeNotes.Factory notesFactory,
ApprovalsUtil approvalsUtil,
ChangeMessagesUtil cmUtil,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
PatchSetUtil psUtil,
PatchListCache patchListCache,
NotesMigration notesMigration,
@@ -538,7 +541,7 @@
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.cmUtil = cmUtil;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
@@ -548,6 +551,11 @@
this.project = null;
}
+ public ChangeData setLazyLoad(boolean load) {
+ lazyLoad = load;
+ return this;
+ }
+
public ReviewDb db() {
return db;
}
@@ -568,10 +576,7 @@
public List<String> currentFilePaths() throws OrmException {
PatchSet ps = currentPatchSet();
- if (ps == null) {
- return null;
- }
- return filePaths(currentPatchSet);
+ return ps != null ? filePaths(ps) : null;
}
public List<String> filePaths(PatchSet ps) throws OrmException {
@@ -583,35 +588,16 @@
return null;
}
- Optional<PatchList> p = getPatchList(c, ps);
+ Optional<DiffSummary> p = getDiffSummary(c, ps);
if (!p.isPresent()) {
List<String> emptyFileList = Collections.emptyList();
- files.put(ps.getPatchSetId(), emptyFileList);
+ if (lazyLoad) {
+ files.put(ps.getPatchSetId(), emptyFileList);
+ }
return emptyFileList;
}
- r = new ArrayList<>(p.get().getPatches().size());
- for (PatchListEntry e : p.get().getPatches()) {
- if (Patch.COMMIT_MSG.equals(e.getNewName())) {
- continue;
- }
- switch (e.getChangeType()) {
- case ADDED:
- case MODIFIED:
- case DELETED:
- case COPIED:
- case REWRITE:
- r.add(e.getNewName());
- break;
-
- case RENAMED:
- r.add(e.getOldName());
- r.add(e.getNewName());
- break;
- }
- }
- Collections.sort(r);
- r = Collections.unmodifiableList(r);
+ r = p.get().getPaths();
files.put(psId, r);
}
return r;
@@ -624,6 +610,9 @@
}
Optional<PatchList> r = patchLists.get(psId);
if (r == null) {
+ if (!lazyLoad) {
+ return Optional.absent();
+ }
try {
r = Optional.of(patchListCache.get(c, ps));
} catch (PatchListNotAvailableException e) {
@@ -634,6 +623,26 @@
return r;
}
+ private Optional<DiffSummary> getDiffSummary(Change c, PatchSet ps) {
+ Integer psId = ps.getId().get();
+ if (diffSummaries == null) {
+ diffSummaries = new HashMap<>();
+ }
+ Optional<DiffSummary> r = diffSummaries.get(psId);
+ if (r == null) {
+ if (!lazyLoad) {
+ return Optional.absent();
+ }
+ try {
+ r = Optional.of(patchListCache.getDiffSummary(c, ps));
+ } catch (PatchListNotAvailableException e) {
+ r = Optional.absent();
+ }
+ diffSummaries.put(psId, r);
+ }
+ return r;
+ }
+
private Optional<ChangedLines> computeChangedLines() throws OrmException {
Change c = change();
if (c == null) {
@@ -653,6 +662,9 @@
public Optional<ChangedLines> changedLines() throws OrmException {
if (changedLines == null) {
+ if (!lazyLoad) {
+ return Optional.absent();
+ }
changedLines = computeChangedLines();
}
return changedLines;
@@ -703,10 +715,7 @@
public ChangeControl changeControl(CurrentUser user) throws OrmException {
if (changeControl != null) {
CurrentUser oldUser = user;
- // TODO(dborowitz): This is a hack; general CurrentUser equality would be
- // better.
- if (user.isIdentifiedUser() && oldUser.isIdentifiedUser()
- && user.getAccountId().equals(oldUser.getAccountId())) {
+ if (sameUser(user, oldUser)) {
return changeControl;
}
throw new IllegalStateException(
@@ -725,13 +734,26 @@
return changeControl;
}
+ private static boolean sameUser(CurrentUser a, CurrentUser b) {
+ // TODO(dborowitz): This is a hack; general CurrentUser equality would be
+ // better.
+ if (a.isInternalUser() && b.isInternalUser()) {
+ return true;
+ } else if (a instanceof AnonymousUser && b instanceof AnonymousUser) {
+ return true;
+ } else if (a.isIdentifiedUser() && b.isIdentifiedUser()) {
+ return a.getAccountId().equals(b.getAccountId());
+ }
+ return false;
+ }
+
void cacheVisibleTo(ChangeControl ctl) {
visibleTo = ctl.getUser();
changeControl = ctl;
}
public Change change() throws OrmException {
- if (change == null) {
+ if (change == null && lazyLoad) {
reloadChange();
}
return change;
@@ -742,20 +764,20 @@
}
public Change reloadChange() throws OrmException {
- if (project == null) {
- notes = notesFactory.createFromIdOnlyWhenNoteDbDisabled(db, legacyId);
- } else {
- notes = notesFactory.create(db, project, legacyId);
- }
+ notes = notesFactory.create(db, project, legacyId);
change = notes.getChange();
if (change == null) {
throw new OrmException("Unable to load change " + legacyId);
}
+ setPatchSets(null);
return change;
}
public ChangeNotes notes() throws OrmException {
if (notes == null) {
+ if (!lazyLoad) {
+ throw new OrmException("ChangeNotes not available, lazyLoad = false");
+ }
notes = notesFactory.create(db, project(), legacyId);
}
return notes;
@@ -780,12 +802,23 @@
public List<PatchSetApproval> currentApprovals()
throws OrmException {
if (currentApprovals == null) {
+ if (!lazyLoad) {
+ return Collections.emptyList();
+ }
Change c = change();
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;
@@ -866,14 +899,11 @@
* @throws OrmException an error occurred reading the database.
*/
public Collection<PatchSet> visiblePatchSets() throws OrmException {
- Predicate<PatchSet> predicate = new Predicate<PatchSet>() {
- @Override
- public boolean apply(PatchSet input) {
- try {
- return changeControl().isPatchVisible(input, db);
- } catch (OrmException e) {
- return false;
- }
+ Predicate<PatchSet> predicate = ps -> {
+ try {
+ return changeControl().isPatchVisible(ps, db);
+ } catch (OrmException e) {
+ return false;
}
};
return FluentIterable.from(patchSets()).filter(predicate).toList();
@@ -908,6 +938,9 @@
public ListMultimap<PatchSet.Id, PatchSetApproval> approvals()
throws OrmException {
if (allApprovals == null) {
+ if (!lazyLoad) {
+ return ImmutableListMultimap.of();
+ }
allApprovals = approvalsUtil.byChange(db, notes());
}
return allApprovals;
@@ -929,6 +962,9 @@
public ReviewerSet reviewers() throws OrmException {
if (reviewers == null) {
+ if (!lazyLoad) {
+ return ReviewerSet.empty();
+ }
reviewers = approvalsUtil.getReviewers(notes(), approvals().values());
}
return reviewers;
@@ -944,6 +980,9 @@
public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException {
if (reviewerUpdates == null) {
+ if (!lazyLoad) {
+ return Collections.emptyList();
+ }
reviewerUpdates = approvalsUtil.getReviewerUpdates(notes());
}
return reviewerUpdates;
@@ -957,10 +996,13 @@
return reviewerUpdates;
}
- public Collection<PatchLineComment> publishedComments()
+ public Collection<Comment> publishedComments()
throws OrmException {
if (publishedComments == null) {
- publishedComments = plcUtil.publishedByChange(db, notes());
+ if (!lazyLoad) {
+ return Collections.emptyList();
+ }
+ publishedComments = commentsUtil.publishedByChange(db, notes());
}
return publishedComments;
}
@@ -968,6 +1010,9 @@
public List<ChangeMessage> messages()
throws OrmException {
if (messages == null) {
+ if (!lazyLoad) {
+ return Collections.emptyList();
+ }
messages = cmUtil.byChange(db, notes());
}
return messages;
@@ -1001,10 +1046,21 @@
if (c.getStatus() == Change.Status.MERGED) {
mergeable = true;
} else {
- PatchSet ps = currentPatchSet();
- if (ps == null || !changeControl().isPatchVisible(ps, db)) {
+ if (!lazyLoad) {
return null;
}
+ PatchSet ps = currentPatchSet();
+ 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();
@@ -1029,6 +1085,9 @@
public Set<Account.Id> editsByUser() throws OrmException {
if (editsByUser == null) {
+ if (!lazyLoad) {
+ return Collections.emptySet();
+ }
Change c = change();
if (c == null) {
return Collections.emptySet();
@@ -1051,13 +1110,16 @@
public Set<Account.Id> draftsByUser() throws OrmException {
if (draftsByUser == null) {
+ if (!lazyLoad) {
+ return Collections.emptySet();
+ }
Change c = change();
if (c == null) {
return Collections.emptySet();
}
draftsByUser = new HashSet<>();
- for (PatchLineComment sc : plcUtil.draftByChange(db, notes)) {
- draftsByUser.add(sc.getAuthor());
+ for (Comment sc : commentsUtil.draftByChange(db, notes)) {
+ draftsByUser.add(sc.author.getId());
}
}
return draftsByUser;
@@ -1065,6 +1127,9 @@
public Set<Account.Id> reviewedBy() throws OrmException {
if (reviewedBy == null) {
+ if (!lazyLoad) {
+ return Collections.emptySet();
+ }
Change c = change();
if (c == null) {
return Collections.emptySet();
@@ -1094,6 +1159,9 @@
public Set<String> hashtags() throws OrmException {
if (hashtags == null) {
+ if (!lazyLoad) {
+ return Collections.emptySet();
+ }
hashtags = notes().getHashtags();
}
return hashtags;
@@ -1106,6 +1174,9 @@
@Deprecated
public Set<Account.Id> starredBy() throws OrmException {
if (starredByUser == null) {
+ if (!lazyLoad) {
+ return Collections.emptySet();
+ }
starredByUser = checkNotNull(starredChangesUtil).byChange(
legacyId, StarredChangesUtil.DEFAULT_LABEL);
}
@@ -1119,6 +1190,9 @@
public ImmutableMultimap<Account.Id, String> stars() throws OrmException {
if (stars == null) {
+ if (!lazyLoad) {
+ return ImmutableMultimap.of();
+ }
stars = checkNotNull(starredChangesUtil).byChange(legacyId);
}
return stars;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index d7c7730..57f5710 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -16,9 +16,9 @@
import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -26,7 +26,6 @@
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -34,9 +33,9 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
@@ -107,6 +106,7 @@
public static final String FIELD_ADDED = "added";
public static final String FIELD_AGE = "age";
+ public static final String FIELD_ASSIGNEE = "assignee";
public static final String FIELD_AUTHOR = "author";
public static final String FIELD_BEFORE = "before";
public static final String FIELD_CHANGE = "change";
@@ -152,7 +152,8 @@
public static final String ARG_ID_USER = "user";
public static final String ARG_ID_GROUP = "group";
-
+ public static final String ARG_ID_OWNER = "owner";
+ public static final Account.Id OWNER_ACCOUNT_ID = new Account.Id(0);
private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
@@ -169,7 +170,7 @@
final ChangeNotes.Factory notesFactory;
final ChangeData.Factory changeDataFactory;
final FieldDef.FillArgs fillArgs;
- final PatchLineCommentsUtil plcUtil;
+ final CommentsUtil commentsUtil;
final AccountResolver accountResolver;
final GroupBackend groupBackend;
final AllProjectsName allProjectsName;
@@ -203,7 +204,7 @@
ChangeNotes.Factory notesFactory,
ChangeData.Factory changeDataFactory,
FieldDef.FillArgs fillArgs,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
@@ -223,7 +224,7 @@
@GerritServerConfig Config cfg) {
this(db, queryProvider, rewriter, opFactories, userFactory, self,
capabilityControlFactory, changeControlGenericFactory, notesFactory,
- changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
+ changeDataFactory, fillArgs, commentsUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
projectCache, listChildProjects, submitDryRun, conflictsCache,
trackingFooters, indexes != null ? indexes.getSearchIndex() : null,
@@ -243,7 +244,7 @@
ChangeNotes.Factory notesFactory,
ChangeData.Factory changeDataFactory,
FieldDef.FillArgs fillArgs,
- PatchLineCommentsUtil plcUtil,
+ CommentsUtil commentsUtil,
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
@@ -272,7 +273,7 @@
this.changeControlGenericFactory = changeControlGenericFactory;
this.changeDataFactory = changeDataFactory;
this.fillArgs = fillArgs;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
this.accountResolver = accountResolver;
this.groupBackend = groupBackend;
this.allProjectsName = allProjectsName;
@@ -296,7 +297,7 @@
return new Arguments(db, queryProvider, rewriter, opFactories, userFactory,
Providers.of(otherUser),
capabilityControlFactory, changeControlGenericFactory, notesFactory,
- changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
+ changeDataFactory, fillArgs, commentsUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
projectCache, listChildProjects, submitDryRun,
conflictsCache, trackingFooters, index, indexConfig, listMembers,
@@ -417,7 +418,8 @@
}
@Operator
- public Predicate<ChangeData> status(String statusName) {
+ public Predicate<ChangeData> status(String statusName)
+ throws QueryParseException {
if ("reviewed".equalsIgnoreCase(statusName)) {
return IsReviewedPredicate.create();
}
@@ -478,6 +480,14 @@
return new IsMergeablePredicate(args.fillArgs);
}
+ if ("assigned".equalsIgnoreCase(value)) {
+ return Predicate.not(new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE)));
+ }
+
+ if ("unassigned".equalsIgnoreCase(value)) {
+ return new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE));
+ }
+
try {
return status(value);
} catch (IllegalArgumentException e) {
@@ -592,6 +602,9 @@
// label:CodeReview=1,group=android_approvers or
// label:CodeReview=1,android_approvers
// user/groups without a label will first attempt to match user
+ // Special case: votes by owners can be tracked with ",owner":
+ // label:Code-Review+2,owner
+ // label:Code-Review+2,user=owner
String[] splitReviewer = name.split(",", 2);
name = splitReviewer[0]; // remove all but the vote piece, e.g.'CodeReview=1'
@@ -601,7 +614,11 @@
for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
- accounts = parseAccount(pair.getValue());
+ if (pair.getValue().equals(ARG_ID_OWNER)) {
+ accounts = Collections.singleton(OWNER_ACCOUNT_ID);
+ } else {
+ accounts = parseAccount(pair.getValue());
+ }
} else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
group = parseGroup(pair.getValue()).getUUID();
} else {
@@ -616,7 +633,11 @@
value + ")");
}
try {
- accounts = parseAccount(value);
+ if (value.equals(ARG_ID_OWNER)) {
+ accounts = Collections.singleton(OWNER_ACCOUNT_ID);
+ } else {
+ accounts = parseAccount(value);
+ }
} catch (QueryParseException qpex) {
// If it doesn't match an account, see if it matches a group
// (accounts get precedence)
@@ -632,14 +653,9 @@
// expand a group predicate into multiple user predicates
if (group != null) {
Set<Account.Id> allMembers =
- new HashSet<>(Lists.transform(
- args.listMembers.get().setRecursive(true).apply(group),
- new Function<AccountInfo, Account.Id>() {
- @Override
- public Account.Id apply(AccountInfo accountInfo) {
- return new Account.Id(accountInfo._accountId);
- }
- }));
+ args.listMembers.get().setRecursive(true).apply(group).stream()
+ .map(a -> new Account.Id(a._accountId))
+ .collect(toSet());
int maxLimit = args.indexConfig.maxLimit();
if (allMembers.size() > maxLimit) {
// limit the number of query terms otherwise Gerrit will barf
@@ -649,9 +665,7 @@
}
}
- return new LabelPredicate(args.projectCache,
- args.changeControlGenericFactory, args.userFactory, args.db,
- name, accounts, group);
+ return new LabelPredicate(args, name, accounts, group);
}
@Operator
@@ -670,8 +684,7 @@
return starredby(parseAccount(who));
}
- private Predicate<ChangeData> starredby(Set<Account.Id> who)
- throws QueryParseException {
+ private Predicate<ChangeData> starredby(Set<Account.Id> who) {
List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
for (Account.Id id : who) {
p.add(starredby(id));
@@ -679,25 +692,8 @@
return Predicate.or(p);
}
- @SuppressWarnings("deprecation")
- private Predicate<ChangeData> starredby(Account.Id who)
- throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.STAR)) {
- return new StarPredicate(who, StarredChangesUtil.DEFAULT_LABEL);
- }
-
- if (args.getSchema().hasField(ChangeField.STARREDBY)) {
- return new IsStarredByPredicate(who);
- }
-
- try {
- // starred changes are not contained in the index, we must read them from
- // git
- return new IsStarredByLegacyPredicate(who, args.starredChangesUtil
- .byAccount(who, StarredChangesUtil.DEFAULT_LABEL));
- } catch (OrmException e) {
- throw new QueryParseException("Failed to query starred changes.", e);
- }
+ private Predicate<ChangeData> starredby(Account.Id who) {
+ return new StarPredicate(who, StarredChangesUtil.DEFAULT_LABEL);
}
@Operator
@@ -736,11 +732,8 @@
return Predicate.or(p);
}
- @SuppressWarnings("deprecation")
private Predicate<ChangeData> draftby(Account.Id who) {
- return args.getSchema().hasField(ChangeField.DRAFTBY)
- ? new HasDraftByPredicate(who)
- : new HasDraftByLegacyPredicate(args, who);
+ return new HasDraftByPredicate(who);
}
@Operator
@@ -802,6 +795,20 @@
}
@Operator
+ public Predicate<ChangeData> assignee(String who) throws QueryParseException,
+ OrmException {
+ return assignee(parseAccount(who));
+ }
+
+ private Predicate<ChangeData> assignee(Set<Account.Id> who) {
+ List<AssigneePredicate> p = Lists.newArrayListWithCapacity(who.size());
+ for (Account.Id id : who) {
+ p.add(new AssigneePredicate(id));
+ }
+ return Predicate.or(p);
+ }
+
+ @Operator
public Predicate<ChangeData> ownerin(String group)
throws QueryParseException {
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 1c92ecf..1ae8591 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -18,6 +18,7 @@
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
import java.util.ArrayList;
@@ -63,7 +64,8 @@
return status.name().toLowerCase();
}
- public static Predicate<ChangeData> parse(String value) {
+ public static Predicate<ChangeData> parse(String value)
+ throws QueryParseException {
String lower = value.toLowerCase();
NavigableMap<String, Predicate<ChangeData>> head =
PREDICATES.tailMap(lower, true);
@@ -75,7 +77,7 @@
return e.getValue();
}
}
- throw new IllegalArgumentException("invalid change status: " + value);
+ throw new QueryParseException("invalid change status: " + value);
}
public static Predicate<ChangeData> open() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
index 48d6e05..1cb6333 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -16,7 +16,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gwtorm.server.OrmException;
@@ -41,8 +41,8 @@
return true;
}
}
- for (PatchLineComment c : cd.publishedComments()) {
- if (Objects.equals(c.getAuthor(), id)) {
+ for (Comment c : cd.publishedComments()) {
+ if (Objects.equals(c.author.getId(), id)) {
return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 69bc2ca..26dbe23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.OrPredicate;
import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
@@ -47,16 +48,23 @@
import java.util.Set;
class ConflictsPredicate extends OrPredicate<ChangeData> {
+ // UI code may depend on this string, so use caution when changing.
+ private static final String TOO_MANY_FILES =
+ "too many files to find conflicts";
+
private final String value;
ConflictsPredicate(Arguments args, String value, List<Change> changes)
- throws OrmException {
+ throws QueryParseException, OrmException {
super(predicates(args, value, changes));
this.value = value;
}
private static List<Predicate<ChangeData>> predicates(final Arguments args,
- String value, List<Change> changes) throws OrmException {
+ String value, List<Change> changes)
+ throws QueryParseException, OrmException {
+ int indexTerms = 0;
+
List<Predicate<ChangeData>> changePredicates =
Lists.newArrayListWithCapacity(changes.size());
final Provider<ReviewDb> db = args.db;
@@ -64,6 +72,16 @@
final ChangeDataCache changeDataCache = new ChangeDataCache(
c, db, args.changeDataFactory, args.projectCache);
List<String> files = listFiles(c, args, changeDataCache);
+ indexTerms += 3 + files.size();
+ if (indexTerms > args.indexConfig.maxTerms()) {
+ // Short-circuit with a nice error message if we exceed the index
+ // backend's term limit. This assumes that "conflicts:foo" is the entire
+ // query; if there are more terms in the input, we might not
+ // short-circuit here, which will result in a more generic error message
+ // later on in the query parsing.
+ throw new QueryParseException(TOO_MANY_FILES);
+ }
+
List<Predicate<ChangeData>> filePredicates =
Lists.newArrayListWithCapacity(files.size());
for (String file : files) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index e752b05..0adf78f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -43,7 +43,7 @@
EqualsLabelPredicate(LabelPredicate.Args args, String label, int expVal,
Account.Id account) {
- super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ super(args.field, ChangeField.formatLabel(label, expVal, account));
this.ccFactory = args.ccFactory;
this.projectCache = args.projectCache;
this.userFactory = args.userFactory;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
deleted file mode 100644
index 45a00c6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
+++ /dev/null
@@ -1,81 +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.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-@Deprecated
-class HasDraftByLegacyPredicate extends ChangeOperatorPredicate
- implements ChangeDataSource {
- private final Arguments args;
- private final Account.Id accountId;
-
- HasDraftByLegacyPredicate(Arguments args,
- Account.Id accountId) {
- super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString());
- this.args = args;
- this.accountId = accountId;
- }
-
- @Override
- public boolean match(final ChangeData object) throws OrmException {
- return !args.plcUtil
- .draftByChangeAuthor(args.db.get(), object.notes(), accountId)
- .isEmpty();
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- Set<Change.Id> ids = new HashSet<>();
- for (PatchLineComment sc :
- args.plcUtil.draftByAuthor(args.db.get(), accountId)) {
- ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
- }
-
- List<ChangeData> r = new ArrayList<>(ids.size());
- // TODO Don't load the changes directly from the database, but provide
- // project name + change ID to changeDataFactory, or delete this predicate.
- for (Change c : args.db.get().changes().get(ids)) {
- r.add(args.changeDataFactory.create(args.db.get(), c));
- }
- return new ListResultSet<>(r);
- }
-
- @Override
- public boolean hasChange() {
- return false;
- }
-
- @Override
- public int getCardinality() {
- return 20;
- }
-
- @Override
- public int getCost() {
- return 0;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 6aa33352..6721052 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -15,18 +15,15 @@
package com.google.gerrit.server.query.change;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.index.change.ChangeField.SUBMISSIONID;
import static com.google.gerrit.server.query.Predicate.and;
import static com.google.gerrit.server.query.Predicate.not;
import static com.google.gerrit.server.query.Predicate.or;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -161,7 +158,7 @@
}
public Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo,
- ReviewDb db, Branch.NameKey branch, List<String> hashes)
+ ReviewDb db, Branch.NameKey branch, Collection<String> hashes)
throws OrmException, IOException {
return byCommitsOnBranchNotMerged(repo, db, branch, hashes,
// Account for all commit predicates plus ref, project, status.
@@ -170,7 +167,7 @@
@VisibleForTesting
Iterable<ChangeData> byCommitsOnBranchNotMerged(Repository repo, ReviewDb db,
- Branch.NameKey branch, List<String> hashes, int indexLimit)
+ Branch.NameKey branch, Collection<String> hashes, int indexLimit)
throws OrmException, IOException {
if (hashes.size() > indexLimit) {
return byCommitsOnBranchNotMergedFromDatabase(repo, db, branch, hashes);
@@ -180,7 +177,7 @@
private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase(
Repository repo, final ReviewDb db, final Branch.NameKey branch,
- List<String> hashes) throws OrmException, IOException {
+ Collection<String> hashes) throws OrmException, IOException {
Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size());
String lastPrefix = null;
for (Ref ref :
@@ -199,24 +196,18 @@
}
}
- return Lists.transform(notesFactory.create(db, branch.getParentKey(),
- changeIds, new com.google.common.base.Predicate<ChangeNotes>() {
- @Override
- public boolean apply(ChangeNotes notes) {
- Change c = notes.getChange();
+ List<ChangeNotes> notes = notesFactory.create(
+ db, branch.getParentKey(), changeIds,
+ cn -> {
+ Change c = cn.getChange();
return c.getDest().equals(branch)
&& c.getStatus() != Change.Status.MERGED;
- }
- }), new Function<ChangeNotes, ChangeData>() {
- @Override
- public ChangeData apply(ChangeNotes notes) {
- return changeDataFactory.create(db, notes);
- }
});
+ return Lists.transform(notes, n -> changeDataFactory.create(db, n));
}
private Iterable<ChangeData> byCommitsOnBranchNotMergedFromIndex(
- Branch.NameKey branch, List<String> hashes) throws OrmException {
+ Branch.NameKey branch, Collection<String> hashes) throws OrmException {
return query(and(
ref(branch),
project(branch.getParentKey()),
@@ -224,7 +215,7 @@
or(commits(hashes))));
}
- private static List<Predicate<ChangeData>> commits(List<String> hashes) {
+ private static List<Predicate<ChangeData>> commits(Collection<String> hashes) {
List<Predicate<ChangeData>> commits = new ArrayList<>(hashes.size());
for (String s : hashes) {
commits.add(commit(s));
@@ -276,7 +267,7 @@
}
public List<ChangeData> bySubmissionId(String cs) throws OrmException {
- if (Strings.isNullOrEmpty(cs) || !schema().hasField(SUBMISSIONID)) {
+ if (Strings.isNullOrEmpty(cs)) {
return Collections.emptyList();
}
return query(new SubmissionIdPredicate(cs));
@@ -290,9 +281,4 @@
}
return query(and(project(project), or(groupPredicates)));
}
-
- @SuppressWarnings("deprecation")
- public List<ChangeData> byIsStarred(Account.Id id) throws OrmException {
- return query(new IsStarredByPredicate(id));
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java
deleted file mode 100644
index 19cbd23..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByLegacyPredicate.java
+++ /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.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.common.collect.Lists;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.query.OrPredicate;
-import com.google.gerrit.server.query.Predicate;
-
-import java.util.List;
-import java.util.Set;
-
-@Deprecated
-class IsStarredByLegacyPredicate extends OrPredicate<ChangeData> {
- private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
- List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
- for (Change.Id id : ids) {
- r.add(new LegacyChangeIdPredicate(id));
- }
- return r;
- }
-
- private final Account.Id accountId;
- private final Set<Change.Id> starredChanges;
-
- IsStarredByLegacyPredicate(Account.Id accountId,
- Set<Change.Id> starredChanges) {
- super(predicates(starredChanges));
- this.accountId = accountId;
- this.starredChanges = starredChanges;
- }
-
- @Override
- public boolean match(final ChangeData object) {
- return starredChanges.contains(object.getId());
- }
-
- @Override
- public int getCost() {
- return 0;
- }
-
- @Override
- public String toString() {
- return ChangeQueryBuilder.FIELD_STARREDBY + ":" + accountId.toString();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
deleted file mode 100644
index 929ed18..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ /dev/null
@@ -1,44 +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.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gwtorm.server.OrmException;
-
-@Deprecated
-class IsStarredByPredicate extends ChangeIndexPredicate {
- private final Account.Id accountId;
-
- IsStarredByPredicate(Account.Id accountId) {
- super(ChangeField.STARREDBY, accountId.toString());
- this.accountId = accountId;
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.starredBy().contains(accountId);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-
- @Override
- public String toString() {
- return ChangeQueryBuilder.FIELD_STARREDBY + ":" + accountId;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 2f815b2..9bed4b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -19,6 +19,8 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.OrPredicate;
@@ -36,6 +38,7 @@
private static final int MAX_LABEL_VALUE = 4;
static class Args {
+ final FieldDef<ChangeData, ?> field;
final ProjectCache projectCache;
final ChangeControl.GenericFactory ccFactory;
final IdentifiedUser.GenericFactory userFactory;
@@ -45,6 +48,7 @@
final AccountGroup.UUID group;
private Args(
+ FieldDef<ChangeData, ?> field,
ProjectCache projectCache,
ChangeControl.GenericFactory ccFactory,
IdentifiedUser.GenericFactory userFactory,
@@ -52,6 +56,7 @@
String value,
Set<Account.Id> accounts,
AccountGroup.UUID group) {
+ this.field = field;
this.projectCache = projectCache;
this.ccFactory = ccFactory;
this.userFactory = userFactory;
@@ -76,11 +81,12 @@
private final String value;
- LabelPredicate(ProjectCache projectCache,
- ChangeControl.GenericFactory ccFactory,
- IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
- String value, Set<Account.Id> accounts, AccountGroup.UUID group) {
- super(predicates(new Args(projectCache, ccFactory, userFactory, dbProvider,
+ @SuppressWarnings("deprecation")
+ LabelPredicate(ChangeQueryBuilder.Arguments a, String value,
+ Set<Account.Id> accounts, AccountGroup.UUID group) {
+ super(predicates(new Args(
+ a.getSchema().getField(ChangeField.LABEL2, ChangeField.LABEL).get(),
+ a.projectCache, a.changeControlGenericFactory, a.userFactory, a.db,
value, accounts, group)));
this.value = value;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 496eff6..5e08ee3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -307,8 +307,7 @@
includeApprovals ? d.approvals().asMap() : null,
includeFiles, d.change(), labelTypes);
for (PatchSetAttribute attribute : c.patchSets) {
- eventFactory.addPatchSetComments(
- attribute, d.publishedComments());
+ eventFactory.addPatchSetComments(attribute, d.publishedComments());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 69a392b..ad32edd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -122,6 +124,7 @@
int cnt = queries.size();
List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries));
List<List<ChangeInfo>> res = json.create(options)
+ .lazyLoad(containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD))
.formatQueryResults(results);
for (int n = 0; n < cnt; n++) {
List<ChangeInfo> info = res.get(n);
@@ -131,4 +134,10 @@
}
return res;
}
+
+ private static boolean containsAnyOf(
+ EnumSet<ListChangesOption> set,
+ ImmutableSet<ListChangesOption> toFind) {
+ return !Sets.intersection(toFind, set).isEmpty();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 1c4fbbb..53834a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -26,21 +26,16 @@
import java.util.List;
class ReviewerPredicate extends ChangeIndexPredicate {
- @SuppressWarnings("deprecation")
static Predicate<ChangeData> create(Arguments args, Account.Id id) {
List<Predicate<ChangeData>> and = new ArrayList<>(2);
- if (args.getSchema().hasField(ChangeField.REVIEWER)) {
- ReviewerStateInternal[] states = ReviewerStateInternal.values();
- List<Predicate<ChangeData>> or = new ArrayList<>(states.length - 1);
- for (ReviewerStateInternal state : states) {
- if (state != ReviewerStateInternal.REMOVED) {
- or.add(new ReviewerPredicate(state, id));
- }
+ ReviewerStateInternal[] states = ReviewerStateInternal.values();
+ List<Predicate<ChangeData>> or = new ArrayList<>(states.length - 1);
+ for (ReviewerStateInternal state : states) {
+ if (state != ReviewerStateInternal.REMOVED) {
+ or.add(new ReviewerPredicate(state, id));
}
- and.add(Predicate.or(or));
- } else {
- and.add(new LegacyReviewerPredicate(id));
}
+ and.add(Predicate.or(or));
// TODO(dborowitz): This really belongs much higher up e.g. QueryProcessor.
if (!args.allowsDrafts) {
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/DatabaseModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
index 9dee9f5..a2046b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
@@ -18,6 +18,8 @@
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
import com.google.gwtorm.jdbc.Database;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Key;
@@ -37,5 +39,6 @@
.to(database)
.in(SINGLETON);
bind(database).toProvider(ReviewDbDatabaseProvider.class);
+ bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
}
}
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..2d9714f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -0,0 +1,106 @@
+// 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 static java.util.stream.Collectors.toList;
+
+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.stream(config.getStringList(ACCESS, subsection, name))
+ .map(r -> {
+ PermissionRule rule = PermissionRule.fromString(r, false);
+ if (rule.getForce()) {
+ rule.setForce(false);
+ updated = true;
+ }
+ return rule.asString(false);
+ })
+ .collect(toList());
+ 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..a7a8191 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_133> C = Schema_133.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_124.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
index 16f0bcf..895c905 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_124.java
@@ -14,7 +14,8 @@
package com.google.gerrit.server.schema;
-import com.google.common.base.Function;
+import static java.util.Comparator.comparing;
+
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
@@ -124,13 +125,7 @@
private Collection<AccountSshKey> fixInvalidSequenceNumbers(
Collection<AccountSshKey> keys) {
- Ordering<AccountSshKey> o =
- Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
- @Override
- public Integer apply(AccountSshKey sshKey) {
- return sshKey.getKey().get();
- }
- });
+ Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get()));
List<AccountSshKey> fixedKeys = new ArrayList<>(keys);
AccountSshKey minKey = o.min(keys);
while (minKey.getKey().get() <= 0) {
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..cee21bd
--- /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("Cannot migrate project " + projectName, 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..a2ba03c
--- /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("Cannot migrate project " + projectName, ex);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java
new file mode 100644
index 0000000..7c1cde8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_132 extends SchemaVersion {
+ @Inject
+ Schema_132(Provider<Schema_131> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java
new file mode 100644
index 0000000..31d330b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_133 extends SchemaVersion {
+ @Inject
+ Schema_133(Provider<Schema_132> prior) {
+ super(prior);
+ }
+}
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/util/RegexListSearcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
index 0a99a8a..bbc97df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
@@ -17,7 +17,6 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Function;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -94,12 +93,7 @@
return Iterables.filter(
list.subList(begin, end),
- new Predicate<T>() {
- @Override
- public boolean apply(T in) {
- return pattern.run(RegexListSearcher.this.apply(in));
- }
- });
+ x -> pattern.run(apply(x)));
}
public boolean hasMatch(List<T> list) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/AssigneeValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
new file mode 100644
index 0000000..5d1191c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/AssigneeValidationListener.java
@@ -0,0 +1,34 @@
+// 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.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+
+/**
+ * Listener to provide validation of assignees.
+ */
+@ExtensionPoint
+public interface AssigneeValidationListener {
+ /**
+ * Invoked by Gerrit before the assignee of a change is modified.
+ *
+ * @param change the change on which the assignee is changed
+ * @param assignee the new assignee. Null if removed
+ * @throws ValidationException if validation fails
+ */
+ void validateAssignee(Change change, Account assignee) throws ValidationException;
+}
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..2182ac1 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,11 +33,12 @@
class Args {
// in arguments
public String messageClass;
+ @Nullable public String htmlBody;
// in/out arguments
public Address smtpFromAddress;
public Set<Address> smtpRcptTo;
- public String body;
+ public String body; // The text body of the email.
public Map<String, EmailHeader> headers;
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
index 1dbdb68..a855868 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
@@ -14,8 +14,10 @@
package gerrit;
+import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.rules.StoredValues;
import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListEntry;
import com.googlecode.prolog_cafe.exceptions.PrologException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -24,6 +26,8 @@
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.Term;
+import java.util.List;
+
/**
* Exports basic commit statistics.
*
@@ -48,7 +52,11 @@
Term a3 = arg3.dereference();
PatchList pl = StoredValues.PATCH_LIST.get(engine);
- if (!a1.unify(new IntegerTerm(pl.getPatches().size() - 1),engine.trail)) { //Account for /COMMIT_MSG.
+ // Account for magic files
+ if (!a1.unify(
+ new IntegerTerm(
+ pl.getPatches().size() - countMagicFiles(pl.getPatches())),
+ engine.trail)) {
return engine.fail();
}
if (!a2.unify(new IntegerTerm(pl.getInsertions()),engine.trail)) {
@@ -59,4 +67,14 @@
}
return cont;
}
+
+ private int countMagicFiles(List<PatchListEntry> entries) {
+ int count = 0;
+ for (PatchListEntry e : entries) {
+ if (Patch.isMagic(e.getNewName())) {
+ count++;
+ }
+ }
+ return count;
+ }
}
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/AbandonedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
new file mode 100644
index 0000000..0f16890
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.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}
+
+/**
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .AbandonedHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has abandoned this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ {if $coverLetter}
+ <pre>{$coverLetter}</pre>
+ {/if}
+{/template}
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/ChangeFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
new file mode 100644
index 0000000..6d04ac3
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -0,0 +1,49 @@
+/**
+ * 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 branch
+ * @param change
+ * @param changeId
+ * @param email
+ * @param messageClass
+ * @param patchSet
+ * @param projectName
+ */
+{template .ChangeFooterHtml autoescape="strict" kind="html"}
+ {if $email.changeUrl or $email.settingsUrl}
+ <p>
+ {if $email.changeUrl}
+ To view, visit <a href="{$email.changeUrl}">this change</a>.
+ {/if}
+ {if $email.changeUrl and $email.settingsUrl}{sp}{/if}
+ {if $email.settingsUrl}
+ To unsubscribe, visit <a href="{$email.settingsUrl}">settings</a>.
+ {/if}
+ </p>
+ {/if}
+
+ <p style="color: #555;">
+ Gerrit-MessageType: {$messageClass}<br/>
+ Gerrit-Change-Id: {$changeId}<br/>
+ Gerrit-PatchSet: {$patchSet.patchSetId}<br/>
+ Gerrit-Project: {$projectName}<br/>
+ Gerrit-Branch: {$branch.shortName}<br/>
+ Gerrit-Owner: {$change.ownerEmail}
+ </p>
+{/template}
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/DeleteReviewerHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
new file mode 100644
index 0000000..2985ef8
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -0,0 +1,49 @@
+/**
+ * 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 change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .DeleteReviewerHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has removed{sp}
+ {foreach $reviewerName in $email.reviewerNames}
+ {if not isFirst($reviewerName)},{sp}{/if}
+ {$reviewerName}
+ {/foreach}{sp}
+ from this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ {if $coverLetter}
+ <pre>{$coverLetter}</pre>
+ {/if}
+{/template}
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/DeleteVoteHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
new file mode 100644
index 0000000..8aa430a
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.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}
+
+/**
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .DeleteVoteHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} removed a vote on this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ {if $coverLetter}
+ <pre>{$coverLetter}</pre>
+ {/if}
+{/template}
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/HeaderHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.soy
new file mode 100644
index 0000000..fdc3fee
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/HeaderHtml.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 .HeaderHtml 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/MergedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
new file mode 100644
index 0000000..33dd7b8
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -0,0 +1,53 @@
+/**
+ * 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 change
+ * @param email
+ * @param fromName
+ */
+{template .MergedHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has submitted this change and it was merged.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ <pre>
+ {$email.changeDetail}
+ </pre>
+
+ <pre>
+ {$email.approvals}
+ </pre>
+
+ {if $email.includeDiff}
+ <pre>
+ {$email.unifiedDiff}
+ </pre>
+ {/if}
+{/template}
\ No newline at end of file
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/NewChangeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
new file mode 100644
index 0000000..fda960d
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -0,0 +1,78 @@
+/**
+ * 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 change
+ * @param email
+ * @param fromName
+ * @param patchSet
+ * @param projectName
+ */
+{template .NewChangeHtml autoescape="strict" kind="html"}
+ {if $email.reviewerNames}
+ <p>
+ Hello{sp}
+ {foreach $reviewerName in $email.reviewerNames}
+ {if not isFirst($reviewerName)},{sp}{/if}
+ {$reviewerName}
+ {/foreach},
+ </p>
+
+ <p>
+ I'd like you to do a code review.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ Please visit <a href="{$email.changeUrl}">this change</a> to review.
+ </p>
+ {/if}
+ {else}
+ <p>
+ {$fromName} has uploaded a new change for review.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ <pre>
+ {$email.changeDetail}
+ </pre>
+
+ {if $email.sshHost}
+ <pre>
+ git pull ssh:{print '//'}{$email.sshHost}/{$projectName}
+ {sp}{$patchSet.refName}
+ </pre>
+ {/if}
+
+ {if $email.includeDiff}
+ <pre>
+ {$email.unifiedDiff}
+ </pre>
+ {/if}
+{/template}
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/ReplacePatchSetHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
new file mode 100644
index 0000000..9df9a71
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -0,0 +1,72 @@
+/**
+ * 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 change
+ * @param email
+ * @param fromName
+ * @param patchSet
+ * @param projectName
+ */
+{template .ReplacePatchSetHtml autoescape="strict" kind="html"}
+ {if $email.reviewerNames}
+ <p>
+ Hello{sp}
+ {foreach $reviewerName in $email.reviewerNames}
+ {$reviewerName},{sp}
+ {/foreach}
+ </p>
+
+ <p>
+ I'd like you to reexamine a change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ Please visit <a href="{$email.changeUrl}">this change</a> to look at{sp}
+ the new patch set (#{$patchSet.patchSetId}).
+ </p>
+ {/if}
+ {else}
+ <p>
+ {$fromName} has uploaded a new patch set (#{$patchSet.patchSetId}).
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ <pre>
+ {$email.changeDetail}
+ </pre>
+
+ {if $email.sshHost}
+ <pre>
+ git pull ssh:{print '//'}{$email.sshHost}/{$projectName}{sp}
+ {$patchSet.refName}
+ </pre>
+ {/if}
+{/template}
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/RestoredHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
new file mode 100644
index 0000000..8a7dcc2
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.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}
+
+/**
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .RestoredHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has restored this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ {if $coverLetter}
+ <pre>{$coverLetter}</pre>
+ {/if}
+{/template}
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/mail/RevertedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
new file mode 100644
index 0000000..03d4dea
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.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}
+
+/**
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .RevertedHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has reverted this change.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ {if $coverLetter}
+ <pre>{$coverLetter}</pre>
+ {/if}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ViewChangeButton.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ViewChangeButton.soy
new file mode 100644
index 0000000..defde55
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ViewChangeButton.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}
+
+/**
+ * Private template to generate "View Change" buttons.
+ * @param email
+ */
+{template .ViewChangeButton private="true" autoescape="strict" kind="html"}
+ <a href="{$email.changeUrl}">View Change</a>
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 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/main/resources/com/google/gerrit/server/tools/root/scripts/preview_submit_test.sh b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/scripts/preview_submit_test.sh
new file mode 100644
index 0000000..d76c239
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/scripts/preview_submit_test.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+# A sceleton script to demonstrate how to use the preview_submit REST API call.
+#
+#
+
+if test -z $server
+then
+ echo "The variable 'server' needs to point to your Gerrit instance"
+ exit 1
+fi
+
+if test -z $changeId
+then
+ echo "The variable 'changeId' must contain a valid change Id"
+ exit 1
+fi
+
+if test -z $gerrituser
+then
+ echo "The variable 'gerrituser' must contain a user/password"
+ exit 1
+fi
+
+curl --digest -u $gerrituser -w '%{http_code}' -o preview \
+ $server/a/changes/$changeId/revisions/current/preview_submit?format=tgz >http_code
+if ! grep 200 http_code >/dev/null
+then
+ # error out:
+ echo "Error previewing submit $changeId due to:"
+ cat preview
+ echo
+else
+ # valid tgz file, extract and obtain a bundle for each project
+ mkdir tmp-bundles
+ (cd tmp-bundles && tar -zxf ../preview)
+ for project in $(cd tmp-bundles && find -type f)
+ do
+ # Projects may contain slashes, so create the required
+ # directory structure
+ mkdir -p $(dirname $project)
+ # $project is in the format of "./path/name/project.git"
+ # remove the leading ./
+ proj=${project:-./}
+ git clone $server/$proj $proj
+
+ # First some nice output:
+ echo "Verify that the bundle is good:"
+ GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
+ git bundle verify tmp-bundles/$proj
+ echo "Checking that the bundle only contains one branch..."
+ if test \
+ "$(GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
+ git bundle list-heads tmp-bundles/$proj |wc -l)" != 1
+ then
+ echo "Submitting $changeId would affect the project"
+ echo "$proj"
+ echo "on multiple branches:"
+ git bundle list-heads
+ echo "This script does not demonstrate this use case."
+ exit 1
+ fi
+ # find the target branch:
+ branch=$(GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
+ git bundle list-heads tmp-bundles/$proj | awk '{print $2}')
+ echo "found branch $branch"
+ echo "fetch the bundle into the repository"
+ GIT_WORK_TREE=$proj GIT_DIR=$proj/.git \
+ git fetch tmp-bundles/$proj $branch
+ echo "and checkout the state"
+ git -C $proj checkout FETCH_HEAD
+ done
+ echo "Now run a test for all of: $(cd tmp-bundles && find -type f)"
+fi
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
index bf36738..222fb14 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -145,6 +145,7 @@
RepositoryConfig.OWNER_GROUP_NAME, ownerGroups);
}
+ @SuppressWarnings("cast")
@Test
public void testBasePathWhenNotConfigured() {
assertThat((Object)repoCfg.getBasePath(new NameKey("someProject"))).isNull();
@@ -158,6 +159,7 @@
.isEqualTo(basePath);
}
+ @SuppressWarnings("cast")
@Test
public void testBasePathForSpecificFilter() {
String basePath = "/someAbsolutePath/someDirectory";
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/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index fabb53d..0a98c40 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -25,12 +25,10 @@
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
@@ -54,6 +52,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeAccountCache;
import com.google.gerrit.testutil.GerritBaseTests;
import com.google.gerrit.testutil.InMemoryRepositoryManager;
@@ -76,12 +75,31 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.runner.RunWith;
import java.sql.Timestamp;
import java.util.TimeZone;
@Ignore
+@RunWith(ConfigSuite.class)
public abstract class AbstractChangeNotesTest extends GerritBaseTests {
+ @ConfigSuite.Default
+ public static Config changeNotesLegacy() {
+ Config cfg = new Config();
+ cfg.setBoolean("notedb", null, "writeJson", false);
+ return cfg;
+ }
+
+ @ConfigSuite.Config
+ public static Config changeNotesJson() {
+ Config cfg = new Config();
+ cfg.setBoolean("notedb", null, "writeJson", true);
+ return cfg;
+ }
+
+ @ConfigSuite.Parameter
+ public Config testConfig;
+
private static final TimeZone TZ =
TimeZone.getTimeZone("America/Los_Angeles");
@@ -110,12 +128,13 @@
protected AllUsersName allUsers;
@Inject
- protected ChangeNoteUtil noteUtil;
-
- @Inject
protected AbstractChangeNotes.Args args;
- private Injector injector;
+ @Inject
+ @GerritServerId
+ private String serverId;
+
+ protected Injector injector;
private String systemTimeZone;
@Before
@@ -143,9 +162,8 @@
injector = Guice.createInjector(new FactoryModule() {
@Override
public void configure() {
- Config cfg = new Config();
install(new GitModule());
- install(NoteDbModule.forTest(cfg));
+ install(NoteDbModule.forTest(testConfig));
bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
bind(String.class).annotatedWith(GerritServerId.class)
.toInstance("gerrit");
@@ -155,7 +173,7 @@
bind(CapabilityControl.Factory.class)
.toProvider(Providers.<CapabilityControl.Factory> of(null));
bind(Config.class).annotatedWith(GerritServerConfig.class)
- .toInstance(cfg);
+ .toInstance(testConfig);
bind(String.class).annotatedWith(AnonymousCowardName.class)
.toProvider(AnonymousCowardNameProvider.class);
bind(String.class).annotatedWith(CanonicalWebUrl.class)
@@ -234,30 +252,22 @@
return label;
}
- protected PatchLineComment newPublishedComment(PatchSet.Id psId,
- String filename, String UUID, CommentRange range, int line,
- IdentifiedUser commenter, String parentUUID, Timestamp t,
- String message, short side, String commitSHA1) {
- return newComment(psId, filename, UUID, range, line, commenter,
- parentUUID, t, message, side, commitSHA1,
- PatchLineComment.Status.PUBLISHED);
- }
+ protected Comment newComment(PatchSet.Id psId, String filename, String UUID,
+ CommentRange range, int line, IdentifiedUser commenter, String parentUUID,
+ Timestamp t, String message, short side, String commitSHA1) {
+ Comment c = new Comment(
+ new Comment.Key(UUID, filename, psId.get()),
+ commenter.getAccountId(),
+ t,
+ side,
+ message,
+ serverId);
+ c.lineNbr = line;
+ c.parentUuid = parentUUID;
+ c.revId = commitSHA1;
+ c.setRange(range);
+ return c;
- protected PatchLineComment newComment(PatchSet.Id psId,
- String filename, String UUID, CommentRange range, int line,
- IdentifiedUser commenter, String parentUUID, Timestamp t,
- String message, short side, String commitSHA1,
- PatchLineComment.Status status) {
- PatchLineComment comment = new PatchLineComment(
- new PatchLineComment.Key(
- new Patch.Key(psId, filename), UUID),
- line, commenter.getAccountId(), parentUUID, t);
- comment.setSide(side);
- comment.setMessage(message);
- comment.setRange(range);
- comment.setRevId(new RevId(commitSHA1));
- comment.setStatus(status);
- return comment;
}
protected static Timestamp truncate(Timestamp ts) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index c093b75..97bf864 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -39,6 +39,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.testutil.TestChanges;
import com.google.gerrit.testutil.TestTimeUtil;
import com.google.gwtorm.client.KeyUtil;
@@ -1259,7 +1260,9 @@
}
private static List<PatchSet> latest(Change c) {
- return ImmutableList.of(new PatchSet(c.currentPatchSetId()));
+ PatchSet ps = new PatchSet(c.currentPatchSetId());
+ ps.setCreatedOn(c.getLastUpdatedOn());
+ return ImmutableList.of(ps);
}
private static List<PatchSetApproval> approvals(PatchSetApproval... ents) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index ab37ec9..a893ea8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -449,6 +449,7 @@
}
private RevCommit writeCommit(String body) throws Exception {
+ ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return writeCommit(body, noteUtil.newIdent(
changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,
"Anonymous Coward"));
@@ -496,6 +497,7 @@
private ChangeNotesParser newParser(ObjectId tip) throws Exception {
walk.reset();
+ ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return new ChangeNotesParser(
newChange().getId(), tip, walk, noteUtil, args.metrics);
}
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..ea33e65 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
@@ -24,7 +24,6 @@
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.fail;
-import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -34,21 +33,23 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.testutil.TestChanges;
@@ -76,6 +77,12 @@
@Inject
private DraftCommentNotes.Factory draftNotesFactory;
+ @Inject
+ private ChangeNoteUtil noteUtil;
+
+ @Inject
+ private @GerritServerId String serverId;
+
@Test
public void tagChangeMessage() throws Exception {
String tag = "jenkins";
@@ -97,18 +104,19 @@
Change c = newChange();
RevCommit commit = tr.commit().message("PS2").create();
ChangeUpdate update = newUpdate(c, changeOwner);
- update.putComment(newPublishedComment(c.currentPatchSetId(), "a.txt",
- "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
- TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
+ update.putComment(Status.PUBLISHED,
+ newComment(c.currentPatchSetId(), "a.txt", "uuid1",
+ new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
+ TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
update.setTag(tag);
update.commit();
ChangeNotes notes = newNotes(c);
- ImmutableListMultimap<RevId, PatchLineComment> comments = notes.getComments();
+ ImmutableListMultimap<RevId, Comment> comments = notes.getComments();
assertThat(comments).hasSize(1);
assertThat(
- comments.entries().asList().get(0).getValue().getTag())
+ comments.entries().asList().get(0).getValue().tag)
.isEqualTo(tag);
}
@@ -153,9 +161,10 @@
RevCommit commit = tr.commit().message("PS2").create();
update = newUpdate(c, changeOwner);
- update.putComment(newPublishedComment(c.currentPatchSetId(), "a.txt",
- "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
- TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
+ update.putComment(Status.PUBLISHED,
+ newComment(c.currentPatchSetId(), "a.txt", "uuid1",
+ new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
+ TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
update.setChangeMessage("coverage verification");
update.setTag(coverageTag);
update.commit();
@@ -174,10 +183,9 @@
assertThat(approval.getTag()).isEqualTo(integrationTag);
assertThat(approval.getValue()).isEqualTo(-1);
- ImmutableListMultimap<RevId, PatchLineComment> comments =
- notes.getComments();
+ ImmutableListMultimap<RevId, Comment> comments = notes.getComments();
assertThat(comments).hasSize(1);
- assertThat(comments.entries().asList().get(0).getValue().getTag())
+ assertThat(comments.entries().asList().get(0).getValue().tag)
.isEqualTo(coverageTag);
ImmutableList<ChangeMessage> messages = notes.getChangeMessages();
@@ -322,7 +330,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 +355,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);
@@ -368,13 +382,9 @@
update.commit();
ChangeNotes notes = newNotes(c);
- List<PatchSetApproval> approvals = Ordering.natural().onResultOf(
- new Function<PatchSetApproval, Integer>() {
- @Override
- public Integer apply(PatchSetApproval in) {
- return in.getAccountId().get();
- }
- }).sortedCopy(notes.getApprovals().get(c.currentPatchSetId()));
+ List<PatchSetApproval> approvals = ReviewDbUtil.intKeyOrdering()
+ .onResultOf(PatchSetApproval::getAccountId)
+ .sortedCopy(notes.getApprovals().get(c.currentPatchSetId()));
assertThat(approvals).hasSize(2);
assertThat(approvals.get(0).getAccountId())
@@ -553,6 +563,72 @@
}
@Test
+ public void assigneeCommit() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setAssignee(otherUserId);
+ ObjectId result = update.commit();
+ assertThat(result).isNotNull();
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit commit = rw.parseCommit(update.getResult());
+ rw.parseBody(commit);
+ String strIdent =
+ otherUser.getName()
+ + " <"
+ + otherUserId
+ + "@"
+ + serverId
+ + ">";
+ assertThat(commit.getFullMessage())
+ .contains("Assignee: " + strIdent);
+ }
+ }
+
+ @Test
+ public void assigneeChangeNotes() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setAssignee(otherUserId);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getChange().getAssignee()).isEqualTo(otherUserId);
+
+ update = newUpdate(c, changeOwner);
+ update.setAssignee(changeOwner.getAccountId());
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getChange().getAssignee())
+ .isEqualTo(changeOwner.getAccountId());
+ }
+
+ @Test
+ public void pastAssigneesChangeNotes() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setAssignee(otherUserId);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+
+ update = newUpdate(c, changeOwner);
+ update.setAssignee(changeOwner.getAccountId());
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.setAssignee(otherUserId);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.removeAssignee();
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getPastAssignees()).hasSize(2);
+ }
+
+ @Test
public void hashtagCommit() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -874,9 +950,10 @@
update.setPatchSetState(PatchSetState.DRAFT);
update.putApproval("Code-Review", (short) 1);
update.setChangeMessage("This is a message");
- update.putComment(newPublishedComment(c.currentPatchSetId(), "a.txt",
- "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
- TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
+ update.putComment(Status.PUBLISHED,
+ newComment(c.currentPatchSetId(), "a.txt", "uuid1",
+ new CommentRange(1, 2, 3, 4), 1, changeOwner, null,
+ TimeUtil.nowTs(), "Comment", (short) 1, commit.name()));
update.commit();
ChangeNotes notes = newNotes(c);
@@ -960,7 +1037,10 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(readNote(notes, commit)).isEqualTo(pushCert);
+ String note = readNote(notes, commit);
+ if (!testJson()) {
+ assertThat(note).isEqualTo(pushCert);
+ }
Map<PatchSet.Id, PatchSet> patchSets = notes.getPatchSets();
assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
@@ -970,29 +1050,33 @@
update = newUpdate(c, changeOwner);
update.setPatchSetId(psId2);
Timestamp ts = TimeUtil.nowTs();
- update.putComment(newPublishedComment(psId2, "a.txt",
- "uuid1", new CommentRange(1, 2, 3, 4), 1, changeOwner, null, ts,
- "Comment", (short) 1, commit.name()));
+ update.putComment(Status.PUBLISHED,
+ newComment(psId2, "a.txt", "uuid1", new CommentRange(1, 2, 3, 4), 1,
+ changeOwner, null, ts, "Comment", (short) 1, commit.name()));
update.commit();
notes = newNotes(c);
- assertThat(readNote(notes, commit)).isEqualTo(
- pushCert
- + "Revision: " + commit.name() + "\n"
- + "Patch-set: 2\n"
- + "File: a.txt\n"
- + "\n"
- + "1:2-3:4\n"
- + ChangeNoteUtil.formatTime(serverIdent, ts) + "\n"
- + "Author: Change Owner <1@gerrit>\n"
- + "UUID: uuid1\n"
- + "Bytes: 7\n"
- + "Comment\n"
- + "\n");
+
patchSets = notes.getPatchSets();
assertThat(patchSets.get(psId1).getPushCertificate()).isNull();
assertThat(patchSets.get(psId2).getPushCertificate()).isEqualTo(pushCert);
assertThat(notes.getComments()).isNotEmpty();
+
+ if (!testJson()) {
+ assertThat(readNote(notes, commit)).isEqualTo(
+ pushCert
+ + "Revision: " + commit.name() + "\n"
+ + "Patch-set: 2\n"
+ + "File: a.txt\n"
+ + "\n"
+ + "1:2-3:4\n"
+ + ChangeNoteUtil.formatTime(serverIdent, ts) + "\n"
+ + "Author: Change Owner <1@gerrit>\n"
+ + "UUID: uuid1\n"
+ + "Bytes: 7\n"
+ + "Comment\n"
+ + "\n");
+ }
}
@Test
@@ -1046,11 +1130,11 @@
RevCommit tipCommit;
try (NoteDbUpdateManager updateManager =
updateManagerFactory.create(project)) {
- PatchLineComment comment1 = newPublishedComment(psId, "file1",
+ Comment comment1 = newComment(psId, "file1",
uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
(short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
update1.setPatchSetId(psId);
- update1.putComment(comment1);
+ update1.putComment(Status.PUBLISHED, comment1);
updateManager.add(update1);
ChangeUpdate update2 = newUpdate(c, otherUser);
@@ -1273,11 +1357,11 @@
PatchSet.Id psId = c.currentPatchSetId();
RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
- PatchLineComment comment = newPublishedComment(psId, "file1",
+ Comment comment = newComment(psId, "file1",
"uuid", null, 0, otherUser, null,
TimeUtil.nowTs(), "message", (short) 1, revId.get());
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1293,11 +1377,11 @@
RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
CommentRange range = new CommentRange(1, 0, 2, 0);
- PatchLineComment comment = newPublishedComment(psId, "file1",
+ Comment comment = newComment(psId, "file1",
"uuid", range, range.getEndLine(), otherUser, null,
TimeUtil.nowTs(), "message", (short) 1, revId.get());
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1313,11 +1397,11 @@
RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
CommentRange range = new CommentRange(0, 0, 0, 0);
- PatchLineComment comment = newPublishedComment(psId, "file",
+ Comment comment = newComment(psId, "file",
"uuid", range, range.getEndLine(), otherUser, null,
TimeUtil.nowTs(), "message", (short) 1, revId.get());
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1333,11 +1417,10 @@
RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
CommentRange range = new CommentRange(1, 2, 3, 4);
- PatchLineComment comment = newPublishedComment(psId, "",
- "uuid", range, range.getEndLine(), otherUser, null,
- TimeUtil.nowTs(), "message", (short) 1, revId.get());
+ Comment comment = newComment(psId, "", "uuid", range, range.getEndLine(),
+ otherUser, null, TimeUtil.nowTs(), "message", (short) 1, revId.get());
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1361,29 +1444,29 @@
Timestamp time3 = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment comment1 = newPublishedComment(psId, "file1",
- uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
- (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+ Comment comment1 = newComment(psId, "file1", uuid1, range1,
+ range1.getEndLine(), otherUser, null, time1, message1, (short) 1,
+ "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
update = newUpdate(c, otherUser);
CommentRange range2 = new CommentRange(2, 1, 3, 1);
- PatchLineComment comment2 = newPublishedComment(psId, "file1",
- uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
- (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+ Comment comment2 = newComment(psId, "file1", uuid2, range2,
+ range2.getEndLine(), otherUser, null, time2, message2, (short) 1,
+ "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
update = newUpdate(c, otherUser);
CommentRange range3 = new CommentRange(3, 0, 4, 1);
- PatchLineComment comment3 = newPublishedComment(psId, "file2",
- uuid3, range3, range3.getEndLine(), otherUser, null, time3, message3,
- (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+ Comment comment3 = newComment(psId, "file2", uuid3, range3,
+ range3.getEndLine(), otherUser, null, time3, message3, (short) 1,
+ "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment3);
+ update.putComment(Status.PUBLISHED, comment3);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1397,34 +1480,37 @@
walk.getObjectReader().open(
note.getData(), Constants.OBJ_BLOB).getBytes();
String noteString = new String(bytes, UTF_8);
- assertThat(noteString).isEqualTo(
- "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
- + "Patch-set: 1\n"
- + "File: file1\n"
- + "\n"
- + "1:1-2:1\n"
- + ChangeNoteUtil.formatTime(serverIdent, time1) + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid1\n"
- + "Bytes: 9\n"
- + "comment 1\n"
- + "\n"
- + "2:1-3:1\n"
- + ChangeNoteUtil.formatTime(serverIdent, time2) + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid2\n"
- + "Bytes: 9\n"
- + "comment 2\n"
- + "\n"
- + "File: file2\n"
- + "\n"
- + "3:0-4:1\n"
- + ChangeNoteUtil.formatTime(serverIdent, time3) + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid3\n"
- + "Bytes: 9\n"
- + "comment 3\n"
- + "\n");
+
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Patch-set: 1\n"
+ + "File: file1\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time1) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid1\n"
+ + "Bytes: 9\n"
+ + "comment 1\n"
+ + "\n"
+ + "2:1-3:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time2) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid2\n"
+ + "Bytes: 9\n"
+ + "comment 2\n"
+ + "\n"
+ + "File: file2\n"
+ + "\n"
+ + "3:0-4:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time3) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid3\n"
+ + "Bytes: 9\n"
+ + "comment 3\n"
+ + "\n");
+ }
}
}
@@ -1441,20 +1527,20 @@
Timestamp time2 = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment comment1 = newPublishedComment(psId, "file1",
+ Comment comment1 = newComment(psId, "file1",
uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
(short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
update = newUpdate(c, otherUser);
CommentRange range2 = new CommentRange(2, 1, 3, 1);
- PatchLineComment comment2 = newPublishedComment(psId, "file1",
+ Comment comment2 = newComment(psId, "file1",
uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
(short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1468,25 +1554,28 @@
walk.getObjectReader().open(
note.getData(), Constants.OBJ_BLOB).getBytes();
String noteString = new String(bytes, UTF_8);
- assertThat(noteString).isEqualTo(
- "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
- + "Base-for-patch-set: 1\n"
- + "File: file1\n"
- + "\n"
- + "1:1-2:1\n"
- + ChangeNoteUtil.formatTime(serverIdent, time1) + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid1\n"
- + "Bytes: 9\n"
- + "comment 1\n"
- + "\n"
- + "2:1-3:1\n"
- + ChangeNoteUtil.formatTime(serverIdent, time2) + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid2\n"
- + "Bytes: 9\n"
- + "comment 2\n"
- + "\n");
+
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Base-for-patch-set: 1\n"
+ + "File: file1\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time1) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid1\n"
+ + "Bytes: 9\n"
+ + "comment 1\n"
+ + "\n"
+ + "2:1-3:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time2) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid2\n"
+ + "Bytes: 9\n"
+ + "comment 2\n"
+ + "\n");
+ }
}
}
@@ -1508,21 +1597,21 @@
PatchSet.Id psId1 = c.currentPatchSetId();
PatchSet.Id psId2 = new PatchSet.Id(c.getId(), psId1.get() + 1);
- PatchLineComment comment1 = newPublishedComment(psId1, "file1",
- uuid1, range1, range1.getEndLine(), otherUser, null, time, message1,
- (short) 0, revId.get());
- PatchLineComment comment2 = newPublishedComment(psId1, "file1",
- uuid2, range2, range2.getEndLine(), otherUser, null, time, message2,
- (short) 0, revId.get());
- PatchLineComment comment3 = newPublishedComment(psId2, "file1",
- uuid3, range1, range1.getEndLine(), otherUser, null, time, message3,
- (short) 0, revId.get());
+ Comment comment1 =
+ newComment(psId1, "file1", uuid1, range1, range1.getEndLine(),
+ otherUser, null, time, message1, (short) 0, revId.get());
+ Comment comment2 =
+ newComment(psId1, "file1", uuid2, range2, range2.getEndLine(),
+ otherUser, null, time, message2, (short) 0, revId.get());
+ Comment comment3 =
+ newComment(psId2, "file1", uuid3, range1, range1.getEndLine(),
+ otherUser, null, time, message3, (short) 0, revId.get());
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(psId2);
- update.putComment(comment3);
- update.putComment(comment2);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment3);
+ update.putComment(Status.PUBLISHED, comment2);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1537,37 +1626,39 @@
note.getData(), Constants.OBJ_BLOB).getBytes();
String noteString = new String(bytes, UTF_8);
String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
- assertThat(noteString).isEqualTo(
- "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
- + "Base-for-patch-set: 1\n"
- + "File: file1\n"
- + "\n"
- + "1:1-2:1\n"
- + timeStr + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid1\n"
- + "Bytes: 9\n"
- + "comment 1\n"
- + "\n"
- + "2:1-3:1\n"
- + timeStr + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid2\n"
- + "Bytes: 9\n"
- + "comment 2\n"
- + "\n"
- + "Base-for-patch-set: 2\n"
- + "File: file1\n"
- + "\n"
- + "1:1-2:1\n"
- + timeStr + "\n"
- + "Author: Other Account <2@gerrit>\n"
- + "UUID: uuid3\n"
- + "Bytes: 9\n"
- + "comment 3\n"
- + "\n");
- }
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Base-for-patch-set: 1\n"
+ + "File: file1\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + timeStr + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid1\n"
+ + "Bytes: 9\n"
+ + "comment 1\n"
+ + "\n"
+ + "2:1-3:1\n"
+ + timeStr + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid2\n"
+ + "Bytes: 9\n"
+ + "comment 2\n"
+ + "\n"
+ + "Base-for-patch-set: 2\n"
+ + "File: file1\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + timeStr + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "UUID: uuid3\n"
+ + "Bytes: 9\n"
+ + "comment 3\n"
+ + "\n");
+ }
+ }
assertThat(notes.getComments()).isEqualTo(
ImmutableMultimap.of(
revId, comment1,
@@ -1576,6 +1667,59 @@
}
@Test
+ public void patchLineCommentNotesFormatRealAuthor() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ String uuid = "uuid";
+ String message = "comment";
+ CommentRange range = new CommentRange(1, 1, 2, 1);
+ Timestamp time = TimeUtil.nowTs();
+ PatchSet.Id psId = c.currentPatchSetId();
+ RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+
+ Comment comment = newComment(psId, "file", uuid, range,
+ range.getEndLine(), otherUser, null, time, message, (short) 1,
+ revId.get());
+ comment.setRealAuthor(changeOwner.getAccountId());
+ update.setPatchSetId(psId);
+ update.putComment(Status.PUBLISHED, comment);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+
+ try (RevWalk walk = new RevWalk(repo)) {
+ ArrayList<Note> notesInTree =
+ Lists.newArrayList(notes.revisionNoteMap.noteMap.iterator());
+ Note note = Iterables.getOnlyElement(notesInTree);
+
+ byte[] bytes =
+ walk.getObjectReader().open(
+ note.getData(), Constants.OBJ_BLOB).getBytes();
+ String noteString = new String(bytes, UTF_8);
+
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Patch-set: 1\n"
+ + "File: file\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "Real-author: Change Owner <1@gerrit>\n"
+ + "UUID: uuid\n"
+ + "Bytes: 7\n"
+ + "comment\n"
+ + "\n");
+ }
+ }
+ assertThat(notes.getComments())
+ .isEqualTo(ImmutableMultimap.of(revId, comment));
+ }
+
+ @Test
public void patchLineCommentNotesFormatWeirdUser() throws Exception {
Account account = new Account(new Account.Id(3), TimeUtil.nowTs());
account.setFullName("Weird\n\u0002<User>\n");
@@ -1590,11 +1734,11 @@
Timestamp time = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment comment = newPublishedComment(psId, "file1",
- uuid, range, range.getEndLine(), user, null, time, "comment",
- (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+ Comment comment = newComment(psId, "file1", uuid, range, range.getEndLine(),
+ user, null, time, "comment", (short) 1,
+ "abcd1234abcd1234abcd1234abcd1234abcd1234");
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1609,22 +1753,24 @@
note.getData(), Constants.OBJ_BLOB).getBytes();
String noteString = new String(bytes, UTF_8);
String timeStr = ChangeNoteUtil.formatTime(serverIdent, time);
- assertThat(noteString).isEqualTo(
- "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
- + "Patch-set: 1\n"
- + "File: file1\n"
- + "\n"
- + "1:1-2:1\n"
- + timeStr + "\n"
- + "Author: Weird\u0002User <3@gerrit>\n"
- + "UUID: uuid\n"
- + "Bytes: 7\n"
- + "comment\n"
- + "\n");
- }
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Patch-set: 1\n"
+ + "File: file1\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + timeStr + "\n"
+ + "Author: Weird\u0002User <3@gerrit>\n"
+ + "UUID: uuid\n"
+ + "Bytes: 7\n"
+ + "comment\n"
+ + "\n");
+ }
+ }
assertThat(notes.getComments())
- .isEqualTo(ImmutableMultimap.of(comment.getRevId(), comment));
+ .isEqualTo(ImmutableMultimap.of(new RevId(comment.revId), comment));
}
@Test
@@ -1642,21 +1788,20 @@
Timestamp now = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment commentForBase =
- newPublishedComment(psId, "filename", uuid1,
- range, range.getEndLine(), otherUser, null, now, messageForBase,
- (short) 0, rev1);
+ Comment commentForBase =
+ newComment(psId, "filename", uuid1, range, range.getEndLine(),
+ otherUser, null, now, messageForBase, (short) 0, rev1);
update.setPatchSetId(psId);
- update.putComment(commentForBase);
+ update.putComment(Status.PUBLISHED, commentForBase);
update.commit();
update = newUpdate(c, otherUser);
- PatchLineComment commentForPS =
- newPublishedComment(psId, "filename", uuid2,
- range, range.getEndLine(), otherUser, null, now, messageForPS,
+ Comment commentForPS =
+ newComment(psId, "filename", uuid2, range, range.getEndLine(),
+ otherUser, null, now, messageForPS,
(short) 1, rev2);
update.setPatchSetId(psId);
- update.putComment(commentForPS);
+ update.putComment(Status.PUBLISHED, commentForPS);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -1679,19 +1824,19 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp timeForComment1 = TimeUtil.nowTs();
Timestamp timeForComment2 = TimeUtil.nowTs();
- PatchLineComment comment1 = newPublishedComment(psId, filename,
- uuid1, range, range.getEndLine(), otherUser, null, timeForComment1,
- "comment 1", side, rev);
+ Comment comment1 =
+ newComment(psId, filename, uuid1, range, range.getEndLine(), otherUser,
+ null, timeForComment1, "comment 1", side, rev);
update.setPatchSetId(psId);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
update = newUpdate(c, otherUser);
- PatchLineComment comment2 = newPublishedComment(psId, filename,
- uuid2, range, range.getEndLine(), otherUser, null, timeForComment2,
- "comment 2", side, rev);
+ Comment comment2 =
+ newComment(psId, filename, uuid2, range, range.getEndLine(), otherUser,
+ null, timeForComment2, "comment 2", side, rev);
update.setPatchSetId(psId);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -1714,19 +1859,19 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newPublishedComment(psId, filename1,
+ Comment comment1 = newComment(psId, filename1,
uuid, range, range.getEndLine(), otherUser, null, now, "comment 1",
side, rev);
update.setPatchSetId(psId);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
update = newUpdate(c, otherUser);
- PatchLineComment comment2 = newPublishedComment(psId, filename2,
+ Comment comment2 = newComment(psId, filename2,
uuid, range, range.getEndLine(), otherUser, null, now, "comment 2",
side, rev);
update.setPatchSetId(psId);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -1748,11 +1893,10 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newPublishedComment(ps1, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
- side, rev1);
+ Comment comment1 = newComment(ps1, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps1", side, rev1);
update.setPatchSetId(ps1);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
incrementPatchSet(c);
@@ -1760,11 +1904,10 @@
update = newUpdate(c, otherUser);
now = TimeUtil.nowTs();
- PatchLineComment comment2 = newPublishedComment(ps2, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
- side, rev2);
+ Comment comment2 = newComment(ps2, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps2", side, rev2);
update.setPatchSetId(ps2);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -1785,11 +1928,10 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newComment(ps1, filename, uuid, range,
- range.getEndLine(), otherUser, null, now, "comment on ps1", side,
- rev, Status.DRAFT);
+ Comment comment1 = newComment(ps1, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps1", side, rev);
update.setPatchSetId(ps1);
- update.putComment(comment1);
+ update.putComment(Status.DRAFT, comment1);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1797,10 +1939,9 @@
ImmutableMultimap.of(new RevId(rev), comment1));
assertThat(notes.getComments()).isEmpty();
- comment1.setStatus(Status.PUBLISHED);
update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
notes = newNotes(c);
@@ -1826,14 +1967,12 @@
// Write two drafts on the same side of one patch set.
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(psId);
- PatchLineComment comment1 = newComment(psId, filename, uuid1,
- range1, range1.getEndLine(), otherUser, null, now, "comment on ps1",
- side, rev, Status.DRAFT);
- PatchLineComment comment2 = newComment(psId, filename, uuid2,
- range2, range2.getEndLine(), otherUser, null, now, "other on ps1",
- side, rev, Status.DRAFT);
- update.putComment(comment1);
- update.putComment(comment2);
+ Comment comment1 = newComment(psId, filename, uuid1, range1,
+ range1.getEndLine(), otherUser, null, now, "comment on ps1", side, rev);
+ Comment comment2 = newComment(psId, filename, uuid2, range2,
+ range2.getEndLine(), otherUser, null, now, "other on ps1", side, rev);
+ update.putComment(Status.DRAFT, comment1);
+ update.putComment(Status.DRAFT, comment2);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1846,8 +1985,7 @@
// Publish first draft.
update = newUpdate(c, otherUser);
update.setPatchSetId(psId);
- comment1.setStatus(Status.PUBLISHED);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
notes = newNotes(c);
@@ -1874,15 +2012,15 @@
// Write two drafts, one on each side of the patchset.
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(psId);
- PatchLineComment baseComment = newComment(psId, filename, uuid1,
- range1, range1.getEndLine(), otherUser, null, now, "comment on base",
- (short) 0, rev1, Status.DRAFT);
- PatchLineComment psComment = newComment(psId, filename, uuid2,
- range2, range2.getEndLine(), otherUser, null, now, "comment on ps",
- (short) 1, rev2, Status.DRAFT);
+ Comment baseComment =
+ newComment(psId, filename, uuid1, range1, range1.getEndLine(),
+ otherUser, null, now, "comment on base", (short) 0, rev1);
+ Comment psComment =
+ newComment(psId, filename, uuid2, range2, range2.getEndLine(),
+ otherUser, null, now, "comment on ps", (short) 1, rev2);
- update.putComment(baseComment);
- update.putComment(psComment);
+ update.putComment(Status.DRAFT, baseComment);
+ update.putComment(Status.DRAFT, psComment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1896,10 +2034,8 @@
update = newUpdate(c, otherUser);
update.setPatchSetId(psId);
- baseComment.setStatus(Status.PUBLISHED);
- psComment.setStatus(Status.PUBLISHED);
- update.putComment(baseComment);
- update.putComment(psComment);
+ update.putComment(Status.PUBLISHED, baseComment);
+ update.putComment(Status.PUBLISHED, psComment);
update.commit();
notes = newNotes(c);
@@ -1923,11 +2059,10 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment = newComment(psId, filename, uuid, range,
- range.getEndLine(), otherUser, null, now, "comment on ps1", side,
- rev, Status.DRAFT);
+ Comment comment = newComment(psId, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps1", side, rev);
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.DRAFT, comment);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -1962,11 +2097,10 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newComment(ps1, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
- side, rev1, Status.DRAFT);
+ Comment comment1 = newComment(ps1, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps1", side, rev1);
update.setPatchSetId(ps1);
- update.putComment(comment1);
+ update.putComment(Status.DRAFT, comment1);
update.commit();
incrementPatchSet(c);
@@ -1974,11 +2108,10 @@
update = newUpdate(c, otherUser);
now = TimeUtil.nowTs();
- PatchLineComment comment2 = newComment(ps2, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
- side, rev2, Status.DRAFT);
+ Comment comment2 = newComment(ps2, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps2", side, rev2);
update.setPatchSetId(ps2);
- update.putComment(comment2);
+ update.putComment(Status.DRAFT, comment2);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -2010,10 +2143,9 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment = newComment(ps1, filename, uuid, range,
- range.getEndLine(), otherUser, null, now, "comment on ps1", side,
- rev, Status.PUBLISHED);
- update.putComment(comment);
+ Comment comment = newComment(ps1, filename, uuid, range, range.getEndLine(),
+ otherUser, null, now, "comment on ps1", side, rev);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
assertThat(repo.exactRef(changeMetaRef(c.getId()))).isNotNull();
@@ -2033,10 +2165,10 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment draft = newComment(ps1, filename, "uuid1", range,
- range.getEndLine(), otherUser, null, now, "draft comment on ps1", side,
- rev, Status.DRAFT);
- update.putComment(draft);
+ Comment draft =
+ newComment(ps1, filename, "uuid1", range, range.getEndLine(), otherUser,
+ null, now, "draft comment on ps1", side, rev);
+ update.putComment(Status.DRAFT, draft);
update.commit();
String draftRef = refsDraftComments(c.getId(), otherUser.getAccountId());
@@ -2044,10 +2176,9 @@
assertThat(old).isNotNull();
update = newUpdate(c, otherUser);
- PatchLineComment pub = newComment(ps1, filename, "uuid2", range,
- range.getEndLine(), otherUser, null, now, "comment on ps1", side,
- rev, Status.PUBLISHED);
- update.putComment(pub);
+ Comment pub = newComment(ps1, filename, "uuid2", range, range.getEndLine(),
+ otherUser, null, now, "comment on ps1", side, rev);
+ update.putComment(Status.PUBLISHED, pub);
update.commit();
assertThat(exactRefAllUsers(draftRef)).isEqualTo(old);
@@ -2063,11 +2194,10 @@
Timestamp now = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment comment = newPublishedComment(
- psId, "filename", uuid, null, 0, otherUser, null, now, messageForBase,
- (short) 0, rev);
+ Comment comment = newComment(psId, "filename", uuid, null, 0, otherUser,
+ null, now, messageForBase, (short) 0, rev);
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -2084,11 +2214,10 @@
Timestamp now = TimeUtil.nowTs();
PatchSet.Id psId = c.currentPatchSetId();
- PatchLineComment comment = newPublishedComment(
- psId, "filename", uuid, null, 1, otherUser, null, now, messageForBase,
- (short) 0, rev);
+ Comment comment = newComment(psId, "filename", uuid, null, 1, otherUser,
+ null, now, messageForBase, (short) 0, rev);
update.setPatchSetId(psId);
- update.putComment(comment);
+ update.putComment(Status.PUBLISHED, comment);
update.commit();
assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
@@ -2112,14 +2241,12 @@
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(ps2);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newComment(ps1, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
- side, rev1, Status.DRAFT);
- PatchLineComment comment2 = newComment(ps2, filename,
- uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
- side, rev2, Status.DRAFT);
- update.putComment(comment1);
- update.putComment(comment2);
+ Comment comment1 = newComment(ps1, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps1", side, rev1);
+ Comment comment2 = newComment(ps2, filename, uuid, range,
+ range.getEndLine(), otherUser, null, now, "comment on ps2", side, rev2);
+ update.putComment(Status.DRAFT, comment1);
+ update.putComment(Status.DRAFT, comment2);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -2128,10 +2255,8 @@
update = newUpdate(c, otherUser);
update.setPatchSetId(ps2);
- comment1.setStatus(Status.PUBLISHED);
- comment2.setStatus(Status.PUBLISHED);
- update.putComment(comment1);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment1);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
notes = newNotes(c);
@@ -2150,14 +2275,12 @@
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newComment(ps1, "file1",
- "uuid1", range, range.getEndLine(), otherUser, null, now, "comment1",
- side, rev1.get(), Status.DRAFT);
- PatchLineComment comment2 = newComment(ps1, "file2",
- "uuid2", range, range.getEndLine(), otherUser, null, now, "comment2",
- side, rev1.get(), Status.DRAFT);
- update.putComment(comment1);
- update.putComment(comment2);
+ Comment comment1 = newComment(ps1, "file1", "uuid1", range,
+ range.getEndLine(), otherUser, null, now, "comment1", side, rev1.get());
+ Comment comment2 = newComment(ps1, "file2", "uuid2", range,
+ range.getEndLine(), otherUser, null, now, "comment2", side, rev1.get());
+ update.putComment(Status.DRAFT, comment1);
+ update.putComment(Status.DRAFT, comment2);
update.commit();
ChangeNotes notes = newNotes(c);
@@ -2167,8 +2290,7 @@
update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
- comment2.setStatus(Status.PUBLISHED);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
notes = newNotes(c);
@@ -2203,14 +2325,14 @@
ChangeUpdate update = newUpdate(c, otherUser);
Timestamp now = TimeUtil.nowTs();
- PatchLineComment comment1 = newComment(ps1, "file1",
- "uuid1", range, range.getEndLine(), otherUser, null, now, "comment on ps1",
- side, rev1.get(), Status.DRAFT);
- PatchLineComment comment2 = newComment(ps1, "file2",
- "uuid2", range, range.getEndLine(), otherUser, null, now, "another comment",
- side, rev1.get(), Status.DRAFT);
- update.putComment(comment1);
- update.putComment(comment2);
+ Comment comment1 =
+ newComment(ps1, "file1", "uuid1", range, range.getEndLine(), otherUser,
+ null, now, "comment on ps1", side, rev1.get());
+ Comment comment2 =
+ newComment(ps1, "file2", "uuid2", range, range.getEndLine(), otherUser,
+ null, now, "another comment", side, rev1.get());
+ update.putComment(Status.DRAFT, comment1);
+ update.putComment(Status.DRAFT, comment2);
update.commit();
String refName = refsDraftComments(c.getId(), otherUserId);
@@ -2218,8 +2340,7 @@
update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
- comment2.setStatus(Status.PUBLISHED);
- update.putComment(comment2);
+ update.putComment(Status.PUBLISHED, comment2);
update.commit();
assertThat(exactRefAllUsers(refName)).isNotNull();
assertThat(exactRefAllUsers(refName)).isNotEqualTo(oldDraftId);
@@ -2229,7 +2350,6 @@
// non-atomically after adding the published comment succeeded.
ChangeDraftUpdate draftUpdate =
newUpdate(c, otherUser).createDraftUpdateIfNull();
- comment2.setStatus(Status.DRAFT);
draftUpdate.putComment(comment2);
try (NoteDbUpdateManager manager =
updateManagerFactory.create(c.getProject())) {
@@ -2242,8 +2362,6 @@
assertThat(draftNotes.load().getComments().get(rev1))
.containsExactly(comment1, comment2);
- comment2.setStatus(Status.PUBLISHED); // Reset for later assertions.
-
// Zombie comment is filtered out of drafts via ChangeNotes.
ChangeNotes notes = newNotes(c);
assertThat(notes.getDraftComments(otherUserId).get(rev1))
@@ -2253,8 +2371,7 @@
update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
- comment1.setStatus(Status.PUBLISHED);
- update.putComment(comment1);
+ update.putComment(Status.PUBLISHED, comment1);
update.commit();
// Updating an unrelated comment causes the zombie comment to get fixed up.
@@ -2268,18 +2385,16 @@
String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
ChangeUpdate update1 = newUpdate(c, otherUser);
- PatchLineComment comment1 = newComment(c.currentPatchSetId(), "filename",
+ Comment comment1 = newComment(c.currentPatchSetId(), "filename",
"uuid1", range, range.getEndLine(), otherUser, null,
- new Timestamp(update1.getWhen().getTime()), "comment 1", (short) 1, rev,
- Status.PUBLISHED);
- update1.putComment(comment1);
+ new Timestamp(update1.getWhen().getTime()), "comment 1", (short) 1, rev);
+ update1.putComment(Status.PUBLISHED, comment1);
ChangeUpdate update2 = newUpdate(c, otherUser);
- PatchLineComment comment2 = newComment(c.currentPatchSetId(), "filename",
+ Comment comment2 = newComment(c.currentPatchSetId(), "filename",
"uuid2", range, range.getEndLine(), otherUser, null,
- new Timestamp(update2.getWhen().getTime()), "comment 2", (short) 1, rev,
- Status.PUBLISHED);
- update2.putComment(comment2);
+ new Timestamp(update2.getWhen().getTime()), "comment 2", (short) 1, rev);
+ update2.putComment(Status.PUBLISHED, comment2);
try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
manager.add(update1);
@@ -2288,10 +2403,29 @@
}
ChangeNotes notes = newNotes(c);
- List<PatchLineComment> comments = notes.getComments().get(new RevId(rev));
+ List<Comment> comments = notes.getComments().get(new RevId(rev));
assertThat(comments).hasSize(2);
- assertThat(comments.get(0).getMessage()).isEqualTo("comment 1");
- assertThat(comments.get(1).getMessage()).isEqualTo("comment 2");
+ assertThat(comments.get(0).message).isEqualTo("comment 1");
+ assertThat(comments.get(1).message).isEqualTo("comment 2");
+ }
+
+ @Test
+ public void realUser() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ update.setChangeMessage("Message on behalf of other user");
+ update.commit();
+
+ ChangeMessage msg = Iterables.getLast(newNotes(c).getChangeMessages());
+ assertThat(msg.getMessage()).isEqualTo("Message on behalf of other user");
+ assertThat(msg.getAuthor()).isEqualTo(otherUserId);
+ assertThat(msg.getRealAuthor()).isEqualTo(changeOwner.getAccountId());
+ }
+
+ private boolean testJson() {
+ return noteUtil.getWriteJson();
}
private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index bf5abba..b674159 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -22,7 +22,9 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.util.RequestId;
+import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.TestChanges;
import org.eclipse.jgit.lib.ObjectId;
@@ -30,10 +32,12 @@
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
+import org.junit.runner.RunWith;
import java.util.Date;
import java.util.TimeZone;
+@RunWith(ConfigSuite.class)
public class CommitMessageOutputTest extends AbstractChangeNotesTest {
@Test
public void approvalsCommitFormatSimple() throws Exception {
@@ -329,6 +333,29 @@
update.getResult());
}
+ @Test
+ public void realUser() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ update.setChangeMessage("Message on behalf of other user");
+ update.commit();
+
+ RevCommit commit = parseCommit(update.getResult());
+ PersonIdent author = commit.getAuthorIdent();
+ assertThat(author.getName()).isEqualTo("Other Account");
+ assertThat(author.getEmailAddress()).isEqualTo("2@gerrit");
+
+ assertBodyEquals("Update patch set 1\n"
+ + "\n"
+ + "Message on behalf of other user\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Real-user: Change Owner <1@gerrit>\n",
+ commit);
+ }
+
private RevCommit parseCommit(ObjectId id) throws Exception {
if (id instanceof RevCommit) {
return (RevCommit) id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
new file mode 100644
index 0000000..f5fdf13
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb.rebuild;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.testutil.TestTimeUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class EventSorterTest {
+ private class TestEvent extends Event {
+ protected TestEvent(Timestamp when) {
+ super(
+ new PatchSet.Id(new Change.Id(1), 1),
+ new Account.Id(1000), new Account.Id(1000),
+ when, changeCreatedOn, null);
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return false;
+ }
+
+ @Override
+ void apply(ChangeUpdate update) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private Timestamp changeCreatedOn;
+
+ @Before
+ public void setUp() {
+ TestTimeUtil.resetWithClockStep(10, TimeUnit.SECONDS);
+ changeCreatedOn = TimeUtil.nowTs();
+ }
+
+ @Test
+ public void naturalSort() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+
+ List<Event> events = events(e2, e1, e3);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e1, e2, e3).inOrder();
+ }
+
+ @Test
+ public void topoSortNoChange() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ e2.addDep(e1);
+
+ List<Event> events = events(e2, e1, e3);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e1, e2, e3).inOrder();
+ }
+
+ @Test
+ public void topoSortOneDep() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ e1.addDep(e2);
+
+ List<Event> events = events(e2, e3, e1);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e2, e1, e3).inOrder();
+ }
+
+ @Test
+ public void topoSortChainOfDeps() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ Event e4 = new TestEvent(TimeUtil.nowTs());
+ e1.addDep(e2);
+ e2.addDep(e3);
+ e3.addDep(e4);
+
+ List<Event> events = events(e1, e2, e3, e4);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e4, e3, e2, e1).inOrder();
+ }
+
+ @Test
+ public void topoSortMultipleDeps() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ Event e4 = new TestEvent(TimeUtil.nowTs());
+ e1.addDep(e2);
+ e1.addDep(e4);
+ e2.addDep(e3);
+
+ // Processing 3 pops 2, processing 4 pops 1.
+ List<Event> events = events(e2, e3, e1, e4);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e3, e2, e4, e1).inOrder();
+ }
+
+ @Test
+ public void topoSortMultipleDepsPreservesNaturalOrder() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ Event e4 = new TestEvent(TimeUtil.nowTs());
+ e1.addDep(e4);
+ e2.addDep(e4);
+ e3.addDep(e4);
+
+ // Processing 4 pops 1, 2, 3 in natural order.
+ List<Event> events = events(e4, e3, e2, e1);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e4, e1, e2, e3).inOrder();
+ }
+
+ @Test
+ public void topoSortCycle() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+
+ // Implementation is not really defined, but infinite looping would be bad.
+ // According to current implementation details, 2 pops 1, 1 pops 2 which was
+ // already seen.
+ List<Event> events = events(e2, e1);
+ new EventSorter(events).sort();
+ assertThat(events).containsExactly(e1, e2).inOrder();
+ }
+
+ @Test
+ public void topoSortDepNotInInputList() {
+ Event e1 = new TestEvent(TimeUtil.nowTs());
+ Event e2 = new TestEvent(TimeUtil.nowTs());
+ Event e3 = new TestEvent(TimeUtil.nowTs());
+ e1.addDep(e3);
+
+ List<Event> events = events(e2, e1);
+ try {
+ new EventSorter(events).sort();
+ fail("expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+ }
+
+ private List<Event> events(Event... es) {
+ return Lists.newArrayList(es);
+ }
+}
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/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index f7b3b11..b65d49d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.api.GerritApi;
@@ -476,22 +475,10 @@
}
protected static Iterable<Integer> ids(AccountInfo... accounts) {
- return FluentIterable.from(Arrays.asList(accounts)).transform(
- new Function<AccountInfo, Integer>() {
- @Override
- public Integer apply(AccountInfo in) {
- return in._accountId;
- }
- });
+ return FluentIterable.of(accounts).transform(a -> a._accountId);
}
protected static Iterable<Integer> ids(Iterable<AccountInfo> accounts) {
- return FluentIterable.from(accounts).transform(
- new Function<AccountInfo, Integer>() {
- @Override
- public Integer apply(AccountInfo in) {
- return in._accountId;
- }
- });
+ return FluentIterable.from(accounts).transform(a -> a._accountId);
}
}
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..4758338 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
@@ -21,9 +21,7 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.junit.Assert.fail;
-import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
@@ -31,12 +29,14 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.GerritApi;
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;
@@ -244,7 +244,8 @@
assertQuery("change:repo~branch~" + k.substring(0, 10), change);
assertQuery("foo~bar");
- assertBadQuery("change:foo~bar");
+ assertThatQueryException("change:foo~bar")
+ .hasMessage("Invalid change format");
assertQuery("otherrepo~branch~" + k);
assertQuery("change:otherrepo~branch~" + k);
assertQuery("repo~otherbranch~" + k);
@@ -342,8 +343,10 @@
assertQuery("status:N", change1);
assertQuery("status:nE", change1);
assertQuery("status:neW", change1);
- assertBadQuery("status:nx");
- assertBadQuery("status:newx");
+ assertThatQueryException("status:nx")
+ .hasMessage("invalid change status: nx");
+ assertThatQueryException("status:newx")
+ .hasMessage("invalid change status: newx");
}
@Test
@@ -619,6 +622,27 @@
assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+2,owner", reviewPlus2Change);
+ assertQuery("label:Code-Review=-2,owner", reviewMinus2Change);
+ }
+
+ @Test
+ public void byLabelNotOwner() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ ChangeInserter ins = newChange(repo, null, null, null, null);
+ Account.Id user1 = createAccount("user1");
+
+ Change reviewPlus1Change = insert(repo, ins);
+
+ // post a review with user1
+ requestContext.setContext(newRequestContext(user1));
+ gApi.changes().id(reviewPlus1Change.getId().get()).current()
+ .review(ReviewInput.recommend());
+
+ assertQuery("label:Code-Review=+1,user=user1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner");
}
private Change[] codeReviewInRange(Map<Integer, Change> changes, int start,
@@ -743,7 +767,8 @@
assertQuery(query, change);
assertQuery(query.withStart(1));
assertQuery(query.withStart(99));
- assertBadQuery(query.withStart(100));
+ assertThatQueryException(query.withStart(100))
+ .hasMessage("Cannot go beyond page 10 of results");
assertQuery(query.withLimit(100).withStart(100));
}
@@ -1436,13 +1461,8 @@
for (int i = 1; i <= 11; i++) {
Iterable<ChangeData> cds = internalChangeQuery.byCommitsOnBranchNotMerged(
repo.getRepository(), db, dest, shas, i);
- Iterable<Integer> ids = FluentIterable.from(cds).transform(
- new Function<ChangeData, Integer>() {
- @Override
- public Integer apply(ChangeData in) {
- return in.getId().get();
- }
- });
+ Iterable<Integer> ids = FluentIterable.from(cds)
+ .transform(in -> in.getId().get());
String name = "limit " + i;
assertThat(ids).named(name).hasSize(n);
assertThat(ids).named(name)
@@ -1583,7 +1603,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(
@@ -1597,16 +1617,19 @@
return inserter.getChange();
}
- protected void assertBadQuery(Object query) throws Exception {
- assertBadQuery(newQuery(query));
+ protected ThrowableSubject assertThatQueryException(Object query)
+ throws Exception {
+ return assertThatQueryException(newQuery(query));
}
- protected void assertBadQuery(QueryRequest query) throws Exception {
+ protected ThrowableSubject assertThatQueryException(QueryRequest query)
+ throws Exception {
try {
query.get();
- fail("expected BadRequestException for query: " + query);
+ throw new AssertionError(
+ "expected BadRequestException for query: " + query);
} catch (BadRequestException e) {
- // Expected.
+ return assertThat(e);
}
}
@@ -1639,24 +1662,22 @@
StringBuilder b = new StringBuilder();
b.append("query '").append(query.getQuery())
.append("' with expected changes ");
- b.append(format(Iterables.transform(Arrays.asList(expectedChanges),
- new Function<Change, Integer>() {
- @Override
- public Integer apply(Change change) {
- return change.getChangeId();
- }
- })));
+ b.append(format(
+ Arrays.stream(expectedChanges).map(Change::getChangeId).iterator()));
b.append(" and result ");
b.append(format(actualIds));
return b.toString();
}
private String format(Iterable<Integer> changeIds) throws RestApiException {
+ return format(changeIds.iterator());
+ }
+
+ private String format(Iterator<Integer> changeIds) throws RestApiException {
StringBuilder b = new StringBuilder();
b.append("[");
- Iterator<Integer> it = changeIds.iterator();
- while (it.hasNext()) {
- int id = it.next();
+ while (changeIds.hasNext()) {
+ int id = changeIds.next();
ChangeInfo c = gApi.changes().id(id).get();
b.append("{").append(id).append(" (").append(c.changeId)
.append("), ").append("dest=").append(
@@ -1665,7 +1686,7 @@
.append("status=").append(c.status).append(", ")
.append("lastUpdated=").append(c.updated.getTime())
.append("}");
- if (it.hasNext()) {
+ if (changeIds.hasNext()) {
b.append(", ");
}
}
@@ -1674,23 +1695,13 @@
}
protected static Iterable<Integer> ids(Change... changes) {
- return FluentIterable.from(Arrays.asList(changes)).transform(
- new Function<Change, Integer>() {
- @Override
- public Integer apply(Change in) {
- return in.getId().get();
- }
- });
+ return FluentIterable.from(Arrays.asList(changes))
+ .transform(in -> in.getId().get());
}
protected static Iterable<Integer> ids(Iterable<ChangeInfo> changes) {
- return FluentIterable.from(changes).transform(
- new Function<ChangeInfo, Integer>() {
- @Override
- public Integer apply(ChangeInfo in) {
- return in._number;
- }
- });
+ return FluentIterable.from(changes)
+ .transform(in -> in._number);
}
protected static long lastUpdatedMs(Change c) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
index f2d563e..b8888e2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
@@ -14,9 +14,9 @@
package com.google.gerrit.testutil;
+import static java.util.stream.Collectors.toList;
+
import com.google.auto.value.AutoValue;
-import com.google.common.base.Predicate;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.errors.EmailException;
@@ -111,17 +111,14 @@
}
}
- public ImmutableList<Message> getMessages(String changeId, String type) {
+ public List<Message> getMessages(String changeId, String type) {
final String idFooter = "\nGerrit-Change-Id: " + changeId + "\n";
final String typeFooter = "\nGerrit-MessageType: " + type + "\n";
- return FluentIterable.from(getMessages())
- .filter(new Predicate<Message>() {
- @Override
- public boolean apply(Message in) {
- return in.body().contains(idFooter)
- && in.body().contains(typeFooter);
- }
- }).toList();
+ return getMessages()
+ .stream()
+ .filter(in -> in.body().contains(idFooter)
+ && in.body().contains(typeFooter))
+ .collect(toList());
}
private void waitForEmails() {
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..71401d7 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;
@@ -36,6 +36,7 @@
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.config.SitePath;
@@ -50,6 +51,8 @@
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
+import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.DiffExecutor;
import com.google.gerrit.server.schema.DataSourceType;
@@ -148,6 +151,8 @@
// TODO(dborowitz): Use jimfs.
bind(Path.class).annotatedWith(SitePath.class).toInstance(Paths.get("."));
bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
+ bind(GerritOptions.class)
+ .toInstance(new GerritOptions(cfg, false, false, false));
bind(PersonIdent.class)
.annotatedWith(GerritPersonIdent.class)
.toProvider(GerritPersonIdentProvider.class);
@@ -175,6 +180,7 @@
.to(InMemoryH2Type.class);
bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
.to(InMemoryDatabase.class);
+ bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
bind(SecureStore.class).to(DefaultSecureStore.class);
@@ -214,6 +220,9 @@
case LUCENE:
install(luceneIndexModule());
break;
+ case ELASTICSEARCH:
+ install(elasticIndexModule());
+ break;
default:
throw new ProvisionException(
"index type unsupported in tests: " + indexType);
@@ -236,14 +245,21 @@
}
private Module luceneIndexModule() {
+ return indexModule("com.google.gerrit.lucene.LuceneIndexModule");
+ }
+
+ private Module elasticIndexModule() {
+ return indexModule("com.google.gerrit.elasticsearch.ElasticIndexModule");
+ }
+
+ private Module indexModule(String moduleClassName) {
try {
Map<String, Integer> singleVersions = new HashMap<>();
int version = cfg.getInt("index", "lucene", "testVersion", -1);
if (version > 0) {
singleVersions.put(ChangeSchemaDefinitions.INSTANCE.getName(), version);
}
- Class<?> clazz =
- Class.forName("com.google.gerrit.lucene.LuceneIndexModule");
+ Class<?> clazz = Class.forName(moduleClassName);
Method m = clazz.getMethod(
"singleVersionWithExplicitVersions", Map.class, int.class);
return (Module) m.invoke(null, singleVersions, 0);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index 61bfe78..c5f4301 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -15,19 +15,24 @@
package com.google.gerrit.testutil;
import static com.google.common.truth.Truth.assertThat;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
-import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeRebuilder;
+import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gwtorm.client.IntKey;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -40,6 +45,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Stream;
@Singleton
public class NoteDbChecker {
@@ -48,37 +54,38 @@
private final Provider<ReviewDb> dbProvider;
private final GitRepositoryManager repoManager;
private final TestNotesMigration notesMigration;
+ private final ChangeBundleReader bundleReader;
private final ChangeNotes.Factory notesFactory;
private final ChangeRebuilder changeRebuilder;
- private final PatchLineCommentsUtil plcUtil;
+ private final CommentsUtil commentsUtil;
@Inject
NoteDbChecker(Provider<ReviewDb> dbProvider,
GitRepositoryManager repoManager,
TestNotesMigration notesMigration,
+ ChangeBundleReader bundleReader,
ChangeNotes.Factory notesFactory,
ChangeRebuilder changeRebuilder,
- PatchLineCommentsUtil plcUtil) {
+ CommentsUtil commentsUtil) {
this.dbProvider = dbProvider;
this.repoManager = repoManager;
+ this.bundleReader = bundleReader;
this.notesMigration = notesMigration;
this.notesFactory = notesFactory;
this.changeRebuilder = changeRebuilder;
- this.plcUtil = plcUtil;
+ this.commentsUtil = commentsUtil;
}
public void rebuildAndCheckAllChanges() throws Exception {
rebuildAndCheckChanges(
- Iterables.transform(
- getUnwrappedDb().changes().all(),
- ReviewDbUtil.changeIdFunction()));
+ getUnwrappedDb().changes().all().toList().stream().map(Change::getId));
}
public void rebuildAndCheckChanges(Change.Id... changeIds) throws Exception {
- rebuildAndCheckChanges(Arrays.asList(changeIds));
+ rebuildAndCheckChanges(Arrays.stream(changeIds));
}
- public void rebuildAndCheckChanges(Iterable<Change.Id> changeIds)
+ private void rebuildAndCheckChanges(Stream<Change.Id> changeIds)
throws Exception {
ReviewDb db = getUnwrappedDb();
@@ -107,11 +114,7 @@
}
public void checkChanges(Change.Id... changeIds) throws Exception {
- checkChanges(Arrays.asList(changeIds));
- }
-
- public void checkChanges(Iterable<Change.Id> changeIds) throws Exception {
- checkActual(readExpected(changeIds), new ArrayList<String>());
+ checkActual(readExpected(Arrays.stream(changeIds)), new ArrayList<>());
}
public void assertNoChangeRef(Project.NameKey project, Change.Id changeId)
@@ -121,24 +124,26 @@
}
}
- private List<ChangeBundle> readExpected(Iterable<Change.Id> changeIds)
+ private List<ChangeBundle> readExpected(Stream<Change.Id> changeIds)
throws Exception {
- ReviewDb db = getUnwrappedDb();
boolean old = notesMigration.readChanges();
try {
notesMigration.setReadChanges(false);
- List<Change.Id> sortedIds =
- ReviewDbUtil.intKeyOrdering().sortedCopy(changeIds);
- List<ChangeBundle> expected = new ArrayList<>(sortedIds.size());
- for (Change.Id id : sortedIds) {
- expected.add(ChangeBundle.fromReviewDb(db, id));
- }
- return expected;
+ return changeIds.sorted(comparing(IntKey::get))
+ .map(this::readBundleUnchecked).collect(toList());
} finally {
notesMigration.setReadChanges(old);
}
}
+ private ChangeBundle readBundleUnchecked(Change.Id id) {
+ try {
+ return bundleReader.fromReviewDb(getUnwrappedDb(), id);
+ } catch (OrmException e) {
+ throw new OrmRuntimeException(e);
+ }
+ }
+
private void checkActual(List<ChangeBundle> allExpected, List<String> msgs)
throws Exception {
ReviewDb db = getUnwrappedDb();
@@ -152,7 +157,7 @@
ChangeBundle actual;
try {
actual = ChangeBundle.fromNotes(
- plcUtil, notesFactory.create(db, c.getProject(), c.getId()));
+ commentsUtil, notesFactory.create(db, c.getProject(), c.getId()));
} catch (Throwable t) {
String msg = "Error converting change: " + c;
msgs.add(msg);
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 54b83e2..fcb844f 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -56,5 +56,4 @@
'//lib:truth',
'//lib/mina:sshd',
],
- source_under_test = [':sshd'],
)
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 25fb7a7..3e31fab 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -385,14 +385,6 @@
}
}
- public void checkExclusivity(final Object arg1, final String arg1name,
- final Object arg2, final String arg2name) throws UnloggedFailure {
- if (arg1 != null && arg2 != null) {
- throw new UnloggedFailure(String.format(
- "%s and %s options are mutually exclusive.", arg1name, arg2name));
- }
- }
-
private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
private final CommandRunnable thunk;
private final String taskName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index eb0d7b2..bd5e9f3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,9 +14,6 @@
package com.google.gerrit.sshd.commands;
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.common.ProjectInfo;
@@ -200,15 +197,11 @@
return childProjects;
}
- private Set<Project.NameKey> getAllParents(final Project.NameKey projectName) {
+ private Set<Project.NameKey> getAllParents(Project.NameKey projectName) {
ProjectState ps = projectCache.get(projectName);
- return ImmutableSet.copyOf(Iterables.transform(
- ps != null ? ps.parents() : Collections.<ProjectState> emptySet(),
- new Function<ProjectState, Project.NameKey> () {
- @Override
- public Project.NameKey apply(ProjectState in) {
- return in.getProject().getNameKey();
- }
- }));
+ if (ps == null) {
+ return Collections.emptySet();
+ }
+ return ps.parents().transform(s -> s.getProject().getNameKey()).toSet();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index f78b4df..e15a792 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -16,7 +16,6 @@
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -56,14 +55,8 @@
@Override
protected void run() throws Failure {
try {
- BanCommit.Input input =
- BanCommit.Input.fromCommits(Lists.transform(commitsToBan,
- new Function<ObjectId, String>() {
- @Override
- public String apply(ObjectId oid) {
- return oid.getName();
- }
- }));
+ BanCommit.Input input = BanCommit.Input.fromCommits(
+ Lists.transform(commitsToBan, ObjectId::getName));
input.reason = reason;
BanResultInfo r = banCommit.apply(new ProjectResource(projectControl), input);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index d3ff06f..4ecf284 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -16,7 +16,6 @@
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -74,12 +73,7 @@
input.name = fullName;
input.sshKey = readSshKey();
input.httpPassword = httpPassword;
- input.groups = Lists.transform(groups, new Function<AccountGroup.Id, String>() {
- @Override
- public String apply(AccountGroup.Id id) {
- return id.toString();
- }
- });
+ input.groups = Lists.transform(groups, AccountGroup.Id::toString);
try {
createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
} catch (RestApiException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 22f9683..f9fd1a9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -14,8 +14,8 @@
package com.google.gerrit.sshd.commands;
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
+import static java.util.stream.Collectors.toList;
+
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -123,30 +123,15 @@
private void addMembers(GroupResource rsrc) throws RestApiException,
OrmException, IOException {
- AddMembers.Input input =
- AddMembers.Input.fromMembers(FluentIterable
- .from(initialMembers)
- .transform(new Function<Account.Id, String>() {
- @Override
- public String apply(Account.Id id) {
- return String.valueOf(id.get());
- }
- })
- .toList());
+ AddMembers.Input input = AddMembers.Input.fromMembers(
+ initialMembers.stream().map(Object::toString).collect(toList()));
addMembers.apply(rsrc, input);
}
private void addIncludedGroups(GroupResource rsrc) throws RestApiException,
OrmException {
- AddIncludedGroups.Input input =
- AddIncludedGroups.Input.fromGroups(FluentIterable.from(initialGroups)
- .transform(new Function<AccountGroup.UUID, String>() {
- @Override
- public String apply(AccountGroup.UUID id) {
- return id.get();
- }
- }).toList());
-
+ AddIncludedGroups.Input input = AddIncludedGroups.Input.fromGroups(
+ initialGroups.stream().map(AccountGroup.UUID::get).collect(toList()));
addIncludedGroups.apply(rsrc, input);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index db4f313..3ef3309 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -15,7 +15,6 @@
package com.google.gerrit.sshd.commands;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
@@ -140,13 +139,7 @@
ProjectInput input = new ProjectInput();
input.name = projectName;
if (ownerIds != null) {
- input.owners = Lists.transform(ownerIds,
- new Function<AccountGroup.UUID, String>() {
- @Override
- public String apply(AccountGroup.UUID uuid) {
- return uuid.get();
- }
- });
+ input.owners = Lists.transform(ownerIds, AccountGroup.UUID::get);
}
if (newParent != null) {
input.parent = newParent.getProject().getName();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index a7e01c5..f654f6a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -14,11 +14,10 @@
package com.google.gerrit.sshd.commands;
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.base.MoreObjects;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.Account;
@@ -110,55 +109,37 @@
private void reportMembersAction(String action, GroupResource group,
List<Account.Id> accountIdList) throws UnsupportedEncodingException,
IOException {
- out.write(String.format(
- "Members %s group %s: %s\n",
- action,
- group.getName(),
- Joiner.on(", ").join(
- Iterables.transform(accountIdList,
- new Function<Account.Id, String>() {
- @Override
- public String apply(Account.Id accountId) {
- return MoreObjects.firstNonNull(accountCache.get(accountId)
- .getAccount().getPreferredEmail(), "n/a");
- }
- }))).getBytes(ENC));
+ String names = accountIdList.stream()
+ .map(accountId ->
+ MoreObjects.firstNonNull(
+ accountCache.get(accountId).getAccount().getPreferredEmail(),
+ "n/a"))
+ .collect(joining(", "));
+ out.write(
+ String.format(
+ "Members %s group %s: %s\n", action, group.getName(), names)
+ .getBytes(ENC));
}
private void reportGroupsAction(String action, GroupResource group,
List<AccountGroup.UUID> groupUuidList)
throws UnsupportedEncodingException, IOException {
- out.write(String.format(
- "Groups %s group %s: %s\n",
- action,
- group.getName(),
- Joiner.on(", ").join(
- Iterables.transform(groupUuidList,
- new Function<AccountGroup.UUID, String>() {
- @Override
- public String apply(AccountGroup.UUID uuid) {
- return groupCache.get(uuid).getName();
- }
- }))).getBytes(ENC));
+ String names = groupUuidList.stream()
+ .map(uuid -> groupCache.get(uuid).getName())
+ .collect(joining(", "));
+ out.write(
+ String.format(
+ "Groups %s group %s: %s\n", action, group.getName(), names)
+ .getBytes(ENC));
}
private AddIncludedGroups.Input fromGroups(List<AccountGroup.UUID> accounts) {
- return AddIncludedGroups.Input.fromGroups(Lists.newArrayList(Iterables
- .transform(accounts, new Function<AccountGroup.UUID, String>() {
- @Override
- public String apply(AccountGroup.UUID uuid) {
- return uuid.toString();
- }
- })));
+ return AddIncludedGroups.Input.fromGroups(
+ accounts.stream().map(Object::toString).collect(toList()));
}
private AddMembers.Input fromMembers(List<Account.Id> accounts) {
- return AddMembers.Input.fromMembers(Lists.newArrayList(Iterables.transform(
- accounts, new Function<Account.Id, String>() {
- @Override
- public String apply(Account.Id id) {
- return id.toString();
- }
- })));
+ return AddMembers.Input.fromMembers(
+ accounts.stream().map(Object::toString).collect(toList()));
}
}
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-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 0edba4f..99ced68 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -18,8 +18,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.AllowedFormats;
import com.google.gerrit.server.change.ArchiveFormat;
-import com.google.gerrit.server.change.GetArchive;
import com.google.gerrit.sshd.AbstractGitCommand;
import com.google.inject.Inject;
@@ -101,7 +101,7 @@
}
@Inject
- private GetArchive.AllowedFormats allowedFormats;
+ private AllowedFormats allowedFormats;
@Inject
private ReviewDb db;
private Options options = new Options();
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK
index cfab096..79ef836 100644
--- a/gerrit-util-http/BUCK
+++ b/gerrit-util-http/BUCK
@@ -34,7 +34,6 @@
'//lib:truth',
'//lib/easymock:easymock',
],
- source_under_test = [':http'],
# TODO(sop) Remove after Buck supports Eclipse
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index 3991b95..34f2672 100644
--- a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -18,7 +18,6 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
@@ -143,18 +142,12 @@
return Iterables.getFirst(parameters.get(name), null);
}
- private static final Function<Collection<String>, String[]> STRING_COLLECTION_TO_ARRAY =
- new Function<Collection<String>, String[]>() {
- @Override
- public String[] apply(Collection<String> values) {
- return values.toArray(new String[0]);
- }
- };
-
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(
- Maps.transformValues(parameters.asMap(), STRING_COLLECTION_TO_ARRAY));
+ Maps.transformValues(
+ parameters.asMap(),
+ vs -> vs.toArray(new String[0])));
}
@Override
@@ -164,7 +157,7 @@
@Override
public String[] getParameterValues(String name) {
- return STRING_COLLECTION_TO_ARRAY.apply(parameters.get(name));
+ return parameters.get(name).toArray(new String[0]);
}
public void setQueryString(String qs) {
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index 6d74a83..5dd1b04 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -5,6 +5,7 @@
srcs = glob(['src/main/java/**/*.java']),
deps = [
'//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
'//gerrit-extension-api:api',
'//gerrit-gpg:gpg',
'//gerrit-httpd:httpd',
diff --git a/gerrit-war/BUILD b/gerrit-war/BUILD
new file mode 100644
index 0000000..9262ad4
--- /dev/null
+++ b/gerrit-war/BUILD
@@ -0,0 +1,71 @@
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+
+java_library(
+ name = 'init',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-cache-h2:cache-h2',
+ '//gerrit-elasticsearch:elasticsearch',
+ '//gerrit-extension-api:api',
+ '//gerrit-gpg:gpg',
+ '//gerrit-httpd:httpd',
+ '//gerrit-lucene:lucene',
+ '//gerrit-oauth:oauth',
+ '//gerrit-openid:openid',
+ '//gerrit-pgm:http',
+ '//gerrit-pgm:init',
+ '//gerrit-pgm:init-api',
+ '//gerrit-pgm:util',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-server/src/main/prolog:common',
+ '//gerrit-sshd:sshd',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:servlet-api-3_1',
+ '//lib/guice:guice',
+ '//lib/guice:guice-servlet',
+ '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/log:api',
+ ],
+ visibility = ['//visibility:public'],
+)
+
+genrule2(
+ name = 'webapp_assets',
+ cmd = 'cd gerrit-war/src/main/webapp; zip -qr $$ROOT/$@ .',
+ srcs = glob(['src/main/webapp/**/*']),
+ out = 'webapp_assets.zip',
+ visibility = ['//visibility:public'],
+)
+
+java_import(
+ name = 'log4j-config',
+ jars = [':log4j-config__jar'],
+ visibility = ['//visibility:public'],
+)
+
+genrule2(
+ name = 'log4j-config__jar',
+ cmd = 'cd gerrit-war/src/main/resources && zip -9Dqr $$ROOT/$@ .',
+ srcs = ['src/main/resources/log4j.properties'],
+ out = 'log4j-config.jar',
+)
+
+java_import(
+ name = 'version',
+ jars = [':gen_version'],
+ visibility = ['//visibility:public'],
+)
+
+genrule2(
+ name = 'gen_version',
+ cmd = ' && '.join([
+ 'cd $$TMP',
+ 'mkdir -p com/google/gerrit/common',
+ 'cat $$ROOT/$(location //:version.txt) >com/google/gerrit/common/Version',
+ 'zip -9Dqr $$ROOT/$@ .',
+ ]),
+ tools = ['//:version.txt'],
+ out = 'gen_version.jar',
+)
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 0bae085..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.1</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..9dba629 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,8 @@
import com.google.common.base.Splitter;
import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.elasticsearch.ElasticIndexModule;
+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 +31,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;
@@ -38,6 +39,7 @@
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.RestCacheAdminModule;
@@ -342,6 +344,8 @@
switch (indexType) {
case LUCENE:
return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+ case ELASTICSEARCH:
+ return ElasticIndexModule.latestVersionWithOnlineUpgrade();
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
}
diff --git a/lib/BUCK b/lib/BUCK
index df7c9b3..14ae9df 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')
@@ -38,9 +39,9 @@
maven_jar(
name = 'gwtorm_client',
- id = 'com.google.gerrit:gwtorm:1.15',
- bin_sha1 = '26a2459f543ed78977535f92e379dc0d6cdde8bb',
- src_sha1 = '9524088d6e46e299b12791cb1a63c4ba6a478b96',
+ id = 'com.google.gerrit:gwtorm:1.16',
+ bin_sha1 = '3e41b6d7bb352fa0539ce23b9bce97cf8c26c3bf',
+ src_sha1 = 'f45b7bacc79a0e5a7f6cf799a2dba23cc5bca19b',
license = 'Apache2.0',
)
@@ -53,9 +54,9 @@
maven_jar(
name = 'gwtjsonrpc',
- id = 'com.google.gerrit:gwtjsonrpc:1.9',
- bin_sha1 = '458f55e92584fbd9ab91a89fa1c37654922a0f2b',
- src_sha1 = 'ba539361c80a26f0d30a2f56068f6d83f44062d8',
+ id = 'com.google.gerrit:gwtjsonrpc:1.11',
+ bin_sha1 = '0990e7eec9eec3a15661edcf9232acbac4aeacec',
+ src_sha1 = 'a682afc46284fb58197a173cb5818770a1e7834a',
license = 'Apache2.0',
)
@@ -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(
@@ -217,8 +222,8 @@
maven_jar(
name = 'jimfs',
- id = 'com.google.jimfs:jimfs:1.0',
- sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97',
+ id = 'com.google.jimfs:jimfs:1.1',
+ sha1 = '8fbd0579dc68aba6186935cc1bee21d2f3e7ec1c',
license = 'DO_NOT_DISTRIBUTE',
deps = [':guava'],
)
@@ -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..292560b 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -1,50 +1,68 @@
+exports_files(glob([
+ "LICENSE-*"
+]))
+
+filegroup(
+ name = 'all-licenses',
+ srcs = glob(['LICENSE-*'], exclude = ['LICENSE-DO_NOT_DISTRIBUTE']),
+ visibility = ['//visibility:public'],
+)
+
java_library(
name = 'servlet-api-3_1',
neverlink = 1,
exports = ['@servlet_api_3_1//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'servlet-api-3_1-without-neverlink',
exports = ['@servlet_api_3_1//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'gwtjsonrpc',
exports = ['@gwtjsonrpc//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'gwtjsonrpc_src',
exports = ['@gwtjsonrpc_src//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'gson',
exports = ['@gson//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'gwtorm_client',
exports = ['@gwtorm_client//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'gwtorm_client_src',
exports = ['@gwtorm_client_src//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'protobuf',
exports = ['@protobuf//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-protobuf'],
)
java_library(
@@ -58,6 +76,7 @@
name = 'guava',
exports = ['@guava//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -69,30 +88,35 @@
'//lib/commons:oro',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'jsch',
exports = ['@jsch//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-jsch'],
)
java_library(
name = 'juniversalchardet',
exports = ['@juniversalchardet//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-MPL1.1'],
)
java_library(
name = 'args4j',
exports = ['@args4j//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-args4j'],
)
java_library(
name = 'automaton',
exports = ['@automaton//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-automaton'],
)
java_library(
@@ -100,6 +124,7 @@
exports = ['@pegdown//jar'],
runtime_deps = [':grappa'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -113,24 +138,28 @@
'//lib/ow2:ow2-asm-util',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'jitescript',
exports = ['@jitescript//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'tukaani-xz',
exports = ['@tukaani_xz//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-xz'],
)
java_library(
name = 'mime-util',
exports = ['@mime_util//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -138,23 +167,28 @@
exports = ['@guava_retrying//jar'],
runtime_deps = [':jsr305'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'jsr305',
exports = ['@jsr305//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'blame-cache',
exports = ['@blame_cache//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
+
java_library(
name = 'h2',
exports = ['@h2//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-h2'],
)
@@ -163,6 +197,7 @@
exports = ['@jimfs//jar'],
runtime_deps = [':guava'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -173,12 +208,14 @@
],
runtime_deps = [':hamcrest-core'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'hamcrest-core',
exports = ['@hamcrest_core//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -189,16 +226,63 @@
':junit',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'javassist',
exports = ['@javassist//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'derby',
exports = ['@derby//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+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'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'icu4j',
+ exports = [ '@icu4j//jar' ],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-icu4j'],
+)
+
+java_library(
+ name = 'postgresql',
+ exports = ['@postgresql//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-postgresql'],
+)
+
+java_library(
+ name = 'commons-io',
+ exports = ['@commons_io//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
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/antlr/BUILD b/lib/antlr/BUILD
index ede7665..92f3d0f 100644
--- a/lib/antlr/BUILD
+++ b/lib/antlr/BUILD
@@ -2,6 +2,7 @@
[java_library(
name = n,
exports = ['@%s//jar' % n],
+ data = ['//lib:LICENSE-antlr'],
) for n in [
'antlr27',
'stringtemplate',
@@ -11,6 +12,7 @@
name = 'java_runtime',
exports = ['@java_runtime//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-antlr'],
)
java_binary(
@@ -28,4 +30,5 @@
':java_runtime',
':stringtemplate',
],
+ data = ['//lib:LICENSE-antlr'],
)
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 733c670..5b4cd6b 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -53,8 +53,8 @@
maven_jar(
name = 'jruby',
- id = 'org.jruby:jruby-complete:1.7.25',
- sha1 = '8eb234259ec88edc05eedab05655f458a84bfcab',
+ id = 'org.jruby:jruby-complete:9.1.5.0',
+ sha1 = '00d0003e99da3c4d830b12c099691ce910c84e39',
license = 'DO_NOT_DISTRIBUTE',
visibility = [],
attach_source = False,
diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD
new file mode 100644
index 0000000..d1b98f8
--- /dev/null
+++ b/lib/asciidoctor/BUILD
@@ -0,0 +1,54 @@
+java_binary(
+ name = "asciidoc",
+ main_class = "AsciiDoctor",
+ runtime_deps = [":asciidoc_lib"],
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "asciidoc_lib",
+ srcs = ["java/AsciiDoctor.java"],
+ deps = [
+ ":asciidoctor",
+ "//lib:args4j",
+ "//lib:guava",
+ "//lib/log:api",
+ "//lib/log:nop",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+java_binary(
+ name = "doc_indexer",
+ main_class = "DocIndexer",
+ runtime_deps = [":doc_indexer_lib"],
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "doc_indexer_lib",
+ srcs = ["java/DocIndexer.java"],
+ deps = [
+ ":asciidoc_lib",
+ "//gerrit-server:constants",
+ "//lib:args4j",
+ "//lib:guava",
+ "//lib/lucene:lucene-analyzers-common",
+ "//lib/lucene:lucene-core-and-backward-codecs",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+java_library(
+ name = "asciidoctor",
+ exports = ["@asciidoctor//jar"],
+ runtime_deps = [":jruby"],
+ visibility = ["//visibility:public"],
+ data = ["//lib:LICENSE-asciidoctor"],
+)
+
+java_library(
+ name = "jruby",
+ exports = ["@jruby//jar"],
+ data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+)
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index 8e18feb1..c66c4aa 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -24,12 +24,14 @@
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
-
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -41,6 +43,7 @@
private static final String DOCTYPE = "article";
private static final String ERUBY = "erb";
+ private static final String REVNUMBER_NAME = "revnumber";
@Option(name = "-b", usage = "set output format backend")
private String backend = "html5";
@@ -60,13 +63,26 @@
@Option(name = "--tmp", usage = "temporary output path")
private File tmpdir;
+ @Option(name = "--mktmp", usage = "create a temporary output path")
+ private boolean mktmp;
+
@Option(name = "-a", usage =
"a list of attributes, in the form key or key=value pair")
private List<String> attributes = new ArrayList<>();
+ @Option(name = "--bazel", usage =
+ "bazel mode: generate multiple output files instead of a single zip file")
+ private boolean bazel;
+
+ @Option(name = "--revnumber-file", usage =
+ "the file contains revnumber string")
+ private File revnumberFile;
+
@Argument(usage = "input files")
private List<String> inputFiles = new ArrayList<>();
+ private String revnumber;
+
public static String mapInFileToOutFile(
String inFile, String inExt, String outExt) {
String basename = new File(inFile).getName();
@@ -82,19 +98,26 @@
return basename + outExt;
}
- private Options createOptions(File outputFile) {
+ private Options createOptions(File base, File outputFile) {
OptionsBuilder optionsBuilder = OptionsBuilder.options();
- optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
- .safe(SafeMode.UNSAFE).baseDir(basedir);
- // XXX(fishywang): ideally we should just output to a string and add the
- // content into zip. But asciidoctor will actually ignore all attributes if
- // not output to a file. So we *have* to output to a file then read the
- // content of the file into zip.
- optionsBuilder.toFile(outputFile);
+ optionsBuilder
+ .backend(backend)
+ .docType(DOCTYPE)
+ .eruby(ERUBY)
+ .safe(SafeMode.UNSAFE)
+ .baseDir(base)
+ // XXX(fishywang): ideally we should just output to a string and add the
+ // content into zip. But asciidoctor will actually ignore all attributes
+ // if not output to a file. So we *have* to output to a file then read
+ // the content of the file into zip.
+ .toFile(outputFile);
AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
attributesBuilder.attributes(getAttributes());
+ if (revnumber != null) {
+ attributesBuilder.attribute(REVNUMBER_NAME, revnumber);
+ }
optionsBuilder.attributes(attributesBuilder.get());
return optionsBuilder.get();
@@ -133,31 +156,52 @@
return;
}
- try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
- for (String inputFile : inputFiles) {
- if (!inputFile.endsWith(inExt)) {
- // We have to use UNSAFE mode in order to make embedding work. But in
- // UNSAFE mode we'll also need css file in the same directory, so we
- // have to add css files into the SRCS.
- continue;
- }
-
- String outName = mapInFileToOutFile(inputFile, inExt, outExt);
- File out = new File(tmpdir, outName);
- out.getParentFile().mkdirs();
- Options options = createOptions(out);
- renderInput(options, new File(inputFile));
- zipFile(out, outName, zip);
+ if (revnumberFile != null) {
+ try (BufferedReader reader =
+ new BufferedReader(new FileReader(revnumberFile))) {
+ revnumber = reader.readLine();
}
+ }
- File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- return name.endsWith(".css");
+ if (mktmp) {
+ tmpdir = Files.createTempDirectory("asciidoctor-").toFile();
+ }
+
+ if (bazel) {
+ renderFiles(inputFiles, null);
+ } else {
+ try (ZipOutputStream zip =
+ new ZipOutputStream(new FileOutputStream(zipFile))) {
+ renderFiles(inputFiles, zip);
+
+ File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".css");
+ }
+ });
+ for (File css : cssFiles) {
+ zipFile(css, css.getName(), zip);
}
- });
- for (File css : cssFiles) {
- zipFile(css, css.getName(), zip);
+ }
+ }
+ }
+
+ private void renderFiles(List<String> inputFiles, ZipOutputStream zip)
+ throws IOException {
+ Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+ for (String inputFile : inputFiles) {
+ String outName = mapInFileToOutFile(inputFile, inExt, outExt);
+ File out = bazel ? new File(outName) : new File(tmpdir, outName);
+ if (!bazel) {
+ out.getParentFile().mkdirs();
+ }
+ File input = new File(inputFile);
+ Options options =
+ createOptions(basedir != null ? basedir : input.getParentFile(), out);
+ asciidoctor.renderFile(input, options);
+ if (zip != null) {
+ zipFile(out, outName, zip);
}
}
}
@@ -171,11 +215,6 @@
zip.closeEntry();
}
- private void renderInput(Options options, File inputFile) {
- Asciidoctor asciidoctor = JRubyAsciidoctor.create();
- asciidoctor.renderFile(inputFile, options);
- }
-
public static void main(String[] args) {
try {
new AsciiDoctor().invoke(args);
diff --git a/lib/auto/BUILD b/lib/auto/BUILD
index e07c36d..c50c105 100644
--- a/lib/auto/BUILD
+++ b/lib/auto/BUILD
@@ -18,4 +18,5 @@
],
exports = ['@auto_value//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/bouncycastle/BUILD b/lib/bouncycastle/BUILD
index 49c54ba..333c355 100644
--- a/lib/bouncycastle/BUILD
+++ b/lib/bouncycastle/BUILD
@@ -3,12 +3,14 @@
neverlink = 1,
exports = ['@bcprov//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'bcprov-without-neverlink',
exports = ['@bcprov//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -16,12 +18,14 @@
neverlink = 1,
exports = ['@bcpg//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'bcpg-without-neverlink',
exports = ['@bcpg//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -29,10 +33,12 @@
neverlink = 1,
exports = ['@bcpkix//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'bcpkix-without-neverlink',
exports = ['@bcpkix//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index a0e0e9a..be50417 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.19.0'
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 = '263bf4acb7c4429be3fe46908af240f9f629d51c',
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 = 'e9ab382c6be240d55f112051bba3f6c637b798ce',
attach_source = False,
license = 'codemirror-original',
visibility = [],
diff --git a/lib/codemirror/BUILD b/lib/codemirror/BUILD
new file mode 100644
index 0000000..0a62d41
--- /dev/null
+++ b/lib/codemirror/BUILD
@@ -0,0 +1,2 @@
+load('//lib/codemirror:cm.bzl', 'pkg_cm')
+pkg_cm()
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl
new file mode 100644
index 0000000..7a0e899
--- /dev/null
+++ b/lib/codemirror/cm.bzl
@@ -0,0 +1,358 @@
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+
+CM_CSS = [
+ 'lib/codemirror.css',
+ 'addon/dialog/dialog.css',
+ 'addon/merge/merge.css',
+ 'addon/scroll/simplescrollbars.css',
+ 'addon/search/matchesonscrollbar.css',
+ 'addon/lint/lint.css',
+]
+
+CM_JS = [
+ 'lib/codemirror.js',
+ 'mode/meta.js',
+ 'keymap/emacs.js',
+ 'keymap/sublime.js',
+ 'keymap/vim.js',
+]
+
+CM_ADDONS = [
+ 'dialog/dialog.js',
+ 'edit/closebrackets.js',
+ 'edit/matchbrackets.js',
+ 'edit/trailingspace.js',
+ 'scroll/annotatescrollbar.js',
+ 'scroll/simplescrollbars.js',
+ 'search/jump-to-line.js',
+ 'search/matchesonscrollbar.js',
+ 'search/searchcursor.js',
+ 'search/search.js',
+ 'selection/mark-selection.js',
+ 'mode/multiplex.js',
+ 'mode/overlay.js',
+ 'mode/simple.js',
+ 'lint/lint.js',
+]
+
+# Available themes must be enumerated here,
+# in gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.java,
+# in gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
+CM_THEMES = [
+ '3024-day',
+ '3024-night',
+ 'abcdef',
+ 'ambiance',
+ 'base16-dark',
+ 'base16-light',
+ 'bespin',
+ 'blackboard',
+ 'cobalt',
+ 'colorforth',
+ 'dracula',
+ 'eclipse',
+ 'elegant',
+ 'erlang-dark',
+ 'hopscotch',
+ 'icecoder',
+ 'isotope',
+ 'lesser-dark',
+ 'liquibyte',
+ 'material',
+ 'mbo',
+ 'mdn-like',
+ 'midnight',
+ 'monokai',
+ 'neat',
+ 'neo',
+ 'night',
+ 'paraiso-dark',
+ 'paraiso-light',
+ 'pastel-on-dark',
+ 'railscasts',
+ 'rubyblue',
+ 'seti',
+ 'solarized',
+ 'the-matrix',
+ 'tomorrow-night-bright',
+ 'tomorrow-night-eighties',
+ 'ttcn',
+ 'twilight',
+ 'vibrant-ink',
+ 'xq-dark',
+ 'xq-light',
+ 'yeti',
+ 'zenburn',
+]
+
+# Available modes must be enumerated here,
+# in gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java,
+# gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java,
+# and in CodeMirror's own mode/meta.js script.
+CM_MODES = [
+ 'apl',
+ 'asciiarmor',
+ 'asn.1',
+ 'asterisk',
+ 'brainfuck',
+ 'clike',
+ 'clojure',
+ 'cmake',
+ 'cobol',
+ 'coffeescript',
+ 'commonlisp',
+ 'crystal',
+ 'css',
+ 'cypher',
+ 'd',
+ 'dart',
+ 'diff',
+ 'django',
+ 'dockerfile',
+ 'dtd',
+ 'dylan',
+ 'ebnf',
+ 'ecl',
+ 'eiffel',
+ 'elm',
+ 'erlang',
+ 'factor',
+ 'fcl',
+ 'forth',
+ 'fortran',
+ 'gas',
+ 'gfm',
+ 'gherkin',
+ 'go',
+ 'groovy',
+ 'haml',
+ 'handlebars',
+ 'haskell-literate',
+ 'haskell',
+ 'haxe',
+ 'htmlembedded',
+ 'htmlmixed',
+ 'http',
+ 'idl',
+ 'javascript',
+ 'jinja2',
+ 'jsx',
+ 'julia',
+ 'livescript',
+ 'lua',
+ 'markdown',
+ 'mathematica',
+ 'mbox',
+ 'mirc',
+ 'mllike',
+ 'modelica',
+ 'mscgen',
+ 'mumps',
+ 'nginx',
+ 'nsis',
+ 'ntriples',
+ 'octave',
+ 'oz',
+ 'pascal',
+ 'pegjs',
+ 'perl',
+ 'php',
+ 'pig',
+ 'powershell',
+ 'properties',
+ 'protobuf',
+ 'pug',
+ 'puppet',
+ 'python',
+ 'q',
+ 'r',
+ 'rpm',
+ 'rst',
+ 'ruby',
+ 'rust',
+ 'sas',
+ 'sass',
+ 'scheme',
+ 'shell',
+ 'sieve',
+ 'slim',
+ 'smalltalk',
+ 'smarty',
+ 'solr',
+ 'soy',
+ 'sparql',
+ 'spreadsheet',
+ 'sql',
+ 'stex',
+ 'stylus',
+ 'swift',
+ 'tcl',
+ 'textile',
+ 'tiddlywiki',
+ 'tiki',
+ 'toml',
+ 'tornado',
+ 'troff',
+ 'ttcn-cfg',
+ 'ttcn',
+ 'turtle',
+ 'twig',
+ 'vb',
+ 'vbscript',
+ 'velocity',
+ 'verilog',
+ 'vhdl',
+ 'vue',
+ 'webidl',
+ 'xml',
+ 'xquery',
+ 'yacas',
+ 'yaml-frontmatter',
+ 'yaml',
+ 'z80',
+]
+
+VERSION = '5.19.0'
+TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
+TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
+LICENSE = '//lib:LICENSE-codemirror-original'
+LICENSE_MINIFIED = '//lib:LICENSE-codemirror-minified'
+
+DIFF_MATCH_PATCH_VERSION = '20121119-1'
+DIFF_MATCH_PATCH_TOP = ('META-INF/resources/webjars/google-diff-match-patch/%s'
+ % DIFF_MATCH_PATCH_VERSION)
+
+def pkg_cm():
+ for archive, suffix, top, license in [
+ ('@codemirror_original//jar', '', TOP, LICENSE),
+ ('@codemirror_minified//jar', '_r', TOP_MINIFIED, LICENSE_MINIFIED)
+ ]:
+ # Main JavaScript and addons
+ genrule2(
+ name = 'cm' + suffix,
+ cmd = ' && '.join([
+ "echo '/** @license' >$@",
+ 'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
+ "echo '*/' >>$@",
+ ] +
+ ['unzip -p $(location %s) %s/%s >>$@' % (archive, top, n) for n in CM_JS] +
+ ['unzip -p $(location %s) %s/addon/%s >>$@' % (archive, top, n)
+ for n in CM_ADDONS]
+ ),
+ tools = [
+ '@codemirror_original//jar',
+ '@codemirror_minified//jar',
+ ],
+ out = 'cm%s.js' % suffix,
+ )
+
+ # Main CSS
+ genrule2(
+ name = 'css' + suffix,
+ cmd = ' && '.join([
+ "echo '/** @license' >$@",
+ 'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
+ "echo '*/' >>$@",
+ ] +
+ ['unzip -p $(location %s) %s/%s >>$@' % (archive, top, n)
+ for n in CM_CSS]
+ ),
+ tools = [
+ '@codemirror_original//jar',
+ '@codemirror_minified//jar',
+ ],
+ out = 'cm%s.css' % suffix,
+ )
+
+ # Modes
+ for n in CM_MODES:
+ genrule2(
+ name = 'mode_%s%s' % (n, suffix),
+ cmd = ' && '.join([
+ "echo '/** @license' >$@",
+ 'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
+ "echo '*/' >>$@",
+ 'unzip -p $(location %s) %s/mode/%s/%s.js >>$@' % (archive, top, n, n),
+ ]
+ ),
+ tools = [
+ '@codemirror_original//jar',
+ '@codemirror_minified//jar',
+ ],
+ out = 'mode_%s%s.js' % (n, suffix),
+ )
+
+ # Themes
+ for n in CM_THEMES:
+ genrule2(
+ name = 'theme_%s%s' % (n, suffix),
+ cmd = ' && '.join([
+ "echo '/** @license' >$@",
+ 'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
+ "echo '*/' >>$@",
+ 'unzip -p $(location %s) %s/theme/%s.css >>$@' % (archive, top, n)
+ ]
+ ),
+ tools = [
+ '@codemirror_original//jar',
+ '@codemirror_minified//jar',
+ ],
+ out = 'theme_%s%s.css' % (n, suffix),
+ )
+
+ # Merge Addon bundled with diff-match-patch
+ genrule2(
+ name = 'addon_merge%s' % suffix,
+ cmd = ' && '.join([
+ "echo '/** @license' >$@",
+ 'unzip -p $(location %s) %s/LICENSE >>$@' % (archive, top),
+ "echo '*/\n' >>$@",
+ "echo '// The google-diff-match-patch library is from https://google-diff-match-patch.googlecode.com/svn-history/r106/trunk/javascript/diff_match_patch.js\n' >> $@",
+ "echo '/** @license' >>$@",
+ "echo 'LICENSE-Apache2.0' >>$@",
+ "echo '*/' >>$@",
+ 'unzip -p $(location @diff_match_patch//jar) %s/diff_match_patch.js >>$@' % DIFF_MATCH_PATCH_TOP,
+ "echo ';' >> $@",
+ 'unzip -p $(location %s) %s/addon/merge/merge.js >>$@' % (archive, top)
+ ]
+ ),
+ tools = [
+ '@diff_match_patch//jar',
+ '@codemirror_original//jar',
+ '@codemirror_minified//jar',
+ ],
+ out = 'addon_merge%s.js' % suffix,
+ )
+
+ # Jar packaging
+ genrule2(
+ name = 'jar' + suffix,
+ cmd = ' && '.join([
+ 'cd $$TMP',
+ 'mkdir -p net/codemirror/{addon,lib,mode,theme}',
+ 'cp $$ROOT/$(location :css%s) net/codemirror/lib/cm.css' % suffix,
+ 'cp $$ROOT/$(location :cm%s) net/codemirror/lib/cm.js' % suffix]
+ + ['cp $$ROOT/$(location :mode_%s%s) net/codemirror/mode/%s.js' % (n, suffix, n)
+ for n in CM_MODES]
+ + ['cp $$ROOT/$(location :theme_%s%s) net/codemirror/theme/%s.css' % (n, suffix, n)
+ for n in CM_THEMES]
+ + ['cp $$ROOT/$(location :addon_merge%s) net/codemirror/addon/merge_bundled.js' % suffix]
+ + ['zip -qr $$ROOT/$@ net/codemirror/{addon,lib,mode,theme}']),
+ tools = [
+ ':addon_merge%s' % suffix,
+ ':cm%s' % suffix,
+ ':css%s' % suffix,
+ ] + [
+ ':mode_%s%s' % (n, suffix) for n in CM_MODES
+ ] + [
+ ':theme_%s%s' % (n, suffix) for n in CM_THEMES
+ ],
+ out = 'codemirror%s.jar' % suffix,
+ )
+
+ native.java_import(
+ name = 'codemirror' + suffix,
+ jars = [':jar%s' % suffix],
+ visibility = ['//visibility:public'],
+ data = [license],
+ )
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/commons/BUCK b/lib/commons/BUCK
index 7c27477..55c07a6 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -47,6 +47,13 @@
)
maven_jar(
+ name = 'lang3',
+ id = 'org.apache.commons:commons-lang3:3.3.2',
+ sha1 = '90a3822c38ec8c996e84c16a3477ef632cbc87a3',
+ license = 'Apache2.0',
+)
+
+maven_jar(
name = 'net',
id = 'commons-net:commons-net:3.5',
sha1 = '342fc284019f590e1308056990fdb24a08f06318',
diff --git a/lib/commons/BUILD b/lib/commons/BUILD
index 8c42e53f..d4d6145 100644
--- a/lib/commons/BUILD
+++ b/lib/commons/BUILD
@@ -1,31 +1,44 @@
+package(default_visibility = ['//visibility:public'])
+
java_library(
name = 'codec',
exports = ['@commons_codec//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'collections',
exports = ['@commons_collections//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'compress',
exports = ['@commons_compress//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'lang',
exports = ['@commons_lang//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lang3',
+ exports = [ '@commons_lang3//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'net',
exports = ['@commons_net//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -33,22 +46,26 @@
exports = ['@commons_dbcp//jar'],
runtime_deps = [':pool'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'pool',
exports = ['@commons_pool//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'oro',
exports = ['@commons_oro//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache1.1'],
)
java_library(
name = 'validator',
exports = ['@commons_validator//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/dropwizard/BUILD b/lib/dropwizard/BUILD
index 9d4a8d3..b456d5e 100644
--- a/lib/dropwizard/BUILD
+++ b/lib/dropwizard/BUILD
@@ -2,4 +2,5 @@
name = 'dropwizard-core',
exports = ['@dropwizard_core//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/easymock/BUCK b/lib/easymock/BUCK
index 93640a0..cabd62e 100644
--- a/lib/easymock/BUCK
+++ b/lib/easymock/BUCK
@@ -2,28 +2,28 @@
maven_jar(
name = 'easymock',
- id = 'org.easymock:easymock:3.4', # When bumping the version
+ id = 'org.easymock:easymock:3.1', # When bumping the version
# number, make sure to also move powermock to a compatible version
- sha1 = '9fdeea183a399f25c2469497612cad131e920fa3',
+ sha1 = '3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e',
license = 'DO_NOT_DISTRIBUTE',
deps = [
- ':cglib-2_2',
+ ':cglib-3_2',
':objenesis',
],
)
maven_jar(
- name = 'cglib-2_2',
- id = 'cglib:cglib-nodep:2.2.2',
- sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+ name = 'cglib-3_2',
+ id = 'cglib:cglib-nodep:3.2.0',
+ sha1 = 'cf1ca207c15b04ace918270b6cb3f5601160cdfd',
license = 'DO_NOT_DISTRIBUTE',
attach_source = False,
)
maven_jar(
name = 'objenesis',
- id = 'org.objenesis:objenesis:2.2',
- sha1 = '3fb533efdaa50a768c394aa4624144cf8df17845',
+ id = 'org.objenesis:objenesis:1.3',
+ sha1 = 'dc13ae4faca6df981fc7aeb5a522d9db446d5d50',
license = 'DO_NOT_DISTRIBUTE',
visibility = ['//lib/powermock:powermock-reflect'],
attach_source = False,
diff --git a/lib/easymock/BUILD b/lib/easymock/BUILD
index df77128..fce3ff7 100644
--- a/lib/easymock/BUILD
+++ b/lib/easymock/BUILD
@@ -2,21 +2,24 @@
name = 'easymock',
exports = ['@easymock//jar'],
runtime_deps = [
- ':cglib-2_2',
+ ':cglib-3_2',
':objenesis',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
- name = 'cglib-2_2',
- exports = ['@cglib_2_2//jar'],
+ name = 'cglib-3_2',
+ exports = ['@cglib_3_2//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
name = 'objenesis',
exports = ['@objenesis//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
diff --git a/lib/elasticsearch/BUCK b/lib/elasticsearch/BUCK
new file mode 100644
index 0000000..373f4d2
--- /dev/null
+++ b/lib/elasticsearch/BUCK
@@ -0,0 +1,104 @@
+include_defs('//lib/maven.defs')
+
+# Java client library for Elasticsearch.
+maven_jar(
+ name = 'elasticsearch',
+ id = 'org.elasticsearch:elasticsearch:2.4.0',
+ sha1 = 'aeb9704a76fa8654c348f38fcbb993a952a7ab07',
+ attach_source = True,
+ repository = MAVEN_CENTRAL,
+ license = 'Apache2.0',
+ deps = [
+ ':jna',
+ ':hppc',
+ ':jsr166e',
+ ':netty',
+ ':t-digest',
+ ':compress-lzf',
+ '//lib/joda:joda-time',
+ '//lib/lucene:lucene-codecs',
+ '//lib/lucene:lucene-highlighter',
+ '//lib/lucene:lucene-join',
+ '//lib/lucene:lucene-memory',
+ '//lib/lucene:lucene-sandbox',
+ '//lib/lucene:lucene-suggest',
+ '//lib/lucene:lucene-queries',
+ '//lib/lucene:lucene-spatial',
+ '//lib/jackson:jackson-core',
+ '//lib/jackson:jackson-dataformat-cbor',
+ '//lib/jackson:jackson-dataformat-smile',
+ ]
+)
+
+# Java REST client for Elasticsearch.
+VERSION = '2.0.3'
+
+maven_jar(
+ name = 'jest-common',
+ id = 'io.searchbox:jest-common:' + VERSION,
+ sha1 = 'f304c66894aaf2f6c17a886bc826f09c7a161cf9',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'jest',
+ id = 'io.searchbox:jest:' + VERSION,
+ sha1 = 'b8f9ed1423489b361804e47f640515ea9f1fa08d',
+ license = 'Apache2.0',
+ deps = [
+ ':elasticsearch',
+ ':jest-common',
+ '//lib/commons:lang3',
+ '//lib/httpcomponents:httpasyncclient',
+ '//lib/httpcomponents:httpclient',
+ '//lib/httpcomponents:httpcore-nio',
+ '//lib/httpcomponents:httpcore-niossl',
+ ],
+)
+
+maven_jar(
+ name = 'compress-lzf',
+ id = 'com.ning:compress-lzf:1.0.2',
+ sha1 = '62896e6fca184c79cc01a14d143f3ae2b4f4b4ae',
+ license = 'Apache2.0',
+ visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+ name = 'hppc',
+ id = 'com.carrotsearch:hppc:0.7.1',
+ sha1 = '8b5057f74ea378c0150a1860874a3ebdcb713767',
+ license = 'Apache2.0',
+ visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+ name = 'jsr166e',
+ id = 'com.twitter:jsr166e:1.1.0',
+ sha1 = '233098147123ee5ddcd39ffc57ff648be4b7e5b2',
+ license = 'Apache2.0',
+ visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+ name = 'netty',
+ id = 'io.netty:netty:3.10.0.Final',
+ sha1 = 'ad61cd1bba067e6634ddd3e160edf0727391ac30',
+ license = 'Apache2.0',
+ visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+ name = 't-digest',
+ id = 'com.tdunning:t-digest:3.0',
+ sha1 = '84ccf145ac2215e6bfa63baa3101c0af41017cfc',
+ license = 'Apache2.0',
+ visibility = ['//lib/elasticsearch:elasticsearch'],
+)
+
+maven_jar(
+ name = 'jna',
+ id = 'net.java.dev.jna:jna:4.1.0',
+ sha1 = '1c12d070e602efd8021891cdd7fd18bc129372d4',
+ license = 'Apache2.0',
+)
diff --git a/lib/elasticsearch/BUILD b/lib/elasticsearch/BUILD
new file mode 100644
index 0000000..6c3d423
--- /dev/null
+++ b/lib/elasticsearch/BUILD
@@ -0,0 +1,92 @@
+package(default_visibility=['//visibility:public'])
+
+java_library(
+ name = 'elasticsearch',
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ exports = [ '@elasticsearch//jar' ],
+ runtime_deps = [
+ ':jna',
+ ':hppc',
+ ':jsr166e',
+ ':netty',
+ ':t-digest',
+ ':compress-lzf',
+ '//lib/joda:joda-time',
+ '//lib/lucene:lucene-codecs',
+ '//lib/lucene:lucene-highlighter',
+ '//lib/lucene:lucene-join',
+ '//lib/lucene:lucene-memory',
+ '//lib/lucene:lucene-sandbox',
+ '//lib/lucene:lucene-suggest',
+ '//lib/lucene:lucene-queries',
+ '//lib/lucene:lucene-spatial',
+ '//lib/jackson:jackson-core',
+ '//lib/jackson:jackson-dataformat-cbor',
+ '//lib/jackson:jackson-dataformat-smile',
+ ]
+)
+
+# Java REST client for Elasticsearch.
+VERSION = '0.1.7'
+
+java_library(
+ name = 'jest-common',
+ exports = [ '@jest_common//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+)
+
+java_library(
+ name = 'jest',
+ exports = [ '@jest//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ runtime_deps = [
+ ':elasticsearch',
+ ':jest-common',
+ '//lib/commons:lang3',
+ '//lib/httpcomponents:httpasyncclient',
+ '//lib/httpcomponents:httpclient',
+ '//lib/httpcomponents:httpcore-nio',
+ '//lib/httpcomponents:httpcore-niossl',
+ ],
+)
+
+java_library(
+ name = 'compress-lzf',
+ exports = [ '@compress_lzf//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ visibility = ['//lib/elasticsearch:__pkg__'],
+)
+
+java_library(
+ name = 'hppc',
+ exports = [ '@hppc//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ visibility = ['//lib/elasticsearch:__pkg__'],
+)
+
+java_library(
+ name = 'jsr166e',
+ exports = [ '@jsr166e//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ visibility = ['//lib/elasticsearch:__pkg__'],
+)
+
+java_library(
+ name = 'netty',
+ exports = [ '@netty//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ visibility = ['//lib/elasticsearch:__pkg__'],
+)
+
+java_library(
+ name = 't-digest',
+ exports = [ '@t_digest//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+ visibility = ['//lib/elasticsearch:__pkg__'],
+)
+
+java_library(
+ name = 'jna',
+ exports = [ '@jna//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+)
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD
new file mode 100644
index 0000000..509ce39
--- /dev/null
+++ b/lib/fonts/BUILD
@@ -0,0 +1,34 @@
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+
+# Source Code Pro. Version 2.010 Roman / 1.030 Italics
+# https://github.com/adobe-fonts/source-code-pro/releases/tag/2.010R-ro%2F1.030R-it
+genrule2(
+ name = 'sourcecodepro',
+ cmd = 'zip -rq $@ $(SRCS)',
+ srcs = [
+ 'SourceCodePro-Regular.woff',
+ 'SourceCodePro-Regular.woff2'
+ ],
+ out = 'sourcecodepro.zip',
+# TODO(hanwen): fix this
+# license = 'OFL1.1',
+ visibility = ['//visibility:public'],
+)
+
+# Open Sans at Revision 53a5266 and converted using a Google woff file
+# converter (same one that Google Fonts uses).
+# https://github.com/google/fonts/tree/master/apache/opensans
+genrule2(
+ name = 'opensans',
+ cmd = 'zip -rq $@ $(SRCS)',
+ srcs = [
+ 'OpenSans-Bold.woff',
+ 'OpenSans-Bold.woff2',
+ 'OpenSans-Regular.woff',
+ 'OpenSans-Regular.woff2'
+ ],
+ out = 'opensans.zip',
+# TODO(hanwen): license.
+# license = 'Apache2.0',
+ visibility = ['//visibility:public'],
+)
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..43018e0 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -3,8 +3,10 @@
exports = [
':guice_library',
':javax-inject',
+ ':multibindings',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -12,6 +14,7 @@
exports = ['@guice_library//jar'],
runtime_deps = ['aopalliance'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -19,6 +22,7 @@
exports = ['@guice_assistedinject//jar'],
runtime_deps = [':guice'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -26,14 +30,25 @@
exports = ['@guice_servlet//jar'],
runtime_deps = [':guice'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'aopalliance',
exports = ['@aopalliance//jar'],
+ data = ['//lib:LICENSE-PublicDomain'],
)
java_library(
name = 'javax-inject',
exports = ['@javax_inject//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'multibindings',
+ exports = [ '@multibindings//jar' ],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
index 44a9341..41000d3 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -1,11 +1,11 @@
include_defs('//lib/maven.defs')
-VERSION = '2.7.0'
+VERSION = '2.8.0-rc2'
maven_jar(
name = 'user',
id = 'com.google.gwt:gwt-user:' + VERSION,
- sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b',
+ sha1 = 'ad99b09a626c20cce2bdacf3726a51b2cd16b99e',
license = 'Apache2.0',
attach_source = False,
)
@@ -13,10 +13,9 @@
maven_jar(
name = 'dev',
id = 'com.google.gwt:gwt-dev:' + VERSION,
- sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982',
+ sha1 = 'd70a6feb4661c07488090cb81303415e9110b15a',
license = 'Apache2.0',
attach_source = False,
- exclude = ['org/eclipse/jetty/*'],
)
maven_jar(
@@ -28,3 +27,47 @@
visibility = ['PUBLIC'],
)
+maven_jar(
+ name = 'jsinterop-annotations',
+ id = 'com.google.jsinterop:jsinterop-annotations:1.0.0',
+ bin_sha1 = '23c3a3c060ffe4817e67673cc8294e154b0a4a95',
+ src_sha1 = '5d7c478efbfccc191430d7c118d7bd2635e43750',
+ license = 'Apache2.0',
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'ant',
+ id = 'ant:ant:1.6.5',
+ bin_sha1 = '7d18faf23df1a5c3a43613952e0e8a182664564b',
+ src_sha1 = '9e0a847494563f35f9b02846a1c1eb4aa2ee5a9a',
+ license = 'Apache2.0',
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'colt',
+ id = 'colt:colt:1.2.0',
+ attach_source = False,
+ bin_sha1 = '0abc984f3adc760684d49e0f11ddf167ba516d4f',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'tapestry',
+ id = 'tapestry:tapestry:4.0.2',
+ attach_source = False,
+ bin_sha1 = 'e855a807425d522e958cbce8697f21e9d679b1f7',
+ license = 'Apache2.0',
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'w3c-css-sac',
+ id = 'org.w3c.css:sac:1.3',
+ attach_source = False,
+ bin_sha1 = 'cdb2dcb4e22b83d6b32b93095f644c3462739e82',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['PUBLIC'],
+)
diff --git a/lib/gwt/BUILD b/lib/gwt/BUILD
index 2168bb4..46d8f6d 100644
--- a/lib/gwt/BUILD
+++ b/lib/gwt/BUILD
@@ -2,8 +2,29 @@
name = n,
exports = ['@%s//jar' % n.replace("-", "_")],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-Apache2.0'],
) for n in [
- 'javax-validation',
+ 'ant',
+ 'colt',
'dev',
+ 'javax-validation',
+ 'jsinterop-annotations',
+ 'tapestry',
'user',
+ 'w3c-css-sac',
]]
+
+java_library(
+ name = 'javax-validation_src',
+ exports = ['@javax_validation_src//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'jsinterop-annotations_src',
+ exports = ['@jsinterop_annotations_src//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
diff --git a/lib/highlightjs/BUILD b/lib/highlightjs/BUILD
new file mode 100644
index 0000000..5fb2a71
--- /dev/null
+++ b/lib/highlightjs/BUILD
@@ -0,0 +1,4 @@
+
+exports_files([
+ 'highlight.min.js',
+])
diff --git a/lib/httpcomponents/BUCK b/lib/httpcomponents/BUCK
index 03669f2..1e56f94 100644
--- a/lib/httpcomponents/BUCK
+++ b/lib/httpcomponents/BUCK
@@ -39,3 +39,25 @@
src_sha1 = '5394d3715181a87009032335a55b0a9789f6e26f',
license = 'Apache2.0',
)
+
+maven_jar(
+ name = 'httpasyncclient',
+ id = 'org.apache.httpcomponents:httpasyncclient:4.1.2',
+ sha1 = '95aa3e6fb520191a0970a73cf09f62948ee614be',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'httpcore-nio',
+ id = 'org.apache.httpcomponents:httpcore-nio:' + VERSION,
+ sha1 = 'a8c5e3c3bfea5ce23fb647c335897e415eb442e3',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'httpcore-niossl',
+ id = 'org.apache.httpcomponents:httpcore-niossl:4.0-alpha6',
+ sha1 = '9c662e7247ca8ceb1de5de629f685c9ef3e4ab58',
+ license = 'Apache2.0',
+ attach_source = False,
+)
diff --git a/lib/httpcomponents/BUILD b/lib/httpcomponents/BUILD
index 74ab00a..c11df29 100644
--- a/lib/httpcomponents/BUILD
+++ b/lib/httpcomponents/BUILD
@@ -1,8 +1,11 @@
+package(default_visibility = ['//visibility:public'])
+
java_library(
name = 'fluent-hc',
exports = ['@fluent_hc//jar'],
runtime_deps = [':httpclient'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -14,16 +17,37 @@
'//lib/log:jcl-over-slf4j',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'httpcore',
exports = ['@httpcore//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'httpmime',
exports = ['@httpmime//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'httpasyncclient',
+ exports = [ '@httpasyncclient//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'httpcore-nio',
+ exports = [ '@httpcore_nio//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'httpcore-niossl',
+ exports = ['@httpcore_niossl//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/jackson/BUCK b/lib/jackson/BUCK
new file mode 100644
index 0000000..46056b5
--- /dev/null
+++ b/lib/jackson/BUCK
@@ -0,0 +1,26 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '2.6.6'
+
+maven_jar(
+ name = 'jackson-core',
+ id = 'com.fasterxml.jackson.core:jackson-core:' + VERSION,
+ sha1 = '02eb801df67aacaf5b1deb4ac626e1964508e47b',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'jackson-dataformat-smile',
+ id = 'com.fasterxml.jackson.dataformat:jackson-dataformat-smile:' + VERSION,
+ sha1 = 'ccbfc948748ed2754a58c1af9e0a02b5cc1aed69',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'jackson-dataformat-cbor',
+ id = 'com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:' + VERSION,
+ sha1 = '34c7b7ff495fc6b049612bdc9db0900a68e112f8',
+ license = 'Apache2.0'
+)
+
+
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
new file mode 100644
index 0000000..87ea42e4
--- /dev/null
+++ b/lib/jackson/BUILD
@@ -0,0 +1,21 @@
+package(default_visibility = [ "//visibility:public"])
+
+VERSION = '2.6.6'
+
+java_library(
+ name = 'jackson-core',
+ exports = [ '@jackson_core//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+)
+
+java_library(
+ name = 'jackson-dataformat-smile',
+ exports = [ '@jackson_dataformat_smile//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+)
+
+java_library(
+ name = 'jackson-dataformat-cbor',
+ exports = [ '@jackson_dataformat_cbor//jar' ],
+ data = [ '//lib:LICENSE-Apache2.0' ],
+)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
index cc22b80..e24cfe5 100644
--- a/lib/jetty/BUCK
+++ b/lib/jetty/BUCK
@@ -1,12 +1,12 @@
include_defs('//lib/maven.defs')
-VERSION = '9.2.14.v20151106'
+VERSION = '9.3.11.v20160721'
EXCLUDE = ['about.html']
maven_jar(
name = 'servlet',
id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
- sha1 = '3a2cd4d8351a38c5d60e0eee010fee11d87483ef',
+ sha1 = 'd550147b85c73ea81084a4ac7915ba7f609021c5',
license = 'Apache2.0',
deps = [':security'],
exclude = EXCLUDE,
@@ -15,7 +15,7 @@
maven_jar(
name = 'security',
id = 'org.eclipse.jetty:jetty-security:' + VERSION,
- sha1 = '2d36974323fcb31e54745c1527b996990835db67',
+ sha1 = '1cbefc5d1196b9e1ca6f4cc36738998a6ebde8bf',
license = 'Apache2.0',
deps = [':server'],
exclude = EXCLUDE,
@@ -25,7 +25,7 @@
maven_jar(
name = 'servlets',
id = 'org.eclipse.jetty:jetty-servlets:' + VERSION,
- sha1 = 'a75c78a0ee544073457ca5ee9db20fdc6ed55225',
+ sha1 = 'a9f7a43977151a463aa21a9b0e882aa3d25452ef',
license = 'Apache2.0',
exclude = EXCLUDE,
visibility = [
@@ -37,7 +37,7 @@
maven_jar(
name = 'server',
id = 'org.eclipse.jetty:jetty-server:' + VERSION,
- sha1 = '70b22c1353e884accf6300093362b25993dac0f5',
+ sha1 = 'd932e0dc1e9bd4839ae446754615163d60271a66',
license = 'Apache2.0',
exported_deps = [
':continuation',
@@ -49,7 +49,7 @@
maven_jar(
name = 'jmx',
id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
- sha1 = '617edc5e966b4149737811ef8b289cd94b831bab',
+ sha1 = '21a658d2f5eb87c23eef4911966625ea95f66d32',
license = 'Apache2.0',
exported_deps = [
':continuation',
@@ -61,7 +61,7 @@
maven_jar(
name = 'continuation',
id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
- sha1 = '8909d62fd7e28351e2da30de6fb4105539b949c0',
+ sha1 = '92a91c0dcc5f5d779a1c9f94038332be3f46c9df',
license = 'Apache2.0',
exclude = EXCLUDE,
)
@@ -69,7 +69,7 @@
maven_jar(
name = 'http',
id = 'org.eclipse.jetty:jetty-http:' + VERSION,
- sha1 = '699ad1f2fa6fb0717e1b308a8c9e1b8c69d81ef6',
+ sha1 = 'dcfb95e5b886a981bb76467b911c5b706117f9cf',
license = 'Apache2.0',
exported_deps = [':io'],
exclude = EXCLUDE,
@@ -78,7 +78,7 @@
maven_jar(
name = 'io',
id = 'org.eclipse.jetty:jetty-io:' + VERSION,
- sha1 = 'dfa4137371a3f08769820138ca1a2184dacda267',
+ sha1 = 'db5f4f481159894a4b670072a34917b5414d0c98',
license = 'Apache2.0',
exported_deps = [':util'],
exclude = EXCLUDE,
@@ -88,7 +88,7 @@
maven_jar(
name = 'util',
id = 'org.eclipse.jetty:jetty-util:' + VERSION,
- sha1 = '0057e00b912ae0c35859ac81594a996007706a0b',
+ sha1 = '1812ffd5a04698051180d582c146ca807760c808',
license = 'Apache2.0',
exclude = EXCLUDE,
visibility = [],
diff --git a/lib/jetty/BUILD b/lib/jetty/BUILD
index da3af1c..d6e6355 100644
--- a/lib/jetty/BUILD
+++ b/lib/jetty/BUILD
@@ -3,6 +3,7 @@
exports = ['@jetty_servlet//jar'],
runtime_deps = [':security'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -10,12 +11,14 @@
exports = ['@jetty_security//jar'],
runtime_deps = [':server'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'servlets',
exports = ['@jetty_servlets//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -26,6 +29,7 @@
':http',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -36,12 +40,14 @@
':http',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'continuation',
exports = ['@jetty_continuation//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -51,6 +57,7 @@
':io',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -59,9 +66,11 @@
'@jetty_io//jar',
':util',
],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'util',
exports = ['@jetty_util//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUILD b/lib/jgit/org.eclipse.jgit.archive/BUILD
index 8fa94f2..7d6fe22 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUILD
+++ b/lib/jgit/org.eclipse.jgit.archive/BUILD
@@ -3,4 +3,5 @@
exports = ['@jgit_archive//jar'],
runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-jgit'],
)
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUILD b/lib/jgit/org.eclipse.jgit.http.server/BUILD
index 6a442cc..a453513 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUILD
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUILD
@@ -1,6 +1,15 @@
+load('//tools/bzl:unsign.bzl', 'unsign_jars')
+
java_library(
- name = 'jgit-servlet',
+ name = 'jgit-servlet-signed',
exports = ['@jgit_servlet//jar'],
runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-jgit'],
+)
+
+unsign_jars(
+ name = 'jgit-servlet',
+ deps = [':jgit-servlet-signed'],
+ visibility = ['//visibility:public'],
)
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUILD b/lib/jgit/org.eclipse.jgit.junit/BUILD
index d00b82c9..7f31261 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUILD
+++ b/lib/jgit/org.eclipse.jgit.junit/BUILD
@@ -1,6 +1,15 @@
+load('//tools/bzl:unsign.bzl', 'unsign_jars')
+
java_library(
- name = 'junit',
+ name = 'junit-signed',
exports = ['@jgit_junit//jar'],
runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
+)
+
+unsign_jars(
+ name = 'junit',
+ deps = [':junit-signed'],
+ visibility = ['//visibility:public'],
)
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD
index a1f9cad..bfebb7e 100644
--- a/lib/jgit/org.eclipse.jgit/BUILD
+++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -1,12 +1,22 @@
+load('//tools/bzl:unsign.bzl', 'unsign_jars')
+
java_library(
- name = 'jgit',
+ name = 'jgit-signed',
exports = ['@jgit//jar'],
runtime_deps = [':ewah'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-jgit'],
)
java_library(
name = 'ewah',
exports = ['@ewah//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+unsign_jars(
+ name = 'jgit',
+ deps = [':jgit-signed'],
+ visibility = ['//visibility:public'],
)
diff --git a/lib/joda/BUILD b/lib/joda/BUILD
index a673bf5..ef759d7 100644
--- a/lib/joda/BUILD
+++ b/lib/joda/BUILD
@@ -3,9 +3,11 @@
exports = ['@joda_time//jar'],
runtime_deps = ['joda-convert'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'joda-convert',
exports = ['@joda_convert//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/js.defs b/lib/js.defs
index c9a4256..f215de9 100644
--- a/lib/js.defs
+++ b/lib/js.defs
@@ -124,7 +124,8 @@
cmds = ['cd $TMP']
for d in deps:
cmds.append('unzip -qo $(location %s)' % d)
- cmds.append('zip -r $OUT bower_components')
+ cmds.append("find bower_components -exec touch -t 198001010000 '{}' ';'")
+ cmds.append('zip -r $OUT bower_components/*')
return ' && '.join(cmds)
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 1c46d35..bb31b94 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -328,10 +328,10 @@
bower_component(
name = 'polymer',
package = 'polymer/polymer',
- version = '1.4.0',
+ version = '1.7.0',
deps = [':webcomponentsjs'],
license = 'polymer',
- sha1 = 'b84725939ead7c7bdf9917b065f68ef8dc790d06',
+ sha1 = 'e70caa58fdee0ce51c805d548f544f74cc27d143',
)
bower_component(
diff --git a/lib/js/BUILD b/lib/js/BUILD
new file mode 100644
index 0000000..249f5d0
--- /dev/null
+++ b/lib/js/BUILD
@@ -0,0 +1,44 @@
+package(default_visibility = [ "//visibility:public" ])
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+load("//tools/bzl:js.bzl", "bower_component", "js_component")
+
+# For updating the bower versions, run
+#
+# python tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl
+#
+
+# For adding a new component as dependency to a bower_component_bundle
+#
+# 1) add a new bower_archive in WORKSPACE
+#
+# 2) add bower_component(name="my_new_dependency", seed=True) here
+#
+# 3) run bower2bazel (see above.)
+#
+# 4) remove bower_component(name="my_new_dependency", .. ) here
+#
+
+
+load("//lib/js:bower_components.bzl", "define_bower_components")
+define_bower_components()
+
+js_component(
+ name = 'highlightjs',
+ srcs = [ "//lib/highlightjs:highlight.min.js" ],
+ license = '//lib:LICENSE-highlightjs',
+)
+
+bower_component(
+ name = 'iron-test-helpers',
+ seed = True,
+)
+
+bower_component(
+ name = 'test-fixture',
+ seed = True,
+)
+
+bower_component(
+ name = 'web-component-tester',
+ seed = True,
+)
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
new file mode 100644
index 0000000..d90a7cc
--- /dev/null
+++ b/lib/js/bower_archives.bzl
@@ -0,0 +1,68 @@
+# DO NOT EDIT
+# generated with the following command:
+#
+# tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl
+#
+
+load("//tools/bzl:js.bzl", "bower_archive")
+def load_bower_archives():
+ bower_archive(
+ name = "iron-a11y-announcer",
+ package = "iron-a11y-announcer",
+ version = "1.0.5",
+ sha1 = "007902c041dd8863a1fe893f62450852f4d8c69b")
+ bower_archive(
+ name = "iron-a11y-keys-behavior",
+ package = "iron-a11y-keys-behavior",
+ version = "1.1.9",
+ sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465")
+ bower_archive(
+ name = "iron-behaviors",
+ package = "iron-behaviors",
+ version = "1.0.17",
+ sha1 = "47df7e1c2b97978dcafa13edb50fbdb702570acd")
+ bower_archive(
+ name = "iron-fit-behavior",
+ package = "iron-fit-behavior",
+ version = "1.2.5",
+ sha1 = "5938815cd227843fc77ebeac480b999600a76157")
+ bower_archive(
+ name = "iron-flex-layout",
+ package = "iron-flex-layout",
+ version = "1.3.1",
+ sha1 = "ba696394abff5e799fc06eb11bff4720129a1b52")
+ bower_archive(
+ name = "iron-form-element-behavior",
+ package = "iron-form-element-behavior",
+ version = "1.0.6",
+ sha1 = "8d9e6530edc1b99bec1a5c34853911fba3701220")
+ bower_archive(
+ name = "iron-meta",
+ package = "iron-meta",
+ version = "1.1.2",
+ sha1 = "dc22fe05e1cb5f94f30a7193d3433ca1808773b8")
+ bower_archive(
+ name = "iron-resizable-behavior",
+ package = "iron-resizable-behavior",
+ version = "1.0.5",
+ sha1 = "2ebe983377dceb3794dd335131050656e23e2beb")
+ bower_archive(
+ name = "iron-validatable-behavior",
+ package = "iron-validatable-behavior",
+ version = "1.1.1",
+ sha1 = "480423380be0536f948735d91bc472f6e7ced5b4")
+ bower_archive(
+ name = "neon-animation",
+ package = "neon-animation",
+ version = "1.2.4",
+ sha1 = "e8ccbb930c4b7ff470b1450baa901618888a7fd3")
+ bower_archive(
+ name = "web-animations-js",
+ package = "web-animations-js",
+ version = "2.2.2",
+ sha1 = "6276a9f227da7d4ccaf77c202b50e174dd11a2c2")
+ bower_archive(
+ name = "webcomponentsjs",
+ package = "webcomponentsjs",
+ version = "0.7.22",
+ sha1 = "8ba97a4a279ec6973a19b171c462a7b5cf454fb9")
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
new file mode 100644
index 0000000..480d6ce
--- /dev/null
+++ b/lib/js/bower_components.bzl
@@ -0,0 +1,162 @@
+# DO NOT EDIT
+# generated with the following command:
+#
+# tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl
+#
+
+load("//tools/bzl:js.bzl", "bower_component")
+def define_bower_components():
+ bower_component(
+ name = "es6-promise",
+ license = "//lib:LICENSE-polymer",
+ seed = True,
+ )
+ bower_component(
+ name = "fetch",
+ license = "//lib:LICENSE-fetch",
+ seed = True,
+ )
+ bower_component(
+ name = "iron-a11y-announcer",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-a11y-keys-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-autogrow-textarea",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-behaviors",
+ ":iron-flex-layout",
+ ":iron-form-element-behavior",
+ ":iron-validatable-behavior",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
+ name = "iron-behaviors",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-a11y-keys-behavior",
+ ":polymer",
+ ],
+ )
+ bower_component(
+ name = "iron-dropdown",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-a11y-keys-behavior",
+ ":iron-behaviors",
+ ":iron-overlay-behavior",
+ ":iron-resizable-behavior",
+ ":neon-animation",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
+ name = "iron-fit-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-flex-layout",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-form-element-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-input",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-a11y-announcer",
+ ":iron-validatable-behavior",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
+ name = "iron-meta",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-overlay-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-a11y-keys-behavior",
+ ":iron-fit-behavior",
+ ":iron-resizable-behavior",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
+ name = "iron-resizable-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ )
+ bower_component(
+ name = "iron-selector",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ seed = True,
+ )
+ bower_component(
+ name = "iron-validatable-behavior",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-meta",
+ ":polymer",
+ ],
+ )
+ bower_component(
+ name = "moment",
+ license = "//lib:LICENSE-moment",
+ seed = True,
+ )
+ bower_component(
+ name = "neon-animation",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-meta",
+ ":iron-resizable-behavior",
+ ":iron-selector",
+ ":polymer",
+ ":web-animations-js",
+ ],
+ )
+ bower_component(
+ name = "page",
+ license = "//lib:LICENSE-polymer",
+ seed = True,
+ )
+ bower_component(
+ name = "polymer",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":webcomponentsjs" ],
+ seed = True,
+ )
+ bower_component(
+ name = "promise-polyfill",
+ license = "//lib:LICENSE-polymer",
+ deps = [ ":polymer" ],
+ seed = True,
+ )
+ bower_component(
+ name = "web-animations-js",
+ license = "//lib:LICENSE-Apache2.0",
+ )
+ bower_component(
+ name = "webcomponentsjs",
+ license = "//lib:LICENSE-polymer",
+ )
diff --git a/lib/log/BUILD b/lib/log/BUILD
index ac92ab6..1e40372 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -2,6 +2,7 @@
name = 'api',
exports = ['@log_api//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-slf4j'],
)
java_library(
@@ -9,6 +10,7 @@
exports = ['@log_nop//jar'],
runtime_deps = [':api'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-slf4j'],
)
java_library(
@@ -16,18 +18,21 @@
exports = ['@impl_log4j//jar'],
runtime_deps = [':log4j'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-slf4j'],
)
java_library(
name = 'jcl-over-slf4j',
exports = ['@jcl_over_slf4j//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-slf4j'],
)
java_library(
name = 'log4j',
exports = ['@log4j//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -38,10 +43,12 @@
'//lib/commons:lang'
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'json-smart',
exports = ['@json_smart//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index c4a9872..8f2efa2 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.
@@ -14,21 +14,32 @@
)
maven_jar(
- name = 'lucene-core',
- id = 'org.apache.lucene:lucene-core:' + VERSION,
- sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164',
+ name = 'lucene-codecs',
+ id = 'org.apache.lucene:lucene-codecs:' + VERSION,
+ sha1 = 'e01fe463d9490bb1b4a6a168e771f7b7255a50b1',
license = 'Apache2.0',
exclude = [
'META-INF/LICENSE.txt',
'META-INF/NOTICE.txt',
],
- visibility = [],
+)
+
+maven_jar(
+ name = 'lucene-core',
+ id = 'org.apache.lucene:lucene-core:' + VERSION,
+ sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+ visibility = ['//gerrit-elasticsearch:elasticsearch'],
)
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 +51,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 = [
@@ -51,9 +62,42 @@
)
maven_jar(
+ name = 'lucene-highlighter',
+ id = 'org.apache.lucene:lucene-highlighter:' + VERSION,
+ sha1 = 'd127ac514e9df965ab0b57d92bbe0c68d3d145b8',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'lucene-join',
+ id = 'org.apache.lucene:lucene-join:'+ VERSION,
+ sha1 = 'dac1b322508f3f2696ecc49a97311d34d8382054',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'lucene-memory',
+ id = 'org.apache.lucene:lucene-memory:' + VERSION,
+ sha1 = '7409db9863d8fbc265c27793c6cc7511304182c2',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+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 = [
@@ -63,9 +107,52 @@
)
maven_jar(
+ name = 'lucene-sandbox',
+ id = 'org.apache.lucene:lucene-sandbox:' + VERSION,
+ sha1 = '30a91f120706ba66732d5a974b56c6971b3c8a16',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'lucene-spatial',
+ id = 'org.apache.lucene:lucene-spatial:' + VERSION,
+ sha1 = '8ed7a9a43d78222038573dd1c295a61f3c0bb0db',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+maven_jar(
+ name = 'lucene-suggest',
+ id = 'org.apache.lucene:lucene-suggest:' + VERSION,
+ sha1 = 'e8316b37dddcf2092a54dab2ce6aad0d5ad78585',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'lucene-queries',
+ id = 'org.apache.lucene:lucene-queries:' + VERSION,
+ sha1 = '692f1ad887cf4e006a23f45019e6de30f3312d3f',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+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 = [
@@ -73,3 +160,4 @@
'META-INF/NOTICE.txt',
],
)
+
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index 679c9f0..4739981 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -1,3 +1,4 @@
+package(default_visibility = [ "//visibility:public"])
load('//tools/bzl:maven.bzl', 'merge_maven_jars')
# core and backward-codecs both provide
@@ -9,6 +10,7 @@
'@lucene_core//jar',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -16,6 +18,21 @@
exports = ['@lucene_analyzers_common//jar'],
runtime_deps = [':lucene-core-and-backward-codecs'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-codecs',
+ exports = ['@lucene_codecs//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-core',
+ exports = ['@lucene_core//jar'],
+ visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -23,6 +40,7 @@
exports = ['@lucene_misc//jar'],
runtime_deps = [':lucene-core-and-backward-codecs'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
@@ -30,4 +48,47 @@
exports = ['@lucene_queryparser//jar'],
runtime_deps = [':lucene-core-and-backward-codecs'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-highlighter',
+ exports = [ '@lucene_highlighter//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-join',
+ exports = [ '@lucene_join//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-memory',
+ exports = [ '@lucene_memory//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-sandbox',
+ exports = [ '@lucene_sandbox//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-spatial',
+ exports = [ '@lucene_spatial//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-suggest',
+ exports = [ '@lucene_suggest//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
+)
+
+java_library(
+ name = 'lucene-queries',
+ exports = [ '@lucene_queries//jar' ],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/mina/BUILD b/lib/mina/BUILD
index 52468a4..b3ba684 100644
--- a/lib/mina/BUILD
+++ b/lib/mina/BUILD
@@ -3,10 +3,12 @@
exports = ['@sshd//jar'],
visibility = ['//visibility:public'],
runtime_deps = [':core'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'core',
exports = ['@mina_core//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
diff --git a/lib/openid/BUILD b/lib/openid/BUILD
index 7d97a86..8c5da45 100644
--- a/lib/openid/BUILD
+++ b/lib/openid/BUILD
@@ -9,15 +9,18 @@
'//lib/guice:guice',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'nekohtml',
exports = ['@nekohtml//jar'],
runtime_deps = [':xerces'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
java_library(
name = 'xerces',
exports = ['@xerces//jar'],
+ data = ['//lib:LICENSE-Apache2.0'],
)
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/lib/ow2/BUILD b/lib/ow2/BUILD
index 0b99b6f..4c37357 100644
--- a/lib/ow2/BUILD
+++ b/lib/ow2/BUILD
@@ -2,12 +2,14 @@
name = 'ow2-asm',
exports = ['@ow2_asm//jar'],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-ow2'],
)
java_library(
name = 'ow2-asm-analysis',
exports = ['@ow2_asm_analysis//jar'],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-ow2'],
)
java_library(
@@ -15,16 +17,19 @@
exports = ['@ow2_asm_commons//jar'],
runtime_deps = [':ow2-asm-tree'],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-ow2'],
)
java_library(
name = 'ow2-asm-tree',
exports = ['@ow2_asm_tree//jar'],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-ow2'],
)
java_library(
name = 'ow2-asm-util',
exports = ['@ow2_asm_util//jar'],
visibility = ["//visibility:public"],
+ data = ['//lib:LICENSE-ow2'],
)
diff --git a/lib/powermock/BUCK b/lib/powermock/BUCK
index b642457..d469a85 100644
--- a/lib/powermock/BUCK
+++ b/lib/powermock/BUCK
@@ -1,12 +1,12 @@
include_defs('//lib/maven.defs')
-VERSION = '1.6.4' # When bumping VERSION, make sure to also move
+VERSION = '1.6.1' # When bumping VERSION, make sure to also move
# easymock to a compatible version
maven_jar(
name = 'powermock-module-junit4',
id = 'org.powermock:powermock-module-junit4:' + VERSION,
- sha1 = '8692eb1d9bb8eb1310ffe8a20c2da7ee6d1b5994',
+ sha1 = 'ea8530b2848542624f110a393513af397b37b9cf',
license = 'DO_NOT_DISTRIBUTE',
deps = [
':powermock-module-junit4-common',
@@ -17,7 +17,7 @@
maven_jar(
name = 'powermock-module-junit4-common',
id = 'org.powermock:powermock-module-junit4-common:' + VERSION,
- sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81',
+ sha1 = '7222ced54dabc310895d02e45c5428ca05193cda',
license = 'DO_NOT_DISTRIBUTE',
deps = [
':powermock-reflect',
@@ -28,7 +28,7 @@
maven_jar(
name = 'powermock-reflect',
id = 'org.powermock:powermock-reflect:' + VERSION,
- sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893',
+ sha1 = '97d25eda8275c11161bcddda6ef8beabd534c878',
license = 'DO_NOT_DISTRIBUTE',
deps = [
'//lib:junit',
@@ -39,7 +39,7 @@
maven_jar(
name = 'powermock-api-easymock',
id = 'org.powermock:powermock-api-easymock:' + VERSION,
- sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45',
+ sha1 = 'aa740ecf89a2f64d410b3d93ef8cd6833009ef00',
license = 'DO_NOT_DISTRIBUTE',
deps = [
':powermock-api-support',
@@ -50,7 +50,7 @@
maven_jar(
name = 'powermock-api-support',
id = 'org.powermock:powermock-api-support:' + VERSION,
- sha1 = '314daafb761541293595630e10a3699ebc07881d',
+ sha1 = '592ee6d929c324109d3469501222e0c76ccf0869',
license = 'DO_NOT_DISTRIBUTE',
deps = [
':powermock-core',
@@ -62,7 +62,7 @@
maven_jar(
name = 'powermock-core',
id = 'org.powermock:powermock-core:' + VERSION,
- sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87',
+ sha1 = '5afc1efce8d44ed76b30af939657bd598e45d962',
license = 'DO_NOT_DISTRIBUTE',
deps = [
':powermock-reflect',
diff --git a/lib/powermock/BUILD b/lib/powermock/BUILD
index 8dc7d23..075b6bf 100644
--- a/lib/powermock/BUILD
+++ b/lib/powermock/BUILD
@@ -6,6 +6,7 @@
'//lib:junit',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -16,6 +17,7 @@
'//lib:junit',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -26,6 +28,7 @@
'//lib/easymock:objenesis',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -36,6 +39,7 @@
'//lib/easymock:easymock',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -47,6 +51,7 @@
'//lib:junit',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
java_library(
@@ -57,4 +62,5 @@
'//lib:junit',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-DO_NOT_DISTRIBUTE'],
)
diff --git a/lib/prolog/BUCK b/lib/prolog/BUCK
index 77fe5ac..854b0f7 100644
--- a/lib/prolog/BUCK
+++ b/lib/prolog/BUCK
@@ -1,12 +1,12 @@
include_defs('//lib/maven.defs')
-VERSION = '1.4.1'
+VERSION = '1.4.2'
REPO = GERRIT
maven_jar(
name = 'runtime',
id = 'com.googlecode.prolog-cafe:prolog-runtime:' + VERSION,
- sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246',
+ sha1 = '4421b4806b6e3a318680f6ab1d57569e857169c6',
license = 'prologcafe',
repository = REPO,
)
@@ -14,7 +14,7 @@
maven_jar(
name = 'compiler',
id = 'com.googlecode.prolog-cafe:prolog-compiler:' + VERSION,
- sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41',
+ sha1 = '7e5a7ca5efe7db7f69e015cf492f8f04665244d8',
license = 'prologcafe',
repository = REPO,
deps = [
@@ -26,7 +26,7 @@
maven_jar(
name = 'io',
id = 'com.googlecode.prolog-cafe:prolog-io:' + VERSION,
- sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa',
+ sha1 = 'd177f6211d1013e0f31a507127f5c87a7f6941f3',
license = 'prologcafe',
repository = REPO,
deps = [':runtime'],
@@ -36,7 +36,7 @@
maven_jar(
name = 'cafeteria',
id = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + VERSION,
- sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641',
+ sha1 = '11f396cb2588b65e6a78070488aaa58d12bf000e',
license = 'prologcafe',
repository = REPO,
deps = [
diff --git a/lib/prolog/BUILD b/lib/prolog/BUILD
index 74d8b80..a45cff2 100644
--- a/lib/prolog/BUILD
+++ b/lib/prolog/BUILD
@@ -2,6 +2,7 @@
name = 'runtime',
exports = ['@prolog_runtime//jar'],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-prologcafe'],
)
java_library(
@@ -12,11 +13,13 @@
':runtime',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-prologcafe'],
)
java_library(
name = 'io',
exports = ['@prolog_io//jar'],
+ data = ['//lib:LICENSE-prologcafe'],
)
java_library(
@@ -27,6 +30,7 @@
'runtime',
],
visibility = ['//visibility:public'],
+ data = ['//lib:LICENSE-prologcafe'],
)
java_binary(
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl
index 3afb031..d4e9e08 100644
--- a/lib/prolog/prolog.bzl
+++ b/lib/prolog/prolog.bzl
@@ -22,7 +22,7 @@
genrule2(
name = name + '__pl2j',
cmd = '$(location //lib/prolog:compiler_bin) ' +
- '$$TMP $@ ' +
+ '$$(dirname $@) $@ ' +
'$(SRCS)',
srcs = srcs,
tools = ['//lib/prolog:compiler_bin'],
diff --git a/plugins/BUILD b/plugins/BUILD
new file mode 100644
index 0000000..ffa0713
--- /dev/null
+++ b/plugins/BUILD
@@ -0,0 +1,22 @@
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+
+CORE = [
+ 'commit-message-length-validator',
+ 'download-commands',
+ 'hooks',
+ 'replication',
+ 'reviewnotes',
+ 'singleusergroup'
+]
+
+genrule2(
+ name = 'core',
+ srcs = ['//plugins/%s:%s_deploy.jar' % (n, n) for n in CORE],
+ cmd = 'mkdir -p $$TMP/WEB-INF/plugins;' +
+ 'for s in $(SRCS) ; do ' +
+ 'ln -s $$ROOT/$$s $$TMP/WEB-INF/plugins;done;' +
+ 'cd $$TMP;' +
+ 'zip -qr $$ROOT/$@ .',
+ out = 'core.zip',
+ visibility = ['//visibility:public'],
+)
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index 9b163e1..76b9115 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit 9b163e113de9f3a49219a02d388f7f46ea2559d3
+Subproject commit 76b9115b830cab453c12dd9014f5130c7b7f2ce5
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 375de37..09981c0 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 375de37939e2b016d3042c20853ce699d54e7a94
+Subproject commit 09981c0638f7241a4f435baaa96bd6112a1edaa9
diff --git a/plugins/download-commands b/plugins/download-commands
index 7b41f3a..6326db6 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 7b41f3a413b46140b050ae5324cbbcdd467d2b3a
+Subproject commit 6326db67dfa45b13a0c427643bbfa617c18855d7
diff --git a/plugins/hooks b/plugins/hooks
index e15dd5b..052ecd9 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit e15dd5bdc5d114d9cb596cc725d4ca7d0be1725b
+Subproject commit 052ecd9c4ed12e1ca0beef53e7bc4fb2e17ee580
diff --git a/plugins/replication b/plugins/replication
index c5123d6..b3606eb 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit c5123d6a5604cc740d6f42485235c0d3ec141c4e
+Subproject commit b3606eb38eb8edc166260184177e68386539381a
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 3f3d572..e9c66c6 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c
+Subproject commit e9c66c6f08edb641d3c935c2fdcaa3fbf3a85d29
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 3ca1167..e985959 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 3ca1167edda713f4bfdcecd9c0e2626797d7027f
+Subproject commit e9859591be48a157c7114d5f3c6acdf27384a408
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
new file mode 100644
index 0000000..9191ab7
--- /dev/null
+++ b/polygerrit-ui/BUILD
@@ -0,0 +1,37 @@
+package(
+ default_visibility=["//visibility:public"]
+)
+
+load("//tools/bzl:js.bzl", "bower_component_bundle")
+load('//tools/bzl:genrule2.bzl', 'genrule2')
+
+bower_component_bundle(
+ name = "polygerrit_components",
+ deps = [
+ '//lib/js:es6-promise',
+ '//lib/js:fetch',
+ '//lib/js:highlightjs',
+ '//lib/js:iron-autogrow-textarea',
+ '//lib/js:iron-dropdown',
+ '//lib/js:iron-input',
+ '//lib/js:iron-overlay-behavior',
+ '//lib/js:iron-selector',
+ '//lib/js:moment',
+ '//lib/js:page',
+ '//lib/js:polymer',
+ '//lib/js:promise-polyfill',
+])
+
+
+genrule2(
+ name = 'fonts',
+ cmd = ' && '.join([
+ 'cd $$TMP; for file in $(SRCS); do unzip -q $$ROOT/$$file; done',
+ 'zip -q $$ROOT/$@ *',
+ ]),
+ srcs = [
+ '//lib/fonts:sourcecodepro.zip',
+ ],
+ out = 'fonts.zip',
+ visibility = ['//visibility:public'],
+)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 383fb50..1e548d5 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -13,9 +13,21 @@
All other platforms: [download from
nodejs.org](https://nodejs.org/en/download/).
-## Optional: installing [go](https://golang.org/)
+## Installing [Buck](https://buckbuild.com/)
-This is only required for running the ```run-server.sh``` script for testing. See below.
+Follow the instructions
+[here](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_installation)
+to get and install Buck.
+
+## Local UI, Production Data
+
+This is a quick and easy way to test your local changes against real data.
+Unfortunately, you can't sign in, so testing certain features will require
+you to use the "test data" technique described below.
+
+### Installing [go](https://golang.org/)
+
+This is required for running the `run-server.sh` script below.
```sh
# Debian/Ubuntu
@@ -27,18 +39,18 @@
All other platforms: [download from golang.org](https://golang.org/)
-# Add [go] to your path
+Then add go to your path:
```
PATH=$PATH:/usr/local/go/bin
```
-## Local UI, Production Data
+### Running the server
To test the local UI against gerrit-review.googlesource.com:
```sh
-./polygerrit-ui/run-server.sh
+./run-server.sh
```
Then visit http://localhost:8081
@@ -47,10 +59,8 @@
One-time setup:
-1. [Install Buck](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_installation)
- for building Gerrit.
-2. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_gerrit_development_war_file)
- and set up a local test site. Docs
+1. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_gerrit_development_war_file)
+2. Set up a local test site. Docs
[here](https://gerrit-review.googlesource.com/Documentation/install-quick.html) and
[here](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
new file mode 100644
index 0000000..29bbfff
--- /dev/null
+++ b/polygerrit-ui/app/BUILD
@@ -0,0 +1,37 @@
+package(
+ default_visibility = ["//visibility:public"])
+load("//tools/bzl:js.bzl", "bower_component_bundle", "vulcanize")
+
+WCT_TEST_PATTERNS = [
+ 'test/*.js',
+ 'test/*.html',
+ '**/*_test.html',
+]
+PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
+APP_SRCS = glob(
+ ['**'],
+ exclude = [
+ 'BUCK',
+ '*~',
+ '**/BUILD',
+ 'index.html',
+ 'test/**',
+ ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
+
+
+bower_component_bundle(
+ name = 'test_components',
+ deps = [
+ '//polygerrit-ui:polygerrit_components',
+ '//lib/js:iron-test-helpers',
+ '//lib/js:test-fixture',
+ '//lib/js:web-component-tester',
+ ],
+)
+
+vulcanize(
+ name = "gr-app",
+ app = 'elements/gr-app.html',
+ srcs = APP_SRCS,
+ deps = [ "//polygerrit-ui:polygerrit_components"],
+)
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
new file mode 100644
index 0000000..fa8289f
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -0,0 +1,61 @@
+<!--
+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.
+-->
+<script>
+(function(window) {
+ 'use strict';
+
+ /** @polymerBehavior Gerrit.PathListBehavior */
+ var PathListBehavior = {
+ specialFilePathCompare: function(a, b) {
+ var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+ // The commit message always goes first.
+ if (a === COMMIT_MESSAGE_PATH) {
+ return -1;
+ }
+ if (b === COMMIT_MESSAGE_PATH) {
+ return 1;
+ }
+
+ var aLastDotIndex = a.lastIndexOf('.');
+ var aExt = a.substr(aLastDotIndex + 1);
+ var aFile = a.substr(0, aLastDotIndex) || a;
+
+ var bLastDotIndex = b.lastIndexOf('.');
+ var bExt = b.substr(bLastDotIndex + 1);
+ var bFile = b.substr(0, bLastDotIndex) || b;
+
+ // Sort header files above others with the same base name.
+ var headerExts = ['h', 'hxx', 'hpp'];
+ if (aFile.length > 0 && aFile === bFile) {
+ if (headerExts.indexOf(aExt) !== -1 &&
+ headerExts.indexOf(bExt) !== -1) {
+ return a.localeCompare(b);
+ }
+ if (headerExts.indexOf(aExt) !== -1) {
+ return -1;
+ }
+ if (headerExts.indexOf(bExt) !== -1) {
+ return 1;
+ }
+ }
+ return aFile.localeCompare(bFile) || a.localeCompare(b);
+ },
+ };
+
+ window.Gerrit = window.Gerrit || {};
+ window.Gerrit.PathListBehavior = PathListBehavior;
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
new file mode 100644
index 0000000..530b7be
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -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.
+-->
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<title>gr-path-list-behavior</title>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-path-list-behavior.html">
+
+<script>
+ suite('gr-path-list-behavior tests', function() {
+ test('special sort', function() {
+ var sort = Gerrit.PathListBehavior.specialFilePathCompare;
+ var testFiles = [
+ '/a.h',
+ '/a.cpp',
+ '/COMMIT_MSG',
+ '/asdasd',
+ '/mrPeanutbutter.py'
+ ];
+ assert.deepEqual(testFiles.sort(sort),
+ ['/COMMIT_MSG', '/a.h', '/a.cpp', '/asdasd', '/mrPeanutbutter.py']);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html
new file mode 100644
index 0000000..b7d71fc
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html
@@ -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.
+-->
+<script>
+(function(window) {
+ 'use strict';
+
+ /** @polymerBehavior Gerrit.URLEncodingBehavior */
+ var URLEncodingBehavior = {
+ /**
+ * Pretty-encodes a URL. Double-encodes the string, and then replaces
+ * benevolent characters for legibility.
+ */
+ encodeURL: function(url, replaceSlashes) {
+ // @see Issue 4255 regarding double-encoding.
+ var output = encodeURIComponent(encodeURIComponent(url));
+ // @see Issue 4577 regarding more readable URLs.
+ output = output.replace(/%253A/g, ':');
+ output = output.replace(/%2520/g, '+');
+ if (replaceSlashes) {
+ output = output.replace(/%252F/g, '/');
+ }
+ return output;
+ },
+ };
+
+ window.Gerrit = window.Gerrit || {};
+ window.Gerrit.URLEncodingBehavior = URLEncodingBehavior;
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior.html
index 4def9b2..b7cf467 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior.html
@@ -81,7 +81,13 @@
COMMIT_FOOTERS: 17,
// Include push certificate information along with any patch sets.
- PUSH_CERTIFICATES: 18
+ PUSH_CERTIFICATES: 18,
+
+ // Include change's reviewer updates.
+ REVIEWER_UPDATES: 19,
+
+ // Set the submittable boolean.
+ SUBMITTABLE: 20
},
listChangesOptionsToHex: function() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 9126785..df1ade3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -14,9 +14,10 @@
limitations under the License.
-->
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/gr-change-list-styles.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
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..275a8cd 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
@@ -44,6 +44,7 @@
behaviors: [
Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
],
_computeChangeURL: function(changeNum) {
@@ -108,11 +109,14 @@
},
_computeProjectURL: function(project) {
- return '/q/status:open+project:' + project;
+ return '/q/status:open+project:' +
+ this.encodeURL(project, false);
},
_computeProjectBranchURL: function(project, branch) {
- return '/q/status:open+project:' + project + '+branch:' + branch;
+ // @see Issue 4255.
+ return this._computeProjectURL(project) +
+ '+branch:' + this.encodeURL(branch, false);
},
});
})();
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-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 1f06dff..91b2f07 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -14,6 +14,7 @@
limitations under the License.
-->
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-change-list/gr-change-list.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 7fbe455..45d9a57 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -23,6 +23,7 @@
* @event title-change
*/
+ behaviors: [Gerrit.URLEncodingBehavior],
properties: {
/**
* URL params passed from the router.
@@ -116,7 +117,8 @@
// Offset could be a string when passed from the router.
offset = +(offset || 0);
var newOffset = Math.max(0, offset + (changesPerPage * direction));
- var href = '/q/' + query;
+ // Double encode URI component.
+ var href = '/q/' + this.encodeURL(query, false);
if (newOffset > 0) {
href += ',' + newOffset;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
new file mode 100644
index 0000000..af2359c
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -0,0 +1,51 @@
+<!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-change-list-view</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="gr-change-list-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list-view></gr-change-list-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list-view tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('url is properly encoded', function() {
+ assert.equal(element._computeNavLink(
+ 'status:open project:platform/frameworks/base', 0, -1, 25),
+ '/q/status:open+project:platform%252Fframeworks%252Fbase'
+ );
+ assert.equal(element._computeNavLink(
+ 'status:open project:platform/frameworks/base', 0, 1, 25),
+ '/q/status:open+project:platform%252Fframeworks%252Fbase,25'
+ );
+ });
+ });
+</script>
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..9960f90 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,
@@ -208,13 +211,13 @@
_computeRevisionActionValues: function(actionsChangeRecord,
primariesChangeRecord, additionalActionsChangeRecord) {
return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, 'revision');
+ additionalActionsChangeRecord, ActionType.REVISION);
},
_computeChangeActionValues: function(actionsChangeRecord,
primariesChangeRecord, additionalActionsChangeRecord) {
return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
- additionalActionsChangeRecord, 'change');
+ additionalActionsChangeRecord, ActionType.CHANGE);
},
_getActionValues: function(actionsChangeRecord, primariesChangeRecord,
@@ -231,6 +234,15 @@
actions[a].__key = a;
actions[a].__type = type;
actions[a].__primary = primaryActionKeys.indexOf(a) !== -1;
+ if (actions[a].label === 'Delete') {
+ // This label is common within change and revision actions. Make it
+ // more explicit to the user.
+ if (type === ActionType.CHANGE) {
+ actions[a].label += ' Change';
+ } else if (type === ActionType.REVISION) {
+ actions[a].label += ' Revision';
+ }
+ }
// Triggers a re-render by ensuring object inequality.
// TODO(andybons): Polyfill for Object.assign.
result.push(Object.assign({}, actions[a]));
@@ -254,12 +266,31 @@
},
_canSubmitChange: function() {
- return this.$.jsAPI.canSubmitChange();
+ return this.$.jsAPI.canSubmitChange(this.change,
+ this._getRevision(this.change, this.patchNum));
+ },
+
+ _getRevision: function(change, patchNum) {
+ var num = window.parseInt(patchNum, 10);
+ for (var hash in change.revisions) {
+ var rev = change.revisions[hash];
+ if (rev._number === num) {
+ return rev;
+ }
+ }
+ return null;
},
_modifyRevertMsg: function() {
return this.$.jsAPI.modifyRevertMsg(this.change,
- this.$.confirmRevertDialog.message);
+ this.$.confirmRevertDialog.message, this.commitMessage);
+ },
+
+ showRevertDialog: function() {
+ this.$.confirmRevertDialog.populateRevertMessage(
+ this.commitMessage, this.change.current_revision);
+ this.$.confirmRevertDialog.message = this._modifyRevertMsg();
+ this._showActionDialog(this.$.confirmRevertDialog);
},
_handleActionTap: function(e) {
@@ -274,9 +305,7 @@
if (type === ActionType.REVISION) {
this._handleRevisionAction(key);
} else if (key === ChangeActions.REVERT) {
- this.$.confirmRevertDialog.populateRevertMessage();
- this.$.confirmRevertDialog.message = this._modifyRevertMsg();
- this._showActionDialog(this.$.confirmRevertDialog);
+ this.showRevertDialog();
} else if (key === ChangeActions.ABANDON) {
this._showActionDialog(this.$.confirmAbandonDialog);
} else {
@@ -290,6 +319,7 @@
this._showActionDialog(this.$.confirmRebase);
break;
case RevisionActions.CHERRYPICK:
+ this.$.confirmCherrypick.branch = '';
this._showActionDialog(this.$.confirmCherrypick);
break;
case RevisionActions.SUBMIT:
@@ -308,6 +338,10 @@
},
_handleConfirmDialogCancel: function() {
+ this._hideAllDialogs();
+ },
+
+ _hideAllDialogs: function() {
var dialogEls =
Polymer.dom(this.root).querySelectorAll('.confirmDialog');
for (var i = 0; i < dialogEls.length; i++) {
@@ -331,7 +365,7 @@
payload.base = el.base;
}
this.$.overlay.close();
- el.hidden = false;
+ el.hidden = true;
this._fireAction('/rebase', this._revisionActions.rebase, true, payload);
},
@@ -347,7 +381,7 @@
return;
}
this.$.overlay.close();
- el.hidden = false;
+ el.hidden = true;
this._fireAction(
'/cherrypick',
this._revisionActions.cherrypick,
@@ -362,7 +396,7 @@
_handleRevertDialogConfirm: function() {
var el = this.$.confirmRevertDialog;
this.$.overlay.close();
- el.hidden = false;
+ el.hidden = true;
this._fireAction('/revert', this.actions.revert, false,
{message: el.message});
},
@@ -370,7 +404,7 @@
_handleAbandonDialogConfirm: function() {
var el = this.$.confirmAbandonDialog;
this.$.overlay.close();
- el.hidden = false;
+ el.hidden = true;
this._fireAction('/abandon', this.actions.abandon, false,
{message: el.message});
},
@@ -393,14 +427,32 @@
},
_showActionDialog: function(dialog) {
+ this._hideAllDialogs();
+
dialog.hidden = false;
- this.$.overlay.open();
+ this.$.overlay.open().then(function() {
+ if (dialog.resetFocus) {
+ dialog.resetFocus();
+ }
+ });
+ },
+
+ // TODO(rmistry): Redo this after
+ // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
+ _setLabelValuesOnRevert: function(newChangeId) {
+ var labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
+ if (labels) {
+ var url = '/changes/' + newChangeId + '/revisions/current/review';
+ this.$.restAPI.send(this.actions.revert.method, url, {labels: labels});
+ }
},
_handleResponse: function(action, response) {
return this.$.restAPI.getResponseObject(response).then(function(obj) {
switch (action.__key) {
case ChangeActions.REVERT:
+ this._setLabelValuesOnRevert(obj.change_id);
+ // Fall through.
case RevisionActions.CHERRYPICK:
page.show(this.changePath(obj._number));
break;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 80aaf3b..a342c87 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -38,23 +38,30 @@
stub('gr-rest-api-interface', {
getChangeRevisionActions: function() {
return Promise.resolve({
+ '/': {
+ method: 'DELETE',
+ label: 'Delete',
+ title: 'Delete draft revision 2',
+ enabled: true,
+ },
cherrypick: {
method: 'POST',
label: 'Cherry Pick',
title: 'Cherry pick change to a different branch',
- enabled: true
+ enabled: true,
},
rebase: {
method: 'POST',
label: 'Rebase',
- title: 'Rebase onto tip of branch or parent change'
+ title: 'Rebase onto tip of branch or parent change',
+ enabled: true,
},
submit: {
method: 'POST',
label: 'Submit',
- title: 'Submit patch set 1 into master',
- enabled: true
- }
+ title: 'Submit patch set 2 into master',
+ enabled: true,
+ },
});
},
send: function(method, url, payload) {
@@ -79,19 +86,65 @@
element = fixture('basic');
element.changeNum = '42';
element.patchNum = '2';
+ element.actions = {
+ '/': {
+ method: 'DELETE',
+ label: 'Delete',
+ title: 'Delete draft change 42',
+ enabled: true
+ },
+ };
return element.reload();
});
- test('submit, rebase, and cherry-pick buttons show', function(done) {
+ test('buttons show', function(done) {
flush(function() {
var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
- assert.equal(buttonEls.length, 3);
+ assert.equal(buttonEls.length, 5);
assert.isFalse(element.hidden);
done();
});
});
+ test('delete buttons have explicit labels', function(done) {
+ flush(function() {
+ var buttonEls =
+ Polymer.dom(element.root).querySelectorAll('[data-action-key="/"]');
+ assert.equal(buttonEls.length, 2);
+ assert.notEqual(buttonEls[0].getAttribute('data-label'),
+ buttonEls[1].getAttribute['data-label']);
+ assert.isTrue(
+ buttonEls[0].getAttribute('data-label') === 'Delete Revision' ||
+ buttonEls[0].getAttribute('data-label') === 'Delete Change'
+ );
+ assert.isTrue(
+ buttonEls[1].getAttribute('data-label') === 'Delete Revision' ||
+ buttonEls[1].getAttribute('data-label') === 'Delete Change'
+ );
+ done();
+ });
+ });
+
+ test('get revision object from change', function() {
+ var revObj = {_number: 2, foo: 'bar'};
+ var change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: revObj,
+ },
+ };
+ assert.deepEqual(element._getRevision(change, '2'), revObj);
+ });
+
test('submit change', function(done) {
+ element.change = {
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ },
+ };
+ element.patchNum = '2';
+
flush(function() {
var submitButton = element.$$('gr-button[data-action-key="submit"]');
assert.ok(submitButton);
@@ -129,6 +182,7 @@
__key: 'rebase',
__type: 'revision',
__primary: false,
+ enabled: true,
label: 'Rebase',
method: 'POST',
title: 'Rebase onto tip of branch or parent change',
@@ -154,6 +208,25 @@
});
});
+ test('two dialogs are not shown at the same time', function(done) {
+ flush(function() {
+ var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
+ assert.ok(rebaseButton);
+ MockInteractions.tap(rebaseButton);
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.confirmRebase.hidden);
+
+ var cherryPickButton =
+ element.$$('gr-button[data-action-key="cherrypick"]');
+ assert.ok(cherryPickButton);
+ MockInteractions.tap(cherryPickButton);
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.confirmRebase.hidden);
+ assert.isFalse(element.$.confirmCherrypick.hidden);
+ done();
+ });
+ });
+
suite('cherry-pick', function() {
var fireActionStub;
var alertStub;
@@ -169,8 +242,9 @@
});
test('works', function() {
- var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
- MockInteractions.tap(rebaseButton);
+ var cherryPickButton =
+ element.$$('gr-button[data-action-key="cherrypick"]');
+ MockInteractions.tap(cherryPickButton);
var action = {
__key: 'cherrypick',
__type: 'revision',
@@ -198,6 +272,25 @@
}
]);
});
+
+ test('branch name cleared when re-open cherrypick', function() {
+ var cherryPickButton =
+ element.$$('gr-button[data-action-key="cherrypick"]');
+ var action = {
+ __key: 'cherrypick',
+ __type: 'revision',
+ __primary: false,
+ enabled: true,
+ label: 'Cherry Pick',
+ method: 'POST',
+ title: 'Cherry pick change to a different branch',
+ };
+ var emptyBranchName = '';
+ element.$.confirmCherrypick.branch = 'master';
+
+ MockInteractions.tap(cherryPickButton);
+ assert.equal(element.$.confirmCherrypick.branch, emptyBranchName);
+ });
});
test('custom actions', function(done) {
@@ -241,6 +334,9 @@
});
test('revert change with plugin hook', function(done) {
+ element.change = {
+ current_revision: 'abc1234',
+ };
var newRevertMsg = 'Modified revert msg';
var modifyRevertMsgStub = sinon.stub(element, '_modifyRevertMsg',
function() { return newRevertMsg; });
@@ -260,6 +356,9 @@
});
test('works', function() {
+ element.change = {
+ current_revision: 'abc1234',
+ };
var populateRevertMsgStub = sinon.stub(
element.$.confirmRevertDialog, 'populateRevertMessage',
function() { return 'original msg'; });
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..108ac92 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
@@ -16,7 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
+<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-label/gr-label.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
@@ -37,17 +37,21 @@
color: #666;
font-weight: bold;
}
+ gr-editable-label {
+ max-width: 9em;
+ }
.labelValueContainer:not(:first-of-type) {
margin-top: .25em;
}
.labelValueContainer .approved,
.labelValueContainer .notApproved {
- display: inline-block;
+ display: inline-flex;
padding: .1em .3em;
border-radius: 3px;
}
.labelValue {
display: inline-block;
+ padding-right: .3em;
}
.approved {
background-color: #d4ffd4;
@@ -128,18 +132,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
@@ -159,7 +151,7 @@
<span class="title">[[labelName]]</span>
<span class="value">
<template is="dom-repeat"
- items="[[_computeLabelValues(labelName, change.labels)]]"
+ items="[[_computeLabelValues(labelName, change.labels.*)]]"
as="label">
<div class="labelValueContainer">
<span class$="[[label.className]]">
@@ -169,7 +161,13 @@
class="labelValue">
[[label.value]]
</gr-label>
- <gr-account-link account="[[label.account]]"></gr-account-link>
+ <gr-account-chip
+ account="[[label.account]]"
+ data-account-id$="[[label.account._account_id]]"
+ label-name="[[labelName]]"
+ removable="[[_computeCanDeleteVote(label.account, mutable)]]"
+ transparent-background
+ on-remove="_onDeleteVote"></gr-account-chip>
</span>
</div>
</template>
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..661a296 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);
},
@@ -96,8 +55,9 @@
return Object.keys(labels).sort();
},
- _computeLabelValues: function(labelName, labels) {
+ _computeLabelValues: function(labelName, _labels) {
var result = [];
+ var labels = _labels.base;
var t = labels[labelName];
if (!t) { return result; }
var approvals = t.all || [];
@@ -128,7 +88,7 @@
_handleTopicChanged: function(e, topic) {
if (!topic.length) { topic = null; }
- this.$.restAPI.setChangeTopic(this.change.id, topic);
+ this.$.restAPI.setChangeTopic(this.change.change_id, topic);
},
_computeTopicReadOnly: function(mutable, change) {
@@ -142,5 +102,46 @@
_computeShowReviewersByState: function(serverConfig) {
return !!serverConfig.note_db_enabled;
},
+
+ /**
+ * A user is able to delete a vote iff the mutable property is true and the
+ * reviewer that left the vote exists in the list of removable_reviewers
+ * received from the backend.
+ *
+ * @param {!Object} reviewer An object describing the reviewer that left the
+ * vote.
+ * @param {boolean} mutable this.mutable describes whether the
+ * change-metadata section is modifiable by the current user.
+ */
+ _computeCanDeleteVote: function(reviewer, mutable) {
+ if (!mutable) { return false; }
+ for (var i = 0; i < this.change.removable_reviewers.length; i++) {
+ if (this.change.removable_reviewers[i]._account_id ===
+ reviewer._account_id) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ _onDeleteVote: function(e) {
+ e.preventDefault();
+ var target = Polymer.dom(e).rootTarget;
+ var labelName = target.labelName;
+ var accountID = parseInt(target.getAttribute('data-account-id'), 10);
+ this._xhrPromise =
+ this.$.restAPI.deleteVote(this.change.id, accountID, labelName)
+ .then(function(response) {
+ if (!response.ok) { return response; }
+
+ var labels = this.change.labels[labelName].all || [];
+ for (var i = 0; i < labels.length; i++) {
+ if (labels[i]._account_id === accountID) {
+ this.splice(['change.labels', labelName, 'all'], i, 1);
+ break;
+ }
+ }
+ }.bind(this));
+ },
});
})();
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..22080e8 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;
@@ -152,5 +77,83 @@
element.serverConfig = {note_db_enabled: true};
assert.isTrue(hasCc());
});
+
+ suite('remove reviewer votes', function() {
+ var sandbox;
+ setup(function() {
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(element, '_computeValueTooltip').returns('');
+ sandbox.stub(element, '_computeTopicReadOnly').returns(true);
+ element.change = {
+ change_id: 'the id',
+ topic: 'the topic',
+ status: 'NEW',
+ submit_type: 'CHERRY_PICK',
+ labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ },
+ },
+ removable_reviewers: [],
+ };
+ });
+
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ test('_computeCanDeleteVote hides delete button', function() {
+ flushAsynchronousOperations();
+ var button = element.$$('gr-account-chip').$$('gr-button');
+ assert.isTrue(button.hasAttribute('hidden'));
+ element.mutable = true;
+ assert.isTrue(button.hasAttribute('hidden'));
+ });
+
+ test('_computeCanDeleteVote shows delete button', function() {
+ element.change.removable_reviewers = [
+ {
+ _account_id: 1,
+ name: 'bojack',
+ }
+ ];
+ element.mutable = true;
+ flushAsynchronousOperations();
+ var button = element.$$('gr-account-chip').$$('gr-button');
+ assert.isFalse(button.hasAttribute('hidden'));
+ });
+
+ test('deletes votes', function(done) {
+ sandbox.stub(element.$.restAPI, 'deleteVote')
+ .returns(Promise.resolve({'ok': true}));
+ element.change.removable_reviewers = [
+ {
+ _account_id: 1,
+ name: 'bojack',
+ }
+ ];
+ element.mutable = true;
+ flushAsynchronousOperations();
+ var button = element.$$('gr-account-chip').$$('gr-button');
+ MockInteractions.tap(button);
+ flushAsynchronousOperations();
+ var spliceStub = sinon.stub(element, 'splice',
+ function(path, index, length) {
+ assert.deepEqual(path, ['change.labels', 'test', 'all']);
+ assert.equal(index, 0);
+ assert.equal(length, 1);
+ spliceStub.restore();
+ done();
+ });
+ });
+
+ test('changing topic calls setChangeTopic', function() {
+ var topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic',
+ function() {});
+ element._handleTopicChanged({}, 'the new topic');
+ assert.isTrue(topicStub.calledWith('the id', 'the new topic'));
+ });
+ });
});
</script>
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..3d4e10d 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,103 @@
</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>
+ <!-- Plugins insert content into following container.
+ Stop-gap until PolyGerrit plugins interface is ready.
+ This will not work with Shadow DOM. -->
+ <div id="change_plugins"></div>
</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="[[_computeLatestPatchNum(_allPatchSets)]]"></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}}"
+ diff-view-mode="{{viewState.diffMode}}"></gr-file-list>
+ </section>
<gr-messages-list id="messageList"
change-num="[[_changeNum]]"
messages="[[_change.messages]]"
@@ -305,6 +316,7 @@
</div>
<gr-overlay id="downloadOverlay" with-backdrop>
<gr-download-dialog
+ id="downloadDialog"
change="[[_change]]"
logged-in="[[_loggedIn]]"
patch-num="[[_patchRange.patchNum]]"
@@ -312,12 +324,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..3d1cac7e 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
@@ -42,6 +42,7 @@
notify: true,
value: function() { return {}; },
},
+ backPage: String,
serverConfig: Object,
keyEventTarget: {
type: Object,
@@ -66,7 +67,11 @@
_hideEditCommitMessage: {
type: Boolean,
computed: '_computeHideEditCommitMessage(_loggedIn, ' +
- '_editingCommitMessage, _change.*, _patchRange.patchNum)',
+ '_editingCommitMessage, _change)',
+ },
+ _latestCommitMessage: {
+ type: String,
+ value: '',
},
_patchRange: Object,
_allPatchSets: {
@@ -78,14 +83,16 @@
value: false,
},
_loading: Boolean,
- _headerContainerEl: Object,
- _headerEl: Object,
_projectConfig: Object,
_replyButtonLabel: {
type: String,
value: 'Reply',
computed: '_computeReplyButtonLabel(_diffDrafts.*)',
},
+ _initialLoadComplete: {
+ type: Boolean,
+ value: false,
+ },
},
behaviors: [
@@ -98,10 +105,6 @@
'_paramsAndChangeChanged(params, _change)',
],
- ready: function() {
- this._headerEl = this.$$('.header');
- },
-
attached: function() {
this._getLoggedIn().then(function(loggedIn) {
this._loggedIn = loggedIn;
@@ -114,34 +117,11 @@
this._handleCommitMessageSave.bind(this));
this.addEventListener('editable-content-cancel',
this._handleCommitMessageCancel.bind(this));
- this.listen(window, 'scroll', '_handleBodyScroll');
+ this.listen(window, 'scroll', '_handleScroll');
},
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');
+ this.unlisten(window, 'scroll', '_handleScroll');
},
_handleEditCommitMessage: function(e) {
@@ -157,7 +137,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 +162,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 +239,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) {
@@ -289,7 +249,11 @@
_handleDownloadTap: function(e) {
e.preventDefault();
- this.$.downloadOverlay.open();
+ this.$.downloadOverlay.open().then(function() {
+ this.$.downloadOverlay
+ .setFocusStops(this.$.downloadDialog.getFocusStops());
+ this.$.downloadDialog.focus();
+ }.bind(this));
},
_handleDownloadDialogClose: function(e) {
@@ -300,7 +264,11 @@
var msg = e.detail.message.message;
var quoteStr = msg.split('\n').map(
function(line) { return '> ' + line; }).join('\n') + '\n\n';
- this.$.replyDialog.draft += quoteStr;
+
+ if (quoteStr !== this.$.replyDialog.quote) {
+ this.$.replyDialog.draft = quoteStr;
+ }
+ this.$.replyDialog.quote = quoteStr;
this._openReplyDialog();
},
@@ -329,33 +297,88 @@
this._openReplyDialog(target);
},
- _paramsChanged: function(value) {
- if (value.view !== this.tagName.toLowerCase()) { return; }
+ _handleScroll: function() {
+ this.debounce('scroll', function() {
+ history.replaceState(
+ {
+ scrollTop: document.body.scrollTop,
+ path: location.pathname,
+ },
+ location.pathname);
+ }, 150);
+ },
- this._changeNum = value.changeNum;
- this._patchRange = {
+ _paramsChanged: function(value) {
+ if (value.view !== this.tagName.toLowerCase()) {
+ this._initialLoadComplete = false;
+ return;
+ }
+
+ var patchChanged = this._patchRange &&
+ (this._patchRange.patchNum !== value.patchNum ||
+ this._patchRange.basePatchNum !== value.basePatchNum);
+
+ if (this._changeNum !== value.changeNum) {
+ this._initialLoadComplete = false;
+ }
+
+ var patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum || 'PARENT',
};
+ if (this._initialLoadComplete && patchChanged) {
+ if (patchRange.patchNum == null) {
+ patchRange.patchNum = this._computeLatestPatchNum(this._allPatchSets);
+ }
+ this._patchRange = patchRange;
+ this._reloadPatchNumDependentResources().then(function() {
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+ change: this._change,
+ patchNum: patchRange.patchNum,
+ });
+ }.bind(this));
+ return;
+ }
+
+ this._changeNum = value.changeNum;
+ this._patchRange = patchRange;
+
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();
- }.bind(this), 1);
-
- this._maybeShowReplyDialog();
-
- this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
- change: this._change,
- patchNum: this._patchRange.patchNum,
- });
+ this._performPostLoadTasks();
}.bind(this));
},
+ _performPostLoadTasks: function() {
+ // Allow the message list and related changes to render before scrolling.
+ // Related changes are loaded here (after everything else) because they
+ // take the longest and are secondary information. Because the element may
+ // alter the total height of the page, the call to potentially scroll to
+ // a linked message is performed after related changes is fully loaded.
+ this.$.relatedChanges.reload().then(function() {
+ this.async(function() {
+ if (!history.state.scrollTop) {
+ this._maybeScrollToMessage();
+
+ } else {
+ document.documentElement.scrollTop =
+ document.body.scrollTop = history.state.scrollTop;
+ }
+ }, 1);
+ }.bind(this));
+
+ this._maybeShowReplyDialog();
+
+ this._maybeShowRevertDialog();
+
+ this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
+ change: this._change,
+ patchNum: this._patchRange.patchNum,
+ });
+
+ this._initialLoadComplete = true;
+ },
+
_paramsAndChangeChanged: function(value) {
// If the change number or patch range is different, then reset the
// selected file index.
@@ -375,6 +398,35 @@
}
},
+ _getLocationSearch: function() {
+ // Not inlining to make it easier to test.
+ return window.location.search;
+ },
+
+ _getUrlParameter: function(param) {
+ var pageURL = this._getLocationSearch().substring(1);
+ var vars = pageURL.split('&');
+ for (var i = 0; i < vars.length; i++) {
+ var name = vars[i].split('=');
+ if (name[0] == param) {
+ return name[0];
+ }
+ }
+ return null;
+ },
+
+ _maybeShowRevertDialog: function() {
+ this._getLoggedIn().then(function(loggedIn) {
+ if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) {
+ // Do not display dialog if not logged-in or the change is not merged.
+ return;
+ }
+ if (!!this._getUrlParameter('revert')) {
+ this.$.actions.showRevertDialog();
+ }
+ }.bind(this));
+ },
+
_maybeShowReplyDialog: function() {
this._getLoggedIn().then(function(loggedIn) {
if (!loggedIn) { return; }
@@ -389,6 +441,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 +463,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 +503,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 +562,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 +575,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,13 +593,29 @@
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('/');
+ this._determinePageBack();
break;
}
},
+ _determinePageBack: function() {
+ // Default backPage to '/' if user came to change view page
+ // via an email link, etc.
+ page.show(this.backPage || '/');
+ },
+
_labelsChanged: function(changeRecord) {
if (!changeRecord) { return; }
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
@@ -527,7 +631,7 @@
},
_handleReloadChange: function() {
- page.show(this.changePath(this._changeNum));
+ this._reload();
},
_handleGetChangeDetailError: function(response) {
@@ -572,6 +676,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(
@@ -603,33 +715,46 @@
}.bind(this));
this._getComments();
- var reloadPatchNumDependentResources = function() {
- return Promise.all([
- this._getCommitInfo(),
- this.$.actions.reload(),
- this.$.fileList.reload(),
- ]);
- }.bind(this);
- var reloadDetailDependentResources = function() {
- if (!this._change) { return Promise.resolve(); }
-
- return Promise.all([
- this.$.relatedChanges.reload(),
- this._getProjectConfig(),
- ]);
- }.bind(this);
-
- this._resetHeaderEl();
-
if (this._patchRange.patchNum) {
- return reloadPatchNumDependentResources().then(function() {
+ return this._reloadPatchNumDependentResources().then(function() {
return detailCompletes;
- }).then(reloadDetailDependentResources);
+ }).then(function() {
+ return this._reloadDetailDependentResources();
+ }.bind(this));
} else {
// The patch number is reliant on the change detail request.
- return detailCompletes.then(reloadPatchNumDependentResources).then(
- reloadDetailDependentResources);
+ return detailCompletes.then(function() {
+ return this._reloadPatchNumDependentResources();
+ }.bind(this)).then(function() {
+ return this._reloadDetailDependentResources();
+ }.bind(this));
}
},
+
+ /**
+ * Kicks off requests for resources that rely on the change detail
+ * (`this._change`) being loaded.
+ */
+ _reloadDetailDependentResources: function() {
+ if (!this._change) { return Promise.resolve(); }
+
+ return this._getProjectConfig().then(function() {
+ return Promise.all([
+ this._getLatestCommitMessage(),
+ this.$.actions.reload(),
+ ]);
+ }.bind(this));
+ },
+
+ /**
+ * Kicks off requests for resources that rely on the patch range
+ * (`this._patchRange`) being defined.
+ */
+ _reloadPatchNumDependentResources: function() {
+ return Promise.all([
+ this._getCommitInfo(),
+ this.$.fileList.reload(),
+ ]);
+ },
});
})();
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..8cc4343 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
@@ -35,6 +35,7 @@
<script>
suite('gr-change-view tests', function() {
var element;
+ var TEST_SCROLL_TOP_PX = 100;
setup(function() {
stub('gr-rest-api-interface', {
@@ -43,44 +44,104 @@
element = fixture('basic');
});
- test('keyboard shortcuts', function() {
- var showStub = sinon.stub(page, 'show');
+ suite('keyboard shortcuts', function() {
+ test('U should navigate to / if no backPage set', 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('U should navigate to backPage if set', function() {
+ element.backPage = '/dashboard/self';
+ var showStub = sinon.stub(page, 'show');
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U'
+ assert(showStub.lastCall.calledWithExactly('/dashboard/self'));
+ showStub.restore();
+ });
- MockInteractions.pressAndReleaseKeyOn(element, 65); // 'A'
- var overlayEl = element.$.replyOverlay;
- assert.isFalse(overlayEl.opened);
- element._loggedIn = true;
+ 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, '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 +181,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 +229,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 +285,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');
@@ -225,6 +319,31 @@
element.fire('change', {}, {node: selectEl});
});
+ test('don’t reload entire page when patchRange changes', function() {
+ var reloadStub = sinon.stub(element, '_reload',
+ function() { return Promise.resolve(); });
+ var reloadPatchDependentStub = sinon.stub(element,
+ '_reloadPatchNumDependentResources',
+ function() { return Promise.resolve(); });
+
+ var value = {
+ view: 'gr-change-view',
+ patchNum: '1',
+ };
+ element._paramsChanged(value);
+ assert.isTrue(reloadStub.calledOnce);
+ element._initialLoadComplete = true;
+
+ value.basePatchNum = '1';
+ value.patchNum = '2';
+ element._paramsChanged(value);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isTrue(reloadPatchDependentStub.calledOnce);
+
+ reloadStub.restore();
+ reloadPatchDependentStub.restore();
+ });
+
test('change status new', function() {
element._changeNum = '1';
element._patchRange = {
@@ -287,33 +406,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 +443,152 @@
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), '');
+ });
+
+ test('getUrlParameter functionality', function() {
+ var locationStub = sinon.stub(element, '_getLocationSearch');
+
+ locationStub.returns('?test');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('?test2=12&test=3');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?test2');
+ assert.isNull(element._getUrlParameter('test'));
+
+ locationStub.restore();
+ });
+
+ test('revert dialog opened with revert param', function(done) {
+ sinon.stub(element.$.restAPI, 'getLoggedIn', function() {
+ return Promise.resolve(true);
+ });
+
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: element.ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+ };
+
+ var urlParamStub = sinon.stub(element, '_getUrlParameter',
+ function(param) {
+ assert.equal(param, 'revert');
+ urlParamStub.restore();
+ element.$.restAPI.getLoggedIn.restore();
+ return param;
+ });
+
+ var revertDialogStub = sinon.stub(element.$.actions, 'showRevertDialog',
+ function() {
+ revertDialogStub.restore();
+ done();
+ });
+
+ element._maybeShowRevertDialog();
+ });
+
+ suite('scroll related tests', function() {
+ test('document scrolling calls function to set scroll height',
+ function(done) {
+ var scrollStub = sinon.stub(element, '_handleScroll',
+ function() {
+ assert.isTrue(scrollStub.called);
+ document.body.style.height =
+ originalHeight + 'px';
+ scrollStub.restore();
+ done();
+ });
+ var originalHeight = document.body.scrollHeight;
+ document.body.style.height = '10000px';
+ document.body.scrollTop = TEST_SCROLL_TOP_PX;
+ element._handleScroll();
+ });
+
+ test('history is loaded correctly', function() {
+ history.replaceState(
+ {
+ scrollTop: 100,
+ path: location.pathname,
+ },
+ location.pathname);
+
+ var reloadStub = sinon.stub(element, '_reload',
+ function() {
+ // When element is reloaded, ensure that the history
+ // state has the scrollTop set earlier. This will then
+ // be reset.
+ assert.isTrue(history.state.scrollTop == 100);
+ return Promise.resolve({});
+ });
+
+ // simulate reloading component, which is done when route
+ // changes to match a regex of change view type.
+ element._paramsChanged({view: 'gr-change-view'});
+ reloadStub.restore();
+ });
+ });
+
+ suite('reply dialog tests', function() {
+ setup(function() {
+ sinon.stub(element.$.replyDialog, '_draftChanged');
+ });
+
+ test('reply from comment adds quote text', function() {
+ var e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.draft, '> quote text\n\n');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from comment replaces quote text', function() {
+ element.$.replyDialog.draft = '> old quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> old quote text\n\n';
+ var e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.draft, '> quote text\n\n');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from same comment preserves quote text', function() {
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ var e = {detail: {message: {message: 'quote text'}}};
+ element._handleMessageReply(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+
+ test('reply from top of page contains previous draft', function() {
+ var div = document.createElement('div');
+ element.$.replyDialog.draft = '> quote text\n\n some draft text';
+ element.$.replyDialog.quote = '> quote text\n\n';
+ var e = {target: div, preventDefault: sinon.spy()};
+ element._handleReplyTap(e);
+ assert.equal(element.$.replyDialog.draft,
+ '> quote text\n\n some draft text');
+ assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index a7d99a7..e4ab44b 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -14,7 +14,9 @@
limitations under the License.
-->
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
<dom-module id="gr-comment-list">
<template>
@@ -39,8 +41,6 @@
}
.message {
flex: 1;
- white-space: pre-wrap;
- word-wrap: break-word;
}
</style>
<template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
@@ -60,7 +60,10 @@
File comment:
</span>
</a>
- <div class="message">[[comment.message]]</div>
+ <gr-linked-text class="message"
+ pre
+ content="[[comment.message]]"
+ config="[[projectConfig.commentlinks]]"></gr-linked-text>
</div>
</template>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index eaafc447..1adfb01 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -16,15 +16,18 @@
Polymer({
is: 'gr-comment-list',
+ behaviors: [Gerrit.PathListBehavior],
properties: {
changeNum: Number,
comments: Object,
patchNum: Number,
+ projectConfig: Object,
},
_computeFilesFromComments: function(comments) {
- return Object.keys(comments || {}).sort();
+ var arr = Object.keys(comments || {});
+ return arr.sort(this.specialFilePathCompare);
},
_computeFileDiffURL: function(file, changeNum, patchNum) {
@@ -55,6 +58,6 @@
return 'PS' + comment.patch_set + ', ';
}
return '';
- }
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 56a927b..a132d43 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -36,9 +36,21 @@
element = fixture('basic');
});
- test('_computeFilesFromComments', function() {
- var comments = {'file_b.html': [], 'file_c.css': [], 'file_a.js': []};
- var expected = ['file_a.js', 'file_b.html', 'file_c.css'];
+ test('_computeFilesFromComments w/ special file path sorting', function() {
+ var comments = {
+ 'file_b.html': [],
+ 'file_c.css': [],
+ 'file_a.js': [],
+ 'test.cc': [],
+ 'test.h': [],
+ };
+ var expected = [
+ 'file_a.js',
+ 'file_b.html',
+ 'file_c.css',
+ 'test.h',
+ 'test.cc'
+ ];
var actual = element._computeFilesFromComments(comments);
assert.deepEqual(actual, expected);
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-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index 0ce1cbb..e47f14f 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -33,6 +33,10 @@
message: String,
},
+ resetFocus: function() {
+ this.$.messageInput.textarea.focus();
+ },
+
_handleConfirmTap: function(e) {
e.preventDefault();
this.fire('confirm', {reason: this.message}, {bubbles: false});
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.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 979a06a..b668d06 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -55,6 +55,7 @@
</label>
<iron-autogrow-textarea
id="messageInput"
+ max-rows="15"
class="message"
bind-value="{{message}}"></iron-autogrow-textarea>
</div>
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..8f621f0 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,28 +30,22 @@
*/
properties: {
- branch: String,
message: String,
- commitInfo: Object,
},
- populateRevertMessage: function() {
+ populateRevertMessage: function(message, commitHash) {
// 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 + '"';
+ if (!commitHash) {
+ alert('Unable to find the commit hash of this change.');
+ return;
}
- // Add '> ' in front of the original commit text.
- var originalCommitText = this.commitInfo.message.replace(/^/gm, '> ');
+ var revertCommitText = 'This reverts commit ' + commitHash + '.';
this.message = revertTitle + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original issue\'s description:\n' + originalCommitText;
+ revertCommitText + '\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
},
_handleConfirmTap: function(e) {
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..f5672d3 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
@@ -38,27 +38,55 @@
element = fixture('basic');
});
+ test('no match', function() {
+ assert.isNotOk(element.message);
+ var alertStub = sinon.stub(window, 'alert');
+ element.populateRevertMessage('not a commitHash in sight', undefined);
+ assert.isTrue(alertStub.calledOnce);
+ alertStub.restore();
+ });
+
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' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original issue\'s description:\n' +
- '> one line commit';
+ element.populateRevertMessage(
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ var expected = 'Revert "one line commit"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
test('multi line', function() {
assert.isNotOk(element.message);
- element.commitInfo = {message: 'many lines\ncommit\n\nmessage\n'};
+ element.populateRevertMessage(
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
+ var expected = 'Revert "many lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ assert.equal(element.message, expected);
+ });
+
+ test('issue above change id', function() {
assert.isNotOk(element.message);
- element.populateRevertMessage();
- var expected = 'Revert of many lines\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original issue\'s description:\n' +
- '> many lines\n> commit\n> \n> message\n> ';
+ element.populateRevertMessage(
+ 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
+ var expected = 'Revert "much lines"\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
+ 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',
+ 'abcd123');
+ var expected = 'Revert "Revert "one line commit""\n\n' +
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index b1e5c01..7c888da 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -100,7 +100,9 @@
</template>
</ul>
<span class="closeButtonContainer">
- <gr-button link on-tap="_handleCloseTap">Close</gr-button>
+ <gr-button id="closeButton"
+ link
+ on-tap="_handleCloseTap">Close</gr-button>
</span>
</header>
<main hidden$="[[!_schemes.length]]" hidden>
@@ -121,7 +123,7 @@
<div class="patchFiles">
<label>Patch file</label>
<div>
- <a href$="[[_computeDownloadLink(change, patchNum)]]">
+ <a id="download" href$="[[_computeDownloadLink(change, patchNum)]]">
[[_computeDownloadFilename(change, patchNum)]]
</a>
<a href$="[[_computeZipDownloadLink(change, patchNum)]]">
@@ -131,7 +133,7 @@
</div>
<div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden>
<label>Archive</label>
- <div class="archives">
+ <div id="archives" class="archives">
<template is="dom-repeat" items="[[config.archives]]" as="format">
<a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
[[format]]
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 2f3e8e1..b331899 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -58,6 +58,18 @@
}.bind(this));
},
+ focus: function() {
+ this.$.download.focus();
+ },
+
+ getFocusStops: function() {
+ var links = this.$$('#archives').querySelectorAll('a');
+ return {
+ start: this.$.closeButton,
+ end: links[links.length - 1],
+ };
+ },
+
_computeDownloadCommands: function(change, patchNum, _selectedScheme) {
var commandObj;
for (var rev in change.revisions) {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 70e934d..ad5086b 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -115,6 +115,14 @@
};
});
+ test('focuses on first download link', function() {
+ var focusStub = sinon.stub(element.$.download, 'focus');
+ element.focus();
+ flushAsynchronousOperations();
+ assert.isTrue(focusStub.called);
+ focusStub.restore();
+ });
+
test('element visibility', function() {
assert.isFalse(element.$$('ul').hasAttribute('hidden'));
assert.isFalse(element.$$('main').hasAttribute('hidden'));
@@ -154,7 +162,6 @@
assert.isFalse(el.hasAttribute('selected'));
});
});
-
});
suite('gr-download-dialog tests', function() {
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..78e4634 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,11 +14,13 @@
limitations under the License.
-->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../diff/gr-diff/gr-diff.html">
<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-file-list">
@@ -87,7 +89,8 @@
.invisible {
visibility: hidden;
}
- .row:not(.header) .stats {
+ .row:not(.header) .stats,
+ .total-stats {
font-family: var(--monospace-font-family);
}
.added {
@@ -100,6 +103,28 @@
color: #C62828;
font-weight: bold;
}
+ .show-hide {
+ margin-left: .4em;
+ }
+ .fileListButton {
+ margin: .5em;
+ }
+ .totalChanges {
+ justify-content: flex-end;
+ padding-right: 2.6em;
+ text-align: right;
+ }
+ 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;
@@ -128,9 +153,18 @@
/
<gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
/
+ <select
+ id="modeSelect"
+ is="gr-select"
+ bind-value="{{diffViewMode}}"
+ on-change="_handleDropdownChange">
+ <option value="SIDE_BY_SIDE">Side By Side</option>
+ <option value="UNIFIED_DIFF">Unified</option>
+ </select>
+ /
<label>
Diff against
- <select on-change="_handlePatchChange">
+ <select id="patchChange" on-change="_handlePatchChange">
<option value="PARENT">Base</option>
<template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum">
<option
@@ -142,11 +176,15 @@
</label>
</div>
</header>
- <template is="dom-repeat" items="[[_files]]" as="file">
+ <template is="dom-repeat"
+ items="[[_shownFiles]]"
+ as="file"
+ initial-count="[[_fileListIncrement]]">
<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 +206,18 @@
<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$="[[!file.__expanded]]"
+ expanded="[[file.__expanded]]"
project="[[change.project]]"
commit="[[change.current_revision]]"
change-num="[[changeNum]]"
@@ -177,13 +225,31 @@
path="[[file.__path]]"
prefs="[[_diffPrefs]]"
project-config="[[projectConfig]]"
- view-mode="[[_userPrefs.diff_view]]"></gr-diff>
+ view-mode="[[_diffMode]]"></gr-diff>
</template>
+ <div class="row totalChanges">
+ <div class="total-stats" hidden$="[[_hideChangeTotals]]">
+ <span class="added">+[[_patchChange.inserted]]</span>
+ <span class="removed">-[[_patchChange.deleted]]</span>
+ </div>
+ </div>
+ <gr-button
+ class="fileListButton"
+ id="incrementButton"
+ hidden$="[[_computeFileListButtonHidden(_numFilesShown, _files)]]"
+ link on-tap="_incrementNumFilesShown">
+ [[_computeIncrementText(_numFilesShown, _files)]]
+ </gr-button>
+ <gr-button
+ class="fileListButton"
+ id="showAllButton"
+ hidden$="[[_computeFileListButtonHidden(_numFilesShown, _files)]]"
+ link on-tap="_showAllFiles">
+ [[_computeShowAllText(_files)]]
+ </gr-button>
<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..478faf5 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
@@ -16,6 +16,11 @@
var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+ var DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ };
+
Polymer({
is: 'gr-file-list',
@@ -27,7 +32,6 @@
drafts: Object,
revisions: Object,
projectConfig: Object,
- topMargin: Number,
selectedIndex: {
type: Number,
notify: true,
@@ -37,10 +41,14 @@
value: function() { return document.body; },
},
change: Object,
-
+ diffViewMode: {
+ type: String,
+ notify: true,
+ },
_files: {
type: Array,
observer: '_filesChanged',
+ value: function() { return []; },
},
_loggedIn: {
type: Boolean,
@@ -54,19 +62,43 @@
_userPrefs: Object,
_localPrefs: Object,
_showInlineDiffs: Boolean,
+ _numFilesShown: {
+ type: Number,
+ value: 75,
+ },
+ _patchChange: {
+ type: Object,
+ computed: '_calculatePatchChange(_files)',
+ },
+ _fileListIncrement: {
+ type: Number,
+ readOnly: true,
+ value: 75,
+ },
+ _hideChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideChangeTotals(_patchChange)',
+ },
+ _shownFiles: {
+ type: Array,
+ computed: '_computeFilesShown(_numFilesShown, _files.*)',
+ },
+ _diffMode: {
+ type: String,
+ computed: '_getDiffViewMode(diffViewMode, _userPrefs)',
+ },
},
behaviors: [
Gerrit.KeyboardShortcutBehavior,
+ Gerrit.URLEncodingBehavior,
],
reload: function() {
if (!this.changeNum || !this.patchRange.patchNum) {
return Promise.resolve();
}
-
this._collapseAllDiffs();
-
var promises = [];
var _this = this;
@@ -88,8 +120,17 @@
this._diffPrefs = prefs;
}.bind(this)));
+ // Initialize with user's diff mode preference. Default to
+ // SIDE_BY_SIDE in the meantime.
+ var setDiffViewMode = this.diffViewMode === null;
+ if (setDiffViewMode) {
+ this.set('diffViewMode', DiffViewMode.SIDE_BY_SIDE);
+ }
promises.push(this._getPreferences().then(function(prefs) {
this._userPrefs = prefs;
+ if (setDiffViewMode) {
+ this.set('diffViewMode', prefs.diff_view);
+ }
}.bind(this)));
},
@@ -97,6 +138,22 @@
return Polymer.dom(this.root).querySelectorAll('gr-diff');
},
+ _calculatePatchChange: function(files) {
+ var filesNoCommitMsg = files.filter(function(files) {
+ return files.__path !== '/COMMIT_MSG';
+ });
+
+ return filesNoCommitMsg.reduce(function(acc, obj) {
+ var inserted = obj.lines_inserted ? obj.lines_inserted : 0;
+ var deleted = obj.lines_deleted ? obj.lines_deleted : 0;
+
+ return {
+ inserted: acc.inserted + inserted,
+ deleted: acc.deleted + deleted,
+ };
+ }, {inserted: 0, deleted: 0});
+ },
+
_getDiffPreferences: function() {
return this.$.restAPI.getDiffPreferences();
},
@@ -121,10 +178,16 @@
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) + '/' +
- encodeURIComponent(this._patchRangeStr(this.patchRange)));
+ var patchRange = Object.assign({}, this.patchRange);
+ patchRange.basePatchNum = Polymer.dom(e).rootTarget.value;
+ page.show(this.encodeURL('/c/' + this.changeNum + '/' +
+ this._patchRangeStr(patchRange), true));
},
_forEachDiff: function(fn) {
@@ -134,12 +197,19 @@
}
},
+ /**
+ * Until upgrading to Polymer 2.0, manual management of reflection between
+ * _shownFiles and _files is necessary. Performance of linkPaths is very
+ * poor.
+ */
_expandAllDiffs: function(e) {
this._showInlineDiffs = true;
- this._forEachDiff(function(diff) {
- diff.hidden = false;
- diff.reload();
- });
+ for (var i = 0; i < this._files.length; i++) {
+ if (i < this._shownFiles.length) {
+ this.set(['_shownFiles', i, '__expanded'], true);
+ }
+ this.set(['_files', i, '__expanded'], true);
+ }
if (e && e.target) {
e.target.blur();
}
@@ -147,9 +217,12 @@
_collapseAllDiffs: function(e) {
this._showInlineDiffs = false;
- this._forEachDiff(function(diff) {
- diff.hidden = true;
- });
+ for (var i = 0; i < this._files.length; i++) {
+ if (i < this._shownFiles.length) {
+ this.set(['_shownFiles', i, '__expanded'], false);
+ }
+ this.set(['_files', i, '__expanded'], false);
+ }
this.$.cursor.handleDiffUpdate();
if (e && e.target) {
e.target.blur();
@@ -212,12 +285,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 +310,18 @@
}
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;
+ // Until Polymer 2.0, manual management of reflection between _files
+ // and _shownFiles is necessary.
+ this.set(['_shownFiles', this.selectedIndex, '__expanded'],
+ !expanded);
+ this.set(['_files', this.selectedIndex, '__expanded'], !expanded);
+ }
break;
case 40: // down
case 74: // 'j'
@@ -243,7 +330,7 @@
this.$.cursor.moveDown();
} else {
this.selectedIndex =
- Math.min(this._files.length - 1, this.selectedIndex + 1);
+ Math.min(this._numFilesShown, this.selectedIndex + 1);
this._scrollToSelectedFile();
}
break;
@@ -352,7 +439,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;
}
@@ -360,6 +447,10 @@
window.scrollTo(0, top - document.body.clientHeight / 2);
},
+ _shouldHideChangeTotals: function(_patchChange) {
+ return (_patchChange.inserted === 0 && _patchChange.deleted === 0);
+ },
+
_computeFileSelected: function(index, selectedIndex) {
return index === selectedIndex;
},
@@ -369,12 +460,8 @@
},
_computeDiffURL: function(changeNum, patchRange, path) {
- return '/c/' +
- encodeURIComponent(changeNum) +
- '/' +
- encodeURIComponent(this._patchRangeStr(patchRange)) +
- '/' +
- path;
+ return this.encodeURL('/c/' + changeNum + '/' +
+ this._patchRangeStr(patchRange) + '/' + path, true);
},
_patchRangeStr: function(patchRange) {
@@ -395,6 +482,14 @@
return classes.join(' ');
},
+ _computeShowHideText: function(expanded) {
+ return expanded ? 'â–¼' : 'â—€';
+ },
+
+ _computeFilesShown: function(numFilesShown, files) {
+ return files.base.slice(0, numFilesShown);
+ },
+
_filesChanged: function() {
this.async(function() {
var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
@@ -404,5 +499,56 @@
['diffs', 0, this.$.cursor.diffs.length].concat(diffElements));
}.bind(this), 1);
},
+
+ _incrementNumFilesShown: function() {
+ this._numFilesShown += this._fileListIncrement;
+ },
+
+ _computeFileListButtonHidden: function(numFilesShown, files) {
+ return numFilesShown >= files.length;
+ },
+
+ _computeIncrementText: function(numFilesShown, files) {
+ if (!files) { return ''; }
+ var text =
+ Math.min(this._fileListIncrement, files.length - numFilesShown);
+ return 'Show ' + text + ' more';
+ },
+
+ _computeShowAllText: function(files) {
+ if (!files) { return ''; }
+ return 'Show all ' + files.length + ' files';
+ },
+
+ _showAllFiles: function() {
+ this._numFilesShown = this._files.length;
+ },
+
+ /**
+ * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
+ * the current state.
+ *
+ * The expected behavior is to use the mode specified in the user's
+ * preferences unless they have manually chosen the alternative view. If the
+ * user navigates up to the change view, it should clear this choice and
+ * revert to the preference the next time a diff is viewed.
+ *
+ * Use side-by-side if the user is not logged in.
+ *
+ * @return {String}
+ */
+ _getDiffViewMode: function() {
+ if (this.diffViewMode) {
+ return this.diffViewMode;
+ } else if (this._userPrefs && this._userPrefs.diff_view) {
+ return this.diffViewMode = this._userPrefs.diff_view;
+ }
+
+ return DiffViewMode.SIDE_BY_SIDE;
+ },
+
+ _handleDropdownChange: function(e) {
+ e.target.blur();
+ },
});
})();
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..b298fef 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
@@ -32,19 +32,36 @@
</template>
</test-fixture>
+<test-fixture id="blank">
+ <template>
+ <div></div>
+ </template>
+</test-fixture>
+
<script>
suite('gr-file-list tests', function() {
var element;
+ var sandbox;
setup(function() {
+ sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn: function() { return Promise.resolve(true); },
+ getPreferences: function() { return Promise.resolve({}); },
+ fetchJSON: function() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat: function() { return Promise.resolve(''); }
});
element = fixture('basic');
});
+ teardown(function() {
+ sandbox.restore();
+ });
+
test('get file list', function(done) {
- var getChangeFilesStub = sinon.stub(element.$.restAPI, 'getChangeFiles',
+ var getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles',
function() {
return Promise.resolve({
'/COMMIT_MSG': {lines_inserted: 9},
@@ -60,16 +77,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 +97,120 @@
});
});
- 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);
+ test('calculate totals for patch number', function() {
+ element._files = [
+ {__path: '/COMMIT_MSG', lines_inserted: 9},
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with a commit message that isn't the first file.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: '/COMMIT_MSG', lines_inserted: 9},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with no commit message.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with files missing either lines_inserted or lines_deleted.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1},
+ {__path: 'myfile.txt', lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 1, deleted: 1});
});
- test('keyboard shortcuts', function() {
- var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
- MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I'
- assert.isTrue(toggleInlineDiffsStub.calledOnce);
- toggleInlineDiffsStub.restore();
+ 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;
+ });
- 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('toggle left diff via shortcut', function() {
+ var toggleLeftDiffStub = sandbox.stub();
+ // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
+ // https://github.com/sinonjs/sinon/issues/781
+ var diffsStub = sinon.stub(element, 'diffs', {get: function() {
+ return [{toggleLeftDiff: toggleLeftDiffStub}];
+ }});
+ MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift'); // 'A'
+ assert.isTrue(toggleLeftDiffStub.calledOnce);
+ diffsStub.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'
+ test('keyboard shortcuts', function() {
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ '.row:not(.header)');
+ assert.equal(elementItems.length, 4);
+ 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 = sandbox.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'
+ flushAsynchronousOperations();
+ assert.isFalse(element.diffs[0].hasAttribute('hidden'));
+ MockInteractions.pressAndReleaseKeyOn(element, 73); // 'I'
+ flushAsynchronousOperations();
+ assert.isTrue(element.diffs[0].hasAttribute('hidden'));
+ element.selectedIndex = 1;
+ MockInteractions.pressAndReleaseKeyOn(element, 73); // 'I'
+ flushAsynchronousOperations();
+ assert.isFalse(element.diffs[1].hasAttribute('hidden'));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I'
+ flushAsynchronousOperations();
+ for (var index in element.diffs) {
+ assert.isFalse(element.diffs[index].hasAttribute('hidden'));
+ }
+ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I'
+ flushAsynchronousOperations();
+ for (var index in element.diffs) {
+ assert.isTrue(element.diffs[index].hasAttribute('hidden'));
+ }
+ });
});
test('comment filtering', function() {
@@ -187,9 +265,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,15 +280,18 @@
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);
assert.isTrue(myFile.checked);
- var saveStub = sinon.stub(element, '_saveReviewedState',
+ var saveStub = sandbox.stub(element, '_saveReviewedState',
function() { return Promise.resolve(); });
MockInteractions.tap(commitMsg);
@@ -241,7 +322,7 @@
});
test('diff against dropdown', function(done) {
- var showStub = sinon.stub(page, 'show');
+ var showStub = sandbox.stub(page, 'show');
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -253,7 +334,7 @@
rev3: {_number: 3},
};
flush(function() {
- var selectEl = element.$$('select');
+ var selectEl = element.$.patchChange;
assert.equal(selectEl.value, 'PARENT');
assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled'));
selectEl.addEventListener('change', function() {
@@ -267,5 +348,91 @@
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 = sandbox.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('path should be properly escaped', function() {
+ element._files = [
+ {__path: 'foo bar/my+file.txt%'},
+ ];
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ flushAsynchronousOperations();
+ // Slashes should be preserved, and spaces should be translated to `+`.
+ // @see Issue 4255 regarding double-encoding.
+ // @see Issue 4577 regarding more readable URLs.
+ assert.equal(
+ element.$$('a').getAttribute('href'),
+ '/c/42/2/foo+bar/my%252Bfile.txt%2525');
+ });
+
+ test('diff mode correctly toggles the diffs', function() {
+ element._files = [
+ {__path: 'myfile.txt', __expanded: false},
+ ];
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.selectedIndex = 0;
+ flushAsynchronousOperations();
+ var select = element.$.modeSelect;
+ var diffDisplay = element.diffs[0];
+ element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
+ assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE');
+ element.set('diffViewMode', 'UNIFIED_DIFF');
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+ assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
+ });
+
+ test('diff mode selector initializes from preferences', function() {
+ var resolvePrefs;
+ var prefsPromise = new Promise(function(resolve) {
+ resolvePrefs = resolve;
+ });
+ sandbox.stub(element, '_getPreferences').returns(prefsPromise);
+
+ // Attach a new gr-file-list so we can intercept the preferences fetch.
+ var view = document.createElement('gr-file-list');
+ var select = view.$.modeSelect;
+ 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');
+ document.getElementById('blank').restore();
+ });
});
</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..287e9e4 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,19 @@
}
.showAvatar.collapsed .contentContainer {
margin-left: calc(var(--default-horizontal-margin) + 1.75em);
- padding: .75em 2em .75em 0;
+ }
+ .showAvatar.collapsed .contentContainer,
+ .hideAvatar.collapsed .contentContainer {
+ margin-right: calc(var(--default-horizontal-margin) + 2.25em);
}
.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 0;
}
.collapsed gr-avatar {
top: .5em;
@@ -114,7 +121,8 @@
<gr-comment-list
comments="[[comments]]"
change-num="[[changeNum]]"
- patch-num="[[message._revision_number]]"></gr-comment-list>
+ patch-num="[[message._revision_number]]"
+ project-config="[[projectConfig]]"></gr-comment-list>
</div>
<a class="date" href$="[[_computeMessageHash(message)]]" on-tap="_handleLinkTap">
<gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
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..643d55d 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
@@ -37,6 +37,7 @@
setup(function() {
element = fixture('basic');
+ sinon.stub(element.$.restAPI, 'getLoggedIn').returns(true);
});
test('reply event', function(done) {
@@ -85,5 +86,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-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 5d2d20d..adde31e 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -74,59 +74,61 @@
}
</style>
<div hidden$="[[!_loading]]">Loading...</div>
- <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
- <h4>Relation chain</h4>
- <template
- is="dom-repeat"
- items="[[_relatedResponse.changes]]"
- as="related">
- <div class$="[[_computeChangeContainerClass(change, related)]]">
- <a href$="[[_computeChangeURL(related._change_number, related._revision_number)]]"
- class$="[[_computeLinkClass(related)]]">
- [[related.commit.subject]]
+ <div hidden$="[[_loading]]">
+ <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
+ <h4>Relation chain</h4>
+ <template
+ is="dom-repeat"
+ items="[[_relatedResponse.changes]]"
+ as="related">
+ <div class$="[[_computeChangeContainerClass(change, related)]]">
+ <a href$="[[_computeChangeURL(related._change_number, related._revision_number)]]"
+ class$="[[_computeLinkClass(related)]]">
+ [[related.commit.subject]]
+ </a>
+ <span class$="[[_computeChangeStatusClass(related)]]">
+ ([[_computeChangeStatus(related)]])
+ </span>
+ </div>
+ </template>
+ </section>
+ <section hidden$="[[!_submittedTogether.length]]" hidden>
+ <h4>Submitted together</h4>
+ <template is="dom-repeat" items="[[_submittedTogether]]" as="change">
+ <a href$="[[_computeChangeURL(change._number)]]"
+ class$="[[_computeLinkClass(change)]]">
+ [[change.project]]: [[change.branch]]: [[change.subject]]
</a>
- <span class$="[[_computeChangeStatusClass(related)]]">
- ([[_computeChangeStatus(related)]])
- </span>
- </div>
- </template>
- </section>
- <section hidden$="[[!_submittedTogether.length]]" hidden>
- <h4>Submitted together</h4>
- <template is="dom-repeat" items="[[_submittedTogether]]" as="change">
- <a href$="[[_computeChangeURL(change._number)]]"
- class$="[[_computeLinkClass(change)]]">
- [[change.project]]: [[change.branch]]: [[change.subject]]
- </a>
- </template>
- </section>
- <section hidden$="[[!_sameTopic.length]]" hidden>
- <h4>Same topic</h4>
- <template is="dom-repeat" items="[[_sameTopic]]" as="change">
- <a href$="[[_computeChangeURL(change._number)]]"
- class$="[[_computeLinkClass(change)]]">
- [[change.project]]: [[change.branch]]: [[change.subject]]
- </a>
- </template>
- </section>
- <section hidden$="[[!_conflicts.length]]" hidden>
- <h4>Merge conflicts</h4>
- <template is="dom-repeat" items="[[_conflicts]]" as="change">
- <a href$="[[_computeChangeURL(change._number)]]"
- class$="[[_computeLinkClass(change)]]">
- [[change.subject]]
- </a>
- </template>
- </section>
- <section hidden$="[[!_cherryPicks.length]]" hidden>
- <h4>Cherry picks</h4>
- <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
- <a href$="[[_computeChangeURL(change._number)]]"
- class$="[[_computeLinkClass(change)]]">
- [[change.subject]]
- </a>
- </template>
- </section>
+ </template>
+ </section>
+ <section hidden$="[[!_sameTopic.length]]" hidden>
+ <h4>Same topic</h4>
+ <template is="dom-repeat" items="[[_sameTopic]]" as="change">
+ <a href$="[[_computeChangeURL(change._number)]]"
+ class$="[[_computeLinkClass(change)]]">
+ [[change.project]]: [[change.branch]]: [[change.subject]]
+ </a>
+ </template>
+ </section>
+ <section hidden$="[[!_conflicts.length]]" hidden>
+ <h4>Merge conflicts</h4>
+ <template is="dom-repeat" items="[[_conflicts]]" as="change">
+ <a href$="[[_computeChangeURL(change._number)]]"
+ class$="[[_computeLinkClass(change)]]">
+ [[change.subject]]
+ </a>
+ </template>
+ </section>
+ <section hidden$="[[!_cherryPicks.length]]" hidden>
+ <h4>Cherry picks</h4>
+ <template is="dom-repeat" items="[[_cherryPicks]]" as="change">
+ <a href$="[[_computeChangeURL(change._number)]]"
+ class$="[[_computeLinkClass(change)]]">
+ [[change.subject]]
+ </a>
+ </template>
+ </section>
+ </div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-related-changes-list.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index f4ee53a..cac45c6 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -68,7 +68,7 @@
}.bind(this)),
];
- return this._getServerConfig().then(function(config) {
+ promises.push(this._getServerConfig().then(function(config) {
if (this.change.topic && !config.change.submit_whole_topic) {
return this._getChangesWithSameTopic().then(function(response) {
this._sameTopic = response;
@@ -77,7 +77,9 @@
this._sameTopic = [];
}
return this._sameTopic;
- }.bind(this)).then(Promise.all(promises)).then(function() {
+ }.bind(this)));
+
+ return Promise.all(promises).then(function() {
this._loading = false;
}.bind(this));
},
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..cb6a6f9 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,26 @@
</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 is="dom-if" if="[[_isClosed(change)]]" id="labelDisabled">
+ <div class="labelDisabledMessage">
+ Setting labels are disabled for this change because it has been
+ closed.
+ </div>
</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..a61de4d 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
@@ -23,6 +23,8 @@
REVIEWERS: 'reviewers',
};
+ var CLOSED_CHANGE_STATUSES = ['ABANDONED', 'MERGED'];
+
Polymer({
is: 'gr-reply-dialog',
@@ -48,7 +50,6 @@
properties: {
change: Object,
patchNum: String,
- revisions: Object,
disabled: {
type: Boolean,
value: false,
@@ -59,6 +60,10 @@
value: '',
observer: '_draftChanged',
},
+ quote: {
+ type: String,
+ value: ''
+ },
diffDrafts: Object,
filterReviewerSuggestion: {
type: Function,
@@ -153,12 +158,21 @@
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);
- obj.labels[label] = selectedVal;
+
+ // Only send the selection if the user changed it.
+ var prevVal = this._getVoteForAccount(this.labels, label,
+ this._account);
+ if (prevVal !== null) {
+ prevVal = parseInt(prevVal, 10);
+ }
+ if (selectedVal !== prevVal) {
+ obj.labels[label] = selectedVal;
+ }
}
if (this.draft != null) {
obj.message = this.draft;
@@ -259,14 +273,8 @@
}.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;
+ _isClosed: function(change) {
+ return CLOSED_CHANGE_STATUSES.indexOf(change.status) !== -1;
},
_computeHideDraftList: function(drafts) {
@@ -291,23 +299,22 @@
return Object.keys(labelsObj).sort();
},
- _computeIndexOfLabelValue: function(
- labels, permittedLabels, labelName, account) {
- var t = labels[labelName];
- if (!t) { return null; }
- var labelValue = t.default_value;
-
- // Is there an existing vote for the current user? If so, use that.
+ _getVoteForAccount: function(labels, labelName, account) {
var votes = labels[labelName];
if (votes.all && votes.all.length > 0) {
for (var i = 0; i < votes.all.length; i++) {
if (votes.all[i]._account_id == account._account_id) {
- labelValue = votes.all[i].value;
- break;
+ return votes.all[i].value;
}
}
}
+ return null;
+ },
+ _computeIndexOfLabelValue: function(
+ labels, permittedLabels, labelName, account) {
+ if (!labels[labelName]) { return null; }
+ var labelValue = this._getVoteForAccount(labels, labelName, account);
var len = permittedLabels[labelName] != null ?
permittedLabels[labelName].length : 0;
for (var i = 0; i < len; i++) {
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..6c77afb 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();
});
@@ -256,6 +244,18 @@
}).then(done);
});
+ test('message disabled dialogue appears for closed change', function() {
+ element.change = {status: 'ABANDONED'};
+ flushAsynchronousOperations();
+ assert.isOk(element.$$('.labelDisabledMessage'));
+ });
+
+ test('message disabled dialogue does not appear for open change',
+ function() {
+ element.change = {status: 'NEW'};
+ assert.isNotOk(element.$$('.labelDisabledMessage'));
+ });
+
test('_getStorageLocation', function() {
var actual = element._getStorageLocation();
assert.equal(actual.changeNum, changeNum);
@@ -312,7 +312,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() {
@@ -389,5 +392,28 @@
assert.strictEqual(
element._chooseFocusTarget(), element.FocusTarget.BODY);
});
+
+ test('only send labels that have changed', function(done) {
+ flush(function() {
+ var saveReviewStub = sinon.stub(element, '_saveReview',
+ function(review) {
+ assert.deepEqual(review.labels, {Verified: -1});
+ return Promise.resolve({ok: true});
+ });
+
+ element.addEventListener('send', function() {
+ saveReviewStub.restore();
+ done();
+ });
+ // Without wrapping this test in flush(), the below two calls to
+ // MockInteractions.tap() cause a race in some situations in shadow DOM.
+ // The send button can be tapped before the others, causing the test to
+ // fail.
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Verified"] > ' +
+ 'gr-button[data-value="-1"]'));
+ MockInteractions.tap(element.$$('.send'));
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 7a9c4f9..a38152a 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -18,6 +18,7 @@
var CHECK_SIGN_IN_INTERVAL_MS = 60000;
var SIGN_IN_WIDTH_PX = 690;
var SIGN_IN_HEIGHT_PX = 500;
+ var TOO_MANY_FILES = 'too many files to find conflicts';
Polymer({
is: 'gr-error-manager',
@@ -38,6 +39,10 @@
this.unlisten(document, 'network-error', '_handleNetworkError');
},
+ _shouldSupressError: function(msg) {
+ return msg.indexOf(TOO_MANY_FILES) > -1;
+ },
+
_handleServerError: function(e) {
if (e.detail.response.status === 403) {
this._getLoggedIn().then(function(loggedIn) {
@@ -49,7 +54,9 @@
}.bind(this));
} else {
e.detail.response.text().then(function(text) {
- this._showAlert('Server error: ' + text);
+ if (!this._shouldSupressError(text)) {
+ this._showAlert('Server error: ' + text);
+ }
}.bind(this));
}
},
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index f633a7e..44cbde0 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -70,6 +70,20 @@
});
});
+ test('suppress TOO_MANY_FILES error', function(done) {
+ var showAlertStub = sandbox.stub(element, '_showAlert');
+ var textSpy = sandbox.spy(function() {
+ return Promise.resolve('too many files to find conflicts');
+ });
+ element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+ assert.isTrue(textSpy.called);
+ textSpy.lastCall.returnValue.then(function() {
+ assert.isFalse(showAlertStub.called);
+ done();
+ });
+ });
+
test('show network error', function(done) {
var consoleErrorStub = sandbox.stub(console, 'error');
var showAlertStub = sandbox.stub(element, '_showAlert');
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..d10567c
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+
+<dom-module id="gr-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..32dd687
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,153 @@
+// 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+\/.+$/;
+
+ var pending = [];
+
+ 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() {
+ var report = (Gerrit._arePluginsLoaded() && !pending.length) ?
+ this.defaultReporter : this.cachingReporter;
+ report.apply(this, arguments);
+ },
+
+ defaultReporter: function(type, category, eventName, eventValue) {
+ var detail = {
+ type: type,
+ category: category,
+ name: eventName,
+ value: eventValue,
+ };
+ document.dispatchEvent(new CustomEvent(type, {detail: detail}));
+ console.log(eventName + ': ' + eventValue);
+ },
+
+ cachingReporter: function(type, category, eventName, eventValue) {
+ if (Gerrit._arePluginsLoaded()) {
+ if (pending.length) {
+ pending.splice(0).forEach(function(args) {
+ this.reporter.apply(this, args);
+ }, this);
+ }
+ this.reporter(type, category, eventName, eventValue);
+ } else {
+ pending.push([type, category, 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);
+ },
+
+ pluginsLoaded: function() {
+ this.timeEnd('PluginsLoaded');
+ },
+
+ _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..082f81b
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,174 @@
+<!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('plugins', function() {
+ setup(function() {
+ element.reporter.restore();
+ sandbox.stub(element, 'defaultReporter');
+ sandbox.stub(Gerrit, '_arePluginsLoaded');
+ });
+
+ test('pluginsLoaded reports time', function() {
+ Gerrit._arePluginsLoaded.returns(true);
+ var nowStub = sinon.stub(element, 'now').returns(42);
+ element.pluginsLoaded();
+ assert.isTrue(element.defaultReporter.calledWithExactly(
+ 'timing-report', 'UI Latency', 'PluginsLoaded', 42
+ ));
+ });
+
+ test('caches reports if plugins are not loaded', function() {
+ Gerrit._arePluginsLoaded.returns(false);
+ element.timeEnd('foo');
+ assert.isFalse(element.defaultReporter.called);
+ });
+
+ test('reports if plugins are loaded', function() {
+ Gerrit._arePluginsLoaded.returns(true);
+ element.timeEnd('foo');
+ assert.isTrue(element.defaultReporter.called);
+ });
+
+ test('reports cached events preserving order', function() {
+ Gerrit._arePluginsLoaded.returns(false);
+ element.timeEnd('foo');
+ Gerrit._arePluginsLoaded.returns(true);
+ element.timeEnd('bar');
+ assert.isTrue(element.defaultReporter.firstCall.calledWith(
+ 'timing-report', 'UI Latency', 'foo'
+ ));
+ assert.isTrue(element.defaultReporter.secondCall.calledWith(
+ 'timing-report', 'UI Latency', 'bar'
+ ));
+ });
+ });
+
+ 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..05b191c 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;
@@ -27,7 +36,11 @@
// Fire asynchronously so that the URL is changed by the time the event
// is processed.
app.async(function() {
- app.fire('location-change');
+ app.fire('location-change', {
+ hash: window.location.hash,
+ pathname: window.location.pathname,
+ });
+ reporting.locationChanged();
}, 1);
next();
});
@@ -46,6 +59,11 @@
}
// For backward compatibility with GWT links.
if (data.hash) {
+ // In certain login flows the server may redirect to a hash without
+ // a leading slash, which page.js doesn't handle correctly.
+ if (data.hash[0] !== '/') {
+ data.hash = '/' + data.hash
+ }
page.redirect(data.hash);
return;
}
@@ -124,14 +142,31 @@
};
// Don't allow diffing the same patch number against itself.
if (params.basePatchNum === params.patchNum) {
+ // TODO(kaspern): Utilize gr-url-encoding-behavior.html when the router
+ // is replaced with a Polymer counterpart.
+ // @see Issue 4255 regarding double-encoding.
+ var path = encodeURIComponent(encodeURIComponent(path));
+ // @see Issue 4577 regarding more readable URLs.
+ path = path.replace(/%252F/g, '/');
+ path = path.replace(/%2520/g, '+');
+
page.redirect('/c/' +
encodeURIComponent(params.changeNum) +
'/' +
encodeURIComponent(params.patchNum) +
'/' +
- encodeURIComponent(params.path));
+ path);
return;
}
+
+ // Check if path has an '@' which indicates it was using GWT style line
+ // numbers. Even if the filename had an '@' in it, it would have already
+ // been URI encoded. Redirect to hash version of path.
+ if (ctx.path.indexOf('@') !== -1) {
+ page.redirect(ctx.path.replace('@', '#'));
+ return;
+ }
+
normalizePatchRangeParams(params);
app.params = 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..a79e830 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
@@ -14,10 +14,13 @@
limitations under the License.
-->
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-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>
@@ -51,8 +54,10 @@
on-commit="_handleInputCommit"
allowNonSuggestedValues
multi
- borderless></gr-autocomplete>
+ borderless
+ tab-complete-without-commit></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..2818121 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,11 +78,16 @@
'tr',
];
+ var MAX_AUTOCOMPLETE_RESULTS = 10;
+
+ var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
Polymer({
is: 'gr-search-bar',
behaviors: [
Gerrit.KeyboardShortcutBehavior,
+ Gerrit.URLEncodingBehavior,
],
listeners: {
@@ -117,41 +122,174 @@
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) {
+ page.show('/q/' + this.encodeURL(this._inputVal, false));
+ }
},
- // 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 || !operators.length) { return []; }
+ return operators
+ // Prioritize results that start with the input.
+ .sort(function(a, b) {
+ var aContains = a.toLowerCase().indexOf(trimmedInput);
+ var bContains = b.toLowerCase().indexOf(trimmedInput);
+ if (aContains === bContains) {
+ return a.localeCompare(b);
+ }
+ if (aContains === -1) {
+ return 1;
+ }
+ if (bContains === -1) {
+ return -1;
+ }
+ return aContains - bContains;
+ })
+ // 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..696efcd 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,110 @@
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,
+ gerrit: 0,
+ gerrittest: 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();
+ });
+ });
+
+ test('Autocomplete doesnt override exact matches to input',
+ function(done) {
+ return element._getSearchSuggestions('ownerin:gerrit')
+ .then(function(suggestions) {
+ assert.equal(suggestions[0].value, 'ownerin:gerrit');
+ 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..3ad7c74 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>
@@ -55,10 +58,20 @@
SYNTAX: 'Diff Syntax Render',
};
+ // If any line of the diff is more than the character limit, then disable
+ // syntax highlighting for the entire file.
+ var SYNTAX_MAX_LINE_LENGTH = 500;
+
Polymer({
is: 'gr-diff-builder',
/**
+ * Fired when the diff begins rendering.
+ *
+ * @event render-start
+ */
+
+ /**
* Fired when the diff is rendered.
*
* @event render
@@ -74,6 +87,7 @@
_builder: Object,
_groups: Array,
_layers: Array,
+ _showTabs: Boolean,
},
get diffElement() {
@@ -89,6 +103,7 @@
this._layers = [
this.$.syntaxLayer,
this._createIntralineLayer(),
+ this._createTabIndicatorLayer(),
this.$.rangeLayer,
];
@@ -99,6 +114,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 +127,25 @@
this._clearDiffContent();
- console.time(TimingLabel.TOTAL);
- console.time(TimingLabel.CONTENT);
+ var reporting = this.$.reporting;
+
+ reporting.time(TimingLabel.TOTAL);
+ reporting.time(TimingLabel.CONTENT);
+ this.fire('render-start');
return this.$.processor.process(this.diff.content).then(function() {
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
- console.timeEnd(TimingLabel.CONTENT);
- console.time(TimingLabel.SYNTAX);
+
+ if (this._anyLineTooLong()) {
+ this.$.syntaxLayer.enabled = false;
+ }
+
+ 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 +349,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
@@ -343,6 +392,18 @@
Polymer.dom.flush();
parent.removeChild(thread);
},
+
+ /**
+ * @returns {Boolean} whether any of the lines in _groups are longer
+ * than SYNTAX_MAX_LINE_LENGTH.
+ */
+ _anyLineTooLong: function() {
+ return this._groups.reduce(function(acc, group) {
+ return acc || group.lines.reduce(function(acc, line) {
+ return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH;
+ }, false);
+ }, false);
+ },
});
})();
</script>
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..c524f7f 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
@@ -163,8 +163,8 @@
};
/**
- * Re-renders the DIV.contentText alement for the given side and range of diff
- * content.
+ * Re-renders the DIV.contentText elements for the given side and range of
+ * diff content.
*/
GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
var lines = [];
@@ -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..341e959 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;
@@ -484,6 +613,31 @@
assert.strictEqual(sections[0], section[0]);
assert.strictEqual(sections[1], section[1]);
});
+
+ test('render-start and render are fired', function() {
+ var fireStub = sinon.stub(element, 'fire');
+ element.render({left: [], right: []}, {});
+ flush(function() {
+ assert.isTrue(fireStub.calledWithExactly('render-start'));
+ assert.isTrue(fireStub.calledWithExactly('render'));
+ done();
+ });
+ });
+
+ test('rendering normal-sized diff does not disable syntax', function() {
+ flush(function() {
+ assert.isTrue(element.$.syntaxLayer.enabled);
+ });
+ });
+
+ test('rendering large diff disables syntax', function() {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff.content[0].a = new Array(501).join('*');
+
+ flush(function() {
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+ });
});
suite('mock-diff', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index c3b6233..864712d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -44,15 +44,20 @@
padding: .5em .7em;
}
.header {
+ cursor: pointer;
display: flex;
- padding-bottom: 0;
font-family: 'Open Sans', sans-serif;
+ padding-bottom: 0;
}
- .headerLeft {
+ .headerMiddle {
+ color: #666;
flex: 1;
+ overflow: hidden;
}
.authorName,
.draftLabel {
+ display: block;
+ float: left;
font-weight: bold;
}
.draftLabel {
@@ -62,6 +67,7 @@
.date {
justify-content: flex-end;
margin-left: 5px;
+ white-space: nowrap;
}
a.date:link,
a.date:visited {
@@ -113,19 +119,62 @@
background-color: #fff;
display: block;
}
+ .show-hide {
+ margin-left: .4em;
+ }
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ color: #000;
+ cursor: pointer;
+ display: block;
+ font-size: .8em;
+ height: 1.1em;
+ margin-top: .1em;
+ }
+ #container .collapsedContent {
+ display: none;
+ }
+ #container.collapsed {
+ padding-bottom: 3px;
+ }
+ #container.collapsed .collapsedContent {
+ display: block;
+ overflow: hidden;
+ padding-left: 5px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #container.collapsed .actions,
+ #container.collapsed gr-linked-text,
+ #container.collapsed iron-autogrow-textarea {
+ display: none;
+ }
</style>
<div id="container"
class="container"
on-mouseenter="_handleMouseEnter"
on-mouseleave="_handleMouseLeave">
- <div class="header" id="header">
+ <div class="header" id="header" on-click="_handleToggleCollapsed">
<div class="headerLeft">
<span class="authorName">[[comment.author.name]]</span>
<span class="draftLabel">DRAFT</span>
</div>
+ <div class="headerMiddle">
+ <span class="collapsedContent">[[comment.message]]</span>
+ </div>
<a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
<gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
</a>
+ <div class="show-hide">
+ <label class="show-hide">
+ <input type="checkbox" class="show-hide"
+ checked$="[[_commentCollapsed]]"
+ on-change="_handleToggleCollapsed">
+ [[_computeShowHideText(_commentCollapsed)]]
+ </label>
+ </div>
</div>
<iron-autogrow-textarea
id="editTextarea"
@@ -137,6 +186,7 @@
<gr-linked-text class="message"
pre
content="[[comment.message]]"
+ collapsed="[[_commentCollapsed]]"
config="[[projectConfig.commentlinks]]"></gr-linked-text>
<div class="actions" hidden$="[[!showActions]]">
<gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
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..791f949 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
@@ -81,6 +81,11 @@
},
patchNum: String,
showActions: Boolean,
+ _commentCollapsed: {
+ type: Boolean,
+ value: true,
+ observer: '_toggleCollapseClass',
+ },
projectConfig: Object,
_xhrPromise: Object, // Used for testing.
@@ -96,10 +101,20 @@
'_loadLocalDraft(changeNum, patchNum, comment)',
],
+ attached: function() {
+ if (this.editing) {
+ this._commentCollapsed = false;
+ }
+ },
+
detached: function() {
this.cancelDebouncer('fire-update');
},
+ _computeShowHideText: function(collapsed) {
+ return collapsed ? 'â—€' : 'â–¼';
+ },
+
save: function() {
this.comment.message = this._messageText;
this.disabled = true;
@@ -198,8 +213,27 @@
},
_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;
+ }
+ },
+
+ _handleToggleCollapsed: function() {
+ this._commentCollapsed = !this._commentCollapsed;
+ },
+
+ _toggleCollapseClass: function(_commentCollapsed) {
+ if (_commentCollapsed) {
+ this.$.container.classList.add('collapsed');
+ } else {
+ this.$.container.classList.remove('collapsed');
}
},
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..b05746e 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
@@ -39,6 +39,12 @@
</test-fixture>
<script>
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ }
+
suite('gr-diff-comment tests', function() {
var element;
setup(function() {
@@ -58,6 +64,32 @@
};
});
+ test('collapsible comments', function() {
+ // When a comment (not draft) is loaded, it should be collapsed
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+
+ // The header middle content is only visible when comments are collapsed.
+ // It shows the message in a condensed way, and limits to a single line.
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When the header row is clicked, the comment should expand
+ MockInteractions.tap(element.$.header);
+ assert.isTrue(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
test('proper event fires on reply', function(done) {
element.addEventListener('reply', function(e) {
assert.ok(e.detail.comment);
@@ -135,11 +167,6 @@
};
});
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') != 'none';
- }
-
test('button visibility states', function() {
element.showActions = false;
assert.isTrue(element.$$('.actions').hasAttribute('hidden'));
@@ -181,6 +208,67 @@
assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
});
+ test('collapsible drafts', function() {
+ element.addEventListener('reply', function(e) {
+ assert.ok(e.detail.comment);
+ done();
+ });
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ MockInteractions.tap(element.$.header);
+ assert.isTrue(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is is not visible');
+
+ // When the edit button is pressed, should still see the actions
+ // and also textarea
+ MockInteractions.tap(element.$$('.edit'));
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+
+ // When toggle again, everything should be hidden except for textarea
+ // and header middle content should be visible
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When toggle again, textarea should remain open in the state it was
+ // before
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
test('draft creation/cancelation', function(done) {
assert.isFalse(element.editing);
MockInteractions.tap(element.$$('.edit'));
@@ -211,6 +299,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..c8eea3c 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
@@ -21,9 +21,8 @@
<template>
<gr-cursor-manager
id="cursorManager"
- scroll="keep-visible"
+ scroll="[[_scrollBehavior]]"
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..e783658 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
@@ -24,6 +24,11 @@
UNIFIED: 'UNIFIED_DIFF',
};
+ var ScrollBehavior = {
+ KEEP_VISIBLE: 'keep-visible',
+ NEVER: 'never',
+ };
+
var LEFT_SIDE_CLASS = 'target-side-left';
var RIGHT_SIDE_CLASS = 'target-side-right';
@@ -54,11 +59,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
@@ -68,6 +68,18 @@
type: Number,
value: null,
},
+
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+ _scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.KEEP_VISIBLE,
+ },
+
+ _listeningForScroll: Boolean,
},
observers: [
@@ -75,6 +87,15 @@
'_diffsChanged(diffs.splices)',
],
+ attached: function() {
+ // Catch when users are scrolling as the view loads.
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ },
+
+ detached: function() {
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ },
+
moveLeft: function() {
this.side = DiffSides.LEFT;
if (this._isTargetBlank()) {
@@ -174,12 +195,25 @@
}
},
+ _handleWindowScroll: function() {
+ if (this._listeningForScroll) {
+ this._scrollBehavior = ScrollBehavior.NEVER;
+ this._listeningForScroll = false;
+ }
+ },
+
handleDiffUpdate: function() {
this._updateStops();
if (!this.diffRow) {
this.reInitCursor();
}
+ this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+ this._listeningForScroll = false;
+ },
+
+ _handleDiffRenderStart: function() {
+ this._listeningForScroll = true;
},
/**
@@ -325,12 +359,15 @@
for (i = splice.index;
i < splice.index + splice.addedCount;
i++) {
+ this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
this.listen(this.diffs[i], 'render', 'handleDiffUpdate');
}
for (i = 0;
i < splice.removed && splice.removed.length;
i++) {
+ this.unlisten(splice.removed[i],
+ 'render-start', '_handleDiffRenderStart');
this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate');
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 5bdd138..a7d98e6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -98,6 +98,17 @@
assert.equal(cursorElement.diffRow, firstDeltaRow);
});
+ test('cursor scroll behavior', function() {
+ cursorElement._handleDiffRenderStart();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+
+ cursorElement._handleWindowScroll();
+ assert.equal(cursorElement._scrollBehavior, 'never');
+
+ cursorElement.handleDiffUpdate();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ });
+
suite('unified diff', function() {
setup(function(done) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index 54294a1..814a760 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -37,5 +37,6 @@
</div>
</template>
<script src="gr-annotation.js"></script>
+ <script src="gr-range-normalizer.js"></script>
<script src="gr-diff-highlight.js"></script>
</dom-module>
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..9d7dc2f 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,15 @@
}
},
+ _normalizeRange: function(range) {
+ range = GrRangeNormalizer.normalize(range);
+ return {
+ start: this._normalizeSelectionSide(range.startContainer,
+ range.startOffset),
+ end: this._normalizeSelectionSide(range.endContainer, range.endOffset),
+ };
+ },
+
/**
* Convert DOM Range selection to concrete numbers (line, column, side).
* Moves range end if it's not inside td.content.
@@ -160,13 +169,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;
}
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..2f1ded9 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('GrRangeNormalizer._getTextOffset computes text offset', function() {
+ var content = stubContent(140, 'left');
+ var child = content.lastChild.lastChild;
+ var result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 73);
+ content = stubContent(146, 'right');
+ child = content.lastChild;
+ result = GrRangeNormalizer._getTextOffset(content, child);
+ assert.equal(result, 0);
+ });
// TODO (viktard): Selection starts in line number.
// TODO (viktard): Empty lines in selection start.
// TODO (viktard): Empty lines in selection end.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
new file mode 100644
index 0000000..8685d7d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -0,0 +1,106 @@
+// 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(window) {
+ 'use strict';
+
+ // Prevent redefinition.
+ if (window.GrRangeNormalizer) { return; }
+
+ // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
+ var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
+
+ var GrRangeNormalizer = {
+ /**
+ * Remap DOM range to whole lines of a diff if necessary. If the start or
+ * end containers are DOM elements that are singular pieces of syntax
+ * 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.
+ */
+ normalize: function(range) {
+ var startContainer = this._getContentTextParent(range.startContainer);
+ var startOffset = range.startOffset + this._getTextOffset(startContainer,
+ range.startContainer);
+ var endContainer = this._getContentTextParent(range.endContainer);
+ var endOffset = range.endOffset + this._getTextOffset(endContainer,
+ range.endContainer);
+ return {
+ startContainer: startContainer,
+ startOffset: startOffset,
+ endContainer: endContainer,
+ endOffset: endOffset,
+ };
+ },
+
+ _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;
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * The DOM API textContent.length calculation is broken when the text
+ * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
+ * @param {Text} A text node.
+ * @return {Number} The length of the text.
+ */
+ _getLength: function(node) {
+ return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length;
+ },
+ };
+
+ window.GrRangeNormalizer = GrRangeNormalizer;
+})(window);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index cbf63d6..57a2e1f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -112,8 +112,8 @@
</div>
</div>
<div class="actions">
- <gr-button primary on-tap="_handleSave">Save</gr-button>
- <gr-button on-tap="_handleCancel">Cancel</gr-button>
+ <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button>
+ <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button>
</div>
</template>
<script src="gr-diff-preferences.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 4103b2e..9bccd23 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -53,6 +53,17 @@
'_localPrefsChanged(localPrefs.*)',
],
+ getFocusStops: function() {
+ return {
+ start: this.$.contextSelect,
+ end: this.$.cancelButton,
+ };
+ },
+
+ resetFocus: function() {
+ this.$.contextSelect.focus();
+ },
+
_prefsChanged: function(changeRecord) {
var prefs = changeRecord.base;
// TODO(andybons): This is not supported in IE. Implement a polyfill.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 0c40d9f..3c4d457 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -62,6 +62,14 @@
assert.isFalse(element._newPrefs.syntax_highlighting);
});
+ test('clicking save button calls _handleSave function', function() {
+ var savePrefs = sinon.stub(element, '_handleSave');
+ MockInteractions.tap(element.$.saveButton);
+ flushAsynchronousOperations();
+ assert(savePrefs.calledOnce);
+ savePrefs.restore();
+ });
+
test('events', function(done) {
var savePromise = new Promise(function(resolve) {
element.addEventListener('save', function() { resolve(); });
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..f91c8ef 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,23 @@
},
_nextStepHandle: Number,
+ _isScrolling: Boolean,
+ },
+
+ attached: function() {
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ },
+
+ detached: function() {
+ this.cancel();
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ },
+
+ _handleWindowScroll: function() {
+ this._isScrolling = true;
+ this.debounce('resetIsScrolling', function() {
+ this._isScrolling = false;
+ }, 50);
},
/**
@@ -100,6 +117,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 +223,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 +286,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 +347,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 +499,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 +511,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..c89d9b5 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']};
@@ -574,5 +591,12 @@
});
});
});
+
+ test('detaching cancels', function() {
+ element = fixture('basic');
+ sandbox.stub(element, 'cancel');
+ element.detached();
+ assert(element.cancel.called);
+ });
});
</script>
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..6a02a2d 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;
@@ -39,5 +43,6 @@
<content></content>
</div>
</template>
+ <script src="../gr-diff-highlight/gr-range-normalizer.js"></script>
<script src="gr-diff-selection.js"></script>
</dom-module>
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..e6d3653 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
@@ -14,11 +14,26 @@
(function() {
'use strict';
+ /**
+ * Possible CSS classes indicating the state of selection. Dynamically added/
+ * removed based on where the user clicks within the diff.
+ */
+ var SelectionClass = {
+ COMMENT: 'selected-comment',
+ LEFT: 'selected-left',
+ RIGHT: 'selected-right',
+ };
+
Polymer({
is: 'gr-diff-selection',
properties: {
+ diff: Object,
_cachedDiffBuilder: Object,
+ _linesCache: {
+ type: Object,
+ value: function() { return {left: null, right: null}; },
+ },
},
listeners: {
@@ -27,7 +42,7 @@
},
attached: function() {
- this.classList.add('selected-right');
+ this.classList.add(SelectionClass.RIGHT);
},
get diffBuilder() {
@@ -43,53 +58,193 @@
if (!lineEl) {
return;
}
+ var commentSelected =
+ e.target.parentNode.classList.contains('gr-diff-comment');
var side = this.diffBuilder.getSideByLineEl(lineEl);
- var targetClass = 'selected-' + side;
- var alternateClass = 'selected-' + (side === 'left' ? 'right' : 'left');
+ var targetClasses = [];
+ targetClasses.push(side === 'left' ?
+ SelectionClass.LEFT :
+ SelectionClass.RIGHT);
- if (this.classList.contains(alternateClass)) {
- this.classList.remove(alternateClass);
+ if (commentSelected) {
+ targetClasses.push(SelectionClass.COMMENT);
}
- if (!this.classList.contains(targetClass)) {
- this.classList.add(targetClass);
+ // Remove any selection classes that do not belong.
+ for (var key in SelectionClass) {
+ if (SelectionClass.hasOwnProperty(key)) {
+ var className = SelectionClass[key];
+ if (targetClasses.indexOf(className) === -1) {
+ this.classList.remove(SelectionClass[key]);
+ }
+ }
+ }
+ // Add new selection classes iff they are not already present.
+ for (var i = 0; i < targetClasses.length; i++) {
+ if (!this.classList.contains(targetClasses[i])) {
+ this.classList.add(targetClasses[i]);
+ }
}
},
- _handleCopy: function(e) {
- if (!e.target.classList.contains('content')) {
- return;
+ _getCopyEventTarget: function(e) {
+ return Polymer.dom(e).rootTarget;
+ },
+
+ /**
+ * Utility function to determine whether an element is a descendant of
+ * another element with the particular className.
+ *
+ * @param {!Element} element
+ * @param {!string} className
+ * @return {boolean}
+ */
+ _elementDescendedFromClass: function(element, className) {
+ while (!element.classList.contains(className)) {
+ if (!element.parentElement ||
+ element === this.diffBuilder.diffElement) {
+ return false;
+ }
+ element = element.parentElement;
}
- var lineEl = this.diffBuilder.getLineElByChild(e.target);
+ return true;
+ },
+
+ _handleCopy: function(e) {
+ var commentSelected = false;
+ var target = this._getCopyEventTarget(e);
+ if (this.classList.contains(SelectionClass.COMMENT)) {
+ commentSelected = true;
+ } else {
+ if (!this._elementDescendedFromClass(target, 'content')) {
+ return;
+ }
+ }
+ var lineEl = this.diffBuilder.getLineElByChild(target);
if (!lineEl) {
return;
}
var side = this.diffBuilder.getSideByLineEl(lineEl);
- var text = this._getSelectedText(side);
- e.clipboardData.setData('Text', text);
- e.preventDefault();
+ var text = this._getSelectedText(side, commentSelected);
+ if (text) {
+ e.clipboardData.setData('Text', text);
+ e.preventDefault();
+ }
},
- _getSelectedText: function(opt_side) {
+ /**
+ * Get the text of the current window selection. If commentSelected is
+ * true, it returns only the text of comments within the selection.
+ * Otherwise it returns the text of the selected diff region.
+ *
+ * @param {!string} The side that is selected.
+ * @param {boolean} Whether or not a comment is selected.
+ * @return {string} The selected text.
+ */
+ _getSelectedText: function(side, commentSelected) {
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;
+ if (commentSelected) {
+ return this._getCommentLines(sel, side);
}
- var contentEls = Polymer.dom(fragment).querySelectorAll(selector);
- if (contentEls.length === 0) {
- return fragment.textContent;
- }
+ var range = GrRangeNormalizer.normalize(sel.getRangeAt(0));
+ var startLineEl = this.diffBuilder.getLineElByChild(range.startContainer);
+ var endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
+ var startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
+ var endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
- var text = '';
- for (var i = 0; i < contentEls.length; i++) {
- text += contentEls[i].textContent + '\n';
+ return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
+ range.endOffset, side);
+ },
+
+ /**
+ * Query the diff object for the selected lines.
+ *
+ * @param {int} startLineNum
+ * @param {int} startOffset
+ * @param {int} endLineNum
+ * @param {int} endOffset
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected diff text.
+ */
+ _getRangeFromDiff: function(startLineNum, startOffset, endLineNum,
+ endOffset, side) {
+ var lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+ if (lines.length) {
+ lines[0] = lines[0].substring(startOffset);
+ lines[lines.length - 1] = lines[lines.length - 1]
+ .substring(0, endOffset);
}
- return text;
+ return lines.join('\n');
+ },
+
+ /**
+ * Query the diff object for the lines from a particular side.
+ *
+ * @param {!string} side The side that is currently selected.
+ * @return {string[]} An array of strings indexed by line number.
+ */
+ _getDiffLines: function(side) {
+ if (this._linesCache[side]) {
+ return this._linesCache[side];
+ }
+ var lines = [];
+ var chunk;
+ var key = side === 'left' ? 'a' : 'b';
+ for (var chunkIndex = 0;
+ chunkIndex < this.diff.content.length;
+ chunkIndex++) {
+ chunk = this.diff.content[chunkIndex];
+ if (chunk.ab) {
+ lines = lines.concat(chunk.ab);
+ } else if (chunk[key]) {
+ lines = lines.concat(chunk[key]);
+ }
+ }
+ this._linesCache[side] = lines;
+ return lines;
+ },
+
+ /**
+ * Query the diffElement for comments and check whether they lie inside the
+ * selection range.
+ *
+ * @param {!Selection} sel The selection of the window.
+ * @param {!string} side The side that is currently selected.
+ * @return {string} The selected comment text.
+ */
+ _getCommentLines: function(sel, side) {
+ var range = sel.getRangeAt(0);
+ var content = [];
+ // Fall back to default copy behavior if the selection lies within one
+ // comment body.
+ if (range.startContainer === range.endContainer) {
+ return;
+ }
+ if (this._elementDescendedFromClass(range.commonAncestorContainer,
+ 'message')) {
+ return;
+ }
+ // Query the diffElement for comments.
+ var messages = this.diffBuilder.diffElement.querySelectorAll(
+ '.side-by-side [data-side="' + side +
+ '"] .message *, .unified .message *');
+
+ for (var i = 0; i < messages.length; i++) {
+ var el = messages[i];
+ // Check if the comment element exists inside the selection.
+ if (sel.containsNode(el, true)) {
+ content.push(el.textContent);
+ }
+ }
+ // Deal with offsets.
+ content[0] = content[0].substring(range.startOffset);
+ if (range.endOffset) {
+ content[content.length - 1] =
+ content[content.length - 1].substring(0, range.endOffset);
+ }
+ return content.join('\n');
},
});
})();
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..71fa233 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,57 @@
<test-fixture id="basic">
<template>
<gr-diff-selection>
- <table>
+ <table id="diffTable" 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>
+ <div data-side="left">
+ <div class="gr-diff-comment-thread">
+ <div class="message">
+ <span>This is a comment</span>
+ </div>
+ </div>
+ </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>
+ <div data-side="right">
+ <div class="gr-diff-comment-thread">
+ <div class="message">
+ <span>This is a comment on the right</span>
+ </div>
+ </div>
+ </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>
+ <div data-side="left">
+ <div class="gr-diff-comment-thread">
+ <div class="message">
+ <span>This is a different comment</span>
+ </div>
+ </div>
+ </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>
@@ -54,6 +87,7 @@
<script>
suite('gr-diff-selection', function() {
var element;
+ var copyEventStub;
var emulateCopyOn = function(target) {
var fakeEvent = {
@@ -63,15 +97,34 @@
setData: sinon.stub(),
},
};
+ element._getCopyEventTarget.returns(target);
element._handleCopy(fakeEvent);
return fakeEvent;
};
setup(function() {
element = fixture('basic');
+ sinon.stub(element, '_getCopyEventTarget');
element._cachedDiffBuilder = {
getLineElByChild: sinon.stub().returns({}),
getSideByLineEl: sinon.stub(),
+ diffElement: element.querySelector('#diffTable'),
+ };
+ element.diff = {
+ content: [
+ {
+ a: ['ba ba'],
+ b: ['some other text'],
+ },
+ {
+ a: ['zin'],
+ b: ['more more more'],
+ },
+ {
+ a: ['ga ga'],
+ b: ['some other text'],
+ },
+ ],
};
});
@@ -102,41 +155,69 @@
assert.isFalse(element._getSelectedText.called);
});
- test('asks for text for right side Elements', function() {
+ test('asks for text for left side Elements', function() {
element._cachedDiffBuilder.getSideByLineEl.returns('left');
sinon.stub(element, '_getSelectedText');
- emulateCopyOn(element.querySelector('td.content'));
- assert.deepEqual(['left'], element._getSelectedText.lastCall.args);
+ emulateCopyOn(element.querySelector('div.contentText'));
+ assert.deepEqual(['left', false], 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'));
+ element._cachedDiffBuilder.getSideByLineEl.returns('left');
+ element._getSelectedText.returns('test');
+ 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() {
+ // Fetch the line number.
+ element._cachedDiffBuilder.getLineElByChild = function(child) {
+ while (!child.classList.contains('content') && child.parentElement) {
+ child = child.parentElement;
+ }
+ return child.previousElementSibling;
+ };
+
element.classList.add('selected-left');
+ element.classList.remove('selected-right');
+
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');
+ selection.removeAllRanges();
+ });
+
+ test('copies comments', function() {
+ element.classList.add('selected-left');
+ element.classList.add('selected-comment');
+ element.classList.remove('selected-right');
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.setStart(element.querySelector('.message *').firstChild, 3);
+ range.setEnd(
+ element.querySelectorAll('.message *')[2].firstChild, 16);
+ selection.addRange(range);
+ assert.equal('s is a comment\nThis is a differ',
+ element._getSelectedText('left', true));
+ selection.removeAllRanges();
});
});
</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..a2af8c5 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,60 @@
}
}
</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"
+ initial-count="75">
+ <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]"
+ selected$="[[_computeFileSelected(path, _path)]]"
+ data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
+ on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
+ </template>
+ </div>
+ </iron-dropdown>
+ </div>
+ <div class="mobileJumpToFileContainer">
+ <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 +197,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>
@@ -195,6 +213,7 @@
</div>
<gr-overlay id="prefsOverlay" with-backdrop>
<gr-diff-preferences
+ id="diffPreferences"
prefs="{{_prefs}}"
local-prefs="{{_localPrefs}}"
on-save="_handlePrefsSave"
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..03609c0 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
@@ -104,6 +104,14 @@
this._setReviewed(true);
}
}.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',
@@ -113,11 +121,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();
},
@@ -170,6 +173,10 @@
this._patchRange.patchNum, this._path, reviewed);
},
+ _checkForModifiers: function(e) {
+ return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
+ },
+
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
@@ -197,6 +204,7 @@
this.$.cursor.moveUp();
break;
case 67: // 'c'
+ if (this._checkForModifiers(e)) { return; }
if (!this.$.diff.isRangeSelected()) {
e.preventDefault();
var line = this.$.cursor.getTargetLineElement();
@@ -207,11 +215,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();
@@ -251,25 +259,55 @@
break;
case 188: // ','
e.preventDefault();
- this.$.prefsOverlay.open();
+ this._openPrefs();
break;
}
},
- _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));
+ },
+
+ _openPrefs: function() {
+ this.$.prefsOverlay.open().then(function() {
+ var diffPreferences = this.$.diffPreferences;
+ var focusStops = diffPreferences.getFocusStops();
+ this.$.prefsOverlay.setFocusStops(focusStops);
+ this.$.diffPreferences.resetFocus();
+ }.bind(this));
+ },
+
+ /**
+ * @param {?string} path The path of the current file being shown.
+ * @param {Array.<string>} fileList The list of files in this change and
+ * 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) {
@@ -426,7 +464,7 @@
_handlePrefsTap: function(e) {
e.preventDefault();
- this.$.prefsOverlay.open();
+ this._openPrefs();
},
_handlePrefsSave: function(e) {
@@ -484,5 +522,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..566c6a1 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>
+</test-fixture>
+
<script>
suite('gr-diff-view tests', function() {
var element;
@@ -97,7 +103,9 @@
'Should navigate to /c/42/');
assert.equal(element.changeViewState.selectedFileIndex, 0);
- var showPrefsStub = sinon.stub(element.$.prefsOverlay, 'open');
+ var showPrefsStub = sinon.stub(element.$.prefsOverlay, 'open',
+ function() { return Promise.resolve({}); });
+
MockInteractions.pressAndReleaseKeyOn(element, 188); // ','
assert(showPrefsStub.calledOnce);
@@ -125,6 +133,26 @@
showStub.restore();
});
+ test('saving diff preferences', function() {
+ var savePrefs = sinon.stub(element, '_handlePrefsSave');
+ var cancelPrefs = sinon.stub(element, '_handlePrefsCancel');
+ element.$.diffPreferences._handleSave();
+ assert(savePrefs.calledOnce);
+ assert(cancelPrefs.notCalled);
+ savePrefs.restore();
+ cancelPrefs.restore();
+ });
+
+ test('cancelling diff preferences', function() {
+ var savePrefs = sinon.stub(element, '_handlePrefsSave');
+ var cancelPrefs = sinon.stub(element, '_handlePrefsCancel');
+ element.$.diffPreferences._handleCancel();
+ assert(cancelPrefs.calledOnce);
+ assert(savePrefs.notCalled);
+ savePrefs.restore();
+ cancelPrefs.restore();
+ });
+
test('keyboard shortcuts with patch range', function() {
element._changeNum = '42';
element._patchRange = {
@@ -338,6 +366,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 +445,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.
@@ -383,7 +457,6 @@
// We will simulate a user change of the selected mode.
var newMode = 'UNIFIED_DIFF';
-
// Set the actual value of the select, and simulate the change event.
select.value = newMode;
element.fire('change', {}, {node: select});
@@ -392,6 +465,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() {
@@ -411,5 +508,13 @@
assert.equal(element.$.cursor.initialLineNumber, 345);
assert.equal(element.$.cursor.side, 'left');
});
+
+ test('_checkForModifiers', function() {
+ assert.isTrue(element._checkForModifiers({altKey: true}));
+ assert.isTrue(element._checkForModifiers({ctrlKey: true}));
+ assert.isTrue(element._checkForModifiers({metaKey: true}));
+ assert.isTrue(element._checkForModifiers({shiftKey: true}));
+ assert.isFalse(element._checkForModifiers({}));
+ });
});
</script>
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..1a6759b 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,11 @@
properties: {
changeNum: String,
+ expanded: {
+ type: Boolean,
+ value: true,
+ observer: '_handleShowDiff',
+ },
patchRange: Object,
path: String,
prefs: {
@@ -84,6 +89,13 @@
this._getLoggedIn().then(function(loggedIn) {
this._loggedIn = loggedIn;
}.bind(this));
+
+ },
+
+ ready: function() {
+ if (this._canRender()) {
+ this.reload();
+ }
},
reload: function() {
@@ -108,7 +120,7 @@
},
getCursorStops: function() {
- if (this.hidden) {
+ if (!this.expanded) {
return [];
}
@@ -141,6 +153,16 @@
this.toggleClass('no-left');
},
+ _handleShowDiff: function() {
+ if (this._canRender()) {
+ this.reload();
+ }
+ },
+
+ _canRender: function() {
+ return this.changeNum && this.patchRange && this.path && this.expanded;
+ },
+
_getCommentThreads: function() {
return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
},
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..b9b49c0 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,22 @@
assert.equal(drafts.length, 1);
assert.equal(drafts[0].id, id);
});
+
+ test('_handleShowDiff reloads when expanded is made true',
+ function(done) {
+ element.expanded = false;
+ element.changeNum = element._comments.meta.changeNum;
+ element.patchRange = element._comments.meta.patchRange;
+ element.path = element._comments.meta.path;
+
+ var stub = sinon.stub(element, 'reload', function() {
+ assert.isTrue(stub.called);
+ stub.restore();
+ done();
+ });
+ var spy = sinon.spy(element, '_handleShowDiff');
+ element.set('expanded', true);
+ });
});
});
});
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-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index d565a12..fbd1ef3 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -74,9 +74,14 @@
return rect;
},
+ _checkForModifiers: function(e) {
+ return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || false;
+ },
+
_handleKey: function(e) {
if (this.shouldSupressKeyboardShortcut(e)) { return; }
if (e.keyCode === 67) { // 'c'
+ if (this._checkForModifiers(e)) { return; }
e.preventDefault();
this._fireCreateComment();
}
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index adc8532..c12966d 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -117,5 +117,13 @@
document.createRange.restore();
});
});
+
+ test('_checkForModifiers', function() {
+ assert.isTrue(element._checkForModifiers({altKey: true}));
+ assert.isTrue(element._checkForModifiers({ctrlKey: true}));
+ assert.isTrue(element._checkForModifiers({metaKey: true}));
+ assert.isTrue(element._checkForModifiers({shiftKey: true}));
+ assert.isFalse(element._checkForModifiers({}));
+ });
});
</script>
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..84e97bd 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) {
@@ -254,13 +255,14 @@
var nodeLength = GrAnnotation.getLength(node);
// Note: HLJS may emit a span with class undefined when it thinks there
// may be a syntax error.
- if (node.tagName === 'SPAN' && node.className !== 'undefined' &&
- CLASS_WHITELIST.hasOwnProperty(node.className)) {
- result.push({
- start: offset,
- length: nodeLength,
- className: node.className,
- });
+ if (node.tagName === 'SPAN' && node.className !== 'undefined') {
+ if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
+ result.push({
+ start: offset,
+ length: nodeLength,
+ className: node.className,
+ });
+ }
if (node.children.length) {
result = result.concat(this._rangesFromElement(node, offset));
}
@@ -278,7 +280,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 +302,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 +359,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..aa37f1a 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();
@@ -370,6 +370,15 @@
assert.equal(result[1].className, className);
});
+ test('_rangesFromString whitelist allows recursion', function() {
+ var str = [
+ '<span class="non-whtelisted-class">',
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
+ '</span>'].join('');
+ var result = element._rangesFromString(str);
+ assert.notEqual(result.length, 0);
+ });
+
test('_isSectionDone', function() {
var state = {sectionIndex: 0, lineIndex: 0};
assert.isFalse(element._isSectionDone(state));
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..633e8f2 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">
@@ -103,7 +104,8 @@
<gr-change-view
params="[[params]]"
server-config="[[_serverConfig]]"
- view-state="{{_viewState.changeView}}"></gr-change-view>
+ view-state="{{_viewState.changeView}}"
+ back-page="[[_lastSearchPage]]"></gr-change-view>
</template>
<template is="dom-if" if="[[_showDiffView]]" restamp="true">
<gr-diff-view
@@ -125,9 +127,11 @@
|
<a class="feedback"
href="https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue"
- target="_blank">
- Report PolyGerrit Bug
- </a>
+ target="_blank">Report PolyGerrit Bug</a>
+ <template is="dom-if" if="[[_computeShowGwtUiLink(_serverConfig)]]">
+ |
+ <a id="gwtLink" href$="/?polygerrit=0#[[_path]]" rel="external">GWT UI</a>
+ </template>
</footer>
<gr-overlay id="keyboardShortcuts" with-backdrop>
<gr-keyboard-shortcuts-dialog
@@ -136,6 +140,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..8ac268e 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -43,11 +43,14 @@
_showSettingsView: Boolean,
_viewState: Object,
_lastError: Object,
+ _lastSearchPage: String,
+ _path: String,
},
listeners: {
'page-error': '_handlePageError',
'title-change': '_handleTitleChange',
+ 'location-change': '_handleLocationChange',
},
observers: [
@@ -72,6 +75,7 @@
},
ready: function() {
+ this.$.reporting.appStarted();
this._viewState = {
changeView: {
changeNum: null,
@@ -107,10 +111,12 @@
},
_loadPlugins: function(plugins) {
+ Gerrit._setPluginsCount(plugins.length);
for (var i = 0; i < plugins.length; i++) {
var scriptEl = document.createElement('script');
scriptEl.defer = true;
scriptEl.src = '/' + plugins[i];
+ scriptEl.onerror = Gerrit._pluginInstalled;
document.body.appendChild(scriptEl);
}
},
@@ -126,6 +132,11 @@
return !!(account && Object.keys(account).length > 0);
},
+ _computeShowGwtUiLink: function(config) {
+ return config.gerrit.web_uis &&
+ config.gerrit.web_uis.indexOf('GWT') !== -1;
+ },
+
_handlePageError: function(e) {
[
'_showChangeListView',
@@ -152,6 +163,23 @@
}
},
+ _handleLocationChange: function(e) {
+ var hash = e.detail.hash.substring(1);
+ var pathname = e.detail.pathname;
+ if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+ pathname += '@' + hash;
+ }
+ this.set('_path', pathname);
+ this._handleSearchPageChange();
+ },
+
+ _handleSearchPageChange: function() {
+ var viewsToCheck = ['gr-change-list-view', 'gr-dashboard-view'];
+ if (viewsToCheck.indexOf(this.params.view) !== -1) {
+ this.set('_lastSearchPage', location.pathname);
+ }
+ },
+
_handleTitleChange: function(e) {
if (e.detail.title) {
document.title = e.detail.title + ' · Gerrit Code Review';
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..9373820
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,91 @@
+<!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(),
+ });
+ var config = {
+ gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
+ plugin: {js_resource_paths: []},
+ };
+ stub('gr-rest-api-interface', {
+ getConfig: function() { return Promise.resolve(config); },
+ });
+
+ element = fixture('basic');
+ flush(done);
+ });
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ test('reporting', function() {
+ assert.isTrue(element.$.reporting.appStarted.calledOnce);
+ });
+
+ test('location change updates gwt footer', function() {
+ element._path = '/test/path';
+ var gwtLink = element.$$('#gwtLink');
+ assert.equal(gwtLink.href,
+ 'http://' + location.host + '/?polygerrit=0#/test/path');
+ });
+
+ test('_handleLocationChange handles hashes', function() {
+ var curLocation = {
+ pathname: '/c/1/1/testfile.txt',
+ hash: '#2',
+ host: location.host,
+ };
+
+ var event = {detail: curLocation};
+ var gwtLink = element.$$('#gwtLink');
+
+ sinon.stub(element, '_handleSearchPageChange');
+
+ element._handleLocationChange(event);
+ assert.equal(gwtLink.href,
+ 'http://' + location.host + '/?polygerrit=0#/c/1/1/testfile.txt@2');
+ });
+
+ test('sets plugins count', function() {
+ sandbox.stub(Gerrit, '_setPluginsCount');
+ element._loadPlugins([]);
+ assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 4f1cb87..8194c21 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -165,10 +165,9 @@
<select
is="gr-select"
bind-value="{{_localPrefs.email_strategy}}">
- <option value="ENABLED">Enabled</option>
- <option
- value="CC_ON_OWN_COMMENTS">CC Me On Comments I Write</option>
- <option value="DISABLED">Disabled</option>
+ <option value="CC_ON_OWN_COMMENTS">Every Comment</option>
+ <option value="ENABLED">Only Comments Left By Others</option>
+ <option value="DISABLED">None</option>
</select>
</span>
</section>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 66576a3..09f7381 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -138,7 +138,7 @@
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 1'));
assert.isFalse(element._canAddProject({id: 'project b'}, 'filter 2'));
- // Can add a projec that is in the list using a new filter.
+ // Can add a project that is in the list using a new filter.
assert.isTrue(element._canAddProject({id: 'project b'}, 'filter 3'));
});
@@ -181,10 +181,11 @@
test('_handleRemoveProject', function() {
assert.equal(element._projectsToRemove, 0);
-
var button = element.$$('table tbody tr:nth-child(2) gr-button');
MockInteractions.tap(button);
+ flushAsynchronousOperations();
+
var rows = element.$$('table tbody').querySelectorAll('tr');
assert.equal(rows.length, 3);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index 360c281..dc1de17 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -53,12 +53,17 @@
padding: 0;
text-decoration: none;
}
+ .transparentBackground,
+ gr-button.transparentBackground {
+ background-color: transparent;
+ }
</style>
- <div class="container">
+ <div class$="container [[_getBackgroundClass(transparentBackground)]]">
<gr-account-link account="[[account]]"></gr-account-link>
<gr-button
hidden$="[[!removable]]" hidden
- class="remove" on-tap="_handleRemoveTap">×</gr-button>
+ class$="remove [[_getBackgroundClass(transparentBackground)]]"
+ on-tap="_handleRemoveTap">×</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 45bf8fe..e33e1fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -28,6 +28,10 @@
type: Boolean,
reflectToAttribute: true,
},
+ transparentBackground: {
+ type: Boolean,
+ value: false,
+ },
},
ready: function() {
@@ -36,6 +40,10 @@
}.bind(this));
},
+ _getBackgroundClass: function(transparent) {
+ return transparent ? 'transparentBackground' : '';
+ },
+
_handleRemoveTap: function(e) {
e.preventDefault();
this.fire('remove', {account: this.account});
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index f136907..9bbcea5 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -47,5 +47,6 @@
</span>
</span>
</template>
+ <script src="../../../scripts/util.js"></script>
<script src="gr-account-label.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 98871cb..40b7cf1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -30,8 +30,11 @@
},
_computeAccountTitle: function(account) {
- if (!account || !account.name) { return; }
- var result = util.escapeHTML(account.name);
+ if (!account || (!account.name && !account.email)) { return; }
+ var result = '';
+ if (account.name) {
+ result += util.escapeHTML(account.name);
+ }
if (account.email) {
result += ' <' + util.escapeHTML(account.email) + '>';
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index d3585ef..8d89692 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -36,7 +36,8 @@
<span>
<a href$="[[_computeOwnerLink(account)]]">
<gr-account-label account="[[account]]"
- avatar-image-size="[[avatarImageSize]]"></gr-account-label>
+ avatar-image-size="[[avatarImageSize]]"
+ show-email="[[_computeShowEmail(account)]]"></gr-account-label>
</a>
</span>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 058b27d..0c2ad0b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -30,5 +30,9 @@
var accountID = account.email || account._account_id;
return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
},
+
+ _computeShowEmail: function(account) {
+ return !!(account && !account.name);
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index 2b5b831..1a84d15 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -48,6 +48,10 @@
assert.equal(element._computeOwnerLink({_account_id: 42}),
'/q/owner:42+status:open');
+
+ assert.equal(element._computeShowEmail({name: 'asd'}), false);
+
+ assert.equal(element._computeShowEmail({}), true);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index cda2492..864114f 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -55,11 +55,11 @@
bind-value="{{text}}"
placeholder="[[placeholder]]"
on-keydown="_handleInputKeydown"
- on-focus="_updateSuggestions"
+ on-focus="_onInputFocus"
autocomplete="off" />
<div
id="suggestions"
- hidden$="[[_computeSuggestionsHidden(_suggestions)]]">
+ hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
<ul>
<template is="dom-repeat" items="[[_suggestions]]">
<li
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..31918dd 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',
@@ -76,6 +78,15 @@
value: false,
},
+ /**
+ * When true, tab key autocompletes but does not fire the commit event.
+ * See Issue 4556.
+ */
+ tabCompleteWithoutCommit: {
+ type: Boolean,
+ value: false,
+ },
+
value: Object,
/**
@@ -99,6 +110,11 @@
value: false,
},
+ _focused: {
+ type: Boolean,
+ value: false,
+ },
+
},
attached: function() {
@@ -131,6 +147,11 @@
this._disableSuggestions = false;
},
+ _onInputFocus: function() {
+ this._focused = true;
+ this._updateSuggestions();
+ },
+
_updateSuggestions: function() {
if (!this.text || this._disableSuggestions) { return; }
if (this.text.length < this.threshold) {
@@ -153,8 +174,8 @@
}.bind(this));
},
- _computeSuggestionsHidden: function(suggestions) {
- return !suggestions.length;
+ _computeSuggestionsHidden: function(suggestions, focused) {
+ return !(suggestions.length && focused);
},
_computeClass: function(borderless) {
@@ -181,10 +202,14 @@
this._cancel();
break;
case 9: // Tab
+ if (this._suggestions.length > 0) {
+ e.preventDefault();
+ this._commit(this.tabCompleteWithoutCommit);
+ }
+ break;
case 13: // Enter
e.preventDefault();
this._commit();
- this._suggestions = [];
break;
}
},
@@ -199,8 +224,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;
}
@@ -209,24 +236,32 @@
_handleBodyClick: function(e) {
var eventPath = Polymer.dom(e).path;
for (var i = 0; i < eventPath.length; i++) {
- if (eventPath[i] == this) {
+ if (eventPath[i] === this) {
return;
}
}
- this._suggestions = [];
+ this._focused = false;
},
_handleSuggestionTap: function(e) {
this.$.cursor.setCursor(e.target);
this._commit();
+ this.focus();
},
- _commit: function() {
+ /**
+ * Commits the suggestion, optionally firing the commit event.
+ *
+ * @param {Boolean} silent Allows for silent committing of an autocomplete
+ * suggestion in order to handle cases like tab-to-complete without
+ * firing the commit event.
+ */
+ _commit: function(silent) {
// Allow values that are not in suggestion list iff suggestions are empty.
if (this._suggestions.length > 0) {
this._updateValue(this._suggestions, this._index);
} else {
- this.value = this.text;
+ this.value = this.text || '';
}
var value = this.value;
@@ -242,7 +277,10 @@
}
}
- this.fire('commit', {value: value});
+ this._suggestions = [];
+ if (!silent) {
+ this.fire('commit', {value: 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..7a26e72 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
@@ -57,6 +57,7 @@
element.text = 'blah';
assert.isTrue(queryStub.called);
+ element._focused = true;
promise.then(function() {
assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
@@ -69,7 +70,6 @@
}
assert.notEqual(element.$.cursor.index, -1);
-
done();
});
});
@@ -85,6 +85,7 @@
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+ element._focused = true;
element.text = 'blah';
promise.then(function() {
@@ -94,7 +95,6 @@
element.addEventListener('cancel', cancelHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 27); // Esc
-
assert.isTrue(cancelHandler.called);
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
@@ -117,7 +117,7 @@
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
assert.equal(element.$.cursor.index, -1);
-
+ element._focused = true;
element.text = 'blah';
promise.then(function() {
@@ -241,5 +241,57 @@
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();
+ });
+
+ test('tabCompleteWithoutCommit flag functions', function() {
+ var commitHandler = sinon.spy();
+ element.addEventListener('commit', commitHandler);
+ element._suggestions = ['tunnel snakes rule!'];
+ element.tabCompleteWithoutCommit = true;
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+ assert.isFalse(commitHandler.called);
+ element.tabCompleteWithoutCommit = false;
+ element._suggestions = ['tunnel snakes rule!'];
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+ assert.isTrue(commitHandler.called);
+ });
+
+ test('_focused flag properly triggered', function() {
+ flushAsynchronousOperations();
+ assert.isFalse(element._focused);
+ element.$.input.focus();
+ assert.isTrue(element._focused);
+ });
+
+ test('_focused flag shows/hides the suggestions', function() {
+ var suggestions = ['hello', 'its me'];
+ assert.isTrue(element._computeSuggestionsHidden(suggestions, false));
+ assert.isFalse(element._computeSuggestionsHidden(suggestions, true));
+ });
+
+ test('tap on suggestion commits and refocuses on input', function() {
+ var focusSpy = sinon.spy(element, 'focus');
+ var commitSpy = sinon.spy(element, '_commit');
+ element._focused = true;
+ element._suggestions = [{name: 'first suggestion'}];
+ assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
+ MockInteractions.tap(element.$$('#suggestions li:first-child'));
+ flushAsynchronousOperations();
+ assert.isTrue(focusSpy.called);
+ assert.isTrue(commitSpy.called);
+ assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+ focusSpy.restore();
+ commitSpy.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-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 5b12c8f..f6117e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -21,7 +21,7 @@
var TimeFormats = {
TIME_12: 'h:mm A', // 2:14 PM
- TIME_24: 'H:mm', // 14:14
+ TIME_24: 'HH:mm', // 14:14
MONTH_DAY: 'MMM DD', // Aug 29
MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997
};
@@ -111,7 +111,12 @@
var date = moment(util.parseDate(dateStr));
if (!date.isValid()) { return ''; }
if (relative) {
- return date.fromNow();
+ var dateFromNow = date.fromNow();
+ if (dateFromNow === 'a few seconds ago') {
+ return 'just now';
+ } else {
+ return dateFromNow;
+ }
}
var now = new Date();
var format = TimeFormats.MONTH_DAY_YEAR;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index d1886e7..8d65bc3 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -98,13 +98,13 @@
test('More than 24 hours but less than six months', function(done) {
testDates('2015-07-29 20:34:14.985000000',
'2015-06-15 03:25:14.985000000',
- 'Jun 15', 'Jun 15, 2015, 3:25', done);
+ 'Jun 15', 'Jun 15, 2015, 03:25', done);
});
test('More than six months', function(done) {
testDates('2015-09-15 20:34:00.000000000',
'2015-01-15 03:25:00.000000000',
- 'Jan 15, 2015', 'Jan 15, 2015, 3:25', done);
+ 'Jan 15, 2015', 'Jan 15, 2015, 03:25', done);
});
});
@@ -174,7 +174,7 @@
});
test('Default preferences are respected', function() {
- assert.equal(element._timeFormat, 'H:mm');
+ assert.equal(element._timeFormat, 'HH:mm');
assert.isFalse(element._relative);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 76a9c77..4fec27f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -18,14 +18,19 @@
<dom-module id="gr-editable-label">
<template>
<style>
+ :host {
+ display: inline-block;
+ }
+ input,
+ label {
+ width: 100%;
+ }
input {
font: inherit;
- max-width: 8em;
}
label {
color: #777;
display: inline-block;
- max-width: 8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 1967b80..5c0535b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -14,6 +14,7 @@
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-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
@@ -23,4 +24,3 @@
<script src="gr-js-api-interface.js"></script>
<script src="gr-public-js-api.js"></script>
</dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 4dfcf48..bb407aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -21,6 +21,7 @@
SUBMIT_CHANGE: 'submitchange',
COMMENT: 'comment',
REVERT: 'revert',
+ POST_REVERT: 'postrevert',
};
var Element = {
@@ -80,11 +81,11 @@
this._eventCallbacks[eventName].push(callback);
},
- canSubmitChange: function() {
+ canSubmitChange: function(change, revision) {
var submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
var cancelSubmit = submitCallbacks.some(function(callback) {
try {
- return callback() === false;
+ return callback(change, revision) === false;
} catch (err) {
console.error(err);
}
@@ -149,15 +150,29 @@
});
},
- modifyRevertMsg: function(change, msg) {
+ modifyRevertMsg: function(change, revertMsg, origMsg) {
this._getEventCallbacks(EventType.REVERT).forEach(function(callback) {
try {
- msg = callback(change, msg);
+ revertMsg = callback(change, revertMsg, origMsg);
} catch (err) {
console.error(err);
}
});
- return msg;
+ return revertMsg;
+ },
+
+ getLabelValuesPostRevert: function(change) {
+ var labels = {};
+ this._getEventCallbacks(EventType.POST_REVERT).forEach(
+ function(callback) {
+ try {
+ labels = callback(change);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ );
+ return labels;
},
_getEventCallbacks: function(type) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 46a555a..766da84 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -103,24 +103,42 @@
});
test('revert event', function(done) {
- function appendToRevertMsg(c, msg) {
- return msg + '\ninfo';
+ function appendToRevertMsg(c, revertMsg, originalMsg) {
+ return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
}
done();
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
assert.equal(errorStub.callCount, 0);
plugin.on(element.EventType.REVERT, throwErrFn);
plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo');
assert.isTrue(errorStub.calledOnce);
plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo\ninfo');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo\n> origTest\ninfo');
assert.isTrue(errorStub.calledTwice);
});
+ test('postrevert event', function(done) {
+ function getLabels(c) {
+ return {'Code-Review': 1};
+ }
+ done();
+
+ assert.deepEqual(element.getLabelValuesPostRevert(null), {});
+ assert.equal(errorStub.callCount, 0);
+
+ plugin.on(element.EventType.POST_REVERT, throwErrFn);
+ plugin.on(element.EventType.POST_REVERT, getLabels);
+ assert.deepEqual(element.getLabelValuesPostRevert(null),
+ {'Code-Review': 1});
+ assert.isTrue(errorStub.calledOnce);
+ });
+
test('labelchange event', function(done) {
var testChange = {_number: 42};
plugin.on(element.EventType.LABEL_CHANGE, throwErrFn);
@@ -156,5 +174,49 @@
});
});
+ test('_setPluginsCount', function(done) {
+ stub('gr-reporting', {
+ pluginsLoaded: function() {
+ assert.equal(Gerrit._pluginsPending, 0);
+ done();
+ }
+ });
+ Gerrit._setPluginsCount(0);
+ });
+
+ test('_arePluginsLoaded', function() {
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ Gerrit._setPluginsCount(1);
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ Gerrit._setPluginsCount(0);
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ });
+
+ test('_pluginInstalled', function(done) {
+ stub('gr-reporting', {
+ pluginsLoaded: function() {
+ done();
+ }
+ });
+ Gerrit._setPluginsCount(2);
+ Gerrit._pluginInstalled();
+ assert.equal(Gerrit._pluginsPending, 1);
+ Gerrit._pluginInstalled();
+ });
+
+ test('install calls _pluginInstalled', function() {
+ var stub = sinon.stub(Gerrit, '_pluginInstalled');
+ Gerrit.install(function(p) { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ assert.isTrue(stub.calledOnce);
+ stub.restore();
+ });
+
+ test('install calls _pluginInstalled on error', function() {
+ var stub = sinon.stub(Gerrit, '_pluginInstalled');
+ Gerrit.install(function() {}, '0.0pre-alpha');
+ assert.isTrue(stub.calledOnce);
+ stub.restore();
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 21d76f1..ad8c135 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -64,6 +64,9 @@
var Gerrit = window.Gerrit || {};
+ // Number of plugins to initialize, -1 means 'not yet known'.
+ Gerrit._pluginsPending = -1;
+
Gerrit.getPluginName = function() {
console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
'Please use self.getPluginName() instead.');
@@ -85,12 +88,14 @@
if (opt_version && opt_version !== API_VERSION) {
console.warn('Only version ' + API_VERSION +
' is supported in PolyGerrit. ' + opt_version + ' was given.');
+ Gerrit._pluginInstalled();
return;
}
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
var src = opt_src || (document.currentScript && document.currentScript.src);
callback(new Plugin(src));
+ Gerrit._pluginInstalled();
};
Gerrit.getLoggedIn = function() {
@@ -101,5 +106,20 @@
// NOOP since PolyGerrit doesn’t support GWT plugins.
};
+ Gerrit._setPluginsCount = function(count) {
+ Gerrit._pluginsPending = count;
+ if (Gerrit._arePluginsLoaded()) {
+ document.createElement('gr-reporting').pluginsLoaded();
+ }
+ };
+
+ Gerrit._pluginInstalled = function() {
+ Gerrit._setPluginsCount(Gerrit._pluginsPending - 1);
+ };
+
+ Gerrit._arePluginsLoaded = function() {
+ return Gerrit._pluginsPending === 0;
+ };
+
window.Gerrit = Gerrit;
})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 4980cba..f34ffcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,6 +14,7 @@
limitations under the License.
-->
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
<script src="../../../bower_components/fetch/fetch.js"></script>
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..6195272 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
@@ -62,11 +62,18 @@
COMMIT_FOOTERS: 17,
// Include push certificate information along with any patch sets.
- PUSH_CERTIFICATES: 18
+ PUSH_CERTIFICATES: 18,
+
+ // Include change's reviewer updates.
+ REVIEWER_UPDATES: 19,
+
+ // Set the submittable boolean.
+ SUBMITTABLE: 20
};
Polymer({
is: 'gr-rest-api-interface',
+ behaviors: [Gerrit.PathListBehavior],
/**
* Fired when an server error occurs.
@@ -94,7 +101,6 @@
fetchJSON: function(url, opt_errFn, opt_cancelCondition, opt_params,
opt_opts) {
opt_opts = opt_opts || {};
-
var fetchOptions = {
credentials: 'same-origin',
headers: opt_opts.headers,
@@ -312,6 +318,10 @@
ListChangesOption.LABELS,
ListChangesOption.DETAILED_ACCOUNTS
);
+ // Issue 4524: respect legacy token with max sortkey.
+ if (opt_offset === 'n,z') {
+ opt_offset = 0;
+ }
var params = {
n: changesPerPage,
O: options,
@@ -348,7 +358,8 @@
var options = this._listChangesOptionsToHex(
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CHANGE_ACTIONS,
- ListChangesOption.DOWNLOAD_COMMANDS
+ ListChangesOption.DOWNLOAD_COMMANDS,
+ ListChangesOption.SUBMITTABLE
);
return this._getChangeDetail(changeNum, options, opt_errFn,
opt_cancelCondition);
@@ -392,13 +403,12 @@
getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchRange) {
return this.getChangeFiles(changeNum, patchRange).then(function(files) {
- return Object.keys(files).sort(this._specialFilePathCompare.bind(this));
+ return Object.keys(files).sort(this.specialFilePathCompare);
}.bind(this));
},
_normalizeChangeFilesResponse: function(response) {
- var paths = Object.keys(response).sort(
- this._specialFilePathCompare.bind(this));
+ var paths = Object.keys(response).sort(this.specialFilePathCompare);
var files = [];
for (var i = 0; i < paths.length; i++) {
var info = response[paths[i]];
@@ -410,42 +420,6 @@
return files;
},
- _specialFilePathCompare: function(a, b) {
- var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
- // The commit message always goes first.
- if (a === COMMIT_MESSAGE_PATH) {
- return -1;
- }
- if (b === COMMIT_MESSAGE_PATH) {
- return 1;
- }
-
- var aLastDotIndex = a.lastIndexOf('.');
- var aExt = a.substr(aLastDotIndex + 1);
- var aFile = a.substr(0, aLastDotIndex);
-
- var bLastDotIndex = b.lastIndexOf('.');
- var bExt = b.substr(bLastDotIndex + 1);
- var bFile = a.substr(0, bLastDotIndex);
-
- // Sort header files above others with the same base name.
- var headerExts = ['h', 'hxx', 'hpp'];
- if (aFile.length > 0 && aFile === bFile) {
- if (headerExts.indexOf(aExt) !== -1 &&
- headerExts.indexOf(bExt) !== -1) {
- return a.localeCompare(b);
- }
- if (headerExts.indexOf(aExt) !== -1) {
- return -1;
- }
- if (headerExts.indexOf(bExt) !== -1) {
- return 1;
- }
- }
-
- return a.localeCompare(b);
- },
-
getChangeRevisionActions: function(changeNum, patchNum) {
return this.fetchJSON(
this.getChangeActionURL(changeNum, patchNum, '/actions')).then(
@@ -467,8 +441,22 @@
});
},
- 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) {
+ var params = {s: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this.fetchJSON('/groups/', opt_errFn, opt_ctx, params);
+ },
+
+ getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+ var params = {p: inputVal};
+ if (opt_n) { params.n = opt_n; }
+ return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params);
+ },
+
+ getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+ var params = {q: inputVal, suggest: null};
+ if (opt_n) { params.n = opt_n; }
+ return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, params);
},
addChangeReviewer: function(changeNum, reviewerID) {
@@ -531,7 +519,7 @@
].join(' ');
var params = {
O: options,
- q: query
+ q: query,
};
return this.fetchJSON('/changes/', null, null, params);
},
@@ -877,5 +865,10 @@
deleteAccountSSHKey: function(id) {
return this.send('DELETE', '/accounts/self/sshkeys/' + id);
},
+
+ deleteVote: function(changeID, account, label) {
+ return this.send('DELETE', '/changes/' + changeID +
+ '/reviewers/' + account + '/votes/' + encodeURIComponent(label));
+ },
});
})();
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..ecd1a07 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
@@ -215,28 +215,54 @@
test('special file path sorting', function() {
assert.deepEqual(
['.b', '/COMMIT_MSG', '.a', 'file'].sort(
- element._specialFilePathCompare),
+ element.specialFilePathCompare),
['/COMMIT_MSG', '.a', '.b', 'file']);
assert.deepEqual(
['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort(
- element._specialFilePathCompare),
+ element.specialFilePathCompare),
['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']);
assert.deepEqual(
['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort(
- element._specialFilePathCompare),
+ element.specialFilePathCompare),
['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']);
assert.deepEqual(
['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort(
- element._specialFilePathCompare),
+ element.specialFilePathCompare),
['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']);
assert.deepEqual(
['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
- element._specialFilePathCompare),
+ 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) {
@@ -298,5 +324,11 @@
});
});
});
+
+ test('legacy n,z key in change url is replaced', function() {
+ var stub = sandbox.stub(element, 'fetchJSON');
+ element.getChanges(1, null, 'n,z');
+ assert.equal(stub.args[0][3].S, 0);
+ });
});
</script>
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..c5df0a2 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -22,18 +22,20 @@
<script src="../bower_components/web-component-tester/browser.js"></script>
<script>
var testFiles = [];
- var basePath = '../elements/';
+ var elementsPath = '../elements/';
+ var behaviorsPath = '../behaviors/';
+ // Elements tests.
[
- '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 +43,32 @@
'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',
+ 'change-list/gr-change-list-view/gr-change-list-view_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 +77,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',
@@ -88,10 +96,18 @@
'shared/gr-select/gr-select_test.html',
'shared/gr-storage/gr-storage_test.html',
].forEach(function(file) {
- file = basePath + file;
+ file = elementsPath + file;
testFiles.push(file);
testFiles.push(file + '?dom=shadow');
});
+ // Behaviors tests.
+ [
+ 'gr-path-list-behavior/gr-path-list-behavior_test.html',
+ ].forEach(function(file) {
+ file = behaviorsPath + file;
+ testFiles.push(file);
+ });
+
WCT.loadSuites(testFiles);
</script>
diff --git a/tools/GoogleFormat.xml b/tools/GoogleFormat.xml
index 8062246..2c65b16 100644
--- a/tools/GoogleFormat.xml
+++ b/tools/GoogleFormat.xml
@@ -45,7 +45,7 @@
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
-<setting id="org.eclipse.jdt.core.compiler.source" value="1.7"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.8"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
@@ -156,7 +156,7 @@
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
-<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.7"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.8"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
@@ -227,7 +227,7 @@
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.7"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="true"/>
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
index e69de29..bfbbd21 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -0,0 +1,6 @@
+
+exports_files([
+ "license-map.py",
+ "test_empty.sh",
+ "test_license.sh",
+])
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl
new file mode 100644
index 0000000..17736cd
--- /dev/null
+++ b/tools/bzl/asciidoc.bzl
@@ -0,0 +1,338 @@
+def documentation_attributes():
+ return [
+ "toc",
+ 'newline="\\n"',
+ 'asterisk="*"',
+ 'plus="+"',
+ 'caret="^"',
+ 'startsb="["',
+ 'endsb="]"',
+ 'tilde="~"',
+ "last-update-label!",
+ "source-highlighter=prettify",
+ "stylesheet=DEFAULT",
+ "linkcss=true",
+ "prettifydir=.",
+ # Just a placeholder, will be filled in asciidoctor java binary:
+ "revnumber=%s",
+ ]
+
+
+def release_notes_attributes():
+ return [
+ 'toc',
+ 'newline="\\n"',
+ 'asterisk="*"',
+ 'plus="+"',
+ 'caret="^"',
+ 'startsb="["',
+ 'endsb="]"',
+ 'tilde="~"',
+ 'last-update-label!',
+ 'stylesheet=DEFAULT',
+ 'linkcss=true',
+ ]
+
+
+def _replace_macros_impl(ctx):
+ cmd = [
+ ctx.file._exe.path,
+ '--suffix', ctx.attr.suffix,
+ "-s", ctx.file.src.path,
+ "-o", ctx.outputs.out.path,
+ ]
+ if ctx.attr.searchbox:
+ cmd.append('--searchbox')
+ else:
+ cmd.append('--no-searchbox')
+ ctx.action(
+ inputs = [ctx.file._exe, ctx.file.src],
+ outputs = [ctx.outputs.out],
+ command = cmd,
+ progress_message = "Replacing macros in %s" % ctx.file.src.short_path,
+ )
+
+_replace_macros = rule(
+ implementation = _replace_macros_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//Documentation:replace_macros.py"),
+ allow_single_file = True,
+ ),
+ "src": attr.label(
+ mandatory = True,
+ allow_single_file = [".txt"],
+ ),
+ "suffix": attr.string(mandatory = True),
+ "searchbox": attr.bool(default = True),
+ "out": attr.output(mandatory = True),
+ },
+)
+
+
+def _generate_asciidoc_args(ctx):
+ args = []
+ if ctx.attr.backend:
+ args.extend(["-b", ctx.attr.backend])
+ revnumber = False
+ for attribute in ctx.attr.attributes:
+ if attribute.startswith("revnumber="):
+ revnumber = True
+ else:
+ args.extend(["-a", attribute])
+ if revnumber:
+ args.extend([
+ "--revnumber-file", ctx.file.version.path,
+ ])
+ for src in ctx.files.srcs:
+ args.append(src.path)
+ return args
+
+
+def _invoke_replace_macros(name, src, suffix, searchbox):
+ fn = src
+ if fn.startswith(":"):
+ fn = src[1:]
+
+ _replace_macros(
+ name = "macros_%s_%s" % (name, fn),
+ src = src,
+ out = fn + suffix,
+ suffix = suffix,
+ searchbox = searchbox,
+ )
+
+ return ":" + fn + suffix, fn.replace(".txt", ".html")
+
+
+def _asciidoc_impl(ctx):
+ args = [
+ "--bazel",
+ "--in-ext", ".txt" + ctx.attr.suffix,
+ "--out-ext", ".html",
+ ]
+ args.extend(_generate_asciidoc_args(ctx))
+ ctx.action(
+ inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
+ outputs = ctx.outputs.outs,
+ executable = ctx.executable._exe,
+ arguments = args,
+ progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+ )
+
+_asciidoc = rule(
+ implementation = _asciidoc_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//lib/asciidoctor:asciidoc"),
+ allow_files = True,
+ executable = True,
+ ),
+ "srcs": attr.label_list(mandatory = True, allow_files = True),
+ "version": attr.label(
+ default = Label("//:version.txt"),
+ allow_single_file = True,
+ ),
+ "suffix": attr.string(mandatory = True),
+ "backend": attr.string(),
+ "attributes": attr.string_list(),
+ "outs": attr.output_list(mandatory = True),
+ },
+)
+
+
+def _genasciidoc_htmlonly(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ **kwargs):
+ SUFFIX = "." + name + "_macros"
+ new_srcs = []
+ outs = ["asciidoctor.css"]
+
+ for src in srcs:
+ new_src, html_name = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+ new_srcs.append(new_src)
+ outs.append(html_name)
+
+ _asciidoc(
+ name = name + "_gen",
+ srcs = new_srcs,
+ suffix = SUFFIX,
+ backend = backend,
+ attributes = attributes,
+ outs = outs,
+ )
+
+ native.filegroup(
+ name = name,
+ data = outs,
+ **kwargs
+ )
+
+
+def genasciidoc(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ resources = True,
+ **kwargs):
+ SUFFIX = "_htmlonly"
+
+ _genasciidoc_htmlonly(
+ name = name + SUFFIX if resources else name,
+ srcs = srcs,
+ attributes = attributes,
+ backend = backend,
+ searchbox = searchbox,
+ **kwargs
+ )
+
+ if resources:
+ htmlonly = ":" + name + SUFFIX
+ native.filegroup(
+ name = name,
+ srcs = [
+ htmlonly,
+ "//Documentation:resources",
+ ],
+ **kwargs
+ )
+
+
+def _asciidoc_html_zip_impl(ctx):
+ args = [
+ "--mktmp",
+ "-z", ctx.outputs.out.path,
+ "--in-ext", ".txt" + ctx.attr.suffix,
+ "--out-ext", ".html",
+ ]
+ args.extend(_generate_asciidoc_args(ctx))
+ ctx.action(
+ inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
+ outputs = [ctx.outputs.out],
+ executable = ctx.executable._exe,
+ arguments = args,
+ progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+ )
+
+_asciidoc_html_zip = rule(
+ implementation = _asciidoc_html_zip_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//lib/asciidoctor:asciidoc"),
+ allow_files = True,
+ executable = True,
+ ),
+ "srcs": attr.label_list(mandatory = True, allow_files = True),
+ "version": attr.label(
+ default = Label("//:version.txt"),
+ allow_single_file = True,
+ ),
+ "suffix": attr.string(mandatory = True),
+ "backend": attr.string(),
+ "attributes": attr.string_list(),
+ },
+ outputs = {
+ "out": "%{name}.zip",
+ }
+)
+
+
+def _genasciidoc_htmlonly_zip(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ **kwargs):
+ SUFFIX = "." + name + "_expn"
+ new_srcs = []
+
+ for src in srcs:
+ new_src, _ = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+ new_srcs.append(new_src)
+
+ _asciidoc_html_zip(
+ name = name,
+ srcs = new_srcs,
+ suffix = SUFFIX,
+ backend = backend,
+ attributes = attributes,
+ )
+
+
+def _asciidoc_zip_impl(ctx):
+ tmpdir = ctx.outputs.out.path + "_tmpdir"
+ cmd = [
+ "p=$PWD",
+ "mkdir -p %s" % tmpdir,
+ "unzip -q %s -d %s/%s/" % (ctx.file.src.path, tmpdir, ctx.attr.directory),
+ ]
+ for r in ctx.files.resources:
+ if r.path == r.short_path:
+ cmd.append("tar -cf- %s | tar -C %s -xf-" % (r.short_path, tmpdir))
+ else:
+ parent = r.path[:-len(r.short_path)]
+ cmd.append(
+ "tar -C %s -cf- %s | tar -C %s -xf-" % (parent, r.short_path, tmpdir))
+ cmd.extend([
+ "cd %s" % tmpdir,
+ "zip -qr $p/%s *" % ctx.outputs.out.path,
+ ])
+ ctx.action(
+ inputs = [ctx.file.src] + ctx.files.resources,
+ outputs = [ctx.outputs.out],
+ command = " && ".join(cmd),
+ progress_message =
+ "Generating asciidoctor zip file %s" % ctx.outputs.out.short_path,
+ )
+
+_asciidoc_zip = rule(
+ implementation = _asciidoc_zip_impl,
+ attrs = {
+ "src": attr.label(
+ mandatory = True,
+ allow_single_file = [".zip"],
+ ),
+ "resources": attr.label_list(mandatory = True, allow_files = True),
+ "directory": attr.string(mandatory = True),
+ },
+ outputs = {
+ "out": "%{name}.zip",
+ }
+)
+
+
+def genasciidoc_zip(
+ name,
+ srcs = [],
+ attributes = [],
+ directory = None,
+ backend = None,
+ searchbox = True,
+ resources = True,
+ **kwargs):
+ SUFFIX = "_htmlonly"
+
+ _genasciidoc_htmlonly_zip(
+ name = name + SUFFIX if resources else name,
+ srcs = srcs,
+ attributes = attributes,
+ backend = backend,
+ searchbox = searchbox,
+ **kwargs
+ )
+
+ if resources:
+ htmlonly = ":" + name + SUFFIX
+ _asciidoc_zip(
+ name = name,
+ src = htmlonly,
+ resources = ["//Documentation:resources"],
+ directory = directory,
+ )
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index 29987ef..c27e28f 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -19,8 +19,6 @@
def gwt_module(gwt_xml=None, resources=[], srcs=[], **kwargs):
if gwt_xml:
resources += [gwt_xml]
- if srcs:
- resources += srcs
java_library2(
srcs = srcs,
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
new file mode 100644
index 0000000..80771da
--- /dev/null
+++ b/tools/bzl/javadoc.bzl
@@ -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.
+
+# Javadoc rule.
+
+def _impl(ctx):
+ zip_output = ctx.outputs.zip
+
+ transitive_jar_set = set()
+ source_jars = set()
+ for l in ctx.attr.libs:
+ source_jars += l.java.source_jars
+ transitive_jar_set += l.java.transitive_deps
+
+ transitive_jar_paths = [j.path for j in transitive_jar_set]
+ dir = ctx.outputs.zip.path + ".dir"
+ source = ctx.outputs.zip.path + ".source"
+ cmd = [
+ "mkdir %s" % source,
+ " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]),
+ "mkdir %s" % dir,
+ " ".join([
+ ctx.file._javadoc.path,
+ "-protected",
+ "-encoding UTF-8",
+ "-charset UTF-8",
+ "-notimestamp",
+ "-quiet",
+ "-windowtitle '%s'" % ctx.attr.title,
+ "-link", "http://docs.oracle.com/javase/7/docs/api",
+ "-sourcepath %s" % source,
+ "-subpackages ",
+ ":".join(ctx.attr.pkgs),
+ " -classpath ",
+ ":".join(transitive_jar_paths),
+ "-d %s" % dir]),
+ "find %s -exec touch -t 198001010000 '{}' ';'" % dir,
+ "(cd %s && zip -qr ../%s *)" % (dir, ctx.outputs.zip.basename),
+ ]
+ ctx.action(
+ inputs = list(transitive_jar_set) + list(source_jars) + ctx.files._jdk,
+ outputs = [zip_output],
+ command = " && ".join(cmd))
+
+java_doc = rule(
+ attrs = {
+ "libs": attr.label_list(allow_files = False),
+ "pkgs": attr.string_list(),
+ "title": attr.string(),
+ "_javadoc": attr.label(
+ default = Label("@local_jdk//:bin/javadoc"),
+ single_file = True,
+ allow_files = True),
+ "_jdk": attr.label(
+ default = Label("@local_jdk//:jdk-default"),
+ allow_files = True),
+ },
+ implementation = _impl,
+ outputs = {"zip" : "%{name}.zip"},
+)
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
new file mode 100644
index 0000000..38d6c91
--- /dev/null
+++ b/tools/bzl/js.bzl
@@ -0,0 +1,366 @@
+NPMJS = "NPMJS"
+GERRIT = "GERRIT"
+
+NPM_VERSIONS = {
+ "bower": '1.7.9',
+ 'crisper': '2.0.2',
+ 'vulcanize': '1.14.8',
+}
+
+NPM_SHA1S = {
+ "bower": 'b7296c2393e0d75edaa6ca39648132dd255812b0',
+ "crisper": '7183c58cea33632fb036c91cefd1b43e390d22a2',
+ 'vulcanize': '679107f251c19ab7539529b1e3fdd40829e6fc63',
+}
+
+
+def _npm_tarball(name):
+ return "%s@%s.npm_binary.tgz" % (name, NPM_VERSIONS[name])
+
+
+def _npm_binary_impl(ctx):
+ """rule to download a NPM archive."""
+ name = ctx.name
+ version= NPM_VERSIONS[name]
+ sha1 = NPM_VERSIONS[name]
+
+ dir = '%s-%s' % (name, version)
+ filename = '%s.tgz' % dir
+ base = '%s@%s.npm_binary.tgz' % (name, version)
+ dest = ctx.path(base)
+ repository = ctx.attr.repository
+ if repository == GERRIT:
+ url = 'http://gerrit-maven.storage.googleapis.com/npm-packages/%s' % filename
+ elif repository == NPMJS:
+ url = 'http://registry.npmjs.org/%s/-/%s' % (name, filename)
+ else:
+ fail('repository %s not in {%s,%s}' % (repository, GERRIT, NPMJS))
+
+ python = ctx.which("python")
+ script = ctx.path(ctx.attr._download_script)
+
+ sha1 = NPM_SHA1S[name]
+ args = [python, script, "-o", dest, "-u", url, "-v", sha1]
+ out = ctx.execute(args)
+ if out.return_code:
+ fail("failed %s: %s" % (args, out.stderr))
+ ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
+
+npm_binary = repository_rule(
+ implementation=_npm_binary_impl,
+ local=True,
+ attrs={
+ # Label resolves within repo of the .bzl file.
+ "_download_script": attr.label(default=Label("//tools:download_file.py")),
+ "repository": attr.string(default=NPMJS),
+ })
+
+
+# for use in repo rules.
+def _run_npm_binary_str(ctx, tarball, args):
+ python_bin = ctx.which("python")
+ return " ".join([
+ python_bin,
+ ctx.path(ctx.attr._run_npm),
+ ctx.path(tarball)] + args)
+
+
+def _bower_archive(ctx):
+ """Download a bower package."""
+ download_name = '%s__download_bower.zip' % ctx.name
+ renamed_name = '%s__renamed.zip' % ctx.name
+ version_name = '%s__version.json' % ctx.name
+
+ cmd = [
+ ctx.which("python"),
+ ctx.path(ctx.attr._download_bower),
+ '-b', '%s' % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
+ '-n', ctx.name,
+ '-p', ctx.attr.package,
+ '-v', ctx.attr.version,
+ '-s', ctx.attr.sha1,
+ '-o', download_name,
+ ]
+
+ out = ctx.execute(cmd)
+ if out.return_code:
+ fail("failed %s: %s" % (" ".join(cmd), out.stderr))
+
+ _bash(ctx, " && " .join([
+ "TMP=$(mktemp -d )",
+ "cd $TMP",
+ "mkdir bower_components",
+ "cd bower_components",
+ "unzip %s" % ctx.path(download_name),
+ "cd ..",
+ "zip -r %s bower_components" % renamed_name,]))
+
+ dep_version = ctx.attr.semver if ctx.attr.semver else ctx.attr.version
+ ctx.file(version_name,
+ '"%s":"%s#%s"' % (ctx.name, ctx.attr.package, dep_version))
+ ctx.file(
+ "BUILD",
+ "\n".join([
+ "package(default_visibility=['//visibility:public'])",
+ "filegroup(name = 'zipfile', srcs = ['%s'], )" % download_name,
+ "filegroup(name = 'version_json', srcs = ['%s'], visibility=['//visibility:public'])" % version_name,
+ ]), False)
+
+
+def _bash(ctx, cmd):
+ cmd_list = ["/bin/bash", "-c", cmd]
+ out = ctx.execute(cmd_list)
+ if out.return_code:
+ fail("failed %s: %s", cmd_list, out.stderr)
+
+
+bower_archive=repository_rule(
+ _bower_archive,
+ attrs={
+ "_bower_archive": attr.label(default=Label("@bower//:%s" % _npm_tarball("bower"))),
+ "_run_npm": attr.label(default=Label("//tools/js:run_npm_binary.py")),
+ "_download_bower": attr.label(default=Label("//tools/js:download_bower.py")),
+ "sha1": attr.string(mandatory=True),
+ "version": attr.string(mandatory=True),
+ "package": attr.string(mandatory=True),
+ "semver": attr.string(),
+ })
+
+
+def _bower_component_impl(ctx):
+ transitive_zipfiles = set([ctx.file.zipfile])
+ for d in ctx.attr.deps:
+ transitive_zipfiles += d.transitive_zipfiles
+
+ transitive_licenses = set()
+ if ctx.file.license:
+ transitive_licenses += set([ctx.file.license])
+
+ for d in ctx.attr.deps:
+ transitive_licenses += d.transitive_licenses
+
+ transitive_versions = set(ctx.files.version_json)
+ for d in ctx.attr.deps:
+ transitive_versions += d.transitive_versions
+
+ return struct(
+ transitive_zipfiles=transitive_zipfiles,
+ transitive_versions=transitive_versions,
+ transitive_licenses=transitive_licenses,
+ )
+
+
+_common_attrs = {
+ "deps": attr.label_list(providers=[
+ "transitive_zipfiles",
+ "transitive_versions",
+ "transitive_licenses",
+ ])
+ }
+
+
+def _js_component(ctx):
+ dir = ctx.outputs.zip.path + ".dir"
+ name = ctx.outputs.zip.basename
+ if name.endswith(".zip"):
+ name = name[:-4]
+ dest = "%s/%s" % (dir, name)
+ cmd = " && ".join([
+ "mkdir -p %s" % dest,
+ "cp %s %s/" % (' '.join([s.path for s in ctx.files.srcs]), dest),
+ "cd %s" % dir,
+ "find . -exec touch -t 198001010000 '{}' ';'",
+ "zip -qr ../%s *" % ctx.outputs.zip.basename
+ ])
+
+ ctx.action(
+ inputs = ctx.files.srcs,
+ outputs = [ctx.outputs.zip],
+ command = cmd,
+ mnemonic = "GenBowerZip")
+
+ licenses = set()
+ if ctx.file.license:
+ licenses += set([ctx.file.license])
+
+ return struct(
+ transitive_zipfiles=list([ctx.outputs.zip]),
+ transitive_versions=set([]),
+ transitive_licenses=licenses)
+
+
+js_component = rule(
+ _js_component,
+ attrs=_common_attrs + {
+ "srcs": attr.label_list(allow_files=[".js"]),
+ "license": attr.label(allow_single_file=True),
+ },
+ outputs={
+ "zip": "%{name}.zip",
+ }
+)
+
+
+_bower_component = rule(
+ _bower_component_impl,
+ attrs=_common_attrs + {
+ "zipfile": attr.label(allow_single_file=[".zip"]),
+ "license": attr.label(allow_single_file=True),
+ "version_json": attr.label(allow_files=[".json"]),
+
+ # If set, define by hand, and don't regenerate this entry in bower2bazel.
+ "seed": attr.bool(default=False)
+ })
+
+
+
+# TODO(hanwen): make license mandatory.
+def bower_component(name, license=None, **kwargs):
+ prefix = "//lib:LICENSE-"
+ if license and not license.startswith(prefix):
+ license = prefix + license
+ _bower_component(
+ name=name,
+ license=license,
+ zipfile="@%s//:zipfile"% name,
+ version_json="@%s//:version_json" % name,
+ **kwargs)
+
+
+def _bower_component_bundle_impl(ctx):
+ """A bunch of bower components zipped up."""
+ zips = set([])
+ for d in ctx.attr.deps:
+ zips += d.transitive_zipfiles
+
+ versions = set([])
+ for d in ctx.attr.deps:
+ versions += d.transitive_versions
+
+ licenses = set([])
+ for d in ctx.attr.deps:
+ licenses += d.transitive_versions
+
+ out_zip = ctx.outputs.zip
+ out_versions = ctx.outputs.version_json
+
+ ctx.action(
+ inputs=list(zips),
+ outputs=[out_zip],
+ command=" && ".join([
+ "p=$PWD",
+ "rm -rf %s.dir" % out_zip.path,
+ "mkdir -p %s.dir/bower_components" % out_zip.path,
+ "cd %s.dir/bower_components" % out_zip.path,
+ "for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
+ "cd ..",
+ "find . -exec touch -t 198001010000 '{}' ';'",
+ "zip -qr $p/%s bower_components/*" % out_zip.path,
+ ]),
+ mnemonic="BowerCombine")
+
+ ctx.action(
+ inputs=list(versions),
+ outputs=[out_versions],
+ mnemonic="BowerVersions",
+ command="(echo '{' ; for j in %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path))
+
+ return struct(
+ transitive_zipfiles=zips,
+ transitive_versions=versions,
+ transitive_licenses=licenses)
+
+
+bower_component_bundle = rule(
+ _bower_component_bundle_impl,
+ attrs=_common_attrs,
+ outputs={
+ "zip": "%{name}.zip",
+ "version_json": "%{name}-versions.json",
+ }
+)
+
+def _vulcanize_impl(ctx):
+ destdir = ctx.outputs.vulcanized.path + ".dir"
+ zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles ]
+
+ hermetic_npm_binary = " ".join([
+ 'python',
+ "$p/" + ctx.file._run_npm.path,
+ "$p/" + ctx.file._vulcanize_archive.path,
+ '--inline-scripts',
+ '--inline-css',
+ '--strip-comments',
+ '--out-html', "$p/" + ctx.outputs.vulcanized.path,
+ ctx.file.app.path
+ ])
+
+ pkg_dir = ctx.attr.pkg.lstrip("/")
+ cmd = " && ".join([
+ # unpack dependencies.
+ "export PATH",
+ "p=$PWD",
+ "rm -rf %s" % destdir,
+ "mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
+ "for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
+ ' '.join([z.path for z in zips]), destdir, pkg_dir),
+ "tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
+ "cd %s" % destdir,
+ hermetic_npm_binary,
+ ])
+ ctx.action(
+ mnemonic = "Vulcanize",
+ inputs = [ctx.file._run_npm, ctx.file.app,
+ ctx.file._vulcanize_archive
+ ] + list(zips) + ctx.files.srcs,
+ outputs = [ctx.outputs.vulcanized],
+ command = cmd)
+
+ hermetic_npm_command = "export PATH && " + " ".join([
+ 'python',
+ ctx.file._run_npm.path,
+ ctx.file._crisper_archive.path,
+ "--always-write-script",
+ "--source", ctx.outputs.vulcanized.path,
+ "--html", ctx.outputs.html.path,
+ "--js", ctx.outputs.js.path])
+
+ ctx.action(
+ mnemonic = "Crisper",
+ inputs = [ctx.file._run_npm, ctx.file.app,
+ ctx.file._crisper_archive, ctx.outputs.vulcanized],
+ outputs = [ctx.outputs.js, ctx.outputs.html],
+ command = hermetic_npm_command)
+
+
+_vulcanize_rule = rule(
+ _vulcanize_impl,
+ attrs = {
+ "deps": attr.label_list(providers=["transitive_zipfiles"]),
+ "app": attr.label(mandatory=True, allow_single_file=True),
+ "srcs": attr.label_list(allow_files=[".js", ".html", ".txt", ".css", ".ico"]),
+
+ "pkg": attr.string(mandatory=True),
+ "_run_npm": attr.label(
+ default=Label("//tools/js:run_npm_binary.py"),
+ allow_single_file=True
+ ),
+ "_vulcanize_archive": attr.label(
+ default=Label("@vulcanize//:%s" % _npm_tarball("vulcanize")),
+ allow_single_file=True
+ ),
+ "_crisper_archive": attr.label(
+ default=Label("@crisper//:%s" % _npm_tarball("crisper")),
+ allow_single_file=True
+ ),
+ },
+ outputs = {
+ "vulcanized": "%{name}.vulcanized.html",
+ "html": "%{name}.crisped.html",
+ "js": "%{name}.crisped.js",
+ }
+)
+
+def vulcanize(*args, **kwargs):
+ """Vulcanize runs vulcanize and crisper on a set of sources."""
+ _vulcanize_rule(*args, pkg=PACKAGE_NAME, **kwargs)
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
new file mode 100644
index 0000000..8469f4f
--- /dev/null
+++ b/tools/bzl/license-map.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+
+# reads bazel query XML files, to join target names with their licenses.
+
+from __future__ import print_function
+
+import argparse
+from collections import defaultdict
+from shutil import copyfileobj
+from sys import stdout, stderr
+import xml.etree.ElementTree as ET
+
+KNOWN_PROVIDED_DEPS = [
+ "//lib/bouncycastle:bcpg",
+ "//lib/bouncycastle:bcpkix",
+ "//lib/bouncycastle:bcprov",
+]
+
+DO_NOT_DISTRIBUTE = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
+
+LICENSE_PREFIX = "//lib:LICENSE-"
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--asciidoctor", action="store_true")
+parser.add_argument("xmls", nargs="+")
+args = parser.parse_args()
+
+entries = defaultdict(list)
+graph = defaultdict(list)
+handled_rules = []
+
+for xml in args.xmls:
+ tree = ET.parse(xml)
+ root = tree.getroot()
+
+ for child in root:
+ rule_name = child.attrib["name"]
+ if rule_name in handled_rules:
+ # already handled in other xml files
+ continue
+
+ handled_rules.append(rule_name)
+ for c in child.getchildren():
+ if c.tag != "rule-input":
+ continue
+
+ license_name = c.attrib["name"]
+ if LICENSE_PREFIX in license_name:
+ if rule_name in KNOWN_PROVIDED_DEPS:
+ continue
+
+ entries[rule_name].append(license_name)
+ graph[license_name].append(rule_name)
+
+if len(graph[DO_NOT_DISTRIBUTE]):
+ print("DO_NOT_DISTRIBUTE license found in:", file=stderr)
+ for target in graph[DO_NOT_DISTRIBUTE]:
+ print(target, file=stderr)
+ exit(1)
+
+if args.asciidoctor:
+ print(
+# We don't want any blank line before "= 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
+
+This distribution includes cryptographic software. The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software. BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted. See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms. The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+For either feature to function, Gerrit requires the
+link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
+and/or the
+link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
+to be installed by the end-user.
+
+== Licenses
+""")
+
+ for n in sorted(graph.keys()):
+ if len(graph[n]) == 0:
+ continue
+
+ name = n[len(LICENSE_PREFIX):]
+ safename = name.replace(".", "_")
+ print()
+ print("[[%s]]" % safename)
+ print("=== " + name)
+ print()
+ for d in sorted(graph[n]):
+ if d.startswith("//lib:") or d.startswith("//lib/"):
+ p = d[len("//lib:"):]
+ else:
+ p = d[d.index(":")+1:].lower()
+ if "__" in p:
+ p = p[:p.index("__")]
+ print("* " + p)
+ print()
+ print("[[%s_license]]" % safename)
+ print("----")
+ with open(n[2:].replace(":", "/")) as fd:
+ copyfileobj(fd, stdout)
+ print()
+ print("----")
+ print()
+
+ print(
+"""
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+""")
+
+else:
+ for k, vs in sorted(entries.items()):
+ for v in vs:
+ print(k, v)
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
new file mode 100644
index 0000000..60ee60b
--- /dev/null
+++ b/tools/bzl/license.bzl
@@ -0,0 +1,58 @@
+
+def normalize_target_name(target):
+ return target.replace("//", "").replace("/", "__").replace(":", "___")
+
+def license_map(name, targets = [], opts = [], **kwargs):
+ """Generate XML for all targets that depend directly on a LICENSE file"""
+ xmls = []
+ tools = [ "//tools/bzl:license-map.py", "//lib:all-licenses" ]
+ for target in targets:
+ subname = name + "_" + normalize_target_name(target) + ".xml"
+ xmls.append("$(location :%s)" % subname)
+ tools.append(subname)
+ native.genquery(
+ name = subname,
+ scope = [ target ],
+
+ # Find everything that depends on a license file, but remove
+ # the license files themselves from this list.
+ expression = 'rdeps(%s, filter("//lib:LICENSE.*", deps(%s)),1) - filter("//lib:LICENSE.*", deps(%s))' % (target, target, target),
+
+ # We are interested in the edges of the graph ({java_library,
+ # license-file} tuples). 'query' provides this in the XML output.
+ opts = [ "--output=xml", ],
+ )
+
+ # post process the XML into our favorite format.
+ native.genrule(
+ name = "gen_license_txt_" + name,
+ cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+ outs = [ name + ".txt" ],
+ tools = tools,
+ **kwargs
+ )
+
+def license_test(name, target):
+ """Make sure a target doesn't depend on DO_NOT_DISTRIBUTE license"""
+ txt = name + "-forbidden.txt"
+
+ # fully qualify target name.
+ if target[0] not in ":/":
+ target = ":" + target
+ if target[0] != "/":
+ target = "//" + PACKAGE_NAME + target
+
+ forbidden = "//lib:LICENSE-DO_NOT_DISTRIBUTE"
+ native.genquery(
+ name = txt,
+ scope = [ target, forbidden ],
+ # Find everything that depends on a license file, but remove
+ # the license files themselves from this list.
+ expression = 'rdeps(%s, "%s", 1) - rdeps(%s, "%s", 0)' % (target, forbidden, target, forbidden),
+ )
+ native.sh_test(
+ name = name,
+ srcs = [ "//tools/bzl:test_license.sh" ],
+ args = [ "$(location :%s)" % txt ],
+ data = [ txt ],
+ )
diff --git a/tools/bzl/maven.bzl b/tools/bzl/maven.bzl
index ce2f483..c255c0c 100644
--- a/tools/bzl/maven.bzl
+++ b/tools/bzl/maven.bzl
@@ -18,10 +18,7 @@
return ('$(location //tools:merge_jars) $@ '
+ ' '.join(['$(location %s)' % j for j in jars]))
-def merge_maven_jars(
- name,
- srcs,
- visibility = []):
+def merge_maven_jars(name, srcs, **kwargs):
native.genrule(
name = '%s__merged_bin' % name,
cmd = cmd(srcs),
@@ -31,5 +28,5 @@
native.java_import(
name = name,
jars = [':%s__merged_bin' % name],
- visibility = visibility,
+ **kwargs
)
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
new file mode 100644
index 0000000..ef0fe51
--- /dev/null
+++ b/tools/bzl/pkg_war.bzl
@@ -0,0 +1,139 @@
+# 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.
+
+# War packaging.
+
+jar_filetype = FileType(['.jar'])
+
+LIBS = [
+ '//gerrit-war:init',
+ '//gerrit-war:log4j-config',
+ '//gerrit-war:version',
+ '//lib:postgresql',
+ '//lib/log:impl_log4j',
+]
+
+PGMLIBS = [
+ '//gerrit-pgm:pgm'
+]
+
+def _add_context(in_file, output):
+ input_path = in_file.path
+ return [
+ 'unzip -qd %s %s' % (output, input_path)
+ ]
+
+def _add_file(in_file, output):
+ output_path = output
+ input_path = in_file.path
+ short_path = in_file.short_path
+ n = in_file.basename
+
+ # TODO(davido): Drop this when provided_deps added to java_library
+ if n.find('-jdk15on-') != -1:
+ return []
+
+ if short_path.startswith('gerrit-'):
+ n = short_path.split('/')[0] + '-' + n
+
+ output_path += n
+ return [
+ 'test -L %s || ln -s $(pwd)/%s %s' % (output_path, input_path, output_path)
+ ]
+
+def _make_war(input_dir, output):
+ return ''.join([
+ '(root=$(pwd) && ',
+ 'cd %s && ' % input_dir,
+ 'zip -9qr ${root}/%s .)' % (output.path),
+ ])
+
+def _war_impl(ctx):
+ war = ctx.outputs.war
+ build_output = war.path + '.build_output'
+ inputs = []
+
+ # Create war layout
+ cmd = [
+ 'set -e;rm -rf ' + build_output,
+ 'mkdir -p ' + build_output,
+ 'mkdir -p %s/WEB-INF/lib' % build_output,
+ 'mkdir -p %s/WEB-INF/pgm-lib' % build_output,
+ ]
+
+ # Add lib
+ transitive_lib_deps = set()
+ for l in ctx.attr.libs:
+ transitive_lib_deps += l.java.transitive_runtime_deps
+
+ for dep in transitive_lib_deps:
+ cmd += _add_file(dep, build_output + '/WEB-INF/lib/')
+ inputs.append(dep)
+
+ # Add pgm lib
+ transitive_pgmlib_deps = set()
+ for l in ctx.attr.pgmlibs:
+ transitive_pgmlib_deps += l.java.transitive_runtime_deps
+
+ for dep in transitive_pgmlib_deps:
+ if dep not in inputs:
+ cmd += _add_file(dep, build_output + '/WEB-INF/pgm-lib/')
+ inputs.append(dep)
+
+ # Add context
+ transitive_context_deps = set()
+ if ctx.attr.context:
+ for jar in ctx.attr.context:
+ if hasattr(jar, 'java'):
+ transitive_context_deps += jar.java.transitive_runtime_deps
+ elif hasattr(jar, 'files'):
+ transitive_context_deps += jar.files
+ for dep in transitive_context_deps:
+ cmd += _add_context(dep, build_output)
+ inputs.append(dep)
+
+ # Add zip war
+ cmd.append(_make_war(build_output, war))
+
+ ctx.action(
+ inputs = inputs,
+ outputs = [war],
+ mnemonic = 'WAR',
+ command = '\n'.join(cmd),
+ use_default_shell_env = True,
+ )
+
+_pkg_war = rule(
+ attrs = {
+ 'context': attr.label_list(allow_files = True),
+ 'libs': attr.label_list(allow_files = jar_filetype),
+ 'pgmlibs': attr.label_list(allow_files = False),
+ },
+ implementation = _war_impl,
+ outputs = {'war' : '%{name}.war'},
+)
+
+def pkg_war(name, ui = 'ui_optdbg', context = []):
+ ui_deps = []
+ if ui:
+ ui_deps.append('//gerrit-gwtui:%s' % ui)
+ _pkg_war(
+ name = name,
+ libs = LIBS,
+ pgmlibs = PGMLIBS,
+ context = context + ui_deps + [
+ '//gerrit-main:main_bin_deploy.jar',
+ '//gerrit-war:webapp_assets',
+ ],
+ )
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..bdd1794
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,34 @@
+
+def gerrit_plugin(
+ name,
+ deps = [],
+ srcs = [],
+ resources = [],
+ manifest_entries = []):
+ # TODO(davido): Fix stamping: run git describe in plugin directory
+ # https://github.com/bazelbuild/bazel/issues/1758
+ manifest_lines = [
+ "Gerrit-ApiType: plugin",
+ "Implementation-Version: 1.0",
+ "Implementation-Vendor: Gerrit Code Review",
+ ]
+ for line in manifest_entries:
+ manifest_lines.append(line.replace('$', '\$'))
+
+ native.java_library(
+ name = name + '__plugin',
+ srcs = srcs,
+ resources = resources,
+ deps = deps + ['//gerrit-plugin-api:lib-neverlink'],
+ visibility = ['//visibility:public'],
+ )
+
+ native.java_binary(
+ name = name,
+ deploy_manifest_lines = manifest_lines,
+ main_class = 'Dummy',
+ runtime_deps = [
+ ':%s__plugin' % name,
+ ],
+ visibility = ['//visibility:public'],
+ )
diff --git a/tools/bzl/test_empty.sh b/tools/bzl/test_empty.sh
new file mode 100755
index 0000000..0d4398d
--- /dev/null
+++ b/tools/bzl/test_empty.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if test -s $1
+then
+ echo "$1 not empty:"
+ cat $1
+ exit 1
+fi
diff --git a/tools/bzl/test_license.sh b/tools/bzl/test_license.sh
new file mode 100755
index 0000000..6ac6dab
--- /dev/null
+++ b/tools/bzl/test_license.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+filtered="$1.filtered"
+
+cat $1 \
+ | grep -v "//lib/bouncycastle:bcpg" \
+ | grep -v "//lib/bouncycastle:bcpkix" \
+ | grep -v "//lib/bouncycastle:bcprov" \
+ > $filtered
+
+if test -s $filtered
+then
+ echo "$filtered not empty:"
+ cat $filtered
+ exit 1
+fi
diff --git a/tools/bzl/unsign.bzl b/tools/bzl/unsign.bzl
new file mode 100644
index 0000000..f42986a
--- /dev/null
+++ b/tools/bzl/unsign.bzl
@@ -0,0 +1,16 @@
+
+def unsign_jars(name, deps, **kwargs):
+ """unsign_jars collects its dependencies into a single java_import.
+
+ As a side effect, the signature is removed.
+ """
+ native.java_binary(
+ name = name + '-unsigned-binary',
+ runtime_deps = deps,
+ main_class = 'dummy'
+ )
+
+ native.java_import(
+ name = name,
+ jars = [ name + '-unsigned-binary_deploy.jar' ],
+ **kwargs)
diff --git a/tools/default.defs b/tools/default.defs
index 191dfe5..fb4c6de 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -201,7 +201,7 @@
':%s__gwt_application' % name +
';cd $TMP' +
';zip -qr $OUT .',
- out = '%s-static.zip' % name,
+ out = '%s-static.jar' % name,
)
gwt_binary(
name = name + '__gwt_application',
diff --git a/tools/download_file.py b/tools/download_file.py
index bd67b50..c9736bf 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -26,10 +26,6 @@
GERRIT_HOME = path.expanduser('~/.gerritcodereview')
CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache', 'downloaded-artifacts')
-# LEGACY_CACHE_DIR is only used to allow existing workspaces to move already
-# downloaded files to the new cache directory.
-# Please remove after 3 months (2015-10-07).
-LEGACY_CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
LOCAL_PROPERTIES = 'local.properties'
@@ -78,16 +74,6 @@
name = '%s-%s' % (path.basename(args.o), h)
return path.join(CACHE_DIR, name)
-# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
-def legacy_cache_entry(args):
- if args.v:
- h = args.v
- else:
- h = sha1(args.u.encode('utf-8')).hexdigest()
- name = '%s-%s' % (path.basename(args.o), h)
- return path.join(LEGACY_CACHE_DIR, name)
-
-
opts = OptionParser()
opts.add_option('-o', help='local output file')
opts.add_option('-u', help='URL to download')
@@ -98,26 +84,15 @@
args, _ = opts.parse_args()
root_dir = args.o
-while root_dir:
+while root_dir and root_dir != "/":
root_dir, n = path.split(root_dir)
if n == 'buck-out':
break
redirects = download_properties(root_dir)
cache_ent = cache_entry(args)
-legacy_cache_ent = legacy_cache_entry(args)
src_url = resolve_url(args.u, redirects)
-# Please remove after 3 months (2015-10-07). See LEGACY_CACHE_DIR above.
-if not path.exists(cache_ent) and path.exists(legacy_cache_ent):
- try:
- safe_mkdirs(path.dirname(cache_ent))
- except OSError as err:
- print('error creating directory %s: %s' %
- (path.dirname(cache_ent), err), file=stderr)
- exit(1)
- shutil.move(legacy_cache_ent, cache_ent)
-
if not path.exists(cache_ent):
try:
safe_mkdirs(path.dirname(cache_ent))
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 0bcde9d..dfd271d 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -15,14 +15,21 @@
'//gerrit-reviewdb:client_tests',
'//gerrit-server:server',
'//gerrit-server:server_tests',
+ '//lib:jimfs',
'//lib/asciidoctor:asciidoc_lib',
'//lib/asciidoctor:doc_indexer_lib',
'//lib/auto:auto-value',
'//lib/bouncycastle:bcprov',
'//lib/bouncycastle:bcpg',
'//lib/bouncycastle:bcpkix',
+ '//lib/gwt:ant',
+ '//lib/gwt:colt',
'//lib/gwt:javax-validation',
'//lib/gwt:javax-validation_src',
+ '//lib/gwt:jsinterop-annotations',
+ '//lib/gwt:jsinterop-annotations_src',
+ '//lib/gwt:tapestry',
+ '//lib/gwt:w3c-css-sac',
'//lib/jetty:servlets',
'//lib/prolog:compiler_lib',
'//polygerrit-ui:polygerrit_components',
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 46f5680..a07cf98 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -28,7 +28,7 @@
JRE = '/'.join([
'org.eclipse.jdt.launching.JRE_CONTAINER',
'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType',
- 'JavaSE-1.7',
+ 'JavaSE-1.8',
])
ROOT = path.abspath(__file__)
@@ -119,6 +119,7 @@
# Classpath entries are absolute for cross-cell support
java_library = re.compile('.*/buck-out/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
+ srcs = re.compile('.*/(__.*__)/.*')
for p in _query_classpath(MAIN):
if p.endswith('-src.jar'):
# gwt_module() depends on -src.jar for Java to JavaScript compiles.
@@ -175,9 +176,19 @@
for j in sorted(libs):
s = None
if j.endswith('.jar'):
- s = j[:-4] + '_src.jar'
+ s = j[:-4] + '-src.jar'
if not path.exists(s):
- s = None
+ m = srcs.match(s)
+ if m:
+ l = m.group(1)
+ if l.endswith('__jar__'):
+ s = s.replace(l, l.replace('__jar__', '_src__'))
+ else:
+ s = s.replace(l, l[:-1] + 'src__')
+ if not path.exists(s):
+ s = None
+ else:
+ s = None
if args.plugins:
classpathentry('lib', j, s, exported=True)
else:
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs
index 8bafddb..b76c04b 100644
--- a/tools/gwt-constants.defs
+++ b/tools/gwt-constants.defs
@@ -14,8 +14,14 @@
]
GWT_TRANSITIVE_DEPS = [
+ '//lib/gwt:ant',
+ '//lib/gwt:colt',
'//lib/gwt:javax-validation',
'//lib/gwt:javax-validation_src',
+ '//lib/gwt:jsinterop-annotations',
+ '//lib/gwt:jsinterop-annotations_src',
+ '//lib/gwt:tapestry',
+ '//lib/gwt:w3c-css-sac',
'//lib/ow2:ow2-asm',
'//lib/ow2:ow2-asm-analysis',
'//lib/ow2:ow2-asm-commons',
diff --git a/tools/java_doc.defs b/tools/java_doc.defs
index 41a8730..583407c6 100644
--- a/tools/java_doc.defs
+++ b/tools/java_doc.defs
@@ -2,23 +2,22 @@
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]
- external_docs.insert(0, 'http://docs.oracle.com/javase/7/docs/api')
+ # 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/8/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',
+ '-Xdoclint:-missing',
'-quiet',
'-protected',
'-encoding UTF-8',
@@ -28,8 +27,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 +35,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>
diff --git a/tools/js/BUCK b/tools/js/BUCK
index ba4f19c..9eb0c91 100644
--- a/tools/js/BUCK
+++ b/tools/js/BUCK
@@ -1,14 +1,26 @@
python_binary(
name = 'bower2buck',
main = 'bower2buck.py',
- deps = ['//tools:util'],
+ deps = [
+ '//tools:util',
+ ":bowerutil",
+ ],
visibility = ['PUBLIC'],
)
+python_library(
+ name = 'bowerutil',
+ srcs = [ 'bowerutil.py' ],
+ visibility = [ 'PUBLIC' ],
+)
+
python_binary(
name = 'download_bower',
main = 'download_bower.py',
- deps = ['//tools:util'],
+ deps = [
+ '//tools:util',
+ ":bowerutil",
+ ],
visibility = ['PUBLIC'],
)
diff --git a/tools/js/BUILD b/tools/js/BUILD
new file mode 100644
index 0000000..fedaf7f
--- /dev/null
+++ b/tools/js/BUILD
@@ -0,0 +1 @@
+exports_files(["run_npm_binary.py"])
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
new file mode 100755
index 0000000..8682012
--- /dev/null
+++ b/tools/js/bower2bazel.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python
+# 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.
+
+"""Suggested call sequence:
+
+python tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl
+"""
+
+from __future__ import print_function
+
+import atexit
+import collections
+import json
+import hashlib
+import optparse
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import glob
+import bowerutil
+
+# Map license names to our canonical names.
+license_map = {
+ "http://polymer.github.io/LICENSE.txt": "polymer",
+ "Apache-2.0": "Apache2.0",
+
+ # TODO(hanwen): remove these, and add appropriate license files under //lib
+ "BSD": "polymer",
+ "MIT": "polymer",
+}
+
+# list of licenses for packages that don't specify one in their bower.json file.
+package_licenses = {
+ "es6-promise": "es6-promise",
+ "fetch": "fetch",
+ "moment": "moment",
+ "page": "page.js",
+ "promise-polyfill": "promise-polyfill",
+ "webcomponentsjs": "polymer", # self-identifies as BSD.
+}
+
+
+def build_bower_json(version_targets):
+ """Generate bower JSON file, return its path."""
+ bower_json = collections.OrderedDict()
+ bower_json['name'] = 'bower2buck-output'
+ bower_json['version'] = '0.0.0'
+ bower_json['description'] = 'Auto-generated bower.json for dependency management'
+ bower_json['private'] = True
+ bower_json['dependencies'] = {}
+
+ for v in version_targets:
+ fn = os.path.join("bazel-out/local-fastbuild/bin", v.lstrip("/").replace(":", "/"))
+ with open(fn) as f:
+ j = json.load(f)
+ if "" in j:
+ # drop dummy entries.
+ del j[""]
+ bower_json['dependencies'].update(j)
+
+ tmpdir = tempfile.mkdtemp()
+ ret = os.path.join(tmpdir, 'bower.json')
+ with open(ret, 'w') as f:
+ json.dump(bower_json, f, indent=2)
+ return ret
+
+
+def bower_command(args):
+ base = subprocess.check_output(["bazel", "info", "output_base"]).strip()
+ exp = os.path.join(base, "external", "bower", "*npm_binary.tgz")
+ fs = sorted(glob.glob(exp))
+ assert len(fs) == 1, "bower tarball not found or have multiple versions %s" % fs
+ return ["python", os.getcwd() + "/tools/js/run_npm_binary.py", sorted(fs)[0]] + args
+
+
+def main(args):
+ opts = optparse.OptionParser()
+ opts.add_option('-w', help='.bzl output for WORKSPACE')
+ opts.add_option('-b', help='.bzl output for //lib:BUILD')
+ opts, args = opts.parse_args()
+
+ target_str = subprocess.check_output([
+ "bazel", "query", "kind(bower_component_bundle, //polygerrit-ui/...)"])
+ seed_str = subprocess.check_output([
+ "bazel", "query", "attr(seed, 1, kind(bower_component, deps(//polygerrit-ui/...)))"])
+ targets = [s for s in target_str.split('\n') if s]
+ seeds = [s for s in seed_str.split('\n') if s]
+ prefix = "//lib/js:"
+ non_seeds = [s for s in seeds if not s.startswith(prefix)]
+ assert not non_seeds, non_seeds
+ seeds = set([s[len(prefix):] for s in seeds])
+
+ version_targets = [t + "-versions.json" for t in targets]
+
+ subprocess.check_call(['bazel', 'build'] + version_targets)
+ bower_json_path = build_bower_json(version_targets)
+ dir = os.path.dirname(bower_json_path)
+ cmd = bower_command(["install"])
+
+ build_out = sys.stdout
+ if opts.b:
+ build_out = open(opts.b + ".tmp", 'w')
+
+ ws_out = sys.stdout
+ if opts.b:
+ ws_out = open(opts.w + ".tmp", 'w')
+
+ header = """# DO NOT EDIT
+# generated with the following command:
+#
+# %s
+#
+
+""" % ' '.join(sys.argv)
+
+ ws_out.write(header)
+ build_out.write(header)
+
+ oldwd = os.getcwd()
+ os.chdir(dir)
+ subprocess.check_call(cmd)
+
+ interpret_bower_json(seeds, ws_out, build_out)
+ ws_out.close()
+ build_out.close()
+
+ os.chdir(oldwd)
+ os.rename(opts.w + ".tmp", opts.w)
+ os.rename(opts.b + ".tmp", opts.b)
+
+
+def dump_workspace(data, seeds, out):
+ out.write('load("//tools/bzl:js.bzl", "bower_archive")\n')
+ out.write('def load_bower_archives():\n')
+
+ for d in data:
+ if d["name"] in seeds:
+ continue
+ out.write(""" bower_archive(
+ name = "%(name)s",
+ package = "%(normalized-name)s",
+ version = "%(version)s",
+ sha1 = "%(bazel-sha1)s")
+""" % d)
+
+
+def dump_build(data, seeds, out):
+ out.write('load("//tools/bzl:js.bzl", "bower_component")\n')
+ out.write('def define_bower_components():\n')
+ for d in data:
+ out.write(" bower_component(\n")
+ out.write(" name = \"%s\",\n" % d["name"])
+ out.write(" license = \"//lib:LICENSE-%s\",\n" % d["bazel-license"])
+ deps = sorted(d.get("dependencies", {}).keys())
+ if deps:
+ if len(deps) == 1:
+ out.write(" deps = [ \":%s\" ],\n" % deps[0])
+ else:
+ out.write(" deps = [\n")
+ for dep in deps:
+ out.write(" \":%s\",\n" % dep)
+ out.write(" ],\n")
+ if d["name"] in seeds:
+ out.write(" seed = True,\n")
+ out.write(" )\n")
+ # done
+
+
+def interpret_bower_json(seeds, ws_out, build_out):
+ out = subprocess.check_output(["find", "bower_components/", "-name", ".bower.json"])
+
+ data = []
+ for f in sorted(out.split('\n')):
+ if not f:
+ continue
+ pkg = json.load(open(f))
+ pkg_name = pkg["name"]
+
+ pkg["bazel-sha1"] = bowerutil.hash_bower_component(
+ hashlib.sha1(), os.path.dirname(f)).hexdigest()
+ license = pkg.get("license", None)
+ if type(license) == type([]):
+ # WTF? Some package specify a list of licenses. ("GPL", "MIT")
+ pick = license[0]
+ sys.stderr.write("package %s has multiple licenses: %s, picking %s" % (pkg_name, ", ".join(license), pick))
+ license = pick
+
+ if license:
+ license = license_map.get(license, license)
+ else:
+ if pkg_name not in package_licenses:
+ msg = "package %s does not specify license." % pkg_name
+ sys.stderr.write(msg)
+ raise Exception(msg)
+ license = package_licenses[pkg_name]
+
+ pkg["bazel-license"] = license
+
+ # TODO(hanwen): bower packages can also have 'fully qualified'
+ # names, ("PolymerElements/iron-ajax") as well as short names
+ # ("iron-ajax"). It is possible for bower.json files to refer to
+ # long names as their dependencies. If any package does this, we
+ # will have to either 1) strip off the prefix (typically github
+ # user?), or 2) build a map of short name <=> fully qualified
+ # name. For now, we just ignore the problem.
+ pkg["normalized-name"] = pkg["name"]
+ data.append(pkg)
+
+ dump_workspace(data, seeds, ws_out)
+ dump_build(data, seeds, build_out)
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/tools/js/bower2buck.py b/tools/js/bower2buck.py
index 81072da..57da475 100755
--- a/tools/js/bower2buck.py
+++ b/tools/js/bower2buck.py
@@ -78,7 +78,7 @@
self.version = bower_json['version']
self.deps = bower_json.get('dependencies', {})
self.license = bower_json.get('license', 'NO LICENSE')
- self.sha1 = util.hash_bower_component(
+ self.sha1 = bowerutil.hash_bower_component(
hashlib.sha1(), os.path.dirname(bower_json_path)).hexdigest()
def to_rule(self, packages):
@@ -106,6 +106,7 @@
def build_bower_json(targets, buck_out):
+ """create bower.json so 'bower install' fetches transitive deps"""
bower_json = collections.OrderedDict()
bower_json['name'] = 'bower2buck-output'
bower_json['version'] = '0.0.0'
@@ -117,6 +118,9 @@
['buck', 'query', '-v', '0',
"filter('__download_bower', deps(%s))" % '+'.join(targets)],
env=BUCK_ENV)
+
+ # __bower_version contains the version number coming from version
+ # attr in BUCK/BUILD
deps = deps.replace('__download_bower', '__bower_version').split()
subprocess.check_call(['buck', 'build'] + deps, env=BUCK_ENV)
diff --git a/tools/js/bowerutil.py b/tools/js/bowerutil.py
new file mode 100644
index 0000000..eb8893b
--- /dev/null
+++ b/tools/js/bowerutil.py
@@ -0,0 +1,47 @@
+# 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.
+
+import os
+from os import path
+
+
+def hash_bower_component(hash_obj, path):
+ """Hash the contents of a bower component directory.
+
+ This is a stable hash of a directory downloaded with `bower install`, minus
+ the .bower.json file, which is autogenerated each time by bower. Used in lieu
+ of hashing a zipfile of the contents, since zipfiles are difficult to hash in
+ a stable manner.
+
+ Args:
+ hash_obj: an open hash object, e.g. hashlib.sha1().
+ path: path to the directory to hash.
+
+ Returns:
+ The passed-in hash_obj.
+ """
+ if not os.path.isdir(path):
+ raise ValueError('Not a directory: %s' % path)
+
+ path = os.path.abspath(path)
+ for root, dirs, files in os.walk(path):
+ dirs.sort()
+ for f in sorted(files):
+ if f == '.bower.json':
+ continue
+ p = os.path.join(root, f)
+ hash_obj.update(p[len(path)+1:])
+ hash_obj.update(open(p).read())
+
+ return hash_obj
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
old mode 100644
new mode 100755
index bcc417c..f5b7bf5
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -23,8 +23,7 @@
import subprocess
import sys
-from tools import util
-
+import bowerutil
CACHE_DIR = os.path.expanduser(os.path.join(
'~', '.gerritcodereview', 'buck-cache', 'downloaded-artifacts'))
@@ -39,16 +38,20 @@
def bower_info(bower, name, package, version):
cmd = bower_cmd(bower, '-l=error', '-j',
'info', '%s#%s' % (package, version))
- p = subprocess.Popen(cmd , stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ try:
+ p = subprocess.Popen(cmd , stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ except:
+ sys.stderr.write("error executing: %s\n" % ' '.join(cmd))
+ raise
out, err = p.communicate()
if p.returncode:
sys.stderr.write(err)
- raise OSError('Command failed: %s' % cmd)
+ raise OSError('Command failed: %s' % ' '.join(cmd))
try:
info = json.loads(out)
except ValueError:
- raise ValueError('invalid JSON from %s:\n%s' % (cmd, out))
+ raise ValueError('invalid JSON from %s:\n%s' % (" ".join(cmd), out))
info_name = info.get('name')
if info_name != name:
raise ValueError('expected package name %s, got: %s' % (name, info_name))
@@ -82,7 +85,11 @@
opts.add_option('-v', help='version number')
opts.add_option('-s', help='expected content sha1')
opts.add_option('-o', help='output file location')
- opts, _ = opts.parse_args()
+ opts, args_ = opts.parse_args(args)
+
+ assert opts.p
+ assert opts.v
+ assert opts.n
cwd = os.getcwd()
outzip = os.path.join(cwd, opts.o)
@@ -100,7 +107,7 @@
if opts.s:
path = os.path.join(bc, opts.n)
- sha1 = util.hash_bower_component(hashlib.sha1(), path).hexdigest()
+ sha1 = bowerutil.hash_bower_component(hashlib.sha1(), path).hexdigest()
if opts.s != sha1:
print((
'%s#%s:\n'
diff --git a/tools/js/run_npm_binary.py b/tools/js/run_npm_binary.py
index d76eff5..d769b98 100644
--- a/tools/js/run_npm_binary.py
+++ b/tools/js/run_npm_binary.py
@@ -25,8 +25,6 @@
import tarfile
import tempfile
-from tools import util
-
def extract(path, outdir, bin):
if os.path.exists(os.path.join(outdir, bin)):
@@ -59,19 +57,21 @@
# finished.
extract_one(tar.getmember(bin))
-
def main(args):
path = args[0]
suffix = '.npm_binary.tgz'
tgz = os.path.basename(path)
+
parts = tgz[:-len(suffix)].split('@')
if not tgz.endswith(suffix) or len(parts) != 2:
print('usage: %s <path/to/npm_binary>' % sys.argv[0], file=sys.stderr)
return 1
- name, version = parts
- sha1 = util.hash_file(hashlib.sha1(), path).hexdigest()
+ name, _ = parts
+
+ # Avoid importing from gerrit because we don't want to depend on the right CWD.
+ sha1 = hashlib.sha1(open(path, 'rb').read()).hexdigest()
outdir = '%s-%s' % (path[:-len(suffix)], sha1)
rel_bin = os.path.join('package', 'bin', name)
bin = os.path.join(outdir, rel_bin)
diff --git a/tools/util.py b/tools/util.py
index 08a803f..6dd6d59 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -70,34 +70,3 @@
break
hash_obj.update(b)
return hash_obj
-
-
-def hash_bower_component(hash_obj, path):
- """Hash the contents of a bower component directory.
-
- This is a stable hash of a directory downloaded with `bower install`, minus
- the .bower.json file, which is autogenerated each time by bower. Used in lieu
- of hashing a zipfile of the contents, since zipfiles are difficult to hash in
- a stable manner.
-
- Args:
- hash_obj: an open hash object, e.g. hashlib.sha1().
- path: path to the directory to hash.
-
- Returns:
- The passed-in hash_obj.
- """
- if not os.path.isdir(path):
- raise ValueError('Not a directory: %s' % path)
-
- path = os.path.abspath(path)
- for root, dirs, files in os.walk(path):
- dirs.sort()
- for f in sorted(files):
- if f == '.bower.json':
- continue
- p = os.path.join(root, f)
- hash_obj.update(p[len(path)+1:])
- hash_file(hash_obj, p)
-
- return hash_obj
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..506330c
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+git_rev=$(git describe --always --match "v[0-9].*" --dirty)
+
+echo "STABLE_BUILD_GERRIT_LABEL ${git_rev}"