Merge branch 'stable-2.13' * stable-2.13: Implement reviewers visibility check for suggestions SuggestReviewersIT: Avoid side-effects between tests Change-Id: Ic622c769ecd9e949d344b98cb61c48cabab4e0c3 Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/.bazelproject b/.bazelproject new file mode 100644 index 0000000..bb2a1a6 --- /dev/null +++ b/.bazelproject
@@ -0,0 +1,24 @@ +# The project view file (.bazelproject) is used to import Gerrit Bazel packages into the IDE. +# +# See: https://ij.bazel.io/docs/project-views.html + +directories: + . + -eclipse-out + -contrib + -gerrit-package-plugins + -logs + -./.metadata + -./.settings + -./.apt_generated + # BUCK excludes; Remove after we have entirely switched to Bazel + -./.buckd + -bucklets + -buck-out + +targets: + //...:all + +java_language_level: 8 + +workspace_type: java
diff --git a/.bazelrc b/.bazelrc deleted file mode 100644 index 00acd27..0000000 --- a/.bazelrc +++ /dev/null
@@ -1 +0,0 @@ -build --strategy=Javac=worker
diff --git a/.buckconfig b/.buckconfig index b347a96..11cfe37 100644 --- a/.buckconfig +++ b/.buckconfig
@@ -19,9 +19,13 @@ [java] jar_spool_mode = direct_to_jar + safe_annotation_processors = com.google.auto.value.processor.AutoAnnotationProcessor,com.google.auto.value.processor.AutoValueProcessor 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..7eb591f 100644 --- a/.buckversion +++ b/.buckversion
@@ -1 +1 @@ -e64a2e2ada022f81e42be750b774024469551398 +e27df656657f93f8d57a7aaac69a5ae0e298a292
diff --git a/.gitignore b/.gitignore index 341d3a5..599089a 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..fb8baff3 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 <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..b859642 --- /dev/null +++ b/BUILD
@@ -0,0 +1,44 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools/bzl:pkg_war.bzl", "pkg_war") + +genrule( + name = "gen_version", + outs = ["version.txt"], + cmd = ("cat bazel-out/volatile-status.txt bazel-out/stable-status.txt | " + + "grep STABLE_BUILD_GERRIT_LABEL | cut -d ' ' -f 2 > $@"), + stamp = 1, + visibility = ["//visibility:public"], +) + +genrule( + name = "LICENSES", + srcs = ["//Documentation:licenses.txt"], + outs = ["LICENSES.txt"], + cmd = "cp $< $@", + visibility = ["//visibility:public"], +) + +pkg_war(name = "gerrit") + +pkg_war( + name = "headless", + ui = None, +) + +pkg_war( + name = "polygerrit", + ui = "polygerrit", +) + +pkg_war( + name = "release", + context = ["//plugins:core"], + doc = True, + ui = "ui_optdbg_r", +) + +pkg_war( + name = "withdocs", + doc = True, +)
diff --git a/Documentation/BUILD b/Documentation/BUILD new file mode 100644 index 0000000..5e3e54e --- /dev/null +++ b/Documentation/BUILD
@@ -0,0 +1,112 @@ +package(default_visibility = ["//visibility:public"]) + +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"], + outs = ["prettify.min.css"], + cmd = "cp $< $@", +) + +genrule( + name = "prettify_min_js", + srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.js"], + outs = ["prettify.min.js"], + cmd = "cp $< $@", +) + +filegroup( + name = "resources", + srcs = glob([ + "images/*.jpg", + "images/*.png", + ]) + [ + ":prettify_files", + "//:LICENSES.txt", + ], + visibility = ["//visibility:public"], +) + +license_map( + name = "licenses", + opts = ["--asciidoctor"], + targets = [ + "//gerrit-pgm:pgm", + "//gerrit-gwtui:ui_module", + "//polygerrit-ui/app:polygerrit_ui", + ], + visibility = ["//visibility:public"], +) + + +license_map( + name = "js_licenses", + targets = [ + '//gerrit-gwtui:ui_module', + "//polygerrit-ui/app:polygerrit_ui", + ], + visibility = ["//visibility:public"], +) + +DOC_DIR = "Documentation" + +SRCS = glob(["*.txt"]) + [":licenses.txt"] + +genrule( + name = "index", + srcs = SRCS, + outs = ["index.jar"], + cmd = "$(location //lib/asciidoctor:doc_indexer) " + + "-o $(OUTS) " + + "--prefix \"%s/\" " % DOC_DIR + + "--in-ext \".txt\" " + + "--out-ext \".html\" " + + "$(SRCS)", + tools = ["//lib/asciidoctor:doc_indexer"], +) + +# 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..96b0fd0 100644 --- a/Documentation/access-control.txt +++ b/Documentation/access-control.txt
@@ -442,6 +442,11 @@ to projects in Gerrit. It can give permission to abandon a specific change to a given ref. +The uploader of a change, anyone granted the <<category_owner,`Owner`>> +permission at the ref or project level, and anyone granted the +<<capability_administrateServer,`Administrate Server`>> +permission can also Abandon changes. + This also grants the permission to restore a change if the user also has link:#category_push[push permission] on the change's destination ref. @@ -466,7 +471,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 +488,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 @@ -553,6 +574,12 @@ `refs/heads/qa/`. See <<project_owners,project owners>> to find out more about this role. +For the `All-Projects` root project any `Owner` access right on +'refs/*' is ignored since this permission would allow users to edit the +global capabilities, which is the same as being able to administrate +the Gerrit server (e.g. the user could assign the `Administrate Server` +capability to the own account). + [[category_push]] === Push @@ -599,11 +626,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 +670,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 +698,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 +709,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 @@ -796,6 +830,15 @@ the caller needs to have the Submit permission on `refs/for/<ref>` (e.g. on `refs/for/refs/heads/master`). +Submitting to the `refs/meta/config` branch is only allowed to project +owners. Any explicit submit permissions for non-project-owners on this +branch are ignored. By submitting to the `refs/meta/config` branch the +configuration of the project is changed, which can include changes to +the access rights of the project. Allowing this to be done by a +non-project-owner would open a security hole enabling editing of access +rights, and thus granting of powers beyond submitting to the +configuration. + [[category_submit_on_behalf_of]] === Submit (On Behalf Of) @@ -863,6 +906,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 +1048,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 +1118,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..9814b87 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -67,6 +67,15 @@ + Default is 20. +[[addReviewer.baseWeight]]addReviewer.baseWeight:: ++ +The weight that will be applied in the default reviewer ranking algorithm. +This can be increased or decreased to give more or less influence to plugins. +If set to zero, the base ranking will not have any effect. Reviewers will then +be ordered as ranked by the plugins (if there are any). ++ +By default 1. + [[auth]] === Section auth @@ -443,7 +452,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 +461,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 +478,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 +651,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 +667,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 +742,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 +1034,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. @@ -1478,7 +1539,7 @@ + * `MAXDB` + -Connect to an SAP MaxDb database server. +Connect to an SAP MaxDB database server. + * `MYSQL` + @@ -1767,6 +1828,15 @@ For this reason `zip` format is always excluded from formats offered through the `Download` drop down or accessible in the REST API. +[[download.maxBundleSize]]download.maxBundleSize:: ++ +Specifies the maximum size of a bundle in bytes that can be downloaded. +As bundles are kept in memory this setting is to protect the server +from a single request consuming too much heap when generating +a bundle and thereby impacting other users. ++ +Defaults to 100MB. + [[gc]] === Section gc @@ -1936,6 +2006,23 @@ by the system administrator, and might not even be running on the same host as Gerrit. +[[gerrit.installModule]]gerrit.installModule:: ++ +Repeatable list of class name of additional Guice modules to load at +Gerrit startup and init phases. +Classes are resolved using the primary Gerrit class loader, hence the +class needs to be either declared in Gerrit or an additional JAR +located under the `/lib` directory. ++ +By default unset. ++ +Example: +---- +[gerrit] + installModule = com.googlesource.gerrit.libmodule.MyModule + installModule = com.example.abc.OurSpecialSauceModule +---- + [[gerrit.reportBugUrl]]gerrit.reportBugUrl:: + URL to direct users to when they need to report a bug. @@ -2409,6 +2496,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 +2624,44 @@ maxBufferedDocs = 500 ---- + +==== Elasticsearch configuration + +WARNING: The Elasticsearch support is incomplete. Online reindexing +is not implemented yet. + +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.prefix]]index.prefix:: ++ +This setting can be used to prefix index names to allow multiple Gerrit +instances in a single Elasticsearch cluster. Prefix 'gerrit1_' would result in a +change index named 'gerrit1_changes_0001'. ++ +Not set by default. + [[ldap]] === Section ldap @@ -2795,6 +2924,15 @@ + Default is `cn`. +[[ldap.mandatoryGroup]]ldap.mandatoryGroup:: ++ +All users must be a member of this group to allow account creation or +authentication. ++ +Setting mandatoryGroup implies enabling of `ldap.fetchMemberOfEagerly` ++ +By default, unset. + [[ldap.localUsernameToLowerCase]]ldap.localUsernameToLowerCase:: + Converts the local username, that is used to login into the Gerrit @@ -3317,6 +3455,67 @@ + Default is 1. +[[receiveemail]] +=== Section receiveemail + +[[receiveemail.protocol]]receiveemail.protocol:: ++ +Specifies the protocol used for receiving emails. Valid options are +'POP3', 'IMAP' and 'NONE'. Note that Gerrit will automatically switch between +POP3 and POP3s as well as IMAP and IMAPS depending on the specified +link:#receiveemail.encryption[encryption]. ++ +Defaults to 'NONE' which means that receiving emails is disabled. + +[[receiveemail.host]]receiveemail.host:: ++ +The hostname of the mailserver. Example: 'imap.gmail.com'. ++ +Defaults to an empty string which means that receiving emails is disabled. + +[[receiveemail.port]]receiveemail.port:: ++ +The port the email server exposes for receving emails. ++ +Defaults to the industry standard for a given protocol and encryption: +POP3: 110; POP3S: 995; IMAP: 143; IMAPS: 995. + +[[receiveemail.username]]receiveemail.username:: ++ +Username used for authenticating with the email server. ++ +Defaults to an empty string. + +[[receiveemail.password]]receiveemail.password:: ++ +Password used for authenticating with the email server. ++ +Defaults to an empty string. + +[[receiveemail.encryption]]receiveemail.encryption:: ++ +Encryption standard used for transport layer security between Gerrit and the +email server. Possible values include 'NONE', 'SSL' and 'TLS'. ++ +Defaults to 'NONE'. + +[[receiveemail.fetchInterval]]receiveemail.fetchInterval:: ++ +Time between two consecutive fetches from the email server. Communication with +the email server is not kept alive. Examples: 60s, 10m, 1h. ++ +Defaults to 60 seconds. + +[[receiveemail.enableImapIdle]]receiveemail.enableImapIdle:: ++ +If the IMAP protocol is used for retrieving emails, IMAPv4 IDLE can be used to +keep the connection with the email server alive and receive a push when a new +email is delivered to the inbox. In this case, Gerrit will process the email +immediately and will not have a fetch delay. + ++ +Defaults to false. + [[sendemail]] === Section sendemail @@ -3327,6 +3526,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 +3565,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 +3593,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 +3688,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 @@ -3785,11 +4013,11 @@ [[suggest.from]]suggest.from:: + The number of characters that a user must have typed before suggestions -are provided. If set to 0, suggestions are always provided. +are provided. If set to 0, suggestions are always provided. This is only +used for suggesting accounts when adding members to a group. + By default 0. - [[theme]] === Section theme
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt index 1f9dd33..5a82d5a 100644 --- a/Documentation/config-labels.txt +++ b/Documentation/config-labels.txt
@@ -230,6 +230,19 @@ Allowed range of values are 0 (Patch Set Unlocked) to 1 (Patch Set Locked). +[[label_allowPostSubmit]] +=== `label.Label-Name.allowPostSubmit` + +If true, the label may be voted on for changes that have already been +submitted. If false, the label will not appear in the UI and will not +be accepted when reviewing a closed change. + +In either case, voting on a label after submission is only permitted if +the new vote is at least as high as the old vote by that user. This +avoids creating the false impression that a post-submit vote can change +the past and affect submission somehow. + +Defaults to true. [[label_copyMinScore]] === `label.Label-Name.copyMinScore`
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt index 51ea9c5..b8a2e5f 100644 --- a/Documentation/config-mail.txt +++ b/Documentation/config-mail.txt
@@ -1,163 +1,166 @@ = Gerrit Code Review - Mail Templates -Gerrit uses velocity templates for the bulk of the standard mails it sends out. +Gerrit uses Closure Templates for the bulk of the standard mails it sends out. There are builtin default templates which are used if they are not overridden. These defaults are also provided as examples so that administrators may copy them and easily modify them to tweak their contents. +*Compatibility Note:* previously, Velocity Template Language (VTL) was used as +the template language for Gerrit emails. VTL has now been deprecated in favor of +Soy, but Velocity templates that modify text emails remain supported for now. == Template Locations and Extensions: The default example templates reside under: `'$site_path'/etc/mail` and are -terminated with the double extension `.vm.example`. Modifying these example +terminated with the double extension `.soy.example`. Modifying these example files will have no effect on the behavior of Gerrit. However, copying an example template to an equivalently named file without the `.example` extension and modifying it will allow an administrator to customize the template. - == Supported Mail Templates: Each mail that Gerrit sends out is controlled by at least one template. These are listed below. Change emails are influenced by two additional templates, one to set the subject line, and one to set the footer which gets appended to -all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.) +all the change emails (see `ChangeSubject.soy` and `ChangeFooter.soy` below.) -=== Abandoned.vm +Many types of Gerrit email message support HTML in addition to plain-text. Where +both are supported, templates to control the HTML part have `...Html` appended +in their file names. For example, for "Abandoned" emails, the `Abandoned.soy` +template determines the text part of the message, whereas `AbandonedHtml.soy` +determines the HTML part. -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`. +=== Abandoned.soy and AbandonedHtml.soy -=== AddKey.vm +The "Abandoned" templates will determine the contents of the email related to a +change being abandoned. It is a `ChangeEmail`: see `ChangeSubject.soy` and +ChangeFooter. -The `AddKey.vm` template will determine the contents of the email related to -SSH and GPG keys being added to a user account. This notification is not sent -when the key is administratively added to another user account. +=== AddKey.soy and AddKeyHtml.soy -=== ChangeFooter.vm +AddKey templates will determine the contents of the email related to SSH and GPG +keys being added to a user account. This notification is not sent when the key +is administratively added to another user account. -The `ChangeFooter.vm` template will determine the contents of the footer -text that will be appended to emails related to changes (all `ChangeEmail`s). +=== ChangeFooter.soy and ChangeFooterHtml.soy -=== ChangeSubject.vm +The ChangeFooter templates will determine the contents of the footer that will +be appended to emails related to changes (all `ChangeEmail`s). -The `ChangeSubject.vm` template will determine the contents of the email +=== ChangeSubject.soy + +The `ChangeSubject.soy` template will determine the contents of the email subject line for ALL emails related to changes. -=== Comment.vm +=== Comment.soy -The `Comment.vm` template will determine the contents of the email related to +The `Comment.soy` 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`. +`ChangeSubject.soy`, ChangeFooter and CommentFooter. -=== CommentFooter.vm +=== CommentFooter.soy and CommentFooterHtml.soy -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`, `Comment.vm` and `ChangeFooter.vm`. +The CommentFooter templates will determine the contents of the footer text that +will be appended to emails related to a user submitting comments on changes. +See `ChangeSubject.soy`, Comment and ChangeFooter. -=== DeleteVote.vm +=== DeleteVote.soy and DeleteVoteHtml.soy -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`. +The DeleteVote templates will determine the contents of the email related to +removing votes on changes. It is a `ChangeEmail`: see `ChangeSubject.soy` +and ChangeFooter. -=== DeleteReviewer.vm +=== DeleteReviewer.soy and DeleteReviewerHtml.soy -The `DeleteReviewer.vm` template will determine the contents of the email related -to a user removing a reviewer (with a vote) from a change. It is a -`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`. +The DeleteReviewer templates will determine the contents of the email related to +a user removing a reviewer (with a vote) from a change. It is a +`ChangeEmail`: see `ChangeSubject.soy` and ChangeFooter. -=== Footer.vm +=== Footer.soy and FooterHtml.soy -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. +The Footer templates will determine the contents of the footer text appended to +the end of all outgoing emails after the ChangeFooter and CommentFooter. -=== Merged.vm +=== Merged.soy and MergedHtml.soy -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`. +The Merged templates will determine the contents of the email related to a +change successfully merged to the head. It is a `ChangeEmail`: see +`ChangeSubject.soy` and ChangeFooter. -=== NewChange.vm +=== NewChange.soy and NewChangeHtml.soy -The `NewChange.vm` template will determine the contents of the email related -to a user submitting a new change for review. This includes changes created -by actions made by the user in the Web UI such as cherry picking a commit or -reverting a change. It is a `ChangeEmail`: see `ChangeSubject.vm` and -`ChangeFooter.vm`. +The NewChange templates will determine the contents of the email related to a +user submitting a new change for review. This includes changes created by +actions made by the user in the Web UI such as cherry picking a commit or +reverting a change. It is a `ChangeEmail`: see `ChangeSubject.soy` and +ChangeFooter. -=== RegisterNewEmail.vm +=== RegisterNewEmail.soy -The `RegisterNewEmail.vm` template will determine the contents of the email +The `RegisterNewEmail.soy` template will determine the contents of the email related to registering new email accounts. -=== ReplacePatchSet.vm +=== ReplacePatchSet.soy and ReplacePatchSetHtml.soy -The `ReplacePatchSet.vm` template will determine the contents of the email -related to a user submitting a new patchset for a change. This includes -patchsets created by actions made by the user in the Web UI such as editing -the commit message, cherry picking a commit, or rebasing a change. It is a -`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`. +The ReplacePatchSet templates will determine the contents of the email related +to a user submitting a new patchset for a change. This includes patchsets +created by actions made by the user in the Web UI such as editing the commit +message, cherry picking a commit, or rebasing a change. It is a `ChangeEmail`: +see `ChangeSubject.soy` and ChangeFooter. -=== Restored.vm +=== Restored.soy and RestoredHtml.soy -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`. +The Restored templates will determine the contents of the email related to a +change being restored. It is a `ChangeEmail`: see `ChangeSubject.soy` and +ChangeFooter. -=== Reverted.vm +=== Reverted.soy and RevertedHtml.soy -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`. +The Reverted templates will determine the contents of the email related to a +change being reverted. It is a `ChangeEmail`: see `ChangeSubject.soy` and +ChangeFooter. + +=== SetAssignee.soy and SetAssigneeHtml.soy + +The SetAssignee templates will determine the contents of the email related to a +user being assigned to a change. It is a `ChangeEmail`: see `ChangeSubject.soy` +and ChangeFooter. == Mail Variables and Methods Mail templates can access and display objects currently made available to them -via the velocity context. While the base objects are documented here, it is -possible to call public methods on these objects from templates. Those methods -are not documented here since they could change with every release. As these -templates are meant to be modified only by a qualified sysadmin, it is accepted -that writing templates for Gerrit emails is likely to require some basic -knowledge of the class structure to be useful. Browsing the source code might -be necessary for anything more than a minor formatting change. +via the Soy context. === Warning Be aware that modifying templates can cause them to fail to parse and therefore -not send out the actual email, or worse, calling methods on the available -objects could have internal side effects which would adversely affect the -health of your Gerrit server and/or data. +not send out the actual email. === All OutgoingEmails All outgoing emails have the following variables available to them: -$email:: +$email.settingsUrl:: + -A reference to the class constructing the current `OutgoingEmail`. With this -reference it is possible to call any public method on the OutgoingEmail class -or the current child class inherited from it. +The URL to view the user's settings in the Gerrit web UI. + +$email.gerritHost:: ++ +The name of the Gerrit instance. + +$email.gerritUrl:: ++ +The URL to the Gerrit web UI. $messageClass:: + A String containing the messageClass. -$StringUtils:: -+ -A reference to the Apache `StringUtils` class. This can be very useful for -formatting strings. - === Change Emails -All change related emails have the following additional variables available to them: - -$change:: -+ -A reference to the current `Change` object. +Change related emails have the following template data available to them, in +addition to what's available to all outgoing emails. $changeId:: + @@ -167,30 +170,69 @@ + The text of the `ChangeMessage`. -$branch:: -+ -A reference to the branch of this change (a `Branch.NameKey`). - $fromName:: + The name of the from user. +$email.unifiedDiff:: ++ +The diff of the change. + +$email.changeDetail:: ++ +The details of the change, including the commit message. + +$email.changeUrl:: ++ +The URL to the change in the web UI. + +$email.includeDiff:: ++ +Whether the Gerrit instance is configured to include diffs in emails. + +$change.subject:: ++ +The subject of the current change. + +$change.originalSubject:: ++ +The subject corresponding to the first patch set of the current change. + +$change.shortSubject:: ++ +The subject limited to 63 characters, with an ellipsis if it exceeds that. + +$change.ownerEmail:: ++ +The email address of the owner of the change. + +$branch.shortName:: ++ +The name of the branch targeted by the current change. + $projectName:: + The name of this change's project. -$patchSet:: +$shortProjectName:: + -A reference to the current `PatchSet`. +The project name with the path abbreviated. -$patchSetInfo:: +$sshHost:: + -A reference to the current `PatchSetInfo`. +SSH hostname for the Gerrit instance. +$patchSet.patchSetId:: ++ +The current patch set number. + +$patchSet.refname:: ++ +The refname of the patch set. == SEE ALSO -* link:http://velocity.apache.org/[velocity] +* link:https://developers.google.com/closure/templates/[Closure Templates] GERRIT ------
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt index b7c1415..3a55b48 100644 --- a/Documentation/config-plugins.txt +++ b/Documentation/config-plugins.txt
@@ -658,7 +658,7 @@ This plugin replaces the built-in Gerrit H2 based websession cache with a flatfile based implementation. This implementation is shareable -amongst multiple Gerrit servers, making it useful for multi-master +among multiple Gerrit servers, making it useful for multi-master Gerrit installations. link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/websession-flatfile[
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt index 7121265..34f39c8 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+ @@ -79,6 +84,11 @@ also redefine the text and behavior of the built in label types `Code-Review` and `Verified`. +Optionally a +commentlink+ section can be added to define project-specific +comment links. The +commentlink+ section has the same format as the +link:config-gerrit.html#commentlink[+commentlink+ section in gerrit.config] +which is used to define global comment links. + [[project-section]] === Project section
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..82d3d0a --- /dev/null +++ b/Documentation/dev-bazel.txt
@@ -0,0 +1,326 @@ += Gerrit Code Review - Building with Bazel + +Bazel build is experimental. Major missing parts: + +* Custom plugins +* Test suites for SSH, acceptance, etc. +* tag tests as slow, flaky, etc. + +Nice to have: + +* JGit build from local tree. +* local.properties proxy config. +* coverage + +[[installation]] +== 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 the GWT UI and the +PolyGerrit UI: + +---- + bazel build gerrit +---- + +The output executable WAR will be placed in: + +---- + bazel-bin/gerrit.war +---- + +[[release]] +=== Gerrit Release WAR File + +To build the Gerrit web application that includes the GWT UI, the +PolyGerrit UI and documentation: + +---- + bazel build release +---- + +The output executable WAR will be placed in: + +---- + bazel-bin/release.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.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 +---- + +Install {extension,plugin,gwt}-api to the local maven repository: + +---- + tools/maven/api.sh install bazel +---- + +Install gerrit.war to the local maven repository: + +---- + tools/maven/api.sh war_install bazel +---- + +=== Plugins + +---- + bazel build plugins:core +---- + +The output JAR files for individual plugins will be placed in: + +---- + bazel-genfiles/plugins/<name>/<name>.jar +---- + +The JAR files will also be packaged in: + +---- + bazel-genfiles/plugins/core.zip +---- + +To build a specific plugin: + +---- + bazel build plugins/<name> +---- + +The output JAR file will be be placed in: + +---- + bazel-genfiles/plugins/<name>/<name>.jar +---- + +Note that when building an individual plugin, the `core.zip` package +is not regenerated. + + + +[[IDEs]] +== Using an IDE. + +=== IntelliJ + +The Gerrit build works with Bazel's link:https://ij.bazel.io[IntelliJ plugin]. +Please follow the instructions on <<dev-intellij#,IntelliJ Setup>>. + +=== Eclipse + +==== Generating the Eclipse Project + +Create the Eclipse project: + +---- + tools/eclipse/project_bzl.py +---- + +and then follow the link:dev-eclipse.html#setup[setup instructions]. + +==== Refreshing the Classpath + +If an updated classpath is needed, the Eclipse project can be +refreshed and missing dependency JARs can be downloaded by running +`project_bzl.py` again. + +[[documentation]] +=== Documentation + +To build only the documentation for testing or static hosting: + +---- + bazel build Documentation:searchfree +---- + +The html files will be bundled into `searchfree.zip` in this location: + +---- + bazel-bin/Documentation/searchfree.zip +---- + +To build the executable WAR with the documentation included: + +---- + bazel build withdocs +---- + +The WAR file will be placed in: + +---- + bazel-bin/withdocs.war +---- + +[[tests]] +== Running Unit Tests + +---- + bazel test --build_tests_only //... +---- + +Debugging tests: + +---- + bazel test --test_output=streamed --test_filter=com.gerrit.TestClass.testMethod testTarget +---- + +Debug test example: + +---- + bazel test --test_output=streamed --test_filter=com.google.gerrit.acceptance.api.change.ChangeIT.getAmbiguous //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change:api_change +---- + +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 +---- + +To run the tests against NoteDb backend: + +---- + bazel test --test_env=GERRIT_NOTEDB=READ_WRITE //... +---- + +== Dependencies + +Dependency JARs are normally downloaded as needed, but you can +download everything upfront. This is useful to enable +subsequent builds to run without network access: + +---- + bazel fetch //... +---- + +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). + +[NOTE] When building with Bazel the artifacts are still cached in +`~/.gerritcodereview/buck-cache/`. This allows Bazel to make use of +libraries that were previously downloaded by Buck. + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt index 315c0b0..5dfb367 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 @@ -341,6 +341,12 @@ The HTML report is created in `buck-out/gen/jacoco/code-coverage/index.html`. +To run the tests against NoteDb backend: + +---- + GERRIT_NOTEDB=READ_WRITE buck test +---- + == Dependencies Dependency JARs are normally downloaded automatically, but Buck can inspect @@ -547,7 +553,7 @@ ---- cat > .buckjavaargs <<EOF - -XX:MaxPermSize=512m -Xms8000m -Xmx16000m + -Xms8000m -Xmx16000m EOF ---- @@ -609,6 +615,42 @@ buck test --no-results-cache ---- +== Cross-compiling Java8 to Java7 + +After switching to Java8, we should take care to not end up +with Java8 code in stable branches. We assume that we don't +really want to switch java versions locally every time we switch +branches. + +Given that source level on 'stable-2.13' is 7, source level incompatibility +will be already correctly detected, so that Java8 compiler would refuse +to compile lambdas with -source 7 argument. However, unless bootclasspath +is adjusted to point to Java7 runtime, it's possible to end up with broken +code, that would compile with Java8 but will not run on Java7 runtime. + +To prevent this, add this line to your '.buckconfig.local' in the Gerrit +source root directory when working on stable branches: + +---- +[java] + extra_arguments = -Xbootclasspath/p:/usr/lib64/jvm/java-1.7.0-openjdk-1.7.0/jre/lib/rt.jar +---- + +With this in place, methods that were added only in Java8 in runtime library, +would be correctly refused to compile by Java8: + +---- +$ java -version +openjdk version "1.8.0_101" + +$ buck build gerrit-server:server +/home/davido/projects/gerrit/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java:218: error: cannot find symbol + return Collections.emptySortedSet(); + ^ + symbol: method emptySortedSet() + location: class java.util.Collections +---- + == Upgrading Buck The following tests should be executed, when Buck version is upgraded: @@ -643,6 +685,25 @@ link:#buck-daemon[Using Buck daemon] section above how to temporarily disable `buckd`. +== Error Prone integration + +link:http://errorprone.info[Error Prone] is a static analysis tool for +Java that catches common programming mistakes at compile-time. It can +be permanenty or instantly activated. For permanent activation, add these +lines to your `.buckconfig.local` file in gerrit tree: + +``` + [sanitizers] + error_prone = 1 +``` + +For instant activation, pass this config option to the `build` or `test` +commands: + +``` + buck build --config sanitizers.error_prone=1 gerrit +``` + == Troubleshooting Buck In some cases problems with Buck itself need to be investigated. See for example
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt index 775fe21..206b765 100644 --- a/Documentation/dev-contributing.txt +++ b/Documentation/dev-contributing.txt
@@ -97,6 +97,7 @@ ==== +[[git_commit_settings]] === A sample good Gerrit commit message: ==== Add sample commit message to guidelines doc @@ -341,32 +342,12 @@ * Update to the same GWT version in the `gwtjsonrpc` project, and release a new version. -=== Updating to new version of CodeMirror +=== Finding starter projects to work on -* Clone the git from https://github.com/codemirror/CodeMirror -* Checkout the version needed -* If the needed version is not a tagged version, use `git describe` to determine -the version number: -+ ----- - git describe --tags ----- - -* Create the release zip file: -+ ----- - git archive --format=zip --prefix=codemirror-4.10.0-6-gd0a2dda/ d0a2dda > codemirror-4.10.0-6-gd0a2dda.zip ----- - -* Determine the sha1 hash of the zip file: -+ ----- - openssl sha1 codemirror-4.10.0-6-gd0a2dda.zip ----- - -* Upload the zip file to the -link:https://console.developers.google.com/project/164060093628/storage/gerrit-maven/[ -gerrit-maven] storage bucket +We have created a +link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject] +category in the issue tracker and try to assign easy hack projects to it. If in +doubt, do not hesitate to ask on the developer mailing list. GERRIT ------
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt new file mode 100644 index 0000000..1fe912a --- /dev/null +++ b/Documentation/dev-intellij.txt
@@ -0,0 +1,154 @@ += Gerrit Code Review - IntelliJ Setup + +== Prerequisites +You need an installation of IntelliJ of version 2016.2. + +In addition, Java 8 must be specified on your path or via `JAVA_HOME` so that +building with Bazel via the Bazel plugin is possible. + +TIP: If the synchronization of the project with the BUILD files using the Bazel +plugin fails and IntelliJ reports the error **Could not get Bazel roots**, this +indicates that the Bazel plugin couldn't find Java 8. + +Bazel must be installed as described by +<<dev-bazel#installation,Building with Bazel - Installation>>. + +== Installation of the Bazel plugin + +. Go to *File -> Settings -> Plugins*. +. Click on *Browse Repositories*. +. Search for the plugin `IntelliJ with Bazel`. +. Install it. +. Restart IntelliJ. + +== Creation of IntelliJ project + +. Go to *File -> Import Bazel Project*. +. For *Use existing bazel workspace -> Workspace*, select the directory +containing the Gerrit source code. +. Choose *Import from workspace* and select the `.bazelproject` file which is +located in the top directory of the Gerrit source code. +. Adjust the path of the project data directory and the name of the project if +desired. + +TIP: The project data directory can be separate from the source code. One +advantage of this is that project files don't need to be excluded from version +control. + +Unfortunately, the created project seems to have a broken output path. To fix +it, please complete the following steps: + +. Go to *File -> Project Structure -> Project Settings -> Modules*. +. Switch to the tab *Paths*. +. Click on *Inherit project compile output path*. +. Click on *Use module compile output path*. + +== Recommended settings + +=== Code style +. Go to *File -> Settings -> Editor -> Code Style*. +. Click on *Manage*. +. Click on *Import*. +. Choose `IntelliJ IDEA Code Style XML`. +. Select the file `$(gerrit_source_code)/tools/intellij/Gerrit_Code_Style.xml`. +. Make sure that `Google Format (Gerrit)` is chosen as *Scheme*. + +In addition, the EditorConfig settings (which ensure a consistent style between +Eclipse, IntelliJ, and other editors) should be applied on top of that. Those +settings are in the file `.editorconfig` of the Gerrit source code. IntelliJ +will automatically pick up those settings if the EditorConfig plugin is enabled +and configured correctly as can be verified by: + +. Go to *File -> Settings -> Plugins*. +. Ensure that the EditorConfig plugin is enabled. +. Go to *File -> Settings -> Editor -> Code Style*. +. Ensure that *Enable EditorConfig support* is checked. + +NOTE: If IntelliJ notifies you later on that the EditorConfig settings override +the code style settings, simply confirm that. + +=== Copyright +Copy the folder `$(gerrit_source_code)/tools/intellij/copyright` (not just the +contents) to `$(project_data_directory)/.idea`. If it already exists, replace +it. + +=== File header +By default, IntelliJ adds a file header containing the name of the author and +the current date to new files. To disable that, follow these steps: + +. Go to *File -> Settings -> Editor -> File and Code Templates*. +. Select the tab *Includes*. +. Select *File Header*. +. Remove the template code in the right editor. + +=== Commit message +To simplify the creation of commit messages which are compliant with the +<<dev-contributing#commit-message,Commit Message>> format, do the following: + +. Go to *File -> Settings -> Version Control*. +. Check *Commit message right margin (columns)*. +. Make sure that 72 is specified as value. +. Check *Wrap when typing reaches right margin*. + +In addition, you should follow the instructions of +<<dev-contributing#git_commit_settings,this section>> (if you haven't +done so already): + +* Install the Git hook for the `Change-Id` line. +* Set up the HTTP access. + +Setting up the HTTP access will allow you to commit changes via IntelliJ without +specifying your credentials. The Git hook won't be noticeable during a commit +as it's executed after the commit dialog of IntelliJ was closed. + +== Run configurations +Run configurations can be accessed on the toolbar. To edit them or add new ones, +choose *Edit Configurations* on the drop-down list of the run configurations +or go to *Run -> Edit Configurations*. + +=== Pre-configured run configurations + +In order to be able to use the pre-configured run configurations, the following +steps are necessary: + +. Make sure that the folder `runConfigurations` exists within +`$(project_data_directory)/.idea`. If it doesn't exist, create it. +. Specify the IntelliJ path variable `GERRIT_TESTSITE`. (This configuration is +shared among all IntelliJ projects.) +.. Go to *Settings -> Appearance & Behavior -> Path Variables*. +.. Click on the *+* to add a new path variable. +.. Specify `GERRIT_TESTSITE` as name and the path to your local test site as +value. + +The copied run configurations will be added automatically to the available run +configurations of the IntelliJ project. + +==== Gerrit Daemon +Copy `$(gerrit_source_code)/tools/intellij/gerrit_daemon.xml` to +`$(project_data_directory)/.idea/runConfigurations/`. + +This run configuration starts the Gerrit daemon similarly as +<<dev-readme#run_daemon,Running the Daemon>>. + +NOTE: The <<dev-readme#init,Site Initialization>> has to be completed +before this run configuration works properly. + +=== Unit tests +To create run configurations for unit tests, run or debug them via a right-click +on a method, class, file, or package. The created run configuration is a +temporary one and can be saved to make it permanent. + +Normally, this approach generates JUnit run configurations. When the Bazel +plugin manages a project, it intercepts the creation and creates a Bazel test +run configuration instead, which can be used just like the standard ones. + +TIP: If you would like to execute a test in NoteDb mode, add +`--test_env=GERRIT_NOTEDB=READ_WRITE` to the *Bazel flags* of your run +configuration. + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 70d82e7..c1a4fca 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt
@@ -25,29 +25,8 @@ [[getting-started]] == Getting started -To get started with the development of a plugin there are two -recommended ways: - -. use the Gerrit Plugin Maven archetype to create a new plugin project: -+ -With the Gerrit Plugin Maven archetype you can create a skeleton for a -plugin project. -+ ----- -mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \ - -DarchetypeArtifactId=gerrit-plugin-archetype \ - -DarchetypeVersion=2.13.4 \ - -DgroupId=com.googlesource.gerrit.plugins.testplugin \ - -DartifactId=testplugin ----- -+ -Maven will ask for additional properties and then create the plugin in -the current directory. To change the default property values answer 'n' -when Maven asks to confirm the properties configuration. It will then -ask again for all properties including those with predefined default -values. - -. clone the sample plugin: +To get started with the development of a plugin clone the sample +plugin: + This is a project that demonstrates the various features of the plugin API. It can be taken as an example to develop an own plugin. @@ -57,11 +36,8 @@ ---- + When starting from this example one should take care to adapt the -`Gerrit-ApiVersion` in the `pom.xml` to the version of Gerrit for which -the plugin is developed. If the plugin is developed for a released -Gerrit version (no `SNAPSHOT` version) then the URL for the -`gerrit-api-repository` in the `pom.xml` needs to be changed to -`https://gerrit-api.storage.googleapis.com/release/`. +`Gerrit-ApiVersion` in the `BUILD` to the version of Gerrit for which +the plugin is developed. [[API]] == API @@ -418,6 +394,14 @@ + Update of the secondary index +* `com.google.gerrit.httpd.WebLoginListener`: ++ +User login or logout interactively on the Web user interface. + +The event listener is under the Gerrit http package to automatically +inherit the javax.servlet.http dependencies and allowing to influence +the login or logout flow with additional redirections. + [[stream-events]] == Sending Events to the Events Stream @@ -474,6 +458,14 @@ Certain operations in Gerrit can be validated by plugins by implementing the corresponding link:config-validation.html[listeners]. +[[change-message-modifier]] +== Change Message Modifier + +`com.google.gerrit.server.git.ChangeMessageModifier`: +plugins implementing this can modify commit message of the change being +submitted by Rebase Always and Cherry Pick submit strategies as well as +change being queried with COMMIT_FOOTERS option. + [[receive-pack]] == Receive Pack Initializers @@ -635,7 +627,7 @@ ---- [[search_operators]] -=== Search Operators === +== Search Operators Plugins can define new search operators to extend change searching by implementing the `ChangeQueryBuilder.ChangeOperatorFactory` interface @@ -676,6 +668,43 @@ } ---- +[[search_operands]] +=== Search Operands === + +Plugins can define new search operands to extend change searching. +Plugin methods implementing search operands (returning a +`Predicate<ChangeData>`), must be defined on a class implementing +one of the `ChangeQueryBuilder.ChangeOperandsFactory` interfaces +(.e.g., ChangeQueryBuilder.ChangeHasOperandFactory). The specific +`ChangeOperandFactory` class must also be bound to the `DynamicSet` from +a module's `configure()` method in the plugin. + +The new operand, when used in a search would appear as: + operatorName:operandName_pluginName + +A sample `ChangeHasOperandFactory` class implementing, and registering, a +new `has:sample_pluginName` operand is shown below: + +==== + @Singleton + public class SampleHasOperand implements ChangeHasOperandFactory { + public static class Module extends AbstractModule { + @Override + protected void configure() { + bind(ChangeHasOperandFactory.class) + .annotatedWith(Exports.named("sample") + .to(SampleHasOperand.class); + } + } + + @Override + public Predicate<ChangeData> create(ChangeQueryBuilder builder) + throws QueryParseException { + return new HasSamplePredicate(); + } +==== + + [[simple-configuration]] == Simple Configuration in `gerrit.config` @@ -1112,6 +1141,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`: + @@ -1450,6 +1483,52 @@ }); ---- + +[[action-visitor]] +=== Action Visitors + +In addition to providing new actions, plugins can have fine-grained control +over the link:rest-api-changes.html#action-info[ActionInfo] map, modifying or +removing existing actions, including those contributed by core. + +Visitors are provided the link:rest-api-changes.html#action-info[ActionInfo], +which is mutable, along with copies of the +link:rest-api-changes.html#change-info[ChangeInfo] and +link:rest-api-changes.html#revision-info[RevisionInfo]. They can modify the +action, or return `false` to exclude it from the resulting map. + +These operations only affect the action buttons that are displayed in the UI; +the underlying REST API endpoints are not affected. Multiple plugins may +implement the visitor interface, but the order in which they are run is +undefined. + +For example, to exclude "Cherry-Pick" only from certain projects, and rename +"Abandon": + +[source,java] +---- +public class MyActionVisitor implements ActionVisitor { + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo) { + if (name.equals("abandon")) { + actionInfo.label = "Drop"; + } + return true; + } + + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo, RevisionInfo revisionInfo) { + if (project.startsWith("some-team/") && name.equals("cherrypick")) { + return false; + } + return true; + } +} +---- + + [[top-menu-extensions]] == Top Menu Extensions @@ -1587,30 +1666,11 @@ } ---- + [[gwt_ui_extension]] == GWT UI Extension Plugins can extend the Gerrit UI with own GWT code. -The Maven archetype 'gerrit-plugin-gwt-archetype' can be used to -generate a GWT plugin skeleton. How to use the Maven plugin archetypes -is described in the link:#getting-started[Getting started] section. - -The generated GWT plugin has a link:#top-menu-extensions[top menu] that -opens a GWT dialog box when the user clicks on it. - -In addition to the Gerrit-Plugin API a GWT plugin depends on -`gerrit-plugin-gwtui`. This dependency must be specified in the -`pom.xml`: - -[source,xml] ----- -<dependency> - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-plugin-gwtui</artifactId> - <version>${Gerrit-ApiVersion}</version> -</dependency> ----- - A GWT plugin must contain a GWT module file, e.g. `HelloPlugin.gwt.xml`, that bundles together all the configuration settings of the GWT plugin: @@ -2248,6 +2308,7 @@ } ---- + [[documentation]] == Documentation @@ -2400,6 +2461,44 @@ ---- +[[reviewer-suggestion]] +== Reviewer Suggestion Plugins + +Gerrit provides an extension point that enables Plugins to rank +the list of reviewer suggestion a user receives upon clicking "Add Reviewer" on +the change screen. +Gerrit supports both a default suggestion that appears when the user has not yet +typed anything and a filtered suggestion that is shown as the user starts +typing. +Plugins receive a candidate list and can return a Set of suggested reviewers +containing the Account.Id and a score for each reviewer. +The candidate list is non-binding and plugins can choose to return reviewers not +initially contained in the candidate list. +Server administrators can configure the overall weight of each plugin using the +weight config parameter on [addreviewer "<pluginName-exportName>"]. + +[source, java] +---- +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; + +import java.util.Set; + +public class MyPlugin implements ReviewerSuggestion { + public Set<SuggestedReviewer> suggestReviewers(Project.NameKey project, + @Nullable Change.Id changeId, @Nullable String query, + Set<Account.Id> candidates) { + Set<SuggestedReviewer> suggestions = new HashSet<>(); + // Implement your ranking logic here + return suggestions; + } +} +---- + + == SEE ALSO * link:js-api.html[JavaScript API]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt index 4959ced..fe122d7 100644 --- a/Documentation/dev-readme.txt +++ b/Documentation/dev-readme.txt
@@ -1,6 +1,6 @@ = Gerrit Code Review - Developer Setup -Facebook Buck is needed to compile the code, and an SQL database to +Bazel or Facebook Buck is needed to compile the code, and an SQL database to house the review metadata. H2 is recommended for development databases, as it requires no external server process. @@ -18,11 +18,11 @@ the core plugins, which are included as git submodules, are also cloned. - +[[compile_project]] == Compiling -For details on how to build the source code with Buck, refer to: -link:dev-buck.html#build[Building on the command line with Buck]. +Please refer to either <<dev-buck#,Building with Buck>> or +<<dev-bazel#,Building with Bazel>>. == Switching between branches @@ -40,6 +40,10 @@ git clean -fdx ---- +CAUTION: If you decide to store your Eclipse/IntelliJ project files in the +Gerrit source directories, executing `git clean -fdx` will remove them and hence +screw up your project. + == Configuring Eclipse @@ -52,6 +56,8 @@ == Configuring IntelliJ IDEA +=== Build based on Buck + To use IntelliJ IDEA for development, the easiest way is to follow Eclipse integration and then open it as Eclipse project in IDEA. You need the Eclipse plugin activated in IntelliJ IDEA. @@ -68,6 +74,11 @@ __server_gen__ ---- +=== Build based on Bazel + +Please refer to <<dev-intellij#,IntelliJ Setup>> for detailed +instructions. + == Mac OS X On Mac OS X ensure "Java For Mac OS X 10.5 Update 4" (or later) has @@ -83,29 +94,68 @@ [[init]] == Site Initialization -After compiling (above), run Gerrit's 'init' command to create a -testing site for development use: +After compiling <<compile_project,(above)>>, run Gerrit's 'init' command to +create a testing site for development use: +.Build based on Buck ---- java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite ---- -Accept defaults by pressing Enter until 'init' completes, or add -the '--batch' command line option to avoid them entirely. It is -recommended to change the listen addresses from '*' to 'localhost' to -prevent outside connections from contacting the development instance. +.Build based on Bazel +---- + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/gerrit.war init -d ../gerrit_testsite +---- -The daemon will automatically start in the background and a web -browser will launch to the start page, enabling login via OpenID. +[[special_bazel_java_version]] +NOTE: You must use the same Java version that Bazel used for the build. +This Java version is available at +`$(bazel info output_base)/external/local_jdk/bin/java`. -Shutdown the daemon after registering the administrator account -through the web interface: +During initialization, make two changes to the default settings: + +* Change the listen addresses from '*' to 'localhost' to prevent outside + connections from contacting the development instance; and +* Change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT' to + allow yourself to create and act as arbitrary test accounts on your + development instance. + +Continue through init until it completes. The daemon will automatically start in +the background and a web browser will launch to the start page. From here you +can sign in as the account created during init, register additional accounts, +create projects, and more. + +When you want to shut down the daemon, simply run: ---- ../gerrit_testsite/bin/gerrit.sh stop ---- +[[localdev]] +== Working with the Local Server + +If you need to create additional accounts on your development instance, click +'become' in the upper right corner, select 'Switch User', and then register +a new account. + +Use the `ssh` protocol to clone from and push to the local server. For +example, to clone a repository that you've created through the admin +interface, run: + +---- +git clone ssh://username@localhost:29418/projectname +---- + +Then you'll be able to create changes the same way users do, with + +---- +git push origin HEAD:refs/for/master +---- + + + == Testing @@ -122,17 +172,29 @@ For instructions on running the integration tests with Buck, please refer to: link:dev-buck.html#tests[Running integration tests with Buck]. +For Bazel, please refer to <<dev-bazel#tests,Running Unit Tests with Bazel>>. - +[[run_daemon]] === Running the Daemon The daemon can be directly launched from the build area, without copying to the test site: +.Build based on Buck ---- java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite ---- +.Build based on Bazel +---- + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite +---- + +NOTE: Please refer to <<special_bazel_java_version,this explanation>> +for details why using `java -jar` isn't sufficient. + + === Running the Daemon with Gerrit Inspector link:dev-inspector.html[Gerrit Inspector] is an interactive scriptable @@ -148,10 +210,20 @@ Gerrit Inspect can be started by adding '-s' option to the command used to launch the daemon: +.Build based on Buck ---- java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite -s ---- +.Build based on Bazel +---- + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite -s +---- + +NOTE: Please refer to <<special_bazel_java_version,this explanation>> +for details why using `java -jar` isn't sufficient. + Gerrit Inspector examines Java libraries first, then loads its initialization scripts and then starts a command line prompt on the console: @@ -175,10 +247,20 @@ The embedded H2 database can be queried and updated from the command line. If the daemon is not currently running: +.Build based on Buck ---- java -jar buck-out/gen/gerrit/gerrit.war gsql -d ../gerrit_testsite ---- +.Build based on Bazel +---- + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/gerrit.war gsql -d ../gerrit_testsite -s +---- + +NOTE: Please refer to <<special_bazel_java_version,this explanation>> +for details why using `java -jar` isn't sufficient. + Or, if it is running and the database is in use, connect over SSH using an administrator user account:
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/dev-release.txt b/Documentation/dev-release.txt index 96695db..91a8af4 100644 --- a/Documentation/dev-release.txt +++ b/Documentation/dev-release.txt
@@ -83,6 +83,7 @@ .. link:#push-stable[Push the Stable Branch] .. link:#push-tag[Push the Release Tag] .. link:#upload-documentation[Upload the Documentation] +.. link:#finalize-release-notes[Finalize Release Notes] .. link:#update-issues[Update the Issues] .. link:#announce[Announce on Mailing List] . link:#increase-version[Increase Gerrit Version for Current Development] @@ -128,11 +129,6 @@ ./tools/version.py 2.5 ---- -Also check and update the referenced `archetypeVersion` and the -`archetypeRepository` in the `Documentation/dev-plugins.txt` file. -If the referenced `archetypeVersion` will be available in the Maven central, -delete the line with the `archetypeRepository`. - Commit the changes and create the release tag on the new commit: ---- @@ -153,7 +149,7 @@ ---- buck clean buck build --no-cache release docs - ./tools/maven/api.sh install + ./tools/maven/api.sh install <buck|bazel> ---- * Sanity check WAR @@ -185,13 +181,13 @@ * Push the WAR to Maven Central: + ---- - ./tools/maven/api.sh war_deploy + ./tools/maven/api.sh war_deploy <buck|bazel> ---- * Push the plugin artifacts to Maven Central: + ---- - ./tools/maven/api.sh deploy + ./tools/maven/api.sh deploy <buck|bazel> ---- + If no artifacts are uploaded, clean the `buck-out` folder and retry: @@ -200,12 +196,6 @@ buck clean ; rm -rf buck-out ---- -* Push the plugin Maven archetypes to Maven Central: -+ ----- - ./tools/plugin_archetype_deploy.sh ----- - * To where the artifacts are uploaded depends on the `GERRIT_VERSION` in the `VERSION` file: @@ -327,15 +317,6 @@ [[upload-documentation]] ==== Upload the Documentation -* Build the release notes: -+ ----- - buck build releasenotes ----- - -* Extract the release notes files from the zip file generated from the previous -step: `buck-out/gen/ReleaseNotes/html/html.zip`. - * Extract the documentation files from the zip file generated from `buck build docs`: `buck-out/gen/Documentation/searchfree/searchfree.zip`. @@ -344,6 +325,16 @@ link:https://console.cloud.google.com/storage/browser/gerrit-documentation/?project=api-project-164060093628[ gerrit-documentation] storage bucket. +[[finalize-release-notes]] +=== Finalize the Release Notes + +Upload a change on the homepage project to: + +* Remove 'In Development' caveat from the relevant section. + +* Add links to the released documentation and the .war file, and make the +latest version bold. + [[update-links]] ==== Update homepage links @@ -370,7 +361,7 @@ * Send an email to the mailing list to announce the release, consider including some or all of the following in the email: -** A link to the release and the release notes (if a final release) +** A link to the release and the release notes ** A link to the docs ** Describe the type of release (stable, bug fix, RC) ** Hash values (SHA1, SHA256, MD5) for the release WAR file. @@ -397,8 +388,8 @@ next Gerrit release. The Gerrit version should be set to the snapshot version for the next release. -Use the `version` tool to set the version in the `VERSION` file and plugin -archetypes' `pom.xml` files: +Use the `version` tool to set the version in the `VERSION` file: +: ---- ./tools/version.py 2.11-SNAPSHOT
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..c913aef 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] @@ -60,8 +61,10 @@ == Developer . Getting Started .. link:dev-readme.html[Developer Setup] -.. link:dev-eclipse.html[Eclipse Setup] .. link:dev-buck.html[Building with Buck] +.. link:dev-bazel.html[Building with Bazel] +.. link:dev-eclipse.html[Eclipse Setup] +.. link:dev-intellij.html[IntelliJ Setup] .. link:dev-contributing.html[Contributing to Gerrit] . Plugin Development .. link:dev-plugins.html[Developing Plugins]
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt index 2623256..d665226 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] +* JRE, 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..f0a1730 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] +* JRE, 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-project-owner.txt b/Documentation/intro-project-owner.txt index 7a724f7..03eeeb7 100644 --- a/Documentation/intro-project-owner.txt +++ b/Documentation/intro-project-owner.txt
@@ -70,8 +70,8 @@ commands: ---- - $ git fetch origin refs/meta/config:config - $ git checkout config + $ git fetch ssh://localhost:29418/project refs/meta/config + $ git checkout FETCH_HEAD $ git log project.config ---- @@ -330,7 +330,7 @@ A Prolog submit rule has access to link:prolog-change-facts.html[ information] about the change for which it is testing the -submittability. Amongst others the list of the modified files can be +submittability. Among others the list of the modified files can be accessed, which allows special logic if certain files are touched. For example, a common practice is to require a vote on an additional label, like `Library-Compliance`, if the dependencies of the project are
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt index bb80134..c6dad5b 100644 --- a/Documentation/intro-quick.txt +++ b/Documentation/intro-quick.txt
@@ -208,7 +208,7 @@ can add file comment by double clicking anywhere (not just on the "Patch Set" words) in the table header or single clicking on the icon in the line-number column header. Once published these comments are -viewable to all, allowing discussion of the change to take place. +visible to all, allowing discussion of the change to take place. .Side By Side Patch View image::images/intro-quick-review-line-comment.jpg[Adding a Comment]
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/js-api.txt b/Documentation/js-api.txt index 8c9950e..f3b84d2 100644 --- a/Documentation/js-api.txt +++ b/Documentation/js-api.txt
@@ -701,8 +701,7 @@ accessed through this name. [[Gerrit_css]] -Gerrit.css() -~~~~~~~~~~~~ +=== Gerrit.css() Creates a new unique CSS class and injects it into the document. The name of the class is returned and can be used by the plugin. See link:#Gerrit_html[`Gerrit.html()`] for an easy way to use @@ -805,8 +804,7 @@ The user can return to Gerrit with the back button. [[Gerrit_html]] -Gerrit.html() -~~~~~~~~~~~~~ +=== Gerrit.html() Parses an HTML fragment after performing template replacements. If the HTML has a single root element or node that node is returned, otherwise it is wrapped inside a `<div>` and the div is returned. @@ -900,8 +898,7 @@ ---- [[Gerrit_injectCss]] -Gerrit.injectCss() -~~~~~~~~~~~~~~~~~~ +=== Gerrit.injectCss() Injects CSS rules into the document by appending onto the end of the existing rule list. CSS rules are global to the entire application and must be manually scoped by each plugin. For an automatic scoping
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt index b6d6aad..aef5a318 100644 --- a/Documentation/metrics.txt +++ b/Documentation/metrics.txt
@@ -76,6 +76,11 @@ * `git/upload-pack/phase_writing`: Time spent transferring bytes to client. * `git/upload-pack/pack_bytes`: Distribution of sizes of packs sent to clients. +=== BatchUpdate + +* `batch_update/execute_change_ops`: BatchUpdate change update latency, +excluding reindexing + === NoteDb * `notedb/update_latency`: NoteDb update latency by table. @@ -86,6 +91,17 @@ * `notedb/auto_rebuild_failure_count`: NoteDb auto-rebuilding attempts that failed by table. +=== Reviewer Suggestion + +* `reviewer_suggestion/query_accounts`: Latency for querying accounts for +reviewer suggestion. +* `reviewer_suggestion/recommend_accounts`: Latency for recommending accounts +for reviewer suggestion. +* `reviewer_suggestion/load_accounts`: Latency for loading accounts for +reviewer suggestion. +* `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer +suggestion. + === Replication Plugin * `plugins/replication/replication_latency`: Time spent pushing to remote
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt index d71d19a..54ddcff 100644 --- a/Documentation/project-configuration.txt +++ b/Documentation/project-configuration.txt
@@ -103,7 +103,8 @@ link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`] is enabled and depending changes share the same topic. So generally submitters must remember to submit changes in the right order when using this -submit type. +submit type. If all you want is extra information in the commit message, +consider using the Rebase Always submit strategy. [[rebase_if_necessary]] * Rebase If Necessary @@ -117,6 +118,17 @@ succeed if there is no path conflict. A path conflict occurs when the same file has also been changed on the other side of the merge. +[[rebase_always]] +* Rebase Always ++ +Basically, the same as Rebase If Necessary, but it creates a new patchset even +if fast forward is possible AND like Cherry Pick it ensures footers such as +Change-Id, Reviewed-On, and others are present in resulting commit that is +merged. + +Thus, Rebase Always can be considered similar to Cherry Pick, but with +the important distinction that Rebase Always does not ignore dependencies. + [[content_merge]] If `Allow content merges` is enabled, Gerrit will try to do a content merge when a path conflict occurs.
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 54941c1..a11f6bc 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", @@ -1237,7 +1238,8 @@ { "url": "#/groups/self", "name": "Groups" - } + }, + change_table: [] ] } ---- @@ -1262,6 +1264,7 @@ "changes_per_page": 50, "show_site_header": true, "use_flash_clipboard": true, + "expand_inline_diffs": true, "download_command": "CHECKOUT", "date_format": "STD", "time_format": "HHMM_12", @@ -1294,6 +1297,10 @@ "url": "#/groups/self", "name": "Groups" } + ], + "change_table": [ + "Subject", + "Owner" ] } ---- @@ -1312,6 +1319,7 @@ "changes_per_page": 50, "show_site_header": true, "use_flash_clipboard": true, + "expand_inline_diffs": true, "download_command": "CHECKOUT", "date_format": "STD", "time_format": "HHMM_12", @@ -1344,6 +1352,10 @@ "url": "#/groups/self", "name": "Groups" } + ], + "change_table": [ + "Subject", + "Owner" ] } ---- @@ -1381,7 +1393,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -1412,7 +1425,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -1436,7 +1450,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -2226,6 +2241,8 @@ If true the line numbers are hidden. |`tab_size` || Number of spaces that should be used to display one tab. +|`font_size` || +Default font size in pixels for change to be displayed in the diff view. |'hide_empty_pane' |not set if `false`| Whether empty panes should be hidden. The left pane is empty when a file was added; the right pane is empty when a file was deleted. @@ -2286,6 +2303,8 @@ True if the line numbers should be hidden. |`tab_size` |optional| Number of spaces that should be used to display one tab. +|`font_size` |optional| +Default font size in pixels for change to be displayed in the diff view. |`line_wrapping` |optional| Whether to enable line wrapping or not. |=========================================== @@ -2452,14 +2471,15 @@ Whether the site header should be shown. |`use_flash_clipboard` |not set if `false`| Whether to use the flash clipboard widget. +|`expand_inline_diffs` |not set if `false`| +Whether to expand diffs inline instead of opening as separate page +(PolyGerrit only). |`download_scheme` |optional| The type of download URL the user prefers to use. May be any key from the `schemes` map in 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`. @@ -2468,24 +2488,27 @@ 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. +|`change_table` || +The columns to display in the change table (PolyGerrit only). The default is +empty, which will default columns as determined by the frontend. |`url_aliases` |optional| A map of URL path pairs, where the first URL path is an alias for the second URL path. @@ -2495,6 +2518,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]] @@ -2512,12 +2539,13 @@ Whether the site header should be shown. |`use_flash_clipboard` |optional| Whether to use the flash clipboard widget. +|`expand_inline_diffs` |not set if `false`| +Whether to expand diffs inline instead of opening as separate page +(PolyGerrit only). |`download_scheme` |optional| 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`. @@ -2526,24 +2554,27 @@ 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. +|`change_table` || +The columns to display in the change table (PolyGerrit only). The default is +empty, which will default columns as determined by the frontend. |`url_aliases` |optional| A map of URL path pairs, where the first URL path is an alias for the second URL path. @@ -2553,6 +2584,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 b8c571c..4d79774 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -246,17 +246,17 @@ [[current-files]] -- -* `CURRENT_FILES`: list files modified by the commit, including - basic line counts inserted/deleted per file. Only valid when - the `CURRENT_REVISION` or `ALL_REVISIONS` option is selected. +* `CURRENT_FILES`: list files modified by the commit and magic files, + including basic line counts inserted/deleted per file. Only valid + when the `CURRENT_REVISION` or `ALL_REVISIONS` option is selected. -- [[all-files]] -- -* `ALL_FILES`: list files modified by the commit, including - basic line counts inserted/deleted per file. If only the - `CURRENT_REVISION` was requested then only that commit's - modified files will be output. +* `ALL_FILES`: list files modified by the commit and magic files, + including basic line counts inserted/deleted per file. If only the + `CURRENT_REVISION` was requested then only that commit's modified + files will be output. -- [[detailed-accounts]] @@ -294,13 +294,19 @@ -- * `REVIEWED`: include the `reviewed` field if all of the following are true: - * the change is open - * the caller is authenticated - * the caller has commented on the change more recently than the last update + - the change is open + - the caller is authenticated + - the caller has commented on the change more recently than the last update from the change owner, i.e. this change would show up in the results of 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], @@ -511,6 +517,61 @@ } ---- +[[create-merge-patch-set-for-change]] +=== Create Merge Patch Set For Change +-- +'POST /changes/link:#change-id[\{change-id\}]/merge' +-- + +Update an existing change by using a +link:#merge-patch-set-input[MergePatchSetInput] entity. + +Gerrit will create a merge commit based on the information of +MergePatchSetInput and add a new patch set to the change corresponding +to the new merge commit. + +.Request +---- + POST /changes/test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc/merge HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "source": "refs/12/1234/1" + } +---- + +As response a link:#change-info[ChangeInfo] entity with current revision is +returned that describes the resulting change. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "id": "test~master~Ic5466d107c5294414710935a8ef3b0180fb848dc", + "project": "test", + "branch": "master", + "hashtags": [], + "change_id": "Ic5466d107c5294414710935a8ef3b0180fb848dc", + "subject": "Merge dev_branch into master", + "status": "NEW", + "created": "2016-09-23 18:08:53.238000000", + "updated": "2016-09-23 18:09:25.934000000", + "submit_type": "MERGE_IF_NECESSARY", + "mergeable": true, + "insertions": 5, + "deletions": 0, + "_number": 72, + "owner": { + "_account_id": 1000000 + }, + "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822" + } +---- + [[get-change-detail]] === Get Change Detail -- @@ -824,6 +885,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 +1481,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 ---- @@ -1553,13 +1767,19 @@ HTTP/1.1 204 No Content ---- -[[delete-draft-change]] -=== Delete Draft Change +[[delete-change]] +=== Delete Change -- 'DELETE /changes/link:#change-id[\{change-id\}]' -- -Deletes a draft change. +Deletes a change. + +New or abandoned changes can only be deleted by administrators. The deletion of +merged changes isn't supported at the moment. Draft changes can only be deleted +by their owner or other users who have the permissions to view and delete +drafts. If the draft workflow is disabled, only administrators with those +permissions may delete draft changes. .Request ---- @@ -2135,9 +2355,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 +2636,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 +2703,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 +2792,118 @@ Adding query parameter `links` (for example `/changes/.../commit?links`) returns a link:#commit-info[CommitInfo] with the additional field `web_links`. +[[get-description]] +=== Get Description +-- +'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/description' +-- + +Retrieves the description of a patch set. + +.Request +---- + GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/description HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + "Added Documentation" +---- + +If the patch set does not have a description an empty string is returned. + +[[set-description]] +=== Set Description +-- +'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/description' +-- + +Sets the description of a patch set. + +The new description must be provided in the request body inside a +link:#description-input[DescriptionInput] entity. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/description HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "description": "Added Documentation" + } +---- + +As response the new description is returned. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + "Added Documentation" +---- + +[[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 +3535,63 @@ will suggest the browser save the patch as `commitsha1.diff.base64`, for later processing by command line tools. +If the `path` parameter is set, the returned content is a diff of the single +file that the path refers to. + +[[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 -- @@ -3207,7 +3623,9 @@ ---- If the `other-branches` parameter is specified, the mergeability will also be -checked for all other branches. +checked for all other branches which are listed in the +link:config-project-config.html#branchOrder-section[branchOrder] section in the +project.config file. .Request ---- @@ -3611,6 +4029,102 @@ } ---- +[[list-robot-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 -- @@ -3619,6 +4133,18 @@ Lists the files that were modified, added or deleted in a revision. +In addition the following magic files are included: + +* `/COMMIT_MSG`: ++ +The commit message and headers with the parent commit(s), the author +information and the committer information. + +* `/MERGE_LIST` (for merge commits only): ++ +The list of commits that are being integrated into the destination +branch by submitting the merge commit. + .Request ---- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0 @@ -4107,6 +4633,79 @@ } ---- +[[get-hashtags]] +=== Get Hashtags +-- +'GET /changes/link:#change-id[\{change-id\}]/hashtags' +-- + +Gets the hashtags associated with a change. + +[NOTE] Hashtags are only available when NoteDb is enabled. + +.Request +---- + GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0 +---- + +As response the change's hashtags are returned as a list of strings. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + "hashtag1", + "hashtag2" + ] +---- + +[[set-hashtags]] +=== Set Hashtags +-- +'POST /changes/link:#change-id[\{change-id\}]/hashtags' +-- + +Adds and/or removes hashtags from a change. + +[NOTE] Hashtags are only available when NoteDb is enabled. + +The hashtags to add or remove must be provided in the request body inside a +link:#hashtags-input[HashtagsInput] entity. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/hashtags HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "add" : [ + "hashtag3" + ], + "remove" : [ + "hashtag2" + ] + } +---- + +As response the change's hashtags are returned as a list of strings. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + "hashtag1", + "hashtag3" + ] +---- + [[ids]] == IDs @@ -4141,10 +4740,13 @@ The name of the label. [[file-id]] -\{file-id\} -~~~~~~~~~~~~ +=== \{file-id\} The path of the file. +[[fix-id]] +=== \{fix-id\} +UUID of a suggested fix. + [[revision-id]] === \{revision-id\} Identifier that uniquely identifies one revision of a change. @@ -4166,17 +4768,20 @@ The `AbandonInput` entity contains information for abandoning a change. [options="header",cols="1,^1,5"] -|=========================== -|Field Name ||Description -|`message` |optional| +|============================= +|Field Name ||Description +|`message` |optional| Message to be added as review comment to the change when abandoning the change. -|`notify` |optional| +|`notify` |optional| Notify handling that defines to whom email notifications should be sent after the change is abandoned. + Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + If not set, the default is `ALL`. -|=========================== +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= [[action-info]] === ActionInfo @@ -4242,18 +4847,37 @@ [options="header",cols="1,^1,5"] |=========================== -|Field Name ||Description -|`value` |optional| +|Field Name ||Description +|`value` |optional| The vote that the user has given for the label. If present and zero, the user is permitted to vote on the label. If absent, the user is not permitted to vote on that label. -|`date` |optional| +|`permitted_voting_range` |optional| +The link:#voting-range-info[VotingRangeInfo] the user is authorized to vote +on that label. If present, the user is permitted to vote on the label +regarding the range values. If absent, the user is not permitted to vote +on that label. +|`date` |optional| The time and date describing when the approval was made. -|`tag` |optional| +|`tag` |optional| Value of the `tag` field from link:#review-input[ReviewInput] set while posting the review. NOTE: To apply different tags on on different votes/comments multiple invocations of the REST call are required. +|`post_submit` |not set if `false`| +If true, this vote was made after the change was submitted. +|=========================== + +[[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]] @@ -4340,6 +4964,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` || @@ -4455,11 +5082,13 @@ === CherryPickInput The `CherryPickInput` entity contains information for cherry-picking a change to a new branch. -[options="header",cols="1,6"] +[options="header",cols="1,^1,5"] |=========================== -|Field Name |Description -|`message` |Commit message for the cherry-picked change -|`destination` |Destination branch +|Field Name ||Description +|`message` ||Commit message for the cherry-picked change +|`destination` ||Destination branch +|`parent` |optional, defaults to 1| +Number of the parent relative to which the cherry-pick should be considered. |=========================== [[comment-info]] @@ -4589,23 +5218,54 @@ 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`. +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= + [[delete-vote-input]] === DeleteVoteInput The `DeleteVoteInput` entity contains options for the deletion of a vote. [options="header",cols="1,^1,5"] -|======================= -|Field Name||Description -|`label` |optional| +|============================= +|Field Name ||Description +|`label` |optional| The label for which the vote should be deleted. + If set, must match the label in the URL. -|`notify` |optional| +|`notify` |optional| Notify handling that defines to whom email notifications should be sent after the vote is deleted. + Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + If not set, the default is `ALL`. -|======================= +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= + +[[description-input]] +=== DescriptionInput +The `DescriptionInput` entity contains information for setting a description. + +[options="header",cols="1,6"] +|=========================== +|Field Name |Description +|`description` |The description text. +|=========================== [[diff-content]] === DiffContent @@ -4798,6 +5458,37 @@ a new patch set referring to this commit. |========================== +[[fix-suggestion-info]] +=== FixSuggestionInfo +The `FixSuggestionInfo` entity represents a suggested fix. + +[options="header",cols="1,^1,5"] +|========================== +|Field Name ||Description +|`fix_id` |generated, don't set|The <<fix-id,UUID>> of the suggested +fix. It will be generated automatically and hence will be ignored if it's set +for input objects. +|`description` ||A description of the suggested fix. +|`replacements` ||A list of <<fix-replacement-info,FixReplacementInfo>> +entities indicating how the content of the file on which the comment was placed +should be modified. They should refer to non-overlapping regions. +|========================== + +[[fix-replacement-info]] +=== FixReplacementInfo +The `FixReplacementInfo` entity describes how the content of a file should be +replaced by another content. + +[options="header",cols="1,6"] +|========================== +|Field Name |Description +|`path` |The path of the file which should be modified. Modifications +are only allowed for the file on which the corresponding comment was placed. +|`range` |A <<comment-range,CommentRange>> indicating which content +of the file should be replaced. +|`replacement` |The content which should be used instead of the current one. +|========================== + [[git-person-info]] === GitPersonInfo The `GitPersonInfo` entity contains information about the @@ -4821,10 +5512,23 @@ [options="header",cols="1,6"] |========================== |Field Name |Description -|`id` |The id of the group. +|`id` |The UUID of the group. |`name` |The name of the group. |========================== +[[hashtags-input]] +=== HashtagsInput + +The `HashtagsInput` entity contains information about hashtags to add to, +and/or remove from, a change. + +[options="header",cols="1,^1,5"] +|======================= +|Field Name||Description +|`add` |optional|The list of hashtags to be added to the change. +|`remove |optional|The list of hashtags to be removed from the change. +|======================= + [[included-in-info]] === IncludedInInfo The `IncludedInInfo` entity contains information about the branches a @@ -4943,6 +5647,25 @@ `simple-two-way-in-core`, `ours` or `theirs`, default will use project settings. |============================ +[[merge-patch-set-input]] +=== MergePatchSetInput +The `MergePatchSetInput` entity contains information about updating a new +change by creating a new merge commit. + +[options="header",cols="1,^1,5"] +|================================== +|Field Name ||Description +|`subject` |optional| +The new subject for the change, if not specified, will reuse the current patch +set's subject +|`inheritParent` |optional, default to `false`| +Use the current patch set's first parent as the merge tip when set to `true`. +Otherwise, use the current branch tip of the destination branch. +|`merge` || +The detail of the source commit for merge as a link:#merge-input[MergeInput] +entity. +|================================== + [[move-input]] === MoveInput The `MoveInput` entity contains information for moving a change to a new branch. @@ -4955,6 +5678,23 @@ A message to be posted in this change's comments |=========================== +[[notify-info]] +=== NotifyInfo +The `NotifyInfo` entity contains detailed information about who should +be notified about an update. These notifications are sent out even if a +`notify` option in the request input disables normal notifications. +`NotifyInfo` entities are normally contained in a `notify_details` map +in the request input where the key is the recipient type. The recipient +type can be `TO`, `CC` and `BCC`. + +[options="header",cols="1,^1,5"] +|======================= +|Field Name||Description +|`accounts`|optional| +A list of link:rest-api-accounts.html#account-id[account IDs] that +identify the accounts that should be should be notified. +|======================= + [[problem-info]] === ProblemInfo The `ProblemInfo` entity contains a description of a potential consistency problem @@ -4974,6 +5714,24 @@ 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`. +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= + [[push-certificate-info]] === PushCertificateInfo The `PushCertificateInfo` entity contains information about a push @@ -5127,6 +5885,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,12 +5902,17 @@ 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. + Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + If not set, the default is `ALL`. +|`notify_details` |optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. |`omit_duplicate_comments`|optional| If `true`, comments with the same content at the same place will be omitted. |`on_behalf_of` |optional| @@ -5179,23 +5945,31 @@ to a change. [options="header",cols="1,^1,5"] -|=========================== -|Field Name ||Description -|`reviewer` || +|============================= +|Field Name ||Description +|`reviewer` || The link:rest-api-accounts.html#account-id[ID] of one account that should be added as reviewer or the link:rest-api-groups.html#group-id[ ID] of one group for which all members should be added as reviewers. + If an ID identifies both an account and a group, only the account is added as reviewer to the change. -|`state` |optional| +|`state` |optional| Add reviewer in this state. Possible reviewer states are `REVIEWER` and `CC`. If not given, defaults to `REVIEWER`. -|`confirmed` |optional| +|`confirmed` |optional| Whether adding the reviewer is confirmed. + 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`. +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= [[revision-info]] === RevisionInfo @@ -5253,6 +6027,34 @@ 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 <<comment-info,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. +|`fix_suggestions`|optional|Suggested fixes for this robot comment as a list of +<<fix-suggestion-info,FixSuggestionInfo>> entities. +|=========================== + +[[robot-comment-input]] +=== RobotCommentInput +The `RobotCommentInput` entity contains information for creating an inline +robot comment. + +`RobotCommentInput` has the same fields as +<<robot-comment-info,RobotCommentInfo>>. + [[rule-input]] === RuleInput The `RuleInput` entity contains information to test a Prolog rule. @@ -5296,24 +6098,29 @@ The `SubmitInput` entity contains information for submitting a change. [options="header",cols="1,^1,5"] -|=========================== +|============================= |Field Name ||Description -|`on_behalf_of`|optional| +|`on_behalf_of` |optional| If set, submit the change on behalf of the given user. The value may take any format link:rest-api-accounts.html#account-id[accepted by the accounts REST API]. Using this option requires link:access-control.html#category_submit_on_behalf_of[Submit (On Behalf Of)] permission on the branch. -|`notify`|optional| +|`notify` |optional| Notify handling that defines to whom email notifications should be sent after the change is submitted. + Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + If not set, the default is `ALL`. -|=========================== +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= [[submit-record]] === SubmitRecord The `SubmitRecord` entity describes results from a submit_rule. +Fields in this entity roughly correspond to the fields set by `LABELS` +in link:#label-info[LabelInfo]. [options="header",cols="1,^1,5"] |=========================== @@ -5403,6 +6210,18 @@ The topic will be deleted if not set. |=========================== +[[voting-range-info]] +=== VotingRangeInfo +The `VotingRangeInfo` entity describes the continuous voting range from min +to max values. + +[options="header",cols="1,6"] +|====================== +|Field Name|Description +|`min` |The minimum voting value. +|`max` |The maximum voting value. +|====================== + [[web-link-info]] === WebLinkInfo The `WebLinkInfo` entity describes a link to an external site.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index c7c0878..82d9f3e 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": { @@ -396,7 +407,7 @@ + Returns the cache names as JSON list. + -The cache names are alphabetically sorted. +The cache names are lexicographically sorted. + .Request ---- @@ -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/rest-api-groups.txt b/Documentation/rest-api-groups.txt index 23d4c5b..e43c4bc 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt
@@ -1207,11 +1207,6 @@ [[ids]] == IDs -[[account-id]] -=== link:rest-api-accounts.html#account-id[\{account-id\}] --- --- - [[group-id]] === \{group-id\} Identifier for a group. @@ -1319,7 +1314,7 @@ name. + If not set, the new group will be self-owned. |`members` |optional|The initial members in a list of + -link:#account-id[account ids]. +link:rest-api-accounts.html#account-id[account ids]. |=========================== [[group-options-info]] @@ -1360,8 +1355,7 @@ |========================== [[members-input]] -MembersInput -~~~~~~~~~~~ +=== MembersInput The `MembersInput` entity contains information about accounts that should be added as members to a group or that should be deleted from the group. @@ -1369,11 +1363,11 @@ |========================== |Field Name ||Description |`_one_member`|optional| -The link:#account-id[id] of one account that should be added or -deleted. -|`members` |optional| -A list of link:#account-id[account ids] that identify the accounts that +The link:rest-api-accounts.html#account-id[id] of one account that should be added or deleted. +|`members` |optional| +A list of link:rest-api-accounts.html#account-id[account ids] that +identify the accounts that should be added or deleted. |==========================
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 457a287..d6338f7 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt
@@ -1886,6 +1886,55 @@ } ---- +[[delete-tag]] +=== Delete Tag +-- +'DELETE /projects/link:#project-name[\{project-name\}]/tags/link:#tag-id[\{tag-id\}]' +-- + +Deletes a tag. + +.Request +---- + DELETE /projects/MyProject/tags/v1.0 HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + +[[delete-tags]] +=== Delete Tags +-- +'POST /projects/link:#project-name[\{project-name\}]/tags:delete' +-- + +Delete one or more tags. + +The tags to be deleted must be provided in the request body as a +link:#delete-tags-input[DeleteTagsInput] entity. + +.Request +---- + POST /projects/MyProject/tags:delete HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "tags": [ + "v1.0", + "v2.0" + ] + } +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + +If some tags could not be deleted, the response is "`409 Conflict`" and the +error message is contained in the response body. [[commit-endpoints]] == Commit Endpoints @@ -2408,8 +2457,7 @@ |====================================================== [[config-parameter-info]] -ConfigParameterInfo -~~~~~~~~~~~~~~~~~~~ +=== ConfigParameterInfo The `ConfigParameterInfo` entity describes a project configuration parameter. @@ -2523,6 +2571,18 @@ deleted. |========================== +[[delete-tags-input]] +=== DeleteTagsInput +The `DeleteTagsInput` entity contains information about tags that should +be deleted. + +[options="header",width="50%",cols="1,6"] +|========================== +|Field Name |Description +|`tags` |A list of tag names that identify the tags that should be +deleted. +|========================== + [[gc-input]] === GCInput The `GCInput` entity contains information to run the Git garbage
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt index 838a433..4803d83 100644 --- a/Documentation/user-review-ui.txt +++ b/Documentation/user-review-ui.txt
@@ -297,6 +297,23 @@ image::images/user-review-ui-change-screen-file-list.png[width=800, link="images/user-review-ui-change-screen-file-list.png"] +[[magic-files]] +In addition to the modified files the file list contains magic files +that are generated by Gerrit and which don't exist in the repository. +The magic files contain additional commit data that should be +reviewable and allow users to comment on this data. The magic files are +always listed first. The following magic files exist: + +* `Commit Message`: ++ +The commit message and headers with the parent commit(s), the author +information and the committer information. + +* `Merge List` (for merge commits only): ++ +The list of commits that are being integrated into the destination +branch by submitting the merge commit. + [[change-screen-mark-reviewed]] The checkboxes in front of the file names allow files to be marked as reviewed.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index b04898e..f1f1654 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt
@@ -321,6 +321,20 @@ + Same as <<status,status:'STATE'>>. +is:submittable:: ++ +True if the change is submittable according to the submit rules for +the project, for example if all necessary labels have been voted on. ++ +This operator only takes into account one change at a time, not any +related changes, and does not guarantee that the submit button will +appear for matching changes. To check whether a submit button appears, +use the +link:rest-api-changes.html#get-revision-actions[Get Revision Actions] +API. ++ +Equivalent to <<submittable,submittable:ok>>. + [[mergeable]] is:mergeable:: + @@ -394,6 +408,15 @@ 'COMMITTER' may be the committer's exact email address, or part of the name or email address. +[[submittable]] +submittable:'SUBMIT_STATUS':: ++ +Changes having the given submit record status after applying submit +rules. Valid statuses are in the `status` field of +link:rest-api-changes.html#submit-record[SubmitRecord]. This operator +only applies to the top-level status; individual label statuses can be +searched link:#labels[by label]. + == Argument Quoting @@ -448,8 +471,10 @@ ('user=' or 'group='). If an LDAP group is being referenced make sure to use 'ldap/<groupname>'. -A label name must be followed by a score, or an operator and a score. -The easiest way to explain this is by example. +A label name must be followed by either a score with optional operator, +or a label status. The easiest way to explain this is by example. ++ +First, some examples of scores with operators: `label:Code-Review=2`:: `label:Code-Review=+2`:: @@ -473,8 +498,20 @@ `label:Code-Review>=1`:: + Matches changes with either a +1, +2, or any higher score. ++ +Instead of a numeric vote, you can provide a label status corresponding +to one of the fields in the +link:rest-api-changes.html#submit-record[SubmitRecord] REST API entity. + +`label:Non-Author-Code-Review=need`:: ++ +Matches changes where the submit rules indicate that a label named +`Non-Author-Code-Review` is needed. (See the +link:prolog-cookbook.html#NonAuthorCodeReview[Prolog Cookbook] for how +this label can be configured.) `label:Code-Review=+2,aname`:: +`label:Code-Review=ok,aname`:: + Matches changes with a +2 code review where the reviewer or group is aname. @@ -482,6 +519,14 @@ + Matches changes with a +2 code review where the reviewer is jsmith. +`label:Code-Review=+2,user=owner`:: +`label:Code-Review=ok,user=owner`:: +`label:Code-Review=+2,owner`:: +`label:Code-Review=ok,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 @@ -492,14 +537,17 @@ Matches changes with either a -1, -2, or any lower score. `is:open label:Code-Review+2 label:Verified+1 NOT label:Verified-1 NOT label:Code-Review-2`:: +`is:open label:Code-Review=ok label:Verified=ok`:: + -Matches changes that are ready to be submitted. +Matches changes that are ready to be submitted according to one common +label configuration. (For a more general check, use +link:#submittable[submittable:ok].) `is:open (label:Verified-1 OR label:Code-Review-2)`:: +`is:open (label:Verified=reject OR label:Code-Review:reject)`:: + 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-submodules.txt b/Documentation/user-submodules.txt index 2754b45..13d0755 100644 --- a/Documentation/user-submodules.txt +++ b/Documentation/user-submodules.txt
@@ -28,7 +28,7 @@ When a commit in a project is merged, Gerrit checks for superprojects that are subscribed to the the project and automatically updates those -superprojects with a commit that updates the gilink for the project. +superprojects with a commit that updates the gitlink for the project. This feature is enabled by default and can be disabled via link:config-gerrit.html#submodule.enableSuperProjectSubscriptions[submodule.enableSuperProjectSubscriptions]
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt index ca79b93..6d0c47a 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. @@ -171,17 +177,39 @@ git push ssh://bot@git.example.com:29418/kernel/common HEAD:refs/for/master%notify=NONE ---- +In addition uploaders can explicitly specify accounts that should be +notified, regardless of the value that is given for the `notify` +option. To notify a specific account specify it by an +`notify-to='email'`, `notify-cc='email'` or `notify-bcc='email'` +option. These options can be specified as many times as necessary to +cover all interested parties. Gerrit will automatically avoid sending +duplicate email notifications, such as if one of the specified accounts +had also requested to receive all new change notifications. The +accounts that are specified by `notify-to='email'`, `notify-cc='email'` +and `notify-bcc='email'` will only be notified about this one push. +They are not added as link:#reviewers[reviewers or CCs], hence they are +not automatically signed up to be notified on further updates of the +change. + +---- + git push ssh://bot@git.example.com:29418/kernel/common HEAD:refs/for/master%notify=NONE,notify-to=a@a.com +---- + [[topic]] ==== Topic 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 +427,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 +480,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/README.md b/README.md index 020602f..e0ffd53 100644 --- a/README.md +++ b/README.md
@@ -3,6 +3,8 @@ [Gerrit](https://www.gerritcodereview.com) is a code review and project management tool for Git based projects. +[](https://gerrit-ci.gerritforge.com/job/Gerrit-master/) + ## Objective Gerrit makes reviews easier by showing changes in a side-by-side display, @@ -69,5 +71,21 @@ yum clean all && yum install gerrit-<version>[-<release>] +On Fedora run: + + dnf clean all && dnf install gerrit-<version>[-<release>] + +## Use pre-built Gerrit images on Docker + +Docker images of Gerrit are available on [DockerHub](https://hub.docker.com/u/gerritforge/) + +To run a CentOS 7 based Gerrit image: + + docker run -p 8080:8080 gerritforge/gerrit-centos7[:version] + +To run a Ubuntu 15.04 based Gerrit image: + + docker run -p 8080:8080 gerritforge/gerrit-ubuntu15.04[:version] + _NOTE: release is optional. Last released package of the version is installed if the release number is omitted._
diff --git a/ReleaseNotes/BUILD b/ReleaseNotes/BUILD new file mode 100644 index 0000000..b0c8a13 --- /dev/null +++ b/ReleaseNotes/BUILD
@@ -0,0 +1,25 @@ +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", + resources = False, + searchbox = False, + visibility = ["//visibility:public"], +) + +genasciidoc_zip( + name = "html", + srcs = SRCS, + attributes = release_notes_attributes(), + backend = "html5", + resources = False, + searchbox = False, + visibility = ["//visibility:public"], +)
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt index e746d6e..8f94810 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.1.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -1,228 +1,5 @@ = Release notes for Gerrit 2.12.1 -Gerrit 2.12.1 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war] - -Gerrit 2.12.1 includes the bug fixes done with -link:ReleaseNotes-2.11.6.html[Gerrit 2.11.6] and -link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not* -listed in these release notes. - -== Schema Upgrade - -*WARNING:* This version includes a manual schema upgrade when upgrading -from 2.12. - -When upgrading a site that is already running version 2.12, the `patch_sets` -table must be manually migrated using the `gerrit gsql` SSH command or the -`gqsl` site program. - -For the default H2 database, execute the command: - ----- - alter table patch_sets modify push_certficate clob; ----- - -For MySQL, execute the command: - ----- - alter table patch_sets modify push_certficate text; ----- - -For PostgreSQL, execute the command: - ----- - alter table patch_sets alter column push_certficate type text; ----- - -For other database types, execute the appropriate equivalent command. - -Note that the misspelled `push_certficate` is the actual name of the -column. - -When upgrading from a version earlier than 2.12, this manual step is not -necessary and should be omitted. - - -== Bug Fixes - -=== General - -* Fix column type for signed push certificates. -+ -The column type `VARCHAR(255)` was too small, preventing some PGP push -certificates from being stored. - -* Add the `DRAFT_COMMENTS` option to the list changes REST API endpoint -and mark it as deprecated. -+ -It was removed in version 2.12 because it's not needed any more by the UI, -but this caused failures for clients that still use it. -+ -Now it is added back, although it does not do anything and is marked as -deprecated. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3669[Issue 3669]: -Fix schema migration when migrating to 2.12.x directly from a version -earlier than 2.11. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3733[Issue 3733]: -Correctly detect symlinked log directory on startup. -+ -If `$site_path/logs` was a symlink, the server would not start. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3871[Issue 3871]: -Throw an explicit exception when failing to load a change from the database. -+ -If a change could not be loaded from the database, for example if it was -manually removed from the changes table but references to it were remaining -in other tables, a null change was returned which would then lead to an -'Internal Server Error' that was difficult to track down. Now an error is -raised earlier which will help administrators to find the root cause. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3743[Issue 3743]: -Use submitter identity as committer when using 'Rebase if Necessary' merge -strategy. -+ -When submitting a change that required rebase, the committer was being -set to 'Gerrit Code Review' instead of the name of the submitter. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3758[Issue 3758]: -Fix serving of static resources when deployed in application container. -+ -When deployed in a container, for example Tomcat, it was not possible to -load the UI because static content could not be loaded from the WAR file. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3790[Issue 3790]: -When deployed in a container, for example Tomcat, the 'Documentation' menu -was missing. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3786[Issue 3786]: -Fix SQL statement syntax in schema migration. -+ -An extra semicolon was preventing migration from 2.11.x to 2.12 when using -an Oracle database. - -* Send email using email queue instead of the default queue. -+ -Some emails sent asynchronously were already being sent using that queue -but some were not. This was confusing for a gerrit administrator because -if there is a build up of `send-email` tasks in the queue, he would -think that increasing `sendemail.threadPoolSize` would help but it did not -because some of the email were sent using the default queue which is -configurable using `execution.defaultThreadPoolSize`. - -* Fix XSRF token cookie to honor `auth.cookieSecure` setting. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3767[Issue 3767]: -Fix replication of first patch set for new changes. -+ -When new changes were pushed from the command line, the first patch -set did not get replicated to destinations. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3771[Issue 3771]: -Remove `index.defaultMaxClauseCount` configuration option. -+ -When `index.maxTerms` was either not set (thus no limit) or set to a value -higher than `index.defaultMaxClauseCount` it was possible that viewing the -related changes tab could cause a 'Too many clauses' error for changes that -have a lot of related changes. -+ -The `index.defaultMaxClauseCount` configuration option is removed, and the -existing `index.maxTerms` is reused. The default value of `index.maxTerms` -is reduced from 'no limit' to 1024. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]: -Explicitly set parent project to 'All-Projects' when a project is created -without giving the parent. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3948[Issue 3948]: -Fix submit of project parent updates on `refs/meta/config`. -+ -When submitting a change on `refs/meta/config` to update a project's parent, -the error 'The change must be submitted by a Gerrit administrator' was being -displayed even when the submitter was an admin. The submit was successful -when clicking 'Submit' a second time. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3811[Issue 3811]: -Fix submittability of merge commits that resolve merge conflicts. -+ -If a series of changes contained a change that conflicted with the destination -branch, but the conflict was solved by a merge commit at the tip of the -series, the series was not submittable. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]: -Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook. - -=== UI - -* link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]: -Fix display of 'Related changes' after change is rebased in web UI: - -* link:https://code.google.com/p/gerrit/issues/detail?id=3071[Issue 3071]: -Fix display of submodule differences in side-by-side view. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3718[Issue 3718]: -Hide avatar images when no avatars are available. -+ -The UI was showing a transparent empty image with a border. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3731[Issue 3731]: -Fix syntax higlighting of tcl files. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3863[Issue 3863]: -Fix display of active row marker in tag list. -+ -Clicking on one of the rows would cause the tag name to disappear. - -* link:https://code.google.com/p/gerrit/issues/detail?id=1207[Issue 1207]: -Fix keyboard shortcuts for non-US keyboards on side-by-side diff screen. -+ -The forward/backward navigation keys `[` and `]` only worked on keyboards where -these characters could be typed without using any modifier key (like CTRL, ALT, -etc..). -+ -Note that the problem still exists on the unified diff screen. - -* Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled -and the topic can't be submitted due to some changes not being ready. - -=== Plugins - -* link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]: -Fix repeated reloading of plugins when running on OpenJDK 8. -+ -OpenJDK 8 uses nanotime precision for file modification time on systems that -are POSIX 2008 compatible. This leads to precision incompatibility when -comparing the plugin's JAR file timestamp, resulting in the plugin being -reloaded every minute. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3741[Issue 3741]: -Fix handling of merge validation exceptions emitted by plugins. -+ -If a plugin raised an exception, it was reported to the user as 'Change is -new', rather than 'Missing dependency'. - -* Allow plugins to get the caller in merge validation requests. -+ -Plugins that implement the `MergeValidationListener` interface now get the -caller (the user who initiated the merge) in the `onPreMerge` method. -+ -Existing plugins that implement this interface must be adapted to the new -method signature. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3892[Issue 3892]: -Allow plugins to suggest reviewers based on either change or project -resources. - -=== Documentation - -* Update documentation of `commentlink` to reflect changed search URL. - -* Add missing documentation of valid `database.type` values. - -== Upgrades - -* Upgrade JGit to 4.1.2.201602141800-r. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md#2.12.1[ +Release notes for Gerrit 2.12.1].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt index 8292eb5..35682ed 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.2.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -1,70 +1,5 @@ = Release notes for Gerrit 2.12.2 -Gerrit 2.12.2 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war] - -== Schema Upgrade - -*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[ -2.12.1] but a manual schema upgrade is necessary when upgrading from 2.12. - -When upgrading a site that is already running version 2.12, the `patch_sets` -table must be manually migrated using the `gerrit gsql` SSH command or the -`gqsl` site program. - -For the default H2 database, execute the command: - ----- - alter table patch_sets modify push_certficate clob; ----- - -For MySQL, execute the command: - ----- - alter table patch_sets modify push_certficate text; ----- - -For PostgreSQL, execute the command: - ----- - alter table patch_sets alter column push_certficate type text; ----- - -For other database types, execute the appropriate equivalent command. - -Note that the misspelled `push_certficate` is the actual name of the -column. - -When upgrading from a version earlier than 2.12, or from 2.12.1 having already -done the migration, this manual step is not necessary and should be omitted. - - -== Bug Fixes - -* Upgrade Apache commons-collections to version 3.2.2. -+ -Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[ -remote code execution exploit]. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]: -Explicitly set parent project to 'All-Projects' when a project is created -without giving the parent. - -* Don't add message twice on abandon or restore via ssh review command. -+ -When abandoning or reviewing a change via the ssh `review` command, and -providing a message with the `--message` option, the message was added to -the change twice. - -* Clear the input box after cancelling add reviewer action. -+ -When the action was cancelled, the content of the input box was still -there when opening it again. - -* Fix internal server error when aborting ssh command. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3969[Issue 3969]: -Fix internal server error when submitting a change with 'Rebase If Necessary' -strategy. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md#2.12.2[ +Release notes for Gerrit 2.12.2].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.3.txt b/ReleaseNotes/ReleaseNotes-2.12.3.txt index f51d739..06b18da 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.3.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.3.txt
@@ -1,113 +1,5 @@ = Release notes for Gerrit 2.12.3 -Gerrit 2.12.3 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war] - -Gerrit 2.12.3 includes the bug fixes done with -link:ReleaseNotes-2.11.8.html[Gerrit 2.11.8] and -link:ReleaseNotes-2.11.9.html[Gerrit 2.11.9]. These bug fixes are *not* -listed in these release notes. - -== Schema Upgrade - -*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.2.html[ -2.12.2] but a manual schema upgrade is necessary when upgrading from 2.12. - -When upgrading a site that is already running version 2.12, the `patch_sets` -table must be manually migrated using the `gerrit gsql` SSH command or the -`gqsl` site program. - -For the default H2 database, execute the command: - ----- - alter table patch_sets modify push_certficate clob; ----- - -For MySQL, execute the command: - ----- - alter table patch_sets modify push_certficate text; ----- - -For PostgreSQL, execute the command: - ----- - alter table patch_sets alter column push_certficate type text; ----- - -For other database types, execute the appropriate equivalent command. - -Note that the misspelled `push_certficate` is the actual name of the -column. - -When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2 -having already done the migration, this manual step is not necessary and -should be omitted. - - -== Bug Fixes - -* Fix SSL security issue in the SMTP email relay. -+ -The hostname of the SSL socket was not verified. This made the read -from the socket insecure since without verifying the hostname it may -be link:https://www.cs.utexas.edu/~shmat/shmat_ccs12.pdf[vulnerable -to a man-in-the-middle attack]. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3895[Issue 3895]: -Fix failure to submit with 'Rebase if Necessary' after changes were reordered -with interactive rebase. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4052[Issue 4052]: -Fix failure to start server after upgrade from version 2.9.4. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3891[Issue 3891]: -Fix query with `label:` operator and zero value. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4112[Issue 4112]: -Fix failure to submit changes caused by empty user edit ref. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4087[Issue 4087]: -Fix failure to submit change when a branch is created on the change ref. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4155[Issue 4155]: -Fix tags REST API to correctly return all tags. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4154[Issue 4154]: -Add support for `.team` and several more TLDs in email address validation. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4163[Issue 4163]: -Prevent removal of non-voting reviewers on submit of change. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2647[Issue 2647]: -Fix usage of `CTRL-C` on change screen. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4236[Issue 4236]: -Fix internal error when pushing an amended commit with the `%edit` option. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3426[Issue 3426]: -Fix pushing changes with `%base` option or `newChangeForAllNotInTarget` option. - -* Show 'Submitted Together' tab for changes with same topic. - -* Improve submit button tooltip messages shown when change is not submittable. - -* Fix firing of the `topic-changed` hook. - -* Remove `--dry-run` option from the `Reindex` site program. -+ -The implementation of the option was removed, but the option was mistakenly -added back to the command and did not actually work. - -* Print proper task names in the output of the `show-queues` command. - -* Replication plugin: Double check if a ref is missing locally before deleting -from remote. - -* Show an error message when trying to add a non-existent group to an ACL. - -== Updates - -* Update commons-validator to 1.5.1. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md#2.12.3[ +Release notes for Gerrit 2.12.3].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.4.txt b/ReleaseNotes/ReleaseNotes-2.12.4.txt index 64252c6..8321efa 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.4.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.4.txt
@@ -1,128 +1,5 @@ = Release notes for Gerrit 2.12.4 -Gerrit 2.12.4 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war] - -== Schema Upgrade - -*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.3.html[ -2.12.3] but a manual schema upgrade is necessary when upgrading from 2.12. - -When upgrading a site that is already running version 2.12, the `patch_sets` -table must be manually migrated using the `gerrit gsql` SSH command or the -`gqsl` site program. - -For the default H2 database, execute the command: - ----- - alter table patch_sets modify push_certficate clob; ----- - -For MySQL, execute the command: - ----- - alter table patch_sets modify push_certficate text; ----- - -For PostgreSQL, execute the command: - ----- - alter table patch_sets alter column push_certficate type text; ----- - -For other database types, execute the appropriate equivalent command. - -Note that the misspelled `push_certficate` is the actual name of the -column. - -When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2 -having already done the migration, this manual step is not necessary and -should be omitted. - -== Known Issues - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]: -'value too long for type character varying(255)' in patch_sets table when -migrating to schema version 108. -+ -This error may occur under some circumstances when running the schema -migration from an earlier version of Gerrit. -+ -On sites where this occurs, it can be fixed with a manual schema update -according to the comments in the issue. - -== Bug Fixes - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4400[Issue 4400]: -Fix `AlreadyClosedException` in Lucene index. -+ -If a Lucene indexing thread was interrupted by an SSH connection being -closed, this would also close file handles being used to read the index. -+ -Lucene queries are now executed on background threads to isolate them -from SSH threads. -+ -This may also reduce latency for user dashboards on a multi-core system as -each query for the different sections can now run on separate threads and -return results when ready. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4249[Issue 4249]: -Fix 'Duplicate stages not allowed' error during indexing. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4238[Issue 4238]: -Fix 'not found' error when browsing tree in gitweb. -+ -The `refs/heads/` prefix was incorrectly being added to `HEAD`, causing a -'404 Not Found' error. - -* Allow to read repositories that do not end with `.git`. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4262[Issue 4262]: -Fix GPG push certificate for first patch set of new changes. -+ -The GPG certificate was not being set for the first patch set of new -changes. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4296[Issue 4296]: -Fix internal error when a query does not contain any token. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4241[Issue 4241]: -Fix 'Cannot format velocity template' error when sending notification emails. - -* Fix `sshd.idleTimeout` setting being ignored. -+ -The `sshd.idleTimeout` setting was not being correctly set on the SSHD -backend, causing idle sessions to not time out. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4324[Issue 4324]: -Set the correct uploader on new patch sets created via the inline editor. - -* Log a warning instead of failing when invalid commentlinks are configured. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4136[Issue 4136]: -Fix support for `HEAD` requests in the REST API. -+ -Sending a `HEAD` request failed with '404 Not Found'. - -* Return proper error response when trying to confirm an email that is already -used by another user. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4318[Issue 4318] -Fix 'Rebase if Necessary' merge strategy to prevent introducing a duplicate -commit when submitting a merge commit. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4332[Issue 4332]: -Allow `local` as a valid TLD for outgoing emails. - -* Bypass hostname verification when `sendemail.sslVerify` is disabled. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4398[Issue 4398]: -Replication: Consider ref visibility when scheduling replication. -+ -It was possible for refs to be replicated to remotes despite not being -visible to groups mentioned in the `authGroup` setting. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4036[Issue 4036]: -Fix hanging query when using `is:watched` without authentication. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md#2.12.4[ +Release notes for Gerrit 2.12.4].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.5.txt b/ReleaseNotes/ReleaseNotes-2.12.5.txt index 12d6870..4199fe0 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.5.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.5.txt
@@ -1,101 +1,5 @@ = Release notes for Gerrit 2.12.5 -Gerrit 2.12.5 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war] - -== Schema Upgrade - -*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.4.html[ -2.12.4] but a manual schema upgrade is necessary when upgrading from 2.12. - -When upgrading a site that is already running version 2.12, the `patch_sets` -table must be manually migrated using the `gerrit gsql` SSH command or the -`gqsl` site program. - -For the default H2 database, execute the command: - ----- - alter table patch_sets modify push_certficate clob; ----- - -For MySQL, execute the command: - ----- - alter table patch_sets modify push_certficate text; ----- - -For PostgreSQL, execute the command: - ----- - alter table patch_sets alter column push_certficate type text; ----- - -For other database types, execute the appropriate equivalent command. - -Note that the misspelled `push_certficate` is the actual name of the -column. - -When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2 -having already done the migration, this manual step is not necessary and -should be omitted. - -== Known Issues - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]: -'value too long for type character varying(255)' in patch_sets table when -migrating to schema version 108. -+ -This error may occur under some circumstances when running the schema -migration from an earlier version of Gerrit. -+ -On sites where this occurs, it can be fixed with a manual schema update -according to the comments in the issue. - -== New Features - -* New preference to enable line wrapping in diff screen and inline editor. - -== Bug Fixes - -* Fix the diff and edit preference dialogs for smaller screens. -+ -On smaller screens the options at the bottom of the dialogs would -get cut off, making it difficult to change them. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4521[Issue 4521]: -Fix internal server error during validation of email addresses. -+ -When creating a new account or adding a new email address to an existing -account, the email validation crashed. - -* Lucene stability improvements. -+ -Each Lucene index is now written using a dedicated background thread. Lucene -threads may not be cancelled, to prevent interruptions while writing. - -* Don't try to change username that is already set. -+ -Since Gerrit version 2.1.4 it is not allowed to change the username once -it has been set, and attempting to do so results in an exception. -+ -If `ldap.accountSshUserName` is set in the `gerrit.config` using -`${userPrincipalName.localPart}` to initialize the username from the user's -email address, and then the email address is changed, the username gets -resolved to something different and the account manager tried to change it. -As a result, an exception was raised and the user could no longer log in. -+ -Instead of trying to change the username, a warning is logged. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4006[Issue 4006]: -Prevent search limit parameter from exceeding maximum integer value. - -* Fix internal server error when generating task names. - -* Print proper names for query tasks in the output of the `show-queue` command. - -* Double-check change status when auto-abandoning changes. -+ -It was possible that changes could be updated in the time between the query -results being returned and the change being abandoned. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md#2.12.5[ +Release notes for Gerrit 2.12.5].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt index 84644e8..3eae5e4 100644 --- a/ReleaseNotes/ReleaseNotes-2.12.txt +++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -1,562 +1,5 @@ = Release notes for Gerrit 2.12 - -Gerrit 2.12 is now available: - -link:https://www.gerritcodereview.com/download/gerrit-2.12.war[ -https://www.gerritcodereview.com/download/gerrit-2.12.war] - -== Important Notes - -*WARNING:* This release contains schema changes. To upgrade: ----- - java -jar gerrit.war init -d site_path ----- - -*WARNING:* To use online reindexing when upgrading to 2.12.x, the server must -first be upgraded to 2.8 (or 2.9) and then through 2.10 and 2.11 to 2.12.x. If -reindexing will be done offline, you may ignore this warning and upgrade directly -to 2.12.x. - -*WARNING:* When upgrading from version 2.8.4 or older with a site that uses -Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old -libraries should be manually removed from site's `lib` folder to prevent the -startup failure described in -link:https://code.google.com/p/gerrit/issues/detail?id=3084[issue 3084]. - -*WARNING:* The Solr secondary index is no longer supported. With this release -the only supported secondary index is Lucene. - -*WARNING:* The format of the `ref-updated` event has changed. Users of the -link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[ -Jenkins Gerrit Trigger plugin] with jobs triggering on `ref-updated` should -upgrade to at least -link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[ -version 2.15.1]. If an upgrade of the plugin is not possible, a workaround is -to change the branch configuration to type `Path` with a pattern like -`refs/*/master` instead of `Plain` and `master`. - - -== Release Highlights - -This release includes the following new features. See the sections below for -further details. - -* New change submission workflows: 'Submit Whole Topic' and 'Submitted Together'. - -* Support for GPG Keys and signed pushes. - - -== New Features - -=== New Change Submission Workflows - -* New 'Submit Whole Topic' setting. -+ -When the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#change.submitWholeTopic[ -`change.submitWholeTopic`] setting is enabled, all changes belonging to the same -topic will be submitted at the same time. -+ -This setting should be considered experimental, and is disabled by default. - -* Submission of changes may include ancestors. -+ -If a change is submitted that has submittable ancestor changes, those changes -will also be submitted. - -* The merge queue is removed. -+ -Changes that cannot be submitted due to missing dependencies will no longer -enter the 'Submitted, Merge Pending' state. - - -=== GPG Keys and Signed Pushes - -* Signed push can be enabled by setting -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.enableSignedPush[ -`receive.enableSignedPush`] to true. -+ -When a client pushes with `git push --signed`, Gerrit ensures that the push -certificate is valid and signed with a valid public key stored in the -`refs/meta/gpg-keys` branch of the `All-Users` repository. - -* When signed push is enabled, and -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#gerrit.editGpgKeys[ -`gerrit.editGpgKeys`] is set to true, users may upload their public GPG -key via the REST API or UI. -+ -If this setting is not enabled, GPG keys may only be added by administrators -with direct access to the `All-Users` repository. - -* Administrators may also configure -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSeed[ -`receive.certNonceSeed`] and -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSlop[ -`receive.certNonceSlop`]. - - -=== Secondary Index - -* link:http://code.google.com/p/gerrit/issues/detail?id=3333[Issue 3333]: -Support searching for changes by author and committer. -+ -Changes are indexed by the git author and committer of the latest patch set, -and can be searched with the `author:` and `committer:` operators. -+ -Changes are matched on either the exact whole email address, or on parts of the -name or email address. - -* Add `from:` search operator to match by owner of change or author of comments. - -* Add `commentby:` search operator to search by author of comments. - -* Change the `topic:` search operator to search by the exact topic name. - -* Add `intopic:` search operator to search by topics containing the search term. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3291[Issue 3291]: -Add `has:edit` search operator to match changes that have edit revisions on them. - -* Allow configuration of maximum query size. -+ -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#index.maxTerms[ -`index.maxTerms`] can be set to limit the number of leaf index terms. - -* Expose Lucene index writers for plugins. -+ -Plugins can now reconfigure various Lucene performance related parameters -at runtime. - -* Make Lucene index writers auto-commit writers. -+ -Plugins can now temporarily turn on auto-committing in situations where it makes -sense to enforce all changes to be written to disk ASAP. - - -=== UI - -==== General - -* Edit and diff preferences can be modified from the user preferences screen. -+ -Previously it was only possible to edit these preferences from the actual -diff and edit screens. - -* Add 'Edits' to the 'My' dashboard menu to list changes on which the user -has an unpublished edit revision. - -* Support for URL aliases. -+ -Administrators may define -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#urlAlias[ -URL aliases] to map plugin screens into the Gerrit URL namespace. -+ -Plugins may use user-specific URL aliases to replace certain screens for certain -users. - - -==== Project Screen - -* New tab to list the project's tags, similar to the branch list. - - -==== Inline Editor - -* Store and load edit preferences in git. -+ -Edit preferences are stored and loaded to/from the `All-Users` repository. - -* Add 'auto close brackets' feature. - -* Add 'match brackets' feature. - -* Make the cursor blink rate customizable. - -* Add support for Emacs and Vim key maps. - - -==== Change Screen - -* link:http://code.google.com/p/gerrit/issues/detail?id=3318[Issue 3318]: -Highlight 'Reply' button if there are draft comments on any patch set. -+ -If any patch set of the change has a draft comment by the current user, -the 'Reply' button is highlighted. -+ -The icons depicting draft comments are removed from the revisions drop-down -list. - -* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]: -Publish all draft comments when replying to a change. -+ -All draft comments, including those on older patch sets, are published when -replying to a change. - -* Show file size increase/decrease for binary files. - -* Show uploader if different from change owner. - -* Show push certificate status. - -* Show change subject as tooltip on related changes list. -+ -This helps to identify changes when the subject is truncated in the list. - - -==== Side-By-Side Diff - -* link:http://code.google.com/p/gerrit/issues/detail?id=3293[Issue 3293]: -Add syntax highlighting for Puppet. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3447[Issue 3447]: -Add syntax highlighting for VHDL. - - -==== Group Screen - -* link:http://code.google.com/p/gerrit/issues/detail?id=1479[Issue 1479]: -The group screen now includes an 'Audit Log' panel showing member additions, -removals, and the user who made the change. - - -=== API - -Several new APIs are added. - -==== Accounts - -* Suggest accounts. - -==== Tags - -* List tags. - -* Get tag. - - -=== REST API - -New REST API endpoints and new options on existing endpoints. - - -==== Accounts - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#set-username[ -Set Username]: Set the username of an account. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-detail[ -Get Account Details]: Get the details of an account. -+ -In addition to the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#account-info[ -AccountInfo] fields returned by the existing - link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-account[ -Get Account] endpoint, the new REST endpoint returns the registration date of -the account and the timestamp of when contact information was filed for this -account. - - -==== Changes - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[ -Set Review]: Add an option to omit duplicate comments. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#get-safe-content[ -Download Content]: Download the content of a file from a certain revision, in a -safe format that poses no risk for inadvertent execution of untrusted code. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#submitted-together[ -Get Submitted Together]: Get the list of all changes that will be submitted at -the same time as the change. - -* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[ -Set Review]: Add an option to publish draft comments on all revisions. - -==== Config - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[ -Get Server Info]: Return information about the Gerrit server configuration. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#confirm-email[ -Confirm Email]: Confirm that the user owns an email address. - - -==== Groups - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#list-group[ -List Groups]: Add option to suggest groups. -+ -This allows group auto-completion to be used in a plugin's UI. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#get-audit-log[ -Get Audit Log]: Get the audit log of a Gerrit internal group, showing member -additions, removals, and the user who made the change. - - -==== Projects - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#run-gc[ -Run GC]: Add `aggressive` option to specify whether or not to run an aggressive -garbage collection. - -* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#list-tags[ -List Tags]: Support filtering by substring and regex, and pagination with -`--start` and `--end`. - - -=== SSH - -* Add support for ZLib Compression. -+ -To enable compression use the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sshd.enableCompression[ -`sshd.enableCompression` setting]. - -* Add support for hmac-sha2-256 and hmac-sha2-512 as MACs. - -=== Plugins - -==== General - -* Gerrit client can now pass JavaScriptObjects to extension panels. - -* New UI extension point for header bar in change screen. - -* New UI extension point to password screen. - -* New UI extension points to project info screen. - -* New UI extension point for pop down buttons on change screen. - -* New UI extension point for buttons in header bar on change screen. - -* New UI extension point at bottom of the user preferences screen. - -* New UI extension point for the 'Included In' drop-down panel. -+ -By implementing the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/dev-plugins.html#included-in[ -Included In interface], plugins may add entries to the 'Included In' dropdown -menu on the change screen. - -* Plugins can extend Gerrit screens with GWT controls. - -* Plugins can add custom settings screens. - -* Referencing groups in `project.config`. -+ -Plugins can refer to groups so that when they are renamed, the project -config will also be updated in this section. - -* API - -** Allow to use `CurrentSchemaVersion`. - -** Allow to use `InternalChangeQuery.query()`. - -** Allow to use `JdbcUtil.port()`. - -** Allow to use GWTORM `Key` classes. - - -=== Other - -* link:http://code.google.com/p/gerrit/issues/detail?id=3401[Issue 3401]: -Add option to -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sendemail.allowRegisterNewEmail[ -disable registration of new email addresses]. - -* link:http://code.google.com/p/gerrit/issues/detail?id=2061[Issue 2061] -Add Support for `git-upload-archive`. -+ -This allows use the standard `git archive` command to create an archive -of the content of a repository. - -* Add a background job to automatically abandon inactive changes. -+ -The -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#changeCleanup[ -changeCleanup] configuration can be set to periodically check for inactive -changes and automatically abandon them. - -* Add support for the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_db2[ -DB2 database]. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3441[Issue 3441]: -Add support for the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_derby[ -Apache Derby database]. - -* Download commands plugin: Use commit IDs for download commands when change refs are hidden. -+ -Git has a configuration option to hide refs from the initial advertisement -(`uploadpack.hideRefs`). This option can be used to hide the change refs from -the client. As consequence this prevented fetching changes by change ref from -working. -+ -Setting `download.checkForHiddenChangeRefs` in the `gerrit.config` to true -allows the download commands plugin to check for hidden change refs. - -* Add a new 'Maintain Server' global capability. -+ -Members of a group with the 'Maintain Server' capability may view caches, tasks, -and queues, and invoke the index REST API on changes. - - -== Bug Fixes - -* link:http://code.google.com/p/gerrit/issues/detail?id=3499[Issue 3499]: -Fix syntax highlighting of raw string literals in go. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3643[Issue 3643]: -Fix syntax highlighting of ES6 string templating using backticks. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3653[Issue 3653]: -Correct timezone in sshd log after DST change. -+ -When encountering a DST switch, the timezone wasn't updated until -the server was reloaded. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3306[Issue 3306]: -Allow admins to read, push and create on `refs/users/default`. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3212[Issue 3212]: -Fix failure to run `init` when `--site-path` option is not explicitly given. - -* Make email validation case insensitive. -+ -While link:https://tools.ietf.org/html/rfc5321#section-2.3.11[ -RFC 5321 section 2.3.11] allows for the local-part (the part left of -the '@') of an email address to be case sensitive, the domain portion is -case insensitive according to -link:https://tools.ietf.org/html/rfc1035#section-3.1[RFC 1035 section 3.1]. -And in practice, even the local-part is typically case insensitive also. - -* `commit-msg` hook: Don't add `Change-Id` line on temporary commits. -+ -Commits created with `git commit --fixup` or `git commit --squash` are not -intended to be pushed to Gerrit, and don't need a `Change-Id` line. -+ -This also prevents changes from being accidentally uploaded, at least for -projects that have the 'Require Change-Id' configuration enabled. - -* link:http://code.google.com/p/gerrit/issues/detail?id=3444[Issue 3444]: -download-commands plugin: Fix clone with commit-msg hook when project name -contains '/'. - -* Use full ref name in `refName` attribute of `ref-updated` events. -+ -The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/json.html#refUpdate[ -refUpdate attribute] in `ref-updated` events did not include the full name -of the ref in the `refName` attribute, i.e. `master` was used instead of -`refs/heads/master`. -+ -Support for the new format is added in -link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[ -version 2.15.1 of the Jenkins Gerrit Trigger plugin]. -+ -Users who are unable to upgrade the plugin may instead change the -trigger's branch configuration to type `Path` with a pattern like -`refs/*/master` instead of `Plain` and `master`. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3714[Issue 3714]: -Improve visibility of comments on dark themes. - -* Fix highlighting of search results and trailing whitespaces in intraline -diff chunks. - -* Fix server error when listing annotated/signed tag that has no tagger info. - -* Don't create new account when claimed OAuth identity is unknown. -+ -The Claimed Identity feature was enabled to support old Google OpenID accounts, -that cannot be activated anymore. In some corner cases, when for example the URL -is not from the production Gerrit site, for example on a staging instance, the -OpenID identity may deviate from the original one. In case of mismatch, the lookup -of the user for the claimed identity would fail, causing a new account to be -created. - -* Suggest to upgrade installed plugins per default during site initialization -to new Gerrit version. -+ -The default was 'No' which resulted in some sites not upgrading core -plugins and running the wrong versions. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3698[Issue 3698]: -Fix creation of the administrator user on databases with pre-allocated -auto-increment column values. -+ -When using a database configuration where auto-increment column values are -pre-allocated, it was possible that the 'Administrators' group was created -with an ID other than `1`. In this case, the created admin user was not added -to the correct group, and did not have the correct admin permissions. - -* link:https://code.google.com/p/gerrit/issues/detail?id=3018[Issue 3018]: -Fix query for changes using a label with a group operator. -+ -The `group` operator was being ignored when searching for changes with labels -because the search index does not contain group information. - -* Fix online reindexing of changes that don't already exist in the index. -+ -Changes are now always reloaded from the database during online reindex. - -* Fix reading of plugin documentation. -+ -Under some circumstances it was possible to fail with an IO error. - -== Documentation Updates - -* link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]: -Update documentation of `commentlink.match` regular expression to clarify -that the expression is applied to the rendered HTML. - -* Remove warning about unstable change edit REST API endpoints. -+ -These endpoints should be considered stable since version 2.11. - -* Document that `ldap.groupBase` and `ldap.accountBase` are repeatable. - -== Upgrades - -* Upgrade Asciidoctor to 1.5.2 - -* Upgrade AutoValue to 1.1 - -* Upgrade Bouncy Castle to 1.52 - -* Upgrade CodeMirror to 5.7 - -* Upgrade gson to 2.3.1 - -* Upgrade guava to 19.0-RC2 - -* Upgrade gwtorm to 1.14-20-gec13fdc - -* Upgrade H2 to 1.3.176 - -* Upgrade httpcomponents to 4.4.1 - -* Upgrade Jetty to 9.2.13.v20150730 - -* Upgrade JGit to 4.1.1.201511131810-r - -* Upgrade joda-time to 2.8 - -* Upgrade JRuby to 1.7.18 - -* Upgrade jsch to 0.1.53 - -* Upgrade JUnit to 4.11 - -* Upgrade Lucene to 5.3.0 - -* Upgrade Prolog Cafe 1.4.1 - -* Upgrade servlet API to 8.0.24 - -* Upgrade Truth to version 0.27 - +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.12.md[ +Release notes for Gerrit 2.12].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.1.txt b/ReleaseNotes/ReleaseNotes-2.13.1.txt index 958e726..7b27ad3 100644 --- a/ReleaseNotes/ReleaseNotes-2.13.1.txt +++ b/ReleaseNotes/ReleaseNotes-2.13.1.txt
@@ -1,21 +1,5 @@ = Release notes for Gerrit 2.13.1 -Gerrit 2.13.1 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war] - -== Schema Upgrade - -There are no schema changes from link:ReleaseNotes-2.13.html[2.13]. - -== Bug Fixes - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4618[Issue 4618]: -Fix internal server error after online reindexing completed. - -* Fix internal server error when cloning from slaves and not all refs are -visible. - -* Fix JSON deserialization error causing stream event client to no longer receive -events. +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.13.md#2.13.1[ +Release notes for Gerrit 2.13.1].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.2.txt b/ReleaseNotes/ReleaseNotes-2.13.2.txt index c7be976..72bd218 100644 --- a/ReleaseNotes/ReleaseNotes-2.13.2.txt +++ b/ReleaseNotes/ReleaseNotes-2.13.2.txt
@@ -1,46 +1,5 @@ = Release notes for Gerrit 2.13.2 -Gerrit 2.13.2 is now available: - -link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war[ -https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war] - -== Schema Upgrade - -There are no schema changes from link:ReleaseNotes-2.13.1.html[2.13.1]. - -== Bug Fixes - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4630[Issue 4630]: -Fix server error when navigating up to change while 'Working' is displayed. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4631[Issue 4631]: -Read project watches from database. -+ -Project watches were being read from the git backend by default, but the -migration to git is not yet completed. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4632[Issue 4632]: -Fix server error when deleting multiple SSH keys from the Web UI. -+ -Attempting to delete multiple keys in parallel resulted in a lock failure -when removing the keys from the git backend. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4645[Issue 4645]: -Fix malformed account suggestions. -+ -If the query contained several query terms and one of the query terms was -a substring of 'strong', the suggestion was malformed. - -* Hooks plugin: Fix incorrect value passed to `--change-url` parameter. -+ -The URL was being generated using the change's Change-Id rather than the -change number. - -* Check for CLA when creating project config changes from the web UI. -+ -If contributor agreements were enabled and required for a project, and -the user had not signed a CLA, it was still possible to upload changes -for review on `refs/meta/config` by making changes in the project access -editor and pressing 'Save for Review'. - +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.13.md#2.13.2[ +Release notes for Gerrit 2.13.2].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt index 0afca1a..b3e125d 100644 --- a/ReleaseNotes/ReleaseNotes-2.13.txt +++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -1,471 +1,5 @@ = Release notes for Gerrit 2.13 - -Gerrit 2.13 is now available: - -link:https://www.gerritcodereview.com/download/gerrit-2.13.war[ -https://www.gerritcodereview.com/download/gerrit-2.13.war] - - -== Important Notes - -*WARNING:* This release contains schema changes. To upgrade: ----- - java -jar gerrit.war init -d site_path ----- - -*WARNING:* To use online reindexing for `changes` secondary index when upgrading -to 2.13.x, the server must first be upgraded to 2.8 (or 2.9) and then through -2.10, 2.11 and 2.12. Skipping a version will prevent the online reindexer from -working. - -Gerrit 2.13 introduces a new secondary index for accounts, and this must be -indexed offline before starting Gerrit: ----- - java -jar gerrit.war reindex --index accounts -d site_path ----- - -If reindexing will be done offline, you may ignore these warnings and upgrade -directly to 2.13.x using the following command that will reindex both `changes` -and `accounts` secondary indexes: ----- - java -jar gerrit.war reindex -d site_path ----- - -*WARNING:* The server side hooks functionality is moved to a core plugin. Sites -that make use of server side hooks must install this plugin during site init. - - -== Release Highlights - -* Support for Large File Storage (LFS). - -* Metrics interface. - -* Hooks plugin. - -* Secondary index for accounts. - -* File annotations (blame) in side-by-side diff. - -== New Features - -=== Large File Storage (LFS) - -Gerrit provides an -link:https://gerrit-review.googlesource.com/Documentation/2.13/dev-plugins.html#lfs-extension[ -extension point] that enables development of plugins implementing the -link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[ -LFS protocol]. - -By setting -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#lfs.plugin[ -`lfs.plugin`] the administrator can configure the name of the plugin -which handles LFS requests. - -=== Access control for git submodule subscriptions - -To prevent potential security breaches as described in -link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3311[issue 3311], -it is now only possible for a project to subscribe to a submodule if the -submodule explicitly allows itself to be subscribed. - -Please see the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-submodules.html[ -submodules user guide] for details. - -Note that when upgrading from an earlier version of Gerrit, permissions for -any existing subscriptions will be automatically added during the database -schema migration. - -=== Metrics - -Metrics about Gerrit's internal state can be sent to external -monitoring systems. - -Plugins can provide implementations of the metrics interface to -report metrics to different monitoring systems. The following -plugins are available: - -* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[ -JMX] - -* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[ -Graphite] - -* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[ -Elasticsearch] - -Plugins can also provide their own metrics. - -See the link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/metrics.html[ -metrics documentation] for further details. - -=== Hooks - -Server side hooks are moved to the -link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[ -hooks plugin]. Sites that make use of server side hooks should install this -plugin. After installing the plugin, no additional configuration is needed. -The plugin uses the same configuration settings in `gerrit.config`. - -=== Secondary Index - -* The secondary index now supports indexing of accounts. -+ -The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-reindex.html[ -reindex program] by default reindexes all changes and accounts. A new -option allows to explicitly specify whether to reindex changes or accounts. -+ -The `suggest.fullTextSearch`, `suggest.fullTextSearchMaxMatches` and -`suggest.fullTextSearchRefresh` configuration options are removed. Full text -search is supported by default with the account secondary index. - -* New ssh command to -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/cmd-index-changes.html[ -reindex changes]. - - -=== UI - -* The UI can now be loaded in an iFrame by enabling -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#gerrit.canLoadInIFrame[ -gerrit.canLoadInIFrame] in the site configuration. - -==== Change Screen - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=106[Issue 106]: -Allow to select merge commit's parent for diff base in change screen. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3035[Issue 3035]: -Allow to remove specific votes from a change, while leaving the reviewer on the -change. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3487[Issue 3487]: -Use 'Ctrl-Alt-e' instead of 'e' to open edit mode. - -==== Diff Screens - -* Add all syntax highlighting available in CodeMirror. - -* Improve search experience in diff screen -+ -Ctrl-F, Ctrl-G and Shift-Ctrl-G now bind to the search dialog box provided by -CodeMirror's search add-on. Enter and Shift-Enter navigate among the search -results from the CodeMirror search, just like they do in a normal browser -search. Esc now clears the search result. -+ -If the user sets `Render` to `Slow` in the diff preferences and the file is less -than 4000 lines (huge), then Ctrl-F, Ctrl-G and Shift-Ctrl-G fall back to the -browser search. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2968[Issue 2968]: -Allow to go back to change list by keyboard shortcut from diff screens. - -==== Side-By-Side Diff Screen - -* Blame annotations -+ -By enabling -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#change.allowBlame[ -`change.allowBlame`], blame annotations can be shown in the side-by-side diff -screen gutter. Clicking the annotation opens the relevant change. - -==== User Preferences - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=989[Issue 989]: -New option to control email notifications. -+ -Users can now choose between 'Enabled', 'Disabled' and 'CC Me on Comments I Write'. - -* New option to control adding 'Signed-off-by' footer in commit message of new changes -created online. - -* New option to control auto-indent width in inline editor. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=890[Issue 890]: -New diff option to control whether to skip unchanged files when navigating to -the previous or the next file. - -=== Changes - -In order to avoid potentially confusing behavior, when submitting changes in a -batch, submit type rules may not be used to mix submit types on a single branch, -and trying to submit such a batch will fail. - -=== REST API - -==== Accounts - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3766[Issue 3766]: -Allow users with the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#capability_modifyAccount[ -'ModifyAccount' capability] to get the preferences for other users via the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-user-preferences[ -Get User Preferences] endpoint. - -* Rename 'Suggest Account' to -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#query-account[ -'Query Account'] and add support for arbitrary account queries. -+ -The `_more_accounts` flag is set on the last result when there are more results -than the limit. The `DETAILS` and `ALL_EMAILS` options may be set to control -whether the results should include details (full name, email, username, avatars) -and all emails, respectively. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-watched-projects[ -Get Watched Projects]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-watched-projects[ -Set Watched Projects]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#delete-watched-projects[ -Delete Watched Projects]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-stars[ -Get Star Labels from Change]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-stars[ -Update Star Labels on Change]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-oauth-token[ -Get OAuth Access Token]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#list-contributor-agreements[ -List Contributor Agreements]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#sign-contributor-agreement[ -Sign Contributor Agreement]. - -==== Changes - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3579[Issue 3579]: -Append submitted info to ChangeInfo. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-changes.html#move-change[ -Move Change]. - -==== Groups - -* Add `-s` as an alias for `--suggest` on the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-groups.html#suggest-group[ -Suggest Group] endpoint. - -==== Projects - -* Add `async` option to the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#run-gc[ -Run GC] endpoint to allow garbage collection to run asynchronously. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-access[ -List Access Rights]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#set-access[ -Add, Update and Delete Access Rights]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#create-tag[ -Create Tag]. - -* New endpoint: -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-mergeable-info[ -Get Mergeable Information]. - -=== Plugins - -* Secure settings -+ -Plugins may now store secure settings in `etc/$PLUGIN.secure.config` where they -will be decoded by the Secure Store implementation. - -* Exported dependencies -+ -Gson is now an exported dependency. Plugins no longer need to explicitly add -a dependency on it. - -=== Misc - -* New project option to reject implicit merge commits. -+ -The 'Reject Implicit Merges' option can be enabled to prevent non-merge commits -from implicitly bringing unwanted changes into a branch. This can happen for -example when a commit is made based on one branch but is mistakenly pushed to -another, for example based on `refs/heads/master` but pushed to `refs/for/stable`. - -* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#category_add_patch_set[ -Add Patch Set capability] to control who is allowed to upload a new patch -set to an existing change. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4015[Issue 4015]: -Allow setting a -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#message[ -comment message] when uploading a change. - -* Allow to specify -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#notify[ -who should be notified by email] when uploading a change. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3220[Issue 3220]: -Append approval info to every comment-added stream event and hook. - -* The `administrateServer` capability can be assigned to groups by setting -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#capability.administrateServer[ -capability.administrateServer] in the site configuration. -+ -Configuring this option can be a useful fail-safe to recover a server in the -event an administrator removed all groups from the `administrateServer` -capability, or to ensure that specific groups always have administration -capabilities. - -* New configuration options to configure JGit repository cache parameters. -+ -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheCleanupDelay[ -core.repositoryCacheCleanupDelay] and -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheExpireAfter[ -core.repositoryCacheExpireAfter] can be configured. - -* Accept `-b` as an alias of `--batch` in the -link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-init.html[ -init program]. - - -== Bug Fixes - -* Don't add the same SSH key multiple times. -+ -If an already existing SSH key was added, a duplicate entry was added to the -list of user's SSH keys. - -* Respect the 'Require a valid contributor agreement to upload' setting -when creating changes via the UI. -+ -If a user had not signed a CLA, it was still possible for them to create a new -change with the 'Revert' or 'Cherry Pick' button. - -* Make Lucene index more stable when being interrupted. - -* Don't show the `start` and `idle` columns in the `show-connections` -output when the ssh backend is NIO2. -+ -The NIO2 backend doesn't provide the start and idle times, and the -values being displayed were just dummy values. Now these values are -only displayed for the MINA backend. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4150[Issue 4150]: -Deleting a draft inline comment no longer causes the change's `Updated` field to -be bumped. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4099[Issue 4099]: -Fix SubmitWholeTopic does not update subscriptions. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3603[Issue 3603]: -Fix editing a submodule via inline edit. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4069[Issue 4069]: -Fix highlights in scrollbar overview ruler not moved when extending the -displayed area. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3446[Issue 3446]: -Respect the `Skip Deleted` diff preference. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3445[Issue 3445]: -Respect the `Skip Uncommented` diff preference. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4051[Issue 4051]: -Fix empty `From` email header. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3423[Issue 3423]: -Fix intraline diff for added spaces. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=1867[Issue 1867]: -Remove `no changes made` error case when the only difference between a new -commit and the previous patch set of the change is the committer. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3831[Issue 3831]: -Prevent creating groups with the same name as a system group. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3754[Issue 3754]: -Fix `View All Accounts` permission to allow accounts REST endpoint to access -email info. - -* Make `gitweb.type` default to `disabled` when not explicitly set. -+ -Previously the behavior was not documented and it would default to type -`gitweb`. In cases where there was no gitweb config at all, this would -result in broken links due to `null` being used as the URL. - -* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4488[Issue 4488]: -Improve error message when `Change-Id` line is missing in commit message. -+ -The error message now includes the sha1 of the commit, so that it is -easier to track down which commit failed validation when multiple commits -are pushed at the same time. - -* Don't check mergeability of draft changes. -+ -Draft changes can be deleted but not abandoned so there is no way for -an administrator to get rid of the them on behalf of the users. This can -become a problem when there many draft changes because the mergeability -check can be costly. -+ -The mergeability check is no longer done for draft changes, but will be -done when the draft change is published. - -* Fix internal server error when plugin-provided file history weblink -is null. -+ -It is valid for a plugin to provide a null weblink, but doing so resulted -in an internal server error. - -== Dependency updates - -* Add dependency on blame-cache 0.1-9 - -* Add dependency on guava-retrying 2.0.0 - -* Add dependency on jsr305 3.0.1 - -* Add dependency on metrics-core 3.1.2 - -* Upgrade auto-value to 1.3-rc1 - -* Upgrade commons-net to 3.5 - -* Upgrade CodeMirror to 5.17.0 - -* Upgrade Guava to 19.0 - -* Upgrade Gson to 2.7 - -* Upgrade Guice to 4.1.0 - -* Upgrade gwtjsonrpc to 1.9 - -* Upgrade gwtorm to 1.15 - -* Upgrade javassist to 3.20.0-GA - -* Upgrade Jetty to 9.2.14.v20151106 - -* Upgrade JGit to 4.5.0.201609210915-r - -* Upgrade joda-convert to 1.8.1 - -* Upgrade joda-time to 2.9.4 - -* Upgrade Lucene to 5.5.0 - -* Upgrade mina to 2.0.10 - -* Upgrade sshd-core to 1.2.0 +Release notes have been moved to the project homepage: +link:https://www.gerritcodereview.com/releases/2.13.md[ +Release notes for Gerrit 2.13].
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt index 945f09f..9c28697 100644 --- a/ReleaseNotes/index.txt +++ b/ReleaseNotes/index.txt
@@ -2,18 +2,18 @@ [[s2_13]] == Version 2.13.x -* link:ReleaseNotes-2.13.2.html[2.13.2] -* link:ReleaseNotes-2.13.1.html[2.13.1] -* link:ReleaseNotes-2.13.html[2.13] +* link:https://www.gerritcodereview.com/releases/2.13.md#2.13.2[2.13.2] +* link:https://www.gerritcodereview.com/releases/2.13.md#2.13.1[2.13.1] +* link:https://www.gerritcodereview.com/releases/2.13.md[2.13] [[s2_12]] == Version 2.12.x -* link:ReleaseNotes-2.12.5.html[2.12.5] -* link:ReleaseNotes-2.12.4.html[2.12.4] -* link:ReleaseNotes-2.12.3.html[2.12.3] -* link:ReleaseNotes-2.12.2.html[2.12.2] -* link:ReleaseNotes-2.12.1.html[2.12.1] -* link:ReleaseNotes-2.12.html[2.12] +* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.5[2.12.5] +* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.4[2.12.4] +* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.3[2.12.3] +* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.2[2.12.2] +* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.1[2.12.1] +* link:https://www.gerritcodereview.com/releases/2.12.md[2.12] [[s2_11]] == Version 2.11.x
diff --git a/WORKSPACE b/WORKSPACE index d465b37..37ef5c0 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -1,699 +1,1108 @@ -ANTLR_VERS = '3.5.2' +workspace(name = "gerrit") + +load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL") +load("//lib/codemirror:cm.bzl", "DIFF_MATCH_PATCH_VERSION") + +ANTLR_VERS = "3.5.2" maven_jar( - name = 'java_runtime', - artifact = 'org.antlr:antlr-runtime:' + ANTLR_VERS, - sha1 = 'cd9cd41361c155f3af0f653009dcecb08d8b4afd', + name = "java_runtime", + artifact = "org.antlr:antlr-runtime:" + ANTLR_VERS, + sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd", ) maven_jar( - name = 'stringtemplate', - artifact = 'org.antlr:stringtemplate:4.0.2', - sha1 = 'e28e09e2d44d60506a7bcb004d6c23ff35c6ac08', + name = "stringtemplate", + artifact = "org.antlr:stringtemplate:4.0.2", + sha1 = "e28e09e2d44d60506a7bcb004d6c23ff35c6ac08", ) maven_jar( - name = 'org_antlr', - artifact = 'org.antlr:antlr:' + ANTLR_VERS, - sha1 = 'c4a65c950bfc3e7d04309c515b2177c00baf7764', + name = "org_antlr", + artifact = "org.antlr:antlr:" + ANTLR_VERS, + sha1 = "c4a65c950bfc3e7d04309c515b2177c00baf7764", ) maven_jar( - name = 'antlr27', - artifact = 'antlr:antlr:2.7.7', - sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0', + name = "antlr27", + artifact = "antlr:antlr:2.7.7", + attach_source = False, + 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', + name = "guice_library", + artifact = "com.google.inject:guice:" + GUICE_VERS, + sha1 = "eeb69005da379a10071aa4948c48d89250febb07", ) maven_jar( - name = 'guice_assistedinject', - artifact = 'com.google.inject.extensions:guice-assistedinject:' + GUICE_VERS, - sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca', + name = "guice_assistedinject", + artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS, + sha1 = "af799dd7e23e6fe8c988da12314582072b07edcb", ) maven_jar( - name = 'guice_servlet', - artifact = 'com.google.inject.extensions:guice-servlet:' + GUICE_VERS, - sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164', + name = "guice_servlet", + artifact = "com.google.inject.extensions:guice-servlet:" + GUICE_VERS, + sha1 = "90ac2db772d9b85e2b05417b74f7464bcc061dcb", +) + +maven_jar( + name = "multibindings", + artifact = "com.google.inject.extensions:guice-multibindings:" + GUICE_VERS, + sha1 = "3b27257997ac51b0f8d19676f1ea170427e86d51", ) maven_jar( - name = 'aopalliance', - artifact = 'aopalliance:aopalliance:1.0', - sha1 = '0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8', + name = "aopalliance", + artifact = "aopalliance:aopalliance:1.0", + sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8", ) maven_jar( - name = 'javax_inject', - artifact = 'javax.inject:javax.inject:1', - sha1 = '6975da39a7040257bd51d21a231b76c915872d38', + name = "javax_inject", + artifact = "javax.inject:javax.inject:1", + sha1 = "6975da39a7040257bd51d21a231b76c915872d38", ) maven_jar( - name = 'servlet_api_3_1', - artifact = 'org.apache.tomcat:tomcat-servlet-api:8.0.24', - sha1 = '5d9e2e895e3111622720157d0aa540066d5fce3a', + name = "servlet_api_3_1", + artifact = "org.apache.tomcat:tomcat-servlet-api:8.0.24", + sha1 = "5d9e2e895e3111622720157d0aa540066d5fce3a", ) -GWT_VERS = '2.7.0' +GWT_VERS = "2.8.0" maven_jar( - name = 'user', - artifact = 'com.google.gwt:gwt-user:' + GWT_VERS, - sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b', + name = "user", + artifact = "com.google.gwt:gwt-user:" + GWT_VERS, + sha1 = "518579870499e15531f454f35dca0772d7fa31f7", ) maven_jar( - name = 'dev', - artifact = 'com.google.gwt:gwt-dev:' + GWT_VERS, - sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982', + name = "dev", + artifact = "com.google.gwt:gwt-dev:" + GWT_VERS, + sha1 = "f160a61272c5ebe805cd2d3d3256ed3ecf14893f", ) maven_jar( - name = 'javax_validation', - artifact = 'javax.validation:validation-api:1.0.0.GA', - sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e', + name = "javax_validation", + artifact = "javax.validation:validation-api:1.0.0.GA", + sha1 = "b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e", + src_sha1 = "7a561191db2203550fbfa40d534d4997624cd369", ) -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", + src_sha1 = "5d7c478efbfccc191430d7c118d7bd2635e43750", +) maven_jar( - name = 'jgit', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS, - sha1 = 'c07c9c66da7983095a40945c0bfab211a473c4c5', + name = "ant", + artifact = "ant:ant:1.6.5", + attach_source = False, + sha1 = "7d18faf23df1a5c3a43613952e0e8a182664564b", ) 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', + name = "colt", + artifact = "colt:colt:1.2.0", + attach_source = False, + sha1 = "0abc984f3adc760684d49e0f11ddf167ba516d4f", ) -# 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/' + - '%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS), +maven_jar( + name = "tapestry", + artifact = "tapestry:tapestry:4.0.2", + attach_source = False, + sha1 = "e855a807425d522e958cbce8697f21e9d679b1f7", ) maven_jar( - name = 'ewah', - artifact = 'com.googlecode.javaewah:JavaEWAH:0.7.9', - sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a', + name = "w3c_css_sac", + artifact = "org.w3c.css:sac:1.3", + sha1 = "cdb2dcb4e22b83d6b32b93095f644c3462739e82", ) +load("//lib/jgit:jgit.bzl", "JGIT_VERS") + maven_jar( - name = 'jgit_archive', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS, - sha1 = 'fc3bc40e070c54198a046fcd3a1f7cac47163961', + name = "jgit", + artifact = "org.eclipse.jgit:org.eclipse.jgit:" + JGIT_VERS, + repository = GERRIT, + sha1 = "34315f71bb9becf6ff75947a9c43c415b929ec21", + src_sha1 = "8320c18472870904eb7fb860af353fea818d07e4", + unsign = True, ) maven_jar( - name = 'jgit_junit', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS, - sha1 = 'b4565ee84a6e1d0952010282b9fcf705ac6171a7', + name = "jgit_servlet", + artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + JGIT_VERS, + repository = GERRIT, + sha1 = "927990025d2970995dbb58f03763eeb776fec8fd", + unsign = True, ) maven_jar( - name = 'gwtjsonrpc', - artifact = 'com.google.gerrit:gwtjsonrpc:1.8', - sha1 = 'c264bf2f543cffddceada5cdf031eea06dbd44a0', + name = "javaewah", + artifact = "com.googlecode.javaewah:JavaEWAH:1.1.6", + attach_source = False, + sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6", ) -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', +maven_jar( + name = "jgit_archive", + artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + JGIT_VERS, + repository = GERRIT, + sha1 = "4a5d058915400c1ef497bfeeb5e87d235213e273", ) maven_jar( - name = 'gson', - artifact = 'com.google.code.gson:gson:2.6.2', - sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947', + name = "jgit_junit", + artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + JGIT_VERS, + repository = GERRIT, + sha1 = "8e3cb9b1f632fdfea76b04c286a2c0d8d260ebce", + unsign = True, ) maven_jar( - name = 'gwtorm_client', - artifact = 'com.google.gerrit:gwtorm:1.15', - sha1 = '26a2459f543ed78977535f92e379dc0d6cdde8bb', + name = "gwtjsonrpc", + artifact = "com.google.gerrit:gwtjsonrpc:1.11", + sha1 = "0990e7eec9eec3a15661edcf9232acbac4aeacec", + src_sha1 = "a682afc46284fb58197a173cb5818770a1e7834a", ) -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', +maven_jar( + name = "gson", + artifact = "com.google.code.gson:gson:2.7", + sha1 = "751f548c85fa49f330cecbb1875893f971b33c4e", ) maven_jar( - name = 'protobuf', - artifact = 'com.google.protobuf:protobuf-java:2.5.0', - sha1 = 'a10732c76bfacdbd633a7eb0f7968b1059a65dfa', + name = "gwtorm_client", + artifact = "com.google.gerrit:gwtorm:1.16", + sha1 = "3e41b6d7bb352fa0539ce23b9bce97cf8c26c3bf", + src_sha1 = "f45b7bacc79a0e5a7f6cf799a2dba23cc5bca19b", ) maven_jar( - name = 'joda_time', - artifact = 'joda-time:joda-time:2.8', - sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb', + name = "protobuf", + artifact = "com.google.protobuf:protobuf-java:2.5.0", + sha1 = "a10732c76bfacdbd633a7eb0f7968b1059a65dfa", ) maven_jar( - name = 'joda_convert', - artifact = 'org.joda:joda-convert:1.2', - sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec', + name = "joda_time", + artifact = "joda-time:joda-time:2.9.4", + sha1 = "1c295b462f16702ebe720bbb08f62e1ba80da41b", ) maven_jar( - name = 'guava', - artifact = 'com.google.guava:guava:19.0', - sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9', + name = "joda_convert", + artifact = "org.joda:joda-convert:1.8.1", + sha1 = "675642ac208e0b741bc9118dcbcae44c271b992a", ) +load("//lib:guava.bzl", "GUAVA_VERSION", "GUAVA_BIN_SHA1") + maven_jar( - name = 'velocity', - artifact = 'org.apache.velocity:velocity:1.7', - sha1 = '2ceb567b8f3f21118ecdec129fe1271dbc09aa7a', + name = "guava", + artifact = "com.google.guava:guava:" + GUAVA_VERSION, + sha1 = GUAVA_BIN_SHA1, ) maven_jar( - name = 'jsch', - artifact = 'com.jcraft:jsch:0.1.53', - sha1 = '658b682d5c817b27ae795637dfec047c63d29935', + name = "velocity", + artifact = "org.apache.velocity:velocity:1.7", + sha1 = "2ceb567b8f3f21118ecdec129fe1271dbc09aa7a", +) + +maven_jar( + name = "jsch", + artifact = "com.jcraft:jsch:0.1.54", + sha1 = "da3584329a263616e277e15462b387addd1b208d", +) + +maven_jar( + name = "juniversalchardet", + artifact = "com.googlecode.juniversalchardet:juniversalchardet:1.0.3", + sha1 = "cd49678784c46aa8789c060538e0154013bb421b", +) + +SLF4J_VERS = "1.7.7" + +maven_jar( + name = "log_api", + artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS, + sha1 = "2b8019b6249bb05d81d3a3094e468753e2b21311", +) + +maven_jar( + name = "log_nop", + artifact = "org.slf4j:slf4j-nop:" + SLF4J_VERS, + sha1 = "6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1", +) + +maven_jar( + name = "impl_log4j", + artifact = "org.slf4j:slf4j-log4j12:" + SLF4J_VERS, + sha1 = "58f588119ffd1702c77ccab6acb54bfb41bed8bd", +) + +maven_jar( + name = "jcl_over_slf4j", + artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS, + sha1 = "56003dcd0a31deea6391b9e2ef2f2dc90b205a92", +) + +maven_jar( + name = "log4j", + artifact = "log4j:log4j:1.2.17", + sha1 = "5af35056b4d257e4b64b9e8069c0746e8b08629f", +) + +maven_jar( + name = "jsonevent_layout", + artifact = "net.logstash.log4j:jsonevent-layout:1.7", + sha1 = "507713504f0ddb75ba512f62763519c43cf46fde", +) + +maven_jar( + name = "json_smart", + artifact = "net.minidev:json-smart:1.1.1", + sha1 = "24a2f903d25e004de30ac602c5b47f2d4e420a59", +) + +maven_jar( + name = "args4j", + artifact = "args4j:args4j:2.0.26", + sha1 = "01ebb18ebb3b379a74207d5af4ea7c8338ebd78b", +) + +maven_jar( + name = "commons_codec", + artifact = "commons-codec:commons-codec:1.4", + sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a", +) + +maven_jar( + name = "commons_collections", + artifact = "commons-collections:commons-collections:3.2.2", + sha1 = "8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5", +) + +maven_jar( + name = "commons_compress", + artifact = "org.apache.commons:commons-compress:1.12", + sha1 = "84caa68576e345eb5e7ae61a0e5a9229eb100d7b", ) maven_jar( - name = 'juniversalchardet', - artifact = 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3', - sha1 = 'cd49678784c46aa8789c060538e0154013bb421b', + name = "commons_lang", + artifact = "commons-lang:commons-lang:2.6", + sha1 = "0ce1edb914c94ebc388f086c6827e8bdeec71ac2", ) -SLF4J_VERS = '1.7.7' +maven_jar( + name = "commons_lang3", + artifact = "org.apache.commons:commons-lang3:3.3.2", + sha1 = "90a3822c38ec8c996e84c16a3477ef632cbc87a3", +) maven_jar( - name = 'log_api', - artifact = 'org.slf4j:slf4j-api:' + SLF4J_VERS, - sha1 = '2b8019b6249bb05d81d3a3094e468753e2b21311', + name = "commons_dbcp", + artifact = "commons-dbcp:commons-dbcp:1.4", + sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39", ) maven_jar( - name = 'log_nop', - artifact = 'org.slf4j:slf4j-nop:' + SLF4J_VERS, - sha1 = '6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1', + name = "commons_pool", + artifact = "commons-pool:commons-pool:1.5.5", + sha1 = "7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b", ) maven_jar( - name = 'impl_log4j', - artifact = 'org.slf4j:slf4j-log4j12:' + SLF4J_VERS, - sha1 = '58f588119ffd1702c77ccab6acb54bfb41bed8bd', + name = "commons_net", + artifact = "commons-net:commons-net:3.5", + sha1 = "342fc284019f590e1308056990fdb24a08f06318", ) maven_jar( - name = 'jcl_over_slf4j', - artifact = 'org.slf4j:jcl-over-slf4j:' + SLF4J_VERS, - sha1 = '56003dcd0a31deea6391b9e2ef2f2dc90b205a92', + name = "commons_oro", + artifact = "oro:oro:2.0.8", + sha1 = "5592374f834645c4ae250f4c9fbb314c9369d698", ) maven_jar( - name = 'log4j', - artifact = 'log4j:log4j:1.2.17', - sha1 = '5af35056b4d257e4b64b9e8069c0746e8b08629f', + name = "commons_validator", + artifact = "commons-validator:commons-validator:1.5.1", + sha1 = "86d05a46e8f064b300657f751b5a98c62807e2a0", ) maven_jar( - name = 'jsonevent_layout', - artifact = 'net.logstash.log4j:jsonevent-layout:1.7', - sha1 = '507713504f0ddb75ba512f62763519c43cf46fde', + name = "automaton", + artifact = "dk.brics.automaton:automaton:1.11-8", + sha1 = "6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f", ) maven_jar( - name = 'json_smart', - artifact = 'net.minidev:json-smart:1.1.1', - sha1 = '24a2f903d25e004de30ac602c5b47f2d4e420a59', + name = "pegdown", + artifact = "org.pegdown:pegdown:1.4.2", + sha1 = "d96db502ed832df867ff5d918f05b51ba3879ea7", ) maven_jar( - name = 'args4j', - artifact = 'args4j:args4j:2.0.26', - sha1 = '01ebb18ebb3b379a74207d5af4ea7c8338ebd78b', + name = "grappa", + artifact = "com.github.parboiled1:grappa:1.0.4", + sha1 = "ad4b44b9c305dad7aa1e680d4b5c8eec9c4fd6f5", ) maven_jar( - name = 'commons_codec', - artifact = 'commons-codec:commons-codec:1.4', - sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a', + name = "jitescript", + artifact = "me.qmx.jitescript:jitescript:0.4.0", + sha1 = "2e35862b0435c1b027a21f3d6eecbe50e6e08d54", ) +GREENMAIL_VERS = "1.5.2" + maven_jar( - name = 'commons_collections', - artifact = 'commons-collections:commons-collections:3.2.2', - sha1 = '8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5', + name = "greenmail", + artifact = "com.icegreen:greenmail:" + GREENMAIL_VERS, + sha1 = "6b4862a09f8642da58c109117b24ccc19a4a6d39", ) +MAIL_VERS = "1.5.6" + maven_jar( - name = 'commons_compress', - artifact = 'org.apache.commons:commons-compress:1.7', - sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d', + name = "mail", + artifact = "com.sun.mail:javax.mail:" + MAIL_VERS, + sha1 = "ab5daef2f881c42c8e280cbe918ec4d7fdfd7efe", ) +MIME4J_VERS = "0.8.0" + maven_jar( - name = 'commons_lang', - artifact = 'commons-lang:commons-lang:2.6', - sha1 = '0ce1edb914c94ebc388f086c6827e8bdeec71ac2', + name = "mime4j_core", + artifact = "org.apache.james:apache-mime4j-core:" + MIME4J_VERS, + sha1 = "d54f45fca44a2f210569656b4ca3574b42911c95", ) maven_jar( - name = 'commons_dbcp', - artifact = 'commons-dbcp:commons-dbcp:1.4', - sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39', + name = "mime4j_dom", + artifact = "org.apache.james:apache-mime4j-dom:" + MIME4J_VERS, + sha1 = "6720c93d14225c3e12c4a69768a0370c80e376a3", ) maven_jar( - name = 'commons_pool', - artifact = 'commons-pool:commons-pool:1.5.5', - sha1 = '7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b', + name = "jsoup", + artifact = "org.jsoup:jsoup:1.9.2", + sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4", ) +OW2_VERS = "5.1" + maven_jar( - name = 'commons_net', - artifact = 'commons-net:commons-net:2.2', - sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a', + name = "ow2_asm", + artifact = "org.ow2.asm:asm:" + OW2_VERS, + sha1 = "5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45", ) maven_jar( - name = 'commons_oro', - artifact = 'oro:oro:2.0.8', - sha1 = '5592374f834645c4ae250f4c9fbb314c9369d698', + name = "ow2_asm_analysis", + artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS, + sha1 = "6d1bf8989fc7901f868bee3863c44f21aa63d110", ) maven_jar( - name = 'commons_validator', - artifact = 'commons-validator:commons-validator:1.5.1', - sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0', + name = "ow2_asm_commons", + artifact = "org.ow2.asm:asm-commons:" + OW2_VERS, + sha1 = "25d8a575034dd9cfcb375a39b5334f0ba9c8474e", ) maven_jar( - name = 'automaton', - artifact = 'dk.brics.automaton:automaton:1.11-8', - sha1 = '6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f', + name = "ow2_asm_tree", + artifact = "org.ow2.asm:asm-tree:" + OW2_VERS, + sha1 = "87b38c12a0ea645791ead9d3e74ae5268d1d6c34", ) maven_jar( - name = 'pegdown', - artifact = 'org.pegdown:pegdown:1.4.2', - sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7', + name = "ow2_asm_util", + artifact = "org.ow2.asm:asm-util:" + OW2_VERS, + sha1 = "b60e33a6bd0d71831e0c249816d01e6c1dd90a47", ) maven_jar( - name = 'grappa', - artifact = 'com.github.parboiled1:grappa:1.0.4', - sha1 = 'ad4b44b9c305dad7aa1e680d4b5c8eec9c4fd6f5', + name = "auto_value", + artifact = "com.google.auto.value:auto-value:1.4-rc1", + sha1 = "9347939002003a7a3c3af48271fc2c18734528a4", ) maven_jar( - name = 'jitescript', - artifact = 'me.qmx.jitescript:jitescript:0.4.0', - sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54', + name = "tukaani_xz", + artifact = "org.tukaani:xz:1.4", + sha1 = "18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3", ) -OW2_VERS = '5.0.3' +LUCENE_VERS = "5.5.2" maven_jar( - name = 'ow2_asm', - artifact = 'org.ow2.asm:asm:' + OW2_VERS, - sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa', + name = "lucene_core", + artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS, + sha1 = "de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb", +) + +maven_jar( + name = "lucene_analyzers_common", + artifact = "org.apache.lucene:lucene-analyzers-common:" + LUCENE_VERS, + sha1 = "f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d", ) maven_jar( - name = 'ow2_asm_analysis', - artifact = 'org.ow2.asm:asm-analysis:' + OW2_VERS, - sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3', + name = "lucene_codecs", + artifact = "org.apache.lucene:lucene-codecs:" + LUCENE_VERS, + sha1 = "e01fe463d9490bb1b4a6a168e771f7b7255a50b1", ) maven_jar( - name = 'ow2_asm_commons', - artifact = 'org.ow2.asm:asm-commons:' + OW2_VERS, - sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c', + name = "backward_codecs", + artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS, + sha1 = "c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5", ) maven_jar( - name = 'ow2_asm_tree', - artifact = 'org.ow2.asm:asm-tree:' + OW2_VERS, - sha1 = '287749b48ba7162fb67c93a026d690b29f410bed', + name = "lucene_misc", + artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS, + sha1 = "37bbe5a2fb429499dfbe75d750d1778881fff45d", ) maven_jar( - name = 'ow2_asm_util', - artifact = 'org.ow2.asm:asm-util:' + OW2_VERS, - sha1 = '1512e5571325854b05fb1efce1db75fcced54389', + name = "lucene_queryparser", + artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS, + sha1 = "8ac921563e744463605284c6d9d2d95e1be5b87c", ) maven_jar( - name = 'auto_value', - artifact = 'com.google.auto.value:auto-value:1.2', - sha1 = '6873fed014fe1de1051aae2af68ba266d2934471', + name = "lucene_highlighter", + artifact = "org.apache.lucene:lucene-highlighter:" + LUCENE_VERS, + sha1 = "d127ac514e9df965ab0b57d92bbe0c68d3d145b8", ) maven_jar( - name = 'tukaani_xz', - artifact = 'org.tukaani:xz:1.4', - sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3', + name = "lucene_join", + artifact = "org.apache.lucene:lucene-join:" + LUCENE_VERS, + sha1 = "dac1b322508f3f2696ecc49a97311d34d8382054", ) -LUCENE_VERS = '5.4.1' +maven_jar( + name = "lucene_memory", + artifact = "org.apache.lucene:lucene-memory:" + LUCENE_VERS, + sha1 = "7409db9863d8fbc265c27793c6cc7511304182c2", +) maven_jar( - name = 'lucene_core', - artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS, - sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab', + name = "lucene_sandbox", + artifact = "org.apache.lucene:lucene-sandbox:" + LUCENE_VERS, + sha1 = "30a91f120706ba66732d5a974b56c6971b3c8a16", ) maven_jar( - name = 'lucene_analyzers_common', - artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS, - sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e', + name = "lucene_spatial", + artifact = "org.apache.lucene:lucene-spatial:" + LUCENE_VERS, + sha1 = "8ed7a9a43d78222038573dd1c295a61f3c0bb0db", ) maven_jar( - name = 'backward_codecs', - artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS, - sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f', + name = "lucene_suggest", + artifact = "org.apache.lucene:lucene-suggest:" + LUCENE_VERS, + sha1 = "e8316b37dddcf2092a54dab2ce6aad0d5ad78585", ) maven_jar( - name = 'lucene_misc', - artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS, - sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b', + name = "lucene_queries", + artifact = "org.apache.lucene:lucene-queries:" + LUCENE_VERS, + sha1 = "692f1ad887cf4e006a23f45019e6de30f3312d3f", ) maven_jar( - name = 'lucene_queryparser', - artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS, - sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618', + name = "mime_util", + artifact = "eu.medsea.mimeutil:mime-util:2.1.3", + attach_source = False, + sha1 = "0c9cfae15c74f62491d4f28def0dff1dabe52a47", ) +PROLOG_VERS = "1.4.2" + maven_jar( - name = 'mime_util', - artifact = 'eu.medsea.mimeutil:mime-util:2.1.3', - sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47', + name = "prolog_runtime", + artifact = "com.googlecode.prolog-cafe:prolog-runtime:" + PROLOG_VERS, + attach_source = False, + repository = GERRIT, + sha1 = "4421b4806b6e3a318680f6ab1d57569e857169c6", ) -PROLOG_VERS = '1.4.1' +maven_jar( + name = "prolog_compiler", + artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS, + attach_source = False, + repository = GERRIT, + sha1 = "7e5a7ca5efe7db7f69e015cf492f8f04665244d8", +) maven_jar( - name = 'prolog_runtime', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'com.googlecode.prolog-cafe:prolog-runtime:' + PROLOG_VERS, - sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246', + name = "prolog_io", + artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS, + attach_source = False, + repository = GERRIT, + sha1 = "d177f6211d1013e0f31a507127f5c87a7f6941f3", ) maven_jar( - name = 'prolog_compiler', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'com.googlecode.prolog-cafe:prolog-compiler:' + PROLOG_VERS, - sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41', + name = "cafeteria", + artifact = "com.googlecode.prolog-cafe:prolog-cafeteria:" + PROLOG_VERS, + attach_source = False, + repository = GERRIT, + sha1 = "11f396cb2588b65e6a78070488aaa58d12bf000e", ) maven_jar( - name = 'prolog_io', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'com.googlecode.prolog-cafe:prolog-io:' + PROLOG_VERS, - sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa', + name = "guava_retrying", + artifact = "com.github.rholder:guava-retrying:2.0.0", + sha1 = "974bc0a04a11cc4806f7c20a34703bd23c34e7f4", ) maven_jar( - name = 'cafeteria', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + PROLOG_VERS, - sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641', + name = "jsr305", + artifact = "com.google.code.findbugs:jsr305:3.0.1", + sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d", ) maven_jar( - name = 'guava_retrying', - artifact = 'com.github.rholder:guava-retrying:2.0.0', - sha1 = '974bc0a04a11cc4806f7c20a34703bd23c34e7f4', + name = "blame_cache", + artifact = "com/google/gitiles:blame-cache:0.1-9", + attach_source = False, + repository = GERRIT, + sha1 = "51d35e6f8bbc2412265066cea9653dd758c95826", ) +# Keep this version of Soy synchronized with the version used in Gitiles. maven_jar( - name = 'jsr305', - artifact = 'com.google.code.findbugs:jsr305:2.0.2', - sha1 = '516c03b21d50a644d538de0f0369c620989cd8f0', + name = "soy", + artifact = "com.google.template:soy:2016-08-09", + sha1 = "43d33651e95480d515fe26c10a662faafe3ad1e4", ) maven_jar( - name = 'blame_cache', - repository = 'http://gerrit-maven.storage.googleapis.com/', - artifact = 'com/google/gitiles:blame-cache:0.1-9', - sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826', + 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', - sha1 = '224f03afd2521c6c94632f566beb1bb5ee32cf07', + name = "dropwizard_core", + artifact = "io.dropwizard.metrics:metrics-core:3.1.2", + sha1 = "224f03afd2521c6c94632f566beb1bb5ee32cf07", ) # This version must match the version that also appears in # gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config -BC_VERS = '1.52' +BC_VERS = "1.55" maven_jar( - name = 'bcprov', - artifact = 'org.bouncycastle:bcprov-jdk15on:' + BC_VERS, - sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269', + name = "bcprov", + artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS, + sha1 = "935f2e57a00ec2c489cbd2ad830d4a399708f979", ) maven_jar( - name = 'bcpg', - artifact = 'org.bouncycastle:bcpg-jdk15on:' + BC_VERS, - sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858', + name = "bcpg", + artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS, + sha1 = "54ce841795ecdf10f24e50c48d4fdec59c691699", ) maven_jar( - name = 'bcpkix', - artifact = 'org.bouncycastle:bcpkix-jdk15on:' + BC_VERS, - sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504', + name = "bcpkix", + artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS, + sha1 = "6392d8cba22b722c6570d660ca0b3921ff1bae4f", ) maven_jar( - name = 'sshd', - artifact = 'org.apache.sshd:sshd-core:1.2.0', - sha1 = '4bc24a8228ba83dac832680366cf219da71dae8e', + name = "sshd", + artifact = "org.apache.sshd:sshd-core:1.2.0", + sha1 = "4bc24a8228ba83dac832680366cf219da71dae8e", ) maven_jar( - name = 'mina_core', - artifact = 'org.apache.mina:mina-core:2.0.10', - sha1 = 'a1cb1136b104219d6238de886bf5a3ea4554eb58', + name = "mina_core", + artifact = "org.apache.mina:mina-core:2.0.10", + sha1 = "a1cb1136b104219d6238de886bf5a3ea4554eb58", ) maven_jar( - name = 'h2', - artifact = 'com.h2database:h2:1.3.176', - sha1 = 'fd369423346b2f1525c413e33f8cf95b09c92cbd', + name = "h2", + artifact = "com.h2database:h2:1.3.176", + sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd", ) -HTTPCOMP_VERS = '4.4.1' +HTTPCOMP_VERS = "4.4.1" maven_jar( - name = 'fluent_hc', - artifact = 'org.apache.httpcomponents:fluent-hc:' + HTTPCOMP_VERS, - sha1 = '96fb842b68a44cc640c661186828b60590c71261', + name = "fluent_hc", + artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS, + sha1 = "96fb842b68a44cc640c661186828b60590c71261", ) maven_jar( - name = 'httpclient', - artifact = 'org.apache.httpcomponents:httpclient:' + HTTPCOMP_VERS, - sha1 = '016d0bc512222f1253ee6b64d389c84e22f697f0', + name = "httpclient", + artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS, + sha1 = "016d0bc512222f1253ee6b64d389c84e22f697f0", ) maven_jar( - name = 'httpcore', - artifact = 'org.apache.httpcomponents:httpcore:' + HTTPCOMP_VERS, - sha1 = 'f5aa318bda4c6c8d688c9d00b90681dcd82ce636', + name = "httpcore", + artifact = "org.apache.httpcomponents:httpcore:" + HTTPCOMP_VERS, + sha1 = "f5aa318bda4c6c8d688c9d00b90681dcd82ce636", ) maven_jar( - name = 'httpmime', - artifact = 'org.apache.httpcomponents:httpmime:' + HTTPCOMP_VERS, - sha1 = '2f8757f5ac5e38f46c794e5229d1f3c522e9b1df', + name = "httpmime", + artifact = "org.apache.httpcomponents:httpmime:" + HTTPCOMP_VERS, + sha1 = "2f8757f5ac5e38f46c794e5229d1f3c522e9b1df", ) # Test-only dependencies below. maven_jar( - name = 'jimfs', - artifact = 'com.google.jimfs:jimfs:1.0', - sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97', + name = "jimfs", + artifact = "com.google.jimfs:jimfs:1.1", + sha1 = "8fbd0579dc68aba6186935cc1bee21d2f3e7ec1c", ) maven_jar( - name = 'junit', - artifact = 'junit:junit:4.11', - sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0', + name = "junit", + artifact = "junit:junit:4.11", + sha1 = "4e031bb61df09069aeb2bffb4019e7a5034a4ee0", ) maven_jar( - name = 'hamcrest_core', - artifact = 'org.hamcrest:hamcrest-core:1.3', - sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0', + name = "hamcrest_core", + artifact = "org.hamcrest:hamcrest-core:1.3", + sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0", ) maven_jar( - name = 'truth', - artifact = 'com.google.truth:truth:0.28', - sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4', + name = "truth", + artifact = "com.google.truth:truth:0.30", + sha1 = "9d591b5a66eda81f0b88cf1c748ab8853d99b18b", ) maven_jar( - name = 'easymock', - artifact = 'org.easymock:easymock:3.4', # When bumping the version - sha1 = '9fdeea183a399f25c2469497612cad131e920fa3', + name = "easymock", + 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', + name = "objenesis", + 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', + name = "powermock_module_junit4", + artifact = "org.powermock:powermock-module-junit4:" + POWERM_VERS, + sha1 = "ea8530b2848542624f110a393513af397b37b9cf", ) maven_jar( - name = 'powermock_module_junit4_common', - artifact = 'org.powermock:powermock-module-junit4-common:' + POWERM_VERS, - sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81', + name = "powermock_module_junit4_common", + artifact = "org.powermock:powermock-module-junit4-common:" + POWERM_VERS, + sha1 = "7222ced54dabc310895d02e45c5428ca05193cda", ) maven_jar( - name = 'powermock_reflect', - artifact = 'org.powermock:powermock-reflect:' + POWERM_VERS, - sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893', + name = "powermock_reflect", + artifact = "org.powermock:powermock-reflect:" + POWERM_VERS, + sha1 = "97d25eda8275c11161bcddda6ef8beabd534c878", ) maven_jar( - name = 'powermock_api_easymock', - artifact = 'org.powermock:powermock-api-easymock:' + POWERM_VERS, - sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45', + name = "powermock_api_easymock", + artifact = "org.powermock:powermock-api-easymock:" + POWERM_VERS, + sha1 = "aa740ecf89a2f64d410b3d93ef8cd6833009ef00", ) maven_jar( - name = 'powermock_api_support', - artifact = 'org.powermock:powermock-api-support:' + POWERM_VERS, - sha1 = '314daafb761541293595630e10a3699ebc07881d', + name = "powermock_api_support", + artifact = "org.powermock:powermock-api-support:" + POWERM_VERS, + sha1 = "592ee6d929c324109d3469501222e0c76ccf0869", ) maven_jar( - name = 'powermock_core', - artifact = 'org.powermock:powermock-core:' + POWERM_VERS, - sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87', + name = "powermock_core", + artifact = "org.powermock:powermock-core:" + POWERM_VERS, + sha1 = "5afc1efce8d44ed76b30af939657bd598e45d962", ) maven_jar( - name = 'javassist', - artifact = 'org.javassist:javassist:3.20.0-GA', - sha1 = 'a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0', + name = "javassist", + artifact = "org.javassist:javassist:3.20.0-GA", + sha1 = "a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0", ) maven_jar( - name = 'derby', - artifact = 'org.apache.derby:derby:10.11.1.1', - sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1', + name = "derby", + artifact = "org.apache.derby:derby:10.11.1.1", + attach_source = False, + 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', + name = "jetty_servlet", + artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS, + sha1 = "d550147b85c73ea81084a4ac7915ba7f609021c5", ) maven_jar( - name = 'jetty_security', - artifact = 'org.eclipse.jetty:jetty-security:' + JETTY_VERS, - sha1 = '2d36974323fcb31e54745c1527b996990835db67', + name = "jetty_security", + artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS, + sha1 = "1cbefc5d1196b9e1ca6f4cc36738998a6ebde8bf", ) maven_jar( - name = 'jetty_servlets', - artifact = 'org.eclipse.jetty:jetty-servlets:' + JETTY_VERS, - sha1 = 'a75c78a0ee544073457ca5ee9db20fdc6ed55225', + name = "jetty_servlets", + artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS, + sha1 = "a9f7a43977151a463aa21a9b0e882aa3d25452ef", ) maven_jar( - name = 'jetty_server', - artifact = 'org.eclipse.jetty:jetty-server:' + JETTY_VERS, - sha1 = '70b22c1353e884accf6300093362b25993dac0f5', + name = "jetty_server", + artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS, + sha1 = "d932e0dc1e9bd4839ae446754615163d60271a66", ) maven_jar( - name = 'jetty_jmx', - artifact = 'org.eclipse.jetty:jetty-jmx:' + JETTY_VERS, - sha1 = '617edc5e966b4149737811ef8b289cd94b831bab', + name = "jetty_jmx", + artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS, + sha1 = "21a658d2f5eb87c23eef4911966625ea95f66d32", ) maven_jar( - name = 'jetty_continuation', - artifact = 'org.eclipse.jetty:jetty-continuation:' + JETTY_VERS, - sha1 = '8909d62fd7e28351e2da30de6fb4105539b949c0', + name = "jetty_continuation", + artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS, + sha1 = "92a91c0dcc5f5d779a1c9f94038332be3f46c9df", ) maven_jar( - name = 'jetty_http', - artifact = 'org.eclipse.jetty:jetty-http:' + JETTY_VERS, - sha1 = '699ad1f2fa6fb0717e1b308a8c9e1b8c69d81ef6', + name = "jetty_http", + artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS, + sha1 = "dcfb95e5b886a981bb76467b911c5b706117f9cf", ) maven_jar( - name = 'jetty_io', - artifact = 'org.eclipse.jetty:jetty-io:' + JETTY_VERS, - sha1 = 'dfa4137371a3f08769820138ca1a2184dacda267', + name = "jetty_io", + artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS, + sha1 = "db5f4f481159894a4b670072a34917b5414d0c98", ) maven_jar( - name = 'jetty_util', - artifact = 'org.eclipse.jetty:jetty-util:' + JETTY_VERS, - sha1 = '0057e00b912ae0c35859ac81594a996007706a0b', + name = "jetty_util", + artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS, + sha1 = "1812ffd5a04698051180d582c146ca807760c808", ) maven_jar( - name = 'openid_consumer', - artifact = 'org.openid4java:openid4java:0.9.8', - sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160', + name = "openid_consumer", + artifact = "org.openid4java:openid4java:0.9.8", + sha1 = "de4f1b33d3b0f0b2ab1d32834ec1190b39db4160", ) maven_jar( - name = 'nekohtml', - artifact = 'net.sourceforge.nekohtml:nekohtml:1.9.10', - sha1 = '14052461031a7054aa094f5573792feb6686d3de', + name = "nekohtml", + artifact = "net.sourceforge.nekohtml:nekohtml:1.9.10", + sha1 = "14052461031a7054aa094f5573792feb6686d3de", ) maven_jar( - name = 'xerces', - artifact = 'xerces:xercesImpl:2.8.1', - sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1', + name = "xerces", + artifact = "xerces:xercesImpl:2.8.1", + attach_source = False, + 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:" + DIFF_MATCH_PATCH_VERSION, + attach_source = False, + 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", + attach_source = False, + sha1 = "9c662e7247ca8ceb1de5de629f685c9ef3e4ab58", +) + +load("//tools/bzl:js.bzl", "npm_binary", "bower_archive") + +npm_binary( + name = "bower", +) + +npm_binary( + name = "vulcanize", + repository = GERRIT, +) + +npm_binary( + name = "crisper", + repository = GERRIT, +) + +# bower_archive() seed components. +bower_archive( + name = "iron-autogrow-textarea", + package = "polymerelements/iron-autogrow-textarea", + sha1 = "b9b6874c9a2b5be435557a827ff8bd6661672ee3", + version = "1.0.12", +) + +bower_archive( + name = "es6-promise", + package = "stefanpenner/es6-promise", + sha1 = "a3a797bb22132f1ef75f9a2556173f81870c2e53", + version = "3.3.0", +) + +bower_archive( + name = "fetch", + package = "fetch", + sha1 = "1b05a2bb40c73232c2909dc196de7519fe4db7a9", + version = "1.0.0", +) + +bower_archive( + name = "iron-dropdown", + package = "polymerelements/iron-dropdown", + sha1 = "63e3d669a09edaa31c4f05afc76b53b919ef0595", + version = "1.4.0", +) + +bower_archive( + name = "iron-input", + package = "polymerelements/iron-input", + sha1 = "9bc0c8e81de2527125383cbcf74dd9f27e7fa9ac", + version = "1.0.10", +) + +bower_archive( + name = "iron-overlay-behavior", + package = "polymerelements/iron-overlay-behavior", + sha1 = "83181085fda59446ce74fd0d5ca30c223f38ee4a", + version = "1.7.6", +) + +bower_archive( + name = "iron-selector", + package = "polymerelements/iron-selector", + sha1 = "c57235dfda7fbb987c20ad0e97aac70babf1a1bf", + version = "1.5.2", +) + +bower_archive( + name = "moment", + package = "moment/moment", + sha1 = "fc8ce2c799bab21f6ced7aff928244f4ca8880aa", + version = "2.13.0", +) + +bower_archive( + name = "page", + package = "visionmedia/page.js", + sha1 = "51a05428dd4f68fae1df5f12d0e2b61ba67f7757", + version = "1.7.1", +) + +bower_archive( + name = "polymer", + package = "polymer/polymer", + sha1 = "b84725939ead7c7bdf9917b065f68ef8dc790d06", + version = "1.4.0", +) + +bower_archive( + name = "promise-polyfill", + package = "polymerlabs/promise-polyfill", + sha1 = "a3b598c06cbd7f441402e666ff748326030905d6", + version = "1.0.0", +) + +# bower test stuff + +bower_archive( + name = "iron-test-helpers", + package = "polymerelements/iron-test-helpers", + sha1 = "433b03b106f5ff32049b84150cd70938e18b67ac", + version = "1.2.5", +) + +bower_archive( + name = "test-fixture", + package = "polymerelements/test-fixture", + sha1 = "e373bd21c069163c3a754e234d52c07c77b20d3c", + version = "1.1.1", +) + +bower_archive( + name = "web-component-tester", + package = "web-component-tester", + sha1 = "54556000c33d9ed7949aa546c1b4a1531491a5f0", + version = "4.2.2", +) + +# Bower component transitive dependencies. +load("//lib/js:bower_archives.bzl", "load_bower_archives") + +load_bower_archives()
diff --git a/contrib/.pylintrc b/contrib/.pylintrc deleted file mode 100644 index 9e8882e..0000000 --- a/contrib/.pylintrc +++ /dev/null
@@ -1,301 +0,0 @@ -# lint Python modules using external checkers. -# -# This is the main checker controling the other ones and the reports -# generation. It is itself both a raw checker and an astng checker in order -# to: -# * handle message activation / deactivation at the module level -# * handle some basic but necessary stats'data (number of classes, methods...) -# -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Profiled execution. -profile=no - -# Add <file or directory> to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=SVN - -# Pickle collected data for later comparisons. -persistent=yes - -# Set the cache size for astng objects. -cache-size=500 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable only checker(s) with the given id(s). This option conflicts with the -# disable-checker option -#enable-checker= - -# Enable all checker(s) except those with the given id(s). This option -# conflicts with the enable-checker option -#disable-checker= - -# Enable all messages in the listed categories. -#enable-msg-cat= - -# Disable all messages in the listed categories. -#disable-msg-cat= - -# Enable the message(s) with the given id(s). -enable=RP0004 - -# Disable the message(s) with the given id(s). -disable=R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,W0603,W0703,R0911,C0301,C0302,R0902,R0904,W0142,W0212,E1101,E1103,R0201,W0201,W0122,W0232,RP0001,RP0003,RP0101,RP0002,RP0401,RP0701,RP0801 - -[REPORTS] - -# set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note).You have access to the variables errors warning, statement which -# respectivly contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - -# checks for -# * unused variables / imports -# * undefined variables -# * redefinition of variable from builtins or from an outer scope -# * use of variable before assigment -# -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -# try to find bugs in the code using type inference -# -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamicaly set). -ignored-classes=SQLObject - -# When zope mode is activated, consider the acquired-members option to ignore -# access to some undefined attributes. -zope=no - -# List of members which are usually get through zope's acquisition mecanism and -# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). -acquired-members=REQUEST,acl_users,aq_parent - - -# checks for : -# * doc strings -# * modules / classes / functions / methods / arguments / variables name -# * number of arguments, local variables, branchs, returns and statements in -# functions, methods -# * required module attributes -# * dangerous default values as arguments -# * redefinition of function / method / class -# * uses of the global statement -# -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=_main|__.*__ - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))|(log)$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_,e,d1,d2,v,f,l,d - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - - -# checks for sign of poor/misdesign: -# * number of methods, attributes, local variables... -# * size, complexity of functions, methods -# -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=20 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=30 - - -# checks for -# * external modules dependencies -# * relative / wildcard imports -# * cyclic imports -# * uses of deprecated modules -# -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report R0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report R0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report R0402 must -# not be disabled) -int-import-graph= - - -# checks for : -# * methods without self as first argument -# * overridden methods signature -# * access only to existant members via self -# * attributes not defined in the __init__ method -# * supported interfaces implementation -# * unreachable code -# -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -# checks for similarities and duplicated code. This computation may be -# memory / CPU intensive, so you should disable it if you experiments some -# problems. -# -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -# checks for: -# * warning notes in the code like FIXME, XXX -# * PEP 263: source code with non ascii character but no encoding declaration -# -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -# checks for : -# * unauthorized constructions -# * strict indentation -# * line length -# * use of <> instead of != -# -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). In repo it is 2 spaces. -indent-string=' '
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..dca71e6 100644 --- a/gerrit-acceptance-framework/BUCK +++ b/gerrit-acceptance-framework/BUCK
@@ -1,24 +1,5 @@ SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java']) -DEPS = [ - '//gerrit-gpg:gpg', - '//gerrit-launcher:launcher', - '//gerrit-openid:openid', - '//gerrit-pgm:daemon', - '//gerrit-pgm:http-jetty', - '//gerrit-pgm:util-nodep', - '//gerrit-server/src/main/prolog:common', - '//gerrit-server:testutil', - '//lib/auto:auto-value', - '//lib/httpcomponents:fluent-hc', - '//lib/httpcomponents:httpclient', - '//lib/httpcomponents:httpcore', - '//lib/jetty:servlet', - '//lib/jgit/org.eclipse.jgit.junit:junit', - '//lib/log:impl_log4j', - '//lib/log:log4j', -] - PROVIDED = [ '//gerrit-common:annotations', '//gerrit-common:server', @@ -37,38 +18,57 @@ 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, - exported_deps = DEPS + [ + exported_deps = [ + '//gerrit-antlr:query_exception', + '//gerrit-gpg:gpg', + '//gerrit-launcher:launcher', + '//gerrit-openid:openid', + '//gerrit-pgm:daemon', + '//gerrit-pgm:http-jetty', + '//gerrit-pgm:util-nodep', + '//gerrit-server/src/main/prolog:common', + '//gerrit-server:testutil', + '//lib/auto:auto-value', + '//lib/httpcomponents:fluent-hc', + '//lib/httpcomponents:httpclient', + '//lib/httpcomponents:httpcore', + '//lib/jetty:servlet', + '//lib/jgit/org.eclipse.jgit.junit:junit', + '//lib/log:impl_log4j', + '//lib/log:log4j', '//lib:truth', ], provided_deps = PROVIDED + [ + '//lib/greenmail:greenmail', '//lib:gwtorm', '//lib/guice:guice', '//lib/guice:guice-assistedinject', '//lib/guice:guice-servlet', + '//lib/mail:mail', ], visibility = ['PUBLIC'], ) 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,9 +76,10 @@ 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 + [ + deps = PROVIDED + [ + ':lib', '//lib:guava', '//lib/guice:guice-assistedinject', '//lib/guice:guice_library', @@ -86,7 +87,6 @@ '//lib/guice:javax-inject', '//lib:gwtorm_client', '//lib:junit', - '//lib:truth', ], visibility = ['PUBLIC'], )
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD index 1439ba9..69f132b 100644 --- a/gerrit-acceptance-framework/BUILD +++ b/gerrit-acceptance-framework/BUILD
@@ -1,60 +1,74 @@ -load('//tools/bzl:java.bzl', 'java_library2') +load("//tools/bzl:java.bzl", "java_library2") -SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java']) - -DEPS = [ - '//gerrit-gpg:gpg', - '//gerrit-launcher:launcher', - '//gerrit-openid:openid', - '//gerrit-pgm:daemon', - '//gerrit-pgm:http-jetty', - '//gerrit-pgm:util-nodep', - '//gerrit-server/src/main/prolog:common', - '//gerrit-server:testutil', - '//lib/auto:auto-value', - '//lib/httpcomponents:fluent-hc', - '//lib/httpcomponents:httpclient', - '//lib/httpcomponents:httpcore', - '//lib/jetty:servlet', - '//lib/jgit/org.eclipse.jgit.junit:junit', - '//lib/log:impl_log4j', - '//lib/log:log4j', -] +SRCS = glob(["src/test/java/com/google/gerrit/acceptance/*.java"]) PROVIDED = [ - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-httpd:httpd', - '//gerrit-lucene:lucene', - '//gerrit-pgm:init', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:gson', - '//lib:jsch', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/mina:sshd', - '//lib:servlet-api-3_1', + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-httpd:httpd", + "//gerrit-lucene:lucene", + "//gerrit-pgm:init", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:gson", + "//lib:jsch", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/mina:sshd", + "//lib:servlet-api-3_1", ] java_binary( - name = 'acceptance-framework', - main_class = 'Dummy', - runtime_deps = [':lib'], - visibility = ['//visibility:public'], + name = "acceptance-framework", + testonly = 1, + main_class = "Dummy", + visibility = ["//visibility:public"], + runtime_deps = [":lib"], ) java_library2( - name = 'lib', - srcs = SRCS, - exported_deps = DEPS + [ - '//lib:truth', - ], - deps = PROVIDED + [ # We want these deps to be exported_deps - '//lib:gwtorm', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - ], - visibility = ['//visibility:public'], + name = "lib", + testonly = 1, + srcs = SRCS, + exported_deps = [ + "//gerrit-antlr:query_exception", + "//gerrit-gpg:gpg", + "//gerrit-launcher:launcher", + "//gerrit-openid:openid", + "//gerrit-pgm:daemon", + "//gerrit-pgm:http-jetty", + "//gerrit-pgm:util-nodep", + "//gerrit-server:testutil", + "//gerrit-server/src/main/prolog:common", + "//lib:truth", + "//lib/auto:auto-value", + "//lib/httpcomponents:fluent-hc", + "//lib/httpcomponents:httpclient", + "//lib/httpcomponents:httpcore", + "//lib/jetty:servlet", + "//lib/jgit/org.eclipse.jgit.junit:junit", + "//lib/log:impl_log4j", + "//lib/log:log4j", + ], + visibility = ["//visibility:public"], + deps = PROVIDED + [ + # We want these deps to be exported_deps + "//lib/greenmail:greenmail", + "//lib:gwtorm", + "//lib/guice:guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/mail:mail", + ], +) + +load("//tools/bzl:javadoc.bzl", "java_doc") + +java_doc( + name = "acceptance-framework-javadoc", + testonly = 1, + libs = [":lib"], + pkgs = ["com.google.gerrit.acceptance"], + title = "Gerrit Acceptance Test Framework Documentation", + visibility = ["//visibility:public"], )
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml index 7f8614f..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.4</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 61d748f..3dbc10f 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,26 +17,31 @@ 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 java.util.stream.Collectors.toList; 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.Nullable; 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; @@ -46,7 +51,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; @@ -62,7 +70,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; @@ -71,8 +81,10 @@ 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.mail.send.EmailHeader; import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.ChangeControl; @@ -98,12 +110,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; @@ -115,14 +134,22 @@ 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.Collection; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; @RunWith(ConfigSuite.class) public abstract class AbstractDaemonTest { @@ -218,6 +245,9 @@ @Inject private EventRecorder.Factory eventRecorderFactory; + @Inject + private ChangeIndexCollection changeIndexes; + protected TestRepository<InMemoryRepository> testRepo; protected GerritServer server; protected TestAccount admin; @@ -236,6 +266,9 @@ @Inject protected ChangeNotes.Factory notesFactory; + @Inject + protected Abandon changeAbandoner; + @Rule public ExpectedException exception = ExpectedException.none(); @@ -522,21 +555,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(); @@ -668,6 +706,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(); } @@ -802,6 +856,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); } @@ -825,6 +892,13 @@ .review(ReviewInput.approve()); } + protected void recommend(String id) throws Exception { + gApi.changes() + .id(id) + .revision("current") + .review(ReviewInput.recommend()); + } + protected Map<String, ActionInfo> getActions(String id) throws Exception { return gApi.changes() .id(id) @@ -832,14 +906,15 @@ .actions(); } + protected String getETag(String id) throws Exception { + return gApi.changes() + .id(id) + .current() + .etag(); + } + 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) @@ -910,7 +985,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; } } @@ -943,6 +1019,143 @@ 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(); + } + protected TestRepository<?> createProjectWithPush(String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception { @@ -951,4 +1164,46 @@ grant(Permission.SUBMIT, project, "refs/for/refs/heads/*"); return cloneProject(project); } + + protected void assertPermitted(ChangeInfo info, String label, + Integer... expected) { + assertThat(info.permittedLabels).isNotNull(); + Collection<String> strs = info.permittedLabels.get(label); + if (expected.length == 0) { + assertThat(strs).isNull(); + } else { + assertThat( + strs.stream().map(s -> Integer.valueOf(s.trim())) + .collect(toList())) + .containsExactlyElementsIn(Arrays.asList(expected)); + } + } + + protected void assertNotifyTo(TestAccount expected) { + assertThat(sender.getMessages()).hasSize(1); + Message m = sender.getMessages().get(0); + assertThat(m.rcpt()).containsExactly(expected.emailAddress); + assertThat( + ((EmailHeader.AddressList) m.headers().get("To")).getAddressList()) + .containsExactly(expected.emailAddress); + assertThat(m.headers().get("CC").isEmpty()).isTrue(); + } + + protected void assertNotifyCc(TestAccount expected) { + assertThat(sender.getMessages()).hasSize(1); + Message m = sender.getMessages().get(0); + assertThat(m.rcpt()).containsExactly(expected.emailAddress); + assertThat(m.headers().get("To").isEmpty()).isTrue(); + assertThat( + ((EmailHeader.AddressList) m.headers().get("CC")).getAddressList()) + .containsExactly(expected.emailAddress); + } + + protected void assertNotifyBcc(TestAccount expected) { + assertThat(sender.getMessages()).hasSize(1); + Message m = sender.getMessages().get(0); + assertThat(m.rcpt()).containsExactly(expected.emailAddress); + assertThat(m.headers().get("To").isEmpty()).isTrue(); + assertThat(m.headers().get("CC").isEmpty()).isTrue(); + } }
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/GerritConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java index 4b956a2..4e40d92 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -17,11 +17,13 @@ import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target({METHOD}) @Retention(RUNTIME) +@Repeatable(GerritConfigs.class) public @interface GerritConfig { String name(); String value() default "";
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 d1ec9e6..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
@@ -47,6 +47,7 @@ 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; @@ -129,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..2f1463d 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
@@ -16,8 +16,8 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.base.Optional; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.reviewdb.client.Project; @@ -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; @@ -47,6 +50,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -137,6 +141,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 +168,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 +180,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,14 +222,25 @@ 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); tr.getRevWalk().parseBody(c); - List<String> ids = c.getFooterLines(FooterConstants.CHANGE_ID); - if (ids.isEmpty()) { - return Optional.absent(); - } - return Optional.of(ids.get(ids.size() - 1)); + return Lists.reverse(c.getFooterLines(FooterConstants.CHANGE_ID)).stream() + .findFirst(); } }
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/LightweightPluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java new file mode 100644 index 0000000..3a870cb --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
@@ -0,0 +1,64 @@ +// 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.server.PluginUser; +import com.google.gerrit.server.plugins.PluginGuiceEnvironment; +import com.google.gerrit.server.plugins.TestServerPlugin; +import com.google.inject.Inject; + +import org.junit.After; +import org.junit.Before; + +public class LightweightPluginDaemonTest extends AbstractDaemonTest { + @Inject + private PluginGuiceEnvironment env; + + @Inject + private PluginUser.Factory pluginUserFactory; + + private TestServerPlugin plugin; + + @Before + public void setUp() throws Exception { + TestPlugin testPlugin = getTestPlugin(getClass()); + String name = testPlugin.name(); + plugin = new TestServerPlugin(name, + canonicalWebUrl.get() + "plugins/" + name, + pluginUserFactory.create(name), + getClass().getClassLoader(), + testPlugin.sysModule(), + testPlugin.httpModule(), + testPlugin.sshModule()); + + plugin.start(env); + env.onStartPlugin(plugin); + } + + @After + public void tearDown() { + plugin.stop(env); + env.onStopPlugin(plugin); + } + + private static TestPlugin getTestPlugin(Class<?> clazz) { + for (; clazz != null; clazz = clazz.getSuperclass()) { + if (clazz.getAnnotation(TestPlugin.class) != null) { + return clazz.getAnnotation(TestPlugin.class); + } + } + throw new IllegalStateException("TestPlugin annotation missing"); + } +}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java index dde1875..6c387af 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -18,6 +18,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; +import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.server.config.SitePaths; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -40,7 +41,9 @@ public abstract class PluginDaemonTest extends AbstractDaemonTest { private static final String BUCKLC = "buck"; + private static final String BAZELLC = "bazel"; private static final String BUCKOUT = "buck-out"; + private static final String BAZELOUT = "bazel-out"; private static final String ECLIPSE = "eclipse-out"; private Path gen; @@ -49,6 +52,8 @@ private Path pluginSubPath; private Path pluginSource; private boolean standalone; + private boolean bazel; + private Path basePath; protected String pluginName; protected Path testSite; @@ -87,10 +92,10 @@ return cfg; } - private void locatePaths() { + private void locatePaths() throws IOException { URL pluginClassesUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); - Path basePath = Paths.get(pluginClassesUrl.getPath()).getParent(); + basePath = Paths.get(pluginClassesUrl.getPath()).getParent(); int idx = 0; int buckOutIdx = 0; @@ -99,14 +104,24 @@ if (subPath.endsWith("plugins")) { pluginsIdx = idx; } - if (subPath.endsWith(BUCKOUT) || subPath.endsWith(ECLIPSE)) { + if (subPath.endsWith(BAZELOUT) || subPath.endsWith(ECLIPSE)) { + bazel = true; + buckOutIdx = idx; + } + if (subPath.endsWith(BUCKOUT)) { buckOutIdx = idx; } idx++; } standalone = checkStandalone(basePath); - pluginRoot = basePath.getRoot().resolve(basePath.subpath(0, buckOutIdx)); - gen = pluginRoot.resolve(BUCKOUT).resolve("gen"); + + if (bazel) { + pluginRoot = GerritLauncher.resolveInSourceRoot("."); + gen = pluginRoot.resolve("bazel-out/local-fastbuild/genfiles"); + } else { + pluginRoot = basePath.getRoot().resolve(basePath.subpath(0, buckOutIdx)); + gen = pluginRoot.resolve(BUCKOUT).resolve("gen"); + } if (standalone) { pluginSource = pluginRoot; @@ -117,6 +132,10 @@ } private boolean checkStandalone(Path basePath) { + // TODO(davido): Fix Bazel standalone mode + if (bazel) { + return false; + } String pathCharStringOrNone = "[a-zA-Z0-9._-]*?"; Pattern pattern = Pattern.compile(pathCharStringOrNone + "gerrit" + pathCharStringOrNone); @@ -139,8 +158,19 @@ } private void retrievePluginName() throws IOException { - Path buckFile = pluginSource.resolve("BUCK"); - byte[] bytes = Files.readAllBytes(buckFile); + if (bazel) { + pluginName = basePath.getFileName().toString(); + return; + } + Path buildfile = pluginSource.resolve("BUCK"); + if (!Files.exists(buildfile)) { + buildfile = pluginSource.resolve("BUILD"); + } + if (!Files.exists(buildfile)) { + throw new IllegalStateException("Cannot find build file in: " + + pluginSource); + } + byte[] bytes = Files.readAllBytes(buildfile); String buckContent = new String(bytes, UTF_8).replaceAll("\\s+", ""); Matcher matcher = @@ -158,9 +188,19 @@ } private void buildPluginJar() throws IOException, InterruptedException { - Properties properties = loadBuckProperties(); - String buck = - MoreObjects.firstNonNull(properties.getProperty(BUCKLC), BUCKLC); + Path dir = pluginRoot; + String build; + if (bazel) { + dir = GerritLauncher.resolveInSourceRoot("."); + Properties properties = loadBuildProperties( + dir.resolve(".primary_build_tool")); + build = MoreObjects.firstNonNull( + properties.getProperty(BAZELLC), BAZELLC); + } else { + Properties properties = loadBuildProperties( + gen.resolve(Paths.get("tools/buck/buck.properties"))); + build = MoreObjects.firstNonNull(properties.getProperty(BUCKLC), BUCKLC); + } String target; if (standalone) { target = "//:" + pluginName; @@ -169,16 +209,16 @@ } ProcessBuilder processBuilder = - new ProcessBuilder(buck, "build", target).directory(pluginRoot.toFile()) + new ProcessBuilder(build, "build", target).directory(dir.toFile()) .redirectErrorStream(true); - // otherwise plugin jar creation fails: - processBuilder.environment().put("NO_BUCKD", "1"); - Path forceJar = pluginSource.resolve("src/main/java/ForceJarIfMissing.java"); - // if exists after cancelled test: - Files.deleteIfExists(forceJar); - - Files.createFile(forceJar); + if (!bazel) { + // otherwise plugin jar creation fails: + processBuilder.environment().put("NO_BUCKD", "1"); + // if exists after cancelled test: + Files.deleteIfExists(forceJar); + Files.createFile(forceJar); + } testSite = tempSiteDir.getRoot().toPath(); // otherwise process often hangs: @@ -189,15 +229,14 @@ try { processBuilder.start().waitFor(); } finally { - Files.delete(forceJar); + Files.deleteIfExists(forceJar); // otherwise jar not made next time if missing again: processBuilder.start().waitFor(); } } - private Properties loadBuckProperties() throws IOException { + private Properties loadBuildProperties(Path propertiesPath) throws IOException { Properties properties = new Properties(); - Path propertiesPath = gen.resolve(Paths.get("tools/buck/buck.properties")); if (Files.exists(propertiesPath)) { try (InputStream in = Files.newInputStream(propertiesPath)) { properties.load(in);
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..1b61d1b 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; @@ -51,15 +52,7 @@ public static final String SUBJECT = "test commit"; public static final String FILE_NAME = "a.txt"; public static final String FILE_CONTENT = "some content"; - public static final String PATCH = - "From %s Mon Sep 17 00:00:00 2001\n" + - "From: Administrator <admin@example.com>\n" + - "Date: %s\n" + - "Subject: [PATCH] test commit\n" + - "\n" + - "Change-Id: %s\n" + - "---\n" + - "\n" + + public static final String PATCH_FILE_ONLY = "diff --git a/a.txt b/a.txt\n" + "new file mode 100644\n" + "index 0000000..f0eec86\n" + @@ -68,6 +61,15 @@ "@@ -0,0 +1 @@\n" + "+some content\n" + "\\ No newline at end of file\n"; + public static final String PATCH = + "From %s Mon Sep 17 00:00:00 2001\n" + + "From: Administrator <admin@example.com>\n" + + "Date: %s\n" + + "Subject: [PATCH] test commit\n" + + "\n" + + "Change-Id: %s\n" + + "---\n" + + "\n" + PATCH_FILE_ONLY; public interface Factory { PushOneCommit create( @@ -136,6 +138,7 @@ private String changeId; private Tag tag; private boolean force; + private List<String> pushOptions; private final TestRepository<?>.CommitBuilder commitBuilder; @@ -275,8 +278,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 +290,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 +337,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/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java index 7f08b6f..63e0fa7 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,8 +14,8 @@ package com.google.gerrit.acceptance; -import com.google.common.base.Function; -import com.google.common.collect.FluentIterable; +import static java.util.stream.Collectors.toList; + import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.mail.Address; @@ -25,34 +25,22 @@ import java.io.ByteArrayOutputStream; import java.util.Arrays; +import java.util.List; 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; - } - }); + public static List<Account.Id> ids(List<TestAccount> accounts) { + return accounts.stream().map(a -> a.id).collect(toList()); } - public static FluentIterable<Account.Id> ids(TestAccount... accounts) { + public static List<Account.Id> ids(TestAccount... accounts) { return ids(Arrays.asList(accounts)); } - 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; - } - }); + public static List<String> names(List<TestAccount> accounts) { + return accounts.stream().map(a -> a.fullName).collect(toList()); } - public static FluentIterable<String> names(TestAccount... accounts) { + public static List<String> names(TestAccount... accounts) { return names(Arrays.asList(accounts)); }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestPlugin.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestPlugin.java new file mode 100644 index 0000000..4a838ad --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestPlugin.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.acceptance; + +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}) +@Retention(RUNTIME) +public @interface TestPlugin { + String name(); + + String sysModule() default ""; + String httpModule() default ""; + String sshModule() default ""; +}
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK index d5d0b0d..82d268d 100644 --- a/gerrit-acceptance-tests/BUCK +++ b/gerrit-acceptance-tests/BUCK
@@ -18,6 +18,7 @@ '//gerrit-server:testutil', '//gerrit-server/src/main/prolog:common', '//gerrit-sshd:sshd', + '//gerrit-test-util:test_util', '//lib:args4j', '//lib:gson', @@ -29,11 +30,14 @@ '//lib/bouncycastle:bcpg', '//lib/bouncycastle:bcprov', + '//lib/commons:compress', + '//lib/greenmail:greenmail', '//lib/guice:guice', '//lib/guice:guice-assistedinject', '//lib/guice:guice-servlet', '//lib/log:api', '//lib/jgit/org.eclipse.jgit:jgit', + '//lib/mail:mail', '//lib/mina:sshd', ], visibility = [
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD index 2ec7a05..c7e4a11 100644 --- a/gerrit-acceptance-tests/BUILD +++ b/gerrit-acceptance-tests/BUILD
@@ -1,42 +1,43 @@ -load('//tools/bzl:java.bzl', 'java_library2') +load("//tools/bzl:java.bzl", "java_library2") java_library2( - name = 'lib', - srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']), - exported_deps = [ - '//gerrit-acceptance-framework:lib', - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-gpg:testutil', - '//gerrit-launcher:launcher', - '//gerrit-lucene:lucene', - '//gerrit-httpd:httpd', - '//gerrit-pgm:init', - '//gerrit-pgm:pgm', - '//gerrit-pgm:util', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//gerrit-server:testutil', - '//gerrit-server/src/main/prolog:common', - '//gerrit-sshd:sshd', - - '//lib:args4j', - '//lib:gson', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:h2', - '//lib:jsch', - '//lib:servlet-api-3_1-without-neverlink', - - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcprov', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib/log:api', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/mina:sshd', - ], - visibility = ['//visibility:public'], + name = "lib", + testonly = 1, + srcs = glob(["src/test/java/com/google/gerrit/acceptance/*.java"]), + exported_deps = [ + "//gerrit-acceptance-framework:lib", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-gpg:testutil", + "//gerrit-httpd:httpd", + "//gerrit-launcher:launcher", + "//gerrit-lucene:lucene", + "//gerrit-pgm:init", + "//gerrit-pgm:pgm", + "//gerrit-pgm:util", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//gerrit-server:testutil", + "//gerrit-server/src/main/prolog:common", + "//gerrit-sshd:sshd", + "//gerrit-test-util:test_util", + "//lib:args4j", + "//lib:gson", + "//lib:gwtjsonrpc", + "//lib:gwtorm", + "//lib:h2", + "//lib:jsch", + "//lib:servlet-api-3_1-without-neverlink", + "//lib/bouncycastle:bcpg", + "//lib/bouncycastle:bcprov", + "//lib/commons:compress", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + "//lib/mina:sshd", + ], + visibility = ["//visibility:public"], )
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 index 29aadc1..7b5dfa9 100644 --- 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
@@ -26,17 +26,13 @@ gApi.accounts().create("sandboxuser"); } - private void testUserNotPresent() throws Exception { + @Test + public void userNotPresent1() throws Exception { assertThat(gApi.accounts().query("sandboxuser").get()).isEmpty(); } @Test - public void testUserNotPresent1() throws Exception { - testUserNotPresent(); - } - - @Test - public void testUserNotPresent2() throws Exception { - testUserNotPresent(); + public void userNotPresent2() throws Exception { + assertThat(gApi.accounts().query("sandboxuser").get()).isEmpty(); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java index 2f50480..5773da4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -35,10 +35,8 @@ } @Test - @GerritConfigs({ - @GerritConfig(name = "x.y", value = "z"), - @GerritConfig(name = "a.b", value = "c") - }) + @GerritConfig(name = "x.y", value = "z") + @GerritConfig(name = "a.b", value = "c") public void testMultiple() { assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z"); assertThat(serverConfig.getString("a", null, "b")).isEqualTo("c");
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 6ad20ff..fb1e517 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(); @@ -739,13 +769,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( @@ -757,23 +781,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/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD index 9935eeb..3d62cfc 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -1,7 +1,10 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_account', - srcs = glob(['*IT.java']), - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_account", + labels = [ + "api", + "noci", + ], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java index 9236176..bce9861 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/DiffPreferencesIT.java
@@ -78,6 +78,7 @@ // change all default values i.context *= -1; i.tabSize *= -1; + i.fontSize *= -1; i.lineLength *= -1; i.cursorBlinkRate = 500; i.theme = Theme.MIDNIGHT; @@ -121,9 +122,11 @@ DiffPreferencesInfo d = DiffPreferencesInfo.defaults(); int newLineLength = d.lineLength + 10; int newTabSize = d.tabSize * 2; + int newFontSize = d.fontSize - 2; DiffPreferencesInfo update = new DiffPreferencesInfo(); update.lineLength = newLineLength; update.tabSize = newTabSize; + update.fontSize = newFontSize; gApi.config().server().setDefaultDiffPreferences(update); DiffPreferencesInfo o = gApi.accounts() @@ -133,8 +136,9 @@ // assert configured defaults assertThat(o.lineLength).isEqualTo(newLineLength); assertThat(o.tabSize).isEqualTo(newTabSize); + assertThat(o.fontSize).isEqualTo(newFontSize); // assert hard-coded defaults - assertPrefs(o, d, "lineLength", "tabSize"); + assertPrefs(o, d, "lineLength", "tabSize", "fontSize"); } }
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..5bff9c6 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; @@ -74,8 +75,9 @@ GeneralPreferencesInfo o = gApi.accounts() .id(user42.id.toString()) .getPreferences(); - assertPrefs(o, GeneralPreferencesInfo.defaults(), "my"); + assertPrefs(o, GeneralPreferencesInfo.defaults(), "my", "changeTable"); assertThat(o.my).hasSize(7); + assertThat(o.changeTable).isEmpty(); GeneralPreferencesInfo i = GeneralPreferencesInfo.defaults(); @@ -87,6 +89,9 @@ i.dateFormat = DateFormat.US; i.timeFormat = TimeFormat.HHMM_24; i.emailStrategy = EmailStrategy.DISABLED; + i.defaultBaseForMerges = DefaultBase.AUTO_MERGE; + i.expandInlineDiffs ^= true; + i.highlightAssigneeInChangeTable ^= true; i.relativeDateInChangeTable ^= true; i.sizeBarInChangeTable ^= true; i.legacycidInChangeTable ^= true; @@ -96,6 +101,8 @@ i.diffView = DiffView.UNIFIED_DIFF; i.my = new ArrayList<>(); i.my.add(new MenuItem("name", "url")); + i.changeTable = new ArrayList<>(); + i.changeTable.add("Status"); i.urlAliases = new HashMap<>(); i.urlAliases.put("foo", "bar"); @@ -104,6 +111,7 @@ .setPreferences(i); assertPrefs(o, i, "my"); assertThat(o.my).hasSize(1); + assertThat(o.changeTable).hasSize(1); } @Test @@ -122,6 +130,6 @@ assertThat(o.changesPerPage).isEqualTo(newChangesPerPage); // assert hard-coded defaults - assertPrefs(o, d, "my", "changesPerPage"); + assertPrefs(o, d, "my", "changeTable", "changesPerPage"); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD index 2502cad..3c4e219 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUILD
@@ -1,7 +1,10 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_change', - srcs = glob(['*IT.java']), - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_change", + labels = [ + "api", + "noci", + ], )
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..b4575ae 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
@@ -16,6 +16,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.acceptance.GitUtil.assertPushOk; +import static com.google.gerrit.acceptance.GitUtil.pushHead; import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; import static com.google.gerrit.extensions.client.ReviewerState.CC; @@ -28,58 +30,80 @@ import static com.google.gerrit.server.project.Util.category; import static com.google.gerrit.server.project.Util.value; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; import 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.common.collect.Lists; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AcceptanceTestRequestScope; import com.google.gerrit.acceptance.GerritConfig; -import com.google.gerrit.acceptance.GerritConfigs; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestAccount; 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.NotifyInfo; import com.google.gerrit.extensions.api.changes.RebaseInput; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.RevisionApi; import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.ListChangesOption; +import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.client.SubmitType; 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.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.MergeInput; +import com.google.gerrit.extensions.common.MergePatchSetInput; import com.google.gerrit.extensions.common.RevisionInfo; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Branch; 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.BatchUpdate; +import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.group.SystemGroupBackend; 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 com.google.inject.Inject; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; @@ -88,6 +112,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushResult; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -96,7 +121,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -105,6 +132,12 @@ public class ChangeIT extends AbstractDaemonTest { private String systemTimeZone; + @Inject + private BatchUpdate.Factory updateFactory; + + @Inject + private DynamicSet<ChangeMessageModifier> changeMessageModifiers; + @Before public void setTimeForTesting() { systemTimeZone = System.setProperty("user.timezone", "US/Eastern"); @@ -184,6 +217,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(); @@ -300,7 +391,7 @@ ChangeInfo c2 = gApi.changes().id(changeId).get(); assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2); - // ...and the committer should be correct + // ...and the committer and description should be correct ChangeInfo info = gApi.changes() .id(changeId).get(EnumSet.of( ListChangesOption.CURRENT_REVISION, @@ -309,6 +400,9 @@ info.currentRevision).commit.committer; assertThat(committer.name).isEqualTo(admin.fullName); assertThat(committer.email).isEqualTo(admin.email); + String description = info.revisions.get( + info.currentRevision).description; + assertThat(description).isEqualTo("Rebase"); // Rebasing the second change again should fail exception.expect(ResourceConflictException.class); @@ -330,7 +424,7 @@ } @Test - public void delete() throws Exception { + public void deleteDraftChange() throws Exception { PushOneCommit.Result r = createChange("refs/drafts/master"); assertThat(query(r.getChangeId())).hasSize(1); assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT); @@ -341,43 +435,137 @@ } @Test - public void voteOnClosedChange() throws Exception { - PushOneCommit.Result r = createChange(); - merge(r); - exception.expect(ResourceConflictException.class); - exception.expectMessage("change is closed"); - revision(r).review(ReviewInput.reject()); + public void deleteNewChangeAsAdmin() throws Exception { + PushOneCommit.Result changeResult = createChange(); + String changeId = changeResult.getChangeId(); + + gApi.changes() + .id(changeId) + .delete(); + + assertThat(query(changeId)).isEmpty(); } @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); + @TestProjectInput(cloneAs = "user") + public void deleteNewChangeAsNormalUser() throws Exception { + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + String changeId = changeResult.getChangeId(); + Change.Id id = changeResult.getChange().getId(); - PushOneCommit.Result r = createChange(); - RevisionApi revision = gApi.changes() - .id(r.getChangeId()) - .current(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage(String.format( + "Deleting change %s is not permitted", id)); + gApi.changes() + .id(changeId) + .delete(); + } - ReviewInput in = ReviewInput.recommend(); - in.onBehalfOf = user.id.toString(); - revision.review(in); + @Test + @TestProjectInput(cloneAs = "user") + public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception { + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + changeResult.assertOkStatus(); + String changeId = changeResult.getChangeId(); - ChangeInfo c = gApi.changes() - .id(r.getChangeId()) - .get(); + setApiUser(admin); + gApi.changes() + .id(changeId) + .delete(); - 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); + assertThat(query(changeId)).isEmpty(); + } + + @Test + @TestProjectInput(createEmptyCommit = false) + public void deleteNewChangeForBranchWithoutCommits() throws Exception { + PushOneCommit.Result changeResult = createChange(); + String changeId = changeResult.getChangeId(); + + gApi.changes() + .id(changeId) + .delete(); + + assertThat(query(changeId)).isEmpty(); + } + + @Test + @TestProjectInput(cloneAs = "user") + public void deleteAbandonedChangeAsNormalUser() throws Exception { + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + String changeId = changeResult.getChangeId(); + Change.Id id = changeResult.getChange().getId(); + + setApiUser(user); + gApi.changes() + .id(changeId) + .abandon(); + + exception.expect(AuthException.class); + exception.expectMessage(String.format( + "Deleting change %s is not permitted", id)); + gApi.changes() + .id(changeId) + .delete(); + } + + @Test + @TestProjectInput(cloneAs = "user") + public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception { + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + String changeId = changeResult.getChangeId(); + + gApi.changes() + .id(changeId) + .abandon(); + + gApi.changes() + .id(changeId) + .delete(); + + assertThat(query(changeId)).isEmpty(); + } + + @Test + public void deleteMergedChange() throws Exception { + PushOneCommit.Result changeResult = createChange(); + String changeId = changeResult.getChangeId(); + Change.Id id = changeResult.getChange().getId(); + + merge(changeResult); + + exception.expect(MethodNotAllowedException.class); + exception.expectMessage(String.format( + "Deleting merged change %s is not allowed", id)); + gApi.changes() + .id(changeId) + .delete(); + } + + @Test + public void deleteNewChangeWithMergedPatchSet() throws Exception { + PushOneCommit.Result changeResult = createChange(); + String changeId = changeResult.getChangeId(); + Change.Id id = changeResult.getChange().getId(); + + merge(changeResult); + setChangeStatus(id, Change.Status.NEW); + + exception.expect(ResourceConflictException.class); + exception.expectMessage(String.format( + "Cannot delete change %s: patch set 1 is already merged", id)); + gApi.changes() + .id(changeId) + .delete(); } @Test @@ -538,6 +726,191 @@ } @Test + @TestProjectInput(createEmptyCommit = false) + public void changeNoParentToOneParent() throws Exception { + // create initial commit with no parent and push it as change, so that patch + // set 1 has no parent + RevCommit c = + testRepo.commit().message("Initial commit").insertChangeId().create(); + String id = GitUtil.getChangeId(testRepo, c).get(); + testRepo.reset(c); + + PushResult pr = pushHead(testRepo, "refs/for/master", false); + assertPushOk(pr, "refs/for/master"); + + ChangeInfo change = gApi.changes().id(id).get(); + assertThat(change.revisions.get(change.currentRevision).commit.parents) + .isEmpty(); + + // create another initial commit with no parent and push it directly into + // the remote repository + c = testRepo.amend(c.getId()).message("Initial Empty Commit").create(); + testRepo.reset(c); + pr = pushHead(testRepo, "refs/heads/master", false); + assertPushOk(pr, "refs/heads/master"); + + // create a successor commit and push it as second patch set to the change, + // so that patch set 2 has 1 parent + RevCommit c2 = testRepo.commit().message("Initial commit").parent(c) + .insertChangeId(id.substring(1)).create(); + testRepo.reset(c2); + + pr = pushHead(testRepo, "refs/for/master", false); + assertPushOk(pr, "refs/for/master"); + + change = gApi.changes().id(id).get(); + RevisionInfo rev = change.revisions.get(change.currentRevision); + assertThat(rev.commit.parents).hasSize(1); + assertThat(rev.commit.parents.get(0).commit).isEqualTo(c.name()); + + // check that change kind is correctly detected as REWORK + assertThat(rev.kind).isEqualTo(ChangeKind.REWORK); + } + + @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 +950,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 +1008,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(); @@ -656,6 +1065,65 @@ } @Test + public void implicitlyCcOnNonVotingReview() throws Exception { + PushOneCommit.Result r = createChange(); + setApiUser(user); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(new ReviewInput()); + + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + // If we're not reading from NoteDb, then the CCed user will be returned + // in the REVIEWER state. + ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER; + assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId) + .collect(toList())).containsExactly(user.id.get()); + } + + @Test + public void implicitlyAddReviewerOnVotingReview() throws Exception { + PushOneCommit.Result r = createChange(); + setApiUser(user); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.recommend().message("LGTM")); + + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId) + .collect(toList())).containsExactly(user.id.get()); + + // Further test: remove the vote, then comment again. The user should be + // implicitly re-added to the ReviewerSet, as a CC if we're using NoteDb. + setApiUser(admin); + gApi.changes() + .id(r.getChangeId()) + .reviewer(user.getId().toString()) + .remove(); + c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(c.reviewers.values()).isEmpty(); + + setApiUser(user); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(new ReviewInput().message("hi")); + c = gApi.changes() + .id(r.getChangeId()) + .get(); + ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER; + assertThat(c.reviewers.get(state).stream().map(ai -> ai._accountId) + .collect(toList())).containsExactly(user.id.get()); + } + + @Test public void addReviewerToClosedChange() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes() @@ -696,6 +1164,31 @@ } @Test + public void emailNotificationForFileLevelComment() throws Exception { + String changeId = createChange().getChangeId(); + + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = user.email; + gApi.changes() + .id(changeId) + .addReviewer(in); + sender.clear(); + + ReviewInput review = new ReviewInput(); + ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); + comment.path = PushOneCommit.FILE_NAME; + comment.side = Side.REVISION; + comment.message = "comment 1"; + review.comments = new HashMap<>(); + review.comments.put(comment.path, Lists.newArrayList(comment)); + gApi.changes().id(changeId).current().review(review); + + assertThat(sender.getMessages()).hasSize(1); + Message m = sender.getMessages().get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + } + + @Test public void listVotes() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes() @@ -755,11 +1248,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 +1285,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 +1319,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) @@ -855,10 +1379,7 @@ .review(ReviewInput.approve()); setApiUser(user); - gApi.changes() - .id(r.getChangeId()) - .revision(r.getCommit().name()) - .review(ReviewInput.recommend()); + recommend(r.getChangeId()); setApiUser(admin); sender.clear(); @@ -882,18 +1403,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()) @@ -917,10 +1428,7 @@ .review(ReviewInput.approve()); setApiUser(user); - gApi.changes() - .id(r.getChangeId()) - .revision(r.getCommit().name()) - .review(ReviewInput.recommend()); + recommend(r.getChangeId()); setApiUser(admin); sender.clear(); @@ -935,6 +1443,62 @@ } @Test + public void deleteVoteNotifyAccount() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + DeleteVoteInput in = new DeleteVoteInput(); + in.label = "Code-Review"; + in.notify = NotifyHandling.NONE; + + // notify unrelated account as TO + TestAccount user2 = accounts.user2(); + setApiUser(user); + recommend(r.getChangeId()); + setApiUser(admin); + sender.clear(); + in.notifyDetails = new HashMap<>(); + in.notifyDetails.put(RecipientType.TO, + new NotifyInfo(ImmutableList.of(user2.email))); + gApi.changes() + .id(r.getChangeId()) + .reviewer(user.getId().toString()) + .deleteVote(in); + assertNotifyTo(user2); + + // notify unrelated account as CC + setApiUser(user); + recommend(r.getChangeId()); + setApiUser(admin); + sender.clear(); + in.notifyDetails = new HashMap<>(); + in.notifyDetails.put(RecipientType.CC, + new NotifyInfo(ImmutableList.of(user2.email))); + gApi.changes() + .id(r.getChangeId()) + .reviewer(user.getId().toString()) + .deleteVote(in); + assertNotifyCc(user2); + + // notify unrelated account as BCC + setApiUser(user); + recommend(r.getChangeId()); + setApiUser(admin); + sender.clear(); + in.notifyDetails = new HashMap<>(); + in.notifyDetails.put(RecipientType.BCC, + new NotifyInfo(ImmutableList.of(user2.email))); + gApi.changes() + .id(r.getChangeId()) + .reviewer(user.getId().toString()) + .deleteVote(in); + assertNotifyBcc(user2); + } + + @Test public void deleteVoteNotPermitted() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes() @@ -1205,6 +1769,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(); @@ -1283,6 +1872,41 @@ } @Test + public void customCommitFooters() throws Exception { + PushOneCommit.Result change = createChange(); + RegistrationHandle handle = + changeMessageModifiers.add(new ChangeMessageModifier() { + @Override + public String onSubmit(String newCommitMessage, RevCommit original, + RevCommit mergeTip, Branch.NameKey destination) { + assertThat(original.getName()).isNotEqualTo(mergeTip.getName()); + return newCommitMessage + "Custom: " + destination.get(); + } + }); + ChangeInfo actual; + try { + EnumSet<ListChangesOption> options = EnumSet.of( + ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS); + actual = gApi.changes().id(change.getChangeId()).get(options); + } finally { + handle.remove(); + } + List<String> footers = new ArrayList<>(Arrays.asList( + actual.revisions.get(change.getCommit().getName()).commitWithFooters + .split("\\n"))); + // remove subject + blank line + footers.remove(0); + footers.remove(0); + + List<String> expectedFooters = + Arrays.asList( + "Change-Id: " + change.getChangeId(), "Reviewed-on: " + + canonicalWebUrl.get() + change.getChange().getId(), + "Custom: refs/heads/master"); + assertThat(footers).containsExactlyElementsIn(expectedFooters); + } + + @Test public void defaultSearchDoesNotTouchDatabase() throws Exception { setApiUser(admin); PushOneCommit.Result r1 = createChange(); @@ -1341,10 +1965,8 @@ } @Test - @GerritConfigs({ - @GerritConfig(name = "gerrit.editGpgKeys", value = "true"), - @GerritConfig(name = "receive.enableSignedPush", value = "true"), - }) + @GerritConfig(name = "gerrit.editGpgKeys", value = "true") + @GerritConfig(name = "receive.enableSignedPush", value = "true") public void pushCertificates() throws Exception { PushOneCommit.Result r1 = createChange(); PushOneCommit.Result r2 = amendChange(r1.getChangeId()); @@ -1671,14 +2293,384 @@ + r1.getChange().getId().id + "."); } + @Test + public void createMergePatchSet() throws Exception { + PushOneCommit.Result start = pushTo("refs/heads/master"); + start.assertOkStatus(); + // create a change for master + PushOneCommit.Result r = createChange(); + r.assertOkStatus(); + String changeId = r.getChangeId(); + + testRepo.reset(start.getCommit()); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + String parent = currentMaster.getCommit().getName(); + + // push a commit into dev branch + createBranch(new Branch.NameKey(project, "dev")); + PushOneCommit.Result changeA = pushFactory + .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content") + .to("refs/heads/dev"); + changeA.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "update change by merge ps2"; + gApi.changes().id(changeId).createMergePatchSet(in); + ChangeInfo changeInfo = gApi.changes().id(changeId) + .get(EnumSet.of(ListChangesOption.ALL_REVISIONS, + ListChangesOption.CURRENT_COMMIT, + ListChangesOption.CURRENT_REVISION)); + assertThat(changeInfo.revisions.size()).isEqualTo(2); + assertThat(changeInfo.subject).isEqualTo(in.subject); + assertThat( + changeInfo.revisions.get(changeInfo.currentRevision).commit.parents + .get(0).commit).isEqualTo(parent); + } + + @Test + public void createMergePatchSetInheritParent() throws Exception { + PushOneCommit.Result start = pushTo("refs/heads/master"); + start.assertOkStatus(); + // create a change for master + PushOneCommit.Result r = createChange(); + r.assertOkStatus(); + String changeId = r.getChangeId(); + String parent = r.getCommit().getParent(0).getName(); + + // advance master branch + testRepo.reset(start.getCommit()); + PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); + currentMaster.assertOkStatus(); + + // push a commit into dev branch + createBranch(new Branch.NameKey(project, "dev")); + PushOneCommit.Result changeA = pushFactory + .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content") + .to("refs/heads/dev"); + changeA.assertOkStatus(); + MergeInput mergeInput = new MergeInput(); + mergeInput.source = "dev"; + MergePatchSetInput in = new MergePatchSetInput(); + in.merge = mergeInput; + in.subject = "update change by merge ps2 inherit parent of ps1"; + in.inheritParent = true; + gApi.changes().id(changeId).createMergePatchSet(in); + ChangeInfo changeInfo = gApi.changes().id(changeId) + .get(EnumSet.of(ListChangesOption.ALL_REVISIONS, + ListChangesOption.CURRENT_COMMIT, + ListChangesOption.CURRENT_REVISION)); + + assertThat(changeInfo.revisions.size()).isEqualTo(2); + assertThat(changeInfo.subject).isEqualTo(in.subject); + assertThat( + changeInfo.revisions.get(changeInfo.currentRevision).commit.parents + .get(0).commit).isEqualTo(parent); + assertThat( + changeInfo.revisions.get(changeInfo.currentRevision).commit.parents + .get(0).commit).isNotEqualTo(currentMaster.getCommit().getName()); + } + + @Test + public void checkLabelsForOpenChange() throws Exception { + PushOneCommit.Result r = createChange(); + ChangeInfo change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.status).isEqualTo(ChangeStatus.NEW); + assertThat(change.labels.keySet()).containsExactly("Code-Review"); + assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); + + // add new label and assert that it's returned for existing changes + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + LabelType verified = Util.verified(); + cfg.getLabelSections().put(verified.getName(), verified); + AccountGroup.UUID registeredUsers = + SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + String heads = RefNames.REFS_HEADS + "*"; + Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, + registeredUsers, heads); + saveProjectConfig(project, cfg); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.labels.keySet()) + .containsExactly("Code-Review", "Verified"); + assertThat(change.permittedLabels.keySet()) + .containsExactly("Code-Review", "Verified"); + assertPermitted(change, "Code-Review", -2, -1, 0, 1, 2); + assertPermitted(change, "Verified", -1, 0, 1); + + // add an approval on the new label + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(new ReviewInput().label( + verified.getName(), verified.getMax().getValue())); + + // remove label and assert that it's no longer returned for existing + // changes, even if there is an approval for it + cfg.getLabelSections().remove(verified.getName()); + Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, + heads); + saveProjectConfig(project, cfg); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.labels.keySet()).containsExactly("Code-Review"); + assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); + } + + @Test + public void checkLabelsForMergedChange() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .submit(); + + ChangeInfo change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.labels.keySet()).containsExactly("Code-Review"); + assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); + assertPermitted(change, "Code-Review", 2); + + // add new label and assert that it's returned for existing changes + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + LabelType verified = Util.verified(); + cfg.getLabelSections().put(verified.getName(), verified); + AccountGroup.UUID registeredUsers = + SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + String heads = RefNames.REFS_HEADS + "*"; + Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, + registeredUsers, heads); + saveProjectConfig(project, cfg); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.labels.keySet()) + .containsExactly("Code-Review", "Verified"); + assertThat(change.permittedLabels.keySet()) + .containsExactly("Code-Review", "Verified"); + assertPermitted(change, "Code-Review", 2); + assertPermitted(change, "Verified", 0, 1); + + // ignore the new label by Prolog submit rule and assert that the label is + // no longer returned + GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); + testRepo.reset("config"); + PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo, + "Ignore Verified", + "rules.pl", + "submit_rule(submit(CR)) :-\n" + + " gerrit:max_with_block(-2, 2, 'Code-Review', CR)."); + push2.to(RefNames.REFS_CONFIG); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertPermitted(change, "Code-Review", 2); + assertPermitted(change, "Verified"); + + // add an approval on the new label and assert that the label is now + // returned although it is ignored by the Prolog submit rule and hence not + // included in the submit records + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(new ReviewInput().label( + verified.getName(), verified.getMax().getValue())); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.labels.keySet()) + .containsExactly("Code-Review", "Verified"); + assertPermitted(change, "Code-Review", 2); + assertPermitted(change, "Verified"); + + // remove label and assert that it's no longer returned for existing + // changes, even if there is an approval for it + cfg = projectCache.checkedGet(project).getConfig(); + cfg.getLabelSections().remove(verified.getName()); + Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, + heads); + saveProjectConfig(project, cfg); + + change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.labels.keySet()).containsExactly("Code-Review"); + assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); + assertPermitted(change, "Code-Review", 2); + } + + @Test + public void checkLabelsForMergedChangeWithNonAuthorCodeReview() + throws Exception { + // Configure Non-Author-Code-Review + RevCommit oldHead = getRemoteHead(); + GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); + testRepo.reset("config"); + PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo, + "Configure Non-Author-Code-Review", + "rules.pl", + "submit_rule(S) :-\n" + + " gerrit:default_submit(X),\n" + + " X =.. [submit | Ls],\n" + + " add_non_author_approval(Ls, R),\n" + + " S =.. [submit | R].\n" + + "\n" + + "add_non_author_approval(S1, S2) :-\n" + + " gerrit:commit_author(A),\n" + + " gerrit:commit_label(label('Code-Review', 2), R),\n" + + " R \\= A, !,\n" + + " S2 = [label('Non-Author-Code-Review', ok(R)) | S1].\n" + + "add_non_author_approval(S1," + + " [label('Non-Author-Code-Review', need(_)) | S1])."); + push2.to(RefNames.REFS_CONFIG); + testRepo.reset(oldHead); + + // Allow user to approve + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + AccountGroup.UUID registeredUsers = + SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + String heads = RefNames.REFS_HEADS + "*"; + Util.allow(cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, + registeredUsers, heads); + saveProjectConfig(project, cfg); + + PushOneCommit.Result r = createChange(); + + setApiUser(user); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + setApiUser(admin); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .submit(); + + ChangeInfo change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.labels.keySet()).containsExactly("Code-Review", + "Non-Author-Code-Review"); + assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); + assertPermitted(change, "Code-Review", 0, 1, 2); + } + + @Test + public void checkLabelsForAutoClosedChange() throws Exception { + PushOneCommit.Result r = createChange(); + + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); + PushOneCommit.Result result = push.to("refs/heads/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.status).isEqualTo(ChangeStatus.MERGED); + assertThat(change.labels.keySet()).containsExactly("Code-Review"); + assertPermitted(change, "Code-Review", 0, 1, 2); + } + + @Test + public void checkLabelsForAbandonedChange() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes() + .id(r.getChangeId()) + .abandon(); + + ChangeInfo change = gApi.changes() + .id(r.getChangeId()) + .get(); + assertThat(change.status).isEqualTo(ChangeStatus.ABANDONED); + assertThat(change.labels).isEmpty(); + assertThat(change.permittedLabels).isEmpty(); + } + + @Test + public void maxPermittedValueAllowed() throws Exception { + final int minPermittedValue = -2; + final int maxPermittedValue = +2; + String heads = "refs/heads/*"; + + PushOneCommit.Result r = createChange(); + String triplet = project.get() + "~master~" + r.getChangeId(); + + gApi.changes().id(triplet).addReviewer(user.username); + + ChangeInfo c = gApi.changes() + .id(triplet) + .get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + 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.permittedVotingRange).isNotNull(); + // default values + assertThat(approval.permittedVotingRange.min).isEqualTo(-1); + assertThat(approval.permittedVotingRange.max).isEqualTo(1); + + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + Util.allow(cfg, + Permission.forLabel("Code-Review"), minPermittedValue, maxPermittedValue, + REGISTERED_USERS, heads); + saveProjectConfig(project, cfg); + + c = gApi.changes() + .id(triplet) + .get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + codeReview = c.labels.get("Code-Review"); + assertThat(codeReview.all).hasSize(1); + approval = codeReview.all.get(0); + assertThat(approval._accountId).isEqualTo(user.id.get()); + assertThat(approval.permittedVotingRange).isNotNull(); + assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue); + assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue); + } + + @Test + public void maxPermittedValueBlocked() throws Exception { + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*"); + saveProjectConfig(project, cfg); + + PushOneCommit.Result r = createChange(); + String triplet = project.get() + "~master~" + r.getChangeId(); + + gApi.changes().id(triplet).addReviewer(user.username); + + ChangeInfo c = gApi.changes() + .id(triplet) + .get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + 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.permittedVotingRange).isNull(); + } + 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) @@ -1688,4 +2680,42 @@ assertThat(ctls).hasSize(1); return changeResourceFactory.create(ctls.get(0)); } + + private void setChangeStatus(Change.Id id, Change.Status newStatus) + throws Exception { + try (BatchUpdate batchUpdate = updateFactory + .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus)); + batchUpdate.execute(); + } + + ChangeStatus changeStatus = gApi.changes() + .id(id.get()) + .get() + .status; + assertThat(changeStatus).isEqualTo(newStatus.asChangeStatus()); + } + + private class ChangeStatusUpdateOp extends BatchUpdate.Op { + private final Change.Status newStatus; + + ChangeStatusUpdateOp(Change.Status newStatus) { + this.newStatus = newStatus; + } + + @Override + public boolean updateChange(BatchUpdate.ChangeContext ctx) + throws Exception { + Change change = ctx.getChange(); + + // Change status in database. + change.setStatus(newStatus); + + // Change status in NoteDb. + PatchSet.Id currentPatchSetId = change.currentPatchSetId(); + ctx.getUpdate(currentPatchSetId).setStatus(newStatus); + + return true; + } + } }
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..dd2fd3a 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
@@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.extensions.client.ChangeKind.MERGE_FIRST_PARENT_UPDATE; +import static com.google.gerrit.extensions.client.ChangeKind.NO_CHANGE; import static com.google.gerrit.extensions.client.ChangeKind.NO_CODE_CHANGE; import static com.google.gerrit.extensions.client.ChangeKind.REWORK; import static com.google.gerrit.extensions.client.ChangeKind.TRIVIAL_REBASE; @@ -72,10 +73,12 @@ value(0, "No score"), value(-1, "I would prefer that you didn't submit this"), value(-2, "Do not submit")); + codeReview.setCopyAllScoresIfNoChange(false); cfg.getLabelSections().put(codeReview.getName(), codeReview); LabelType verified = category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); + verified.setCopyAllScoresIfNoChange(false); cfg.getLabelSections().put(verified.getName(), verified); AccountGroup.UUID registeredUsers = @@ -91,7 +94,7 @@ @Test public void notSticky() throws Exception { assertNotSticky(EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, - MERGE_FIRST_PARENT_UPDATE)); + MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)); } @Test @@ -101,7 +104,7 @@ saveProjectConfig(project, cfg); for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE, - NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) { + NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) { testRepo.reset(getRemoteHead()); String changeId = createChange(changeKind); @@ -122,7 +125,7 @@ saveProjectConfig(project, cfg); for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE, - NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) { + NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) { testRepo.reset(getRemoteHead()); String changeId = createChange(changeKind); @@ -147,8 +150,13 @@ vote(admin, changeId, 2, 1); vote(user, changeId, -2, -1); - updateChange(changeId, TRIVIAL_REBASE); + updateChange(changeId, NO_CHANGE); ChangeInfo c = detailedChange(changeId); + assertVotes(c, admin, 2, 0, NO_CHANGE); + assertVotes(c, user, -2, 0, NO_CHANGE); + + updateChange(changeId, TRIVIAL_REBASE); + c = detailedChange(changeId); assertVotes(c, admin, 2, 0, TRIVIAL_REBASE); assertVotes(c, user, -2, 0, TRIVIAL_REBASE); @@ -189,8 +197,13 @@ vote(admin, changeId, 2, 1); vote(user, changeId, -2, -1); - updateChange(changeId, NO_CODE_CHANGE); + updateChange(changeId, NO_CHANGE); ChangeInfo c = detailedChange(changeId); + assertVotes(c, admin, 0, 1, NO_CHANGE); + assertVotes(c, user, 0, -1, NO_CHANGE); + + updateChange(changeId, NO_CODE_CHANGE); + c = detailedChange(changeId); assertVotes(c, admin, 0, 1, NO_CODE_CHANGE); assertVotes(c, user, 0, -1, NO_CODE_CHANGE); @@ -209,8 +222,13 @@ vote(admin, changeId, 2, 1); vote(user, changeId, -2, -1); - updateChange(changeId, MERGE_FIRST_PARENT_UPDATE); + updateChange(changeId, NO_CHANGE); ChangeInfo c = detailedChange(changeId); + assertVotes(c, admin, 2, 0, NO_CHANGE); + assertVotes(c, user, -2, 0, NO_CHANGE); + + updateChange(changeId, MERGE_FIRST_PARENT_UPDATE); + c = detailedChange(changeId); assertVotes(c, admin, 2, 0, MERGE_FIRST_PARENT_UPDATE); assertVotes(c, user, -2, 0, MERGE_FIRST_PARENT_UPDATE); @@ -226,7 +244,7 @@ saveProjectConfig(project, cfg); for (ChangeKind changeKind : EnumSet.of(REWORK, TRIVIAL_REBASE, - NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE)) { + NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) { testRepo.reset(getRemoteHead()); String changeId = createChange(changeKind); @@ -311,6 +329,26 @@ 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, @@ -363,6 +401,8 @@ updateFirstParent(changeId); return; case NO_CHANGE: + noChange(changeId); + return; default: fail("unexpected change kind: " + changeKind); } @@ -379,6 +419,21 @@ assertThat(getChangeKind(changeId)).isEqualTo(NO_CODE_CHANGE); } + private void noChange(String changeId) throws Exception { + ChangeInfo change = gApi.changes().id(changeId).get(); + String commitMessage = + change.revisions.get(change.currentRevision).commit.message; + + TestRepository<?>.CommitBuilder commitBuilder = + testRepo.amendRef("HEAD").insertChangeId(changeId.substring(1)); + commitBuilder.message(commitMessage) + .author(admin.getIdent()) + .committer(new PersonIdent(admin.getIdent(), testRepo.getDate())); + commitBuilder.create(); + GitUtil.pushHead(testRepo, "refs/for/master", false); + assertThat(getChangeKind(changeId)).isEqualTo(NO_CHANGE); + } + private void rework(String changeId) throws Exception { PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, @@ -495,6 +550,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 +568,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/change/SubmitTypeRuleIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java index 1033164..f132e0d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY; import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS; import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY; +import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS; import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY; import static org.junit.Assert.fail; @@ -123,6 +124,10 @@ + "gerrit:commit_message(M)," + "regex_matches('.*REBASE_IF_NECESSARY.*', M)," + "!.\n" + + "submit_type(rebase_always) :-" + + "gerrit:commit_message(M)," + + "regex_matches('.*REBASE_ALWAYS.*', M)," + + "!.\n" + "submit_type(merge_always) :-" + "gerrit:commit_message(M)," + "regex_matches('.*MERGE_ALWAYS.*', M)," @@ -157,8 +162,9 @@ PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2"); PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3"); PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4"); - PushOneCommit.Result r5 = createChange("master", "MERGE_ALWAYS 5"); - PushOneCommit.Result r6 = createChange("master", "CHERRY_PICK 6"); + PushOneCommit.Result r5 = createChange("master", "REBASE_ALWAYS 5"); + PushOneCommit.Result r6 = createChange("master", "MERGE_ALWAYS 6"); + PushOneCommit.Result r7 = createChange("master", "CHERRY_PICK 7"); assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId()); @@ -166,6 +172,7 @@ assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId()); + assertSubmitType(MERGE_IF_NECESSARY, r7.getChangeId()); setRulesPl(SUBMIT_TYPE_FROM_SUBJECT); @@ -173,8 +180,9 @@ assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId()); assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId()); assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId()); - assertSubmitType(MERGE_ALWAYS, r5.getChangeId()); - assertSubmitType(CHERRY_PICK, r6.getChangeId()); + assertSubmitType(REBASE_ALWAYS, r5.getChangeId()); + assertSubmitType(MERGE_ALWAYS, r6.getChangeId()); + assertSubmitType(CHERRY_PICK, r7.getChangeId()); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD index da8274d..6d39131 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_config', - srcs = glob(['*IT.java']), - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_config", + labels = ["api"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD index 1a374f0..1b907765 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUILD
@@ -1,23 +1,23 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_group', - srcs = glob(['*IT.java']), - deps = [ - ':util', - '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util', - ], - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_group", + labels = ["api"], + deps = [ + ":util", + "//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util", + ], ) java_library( - name = 'util', - srcs = ['GroupAssert.java'], - deps = [ - '//gerrit-extension-api:api', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:gwtorm', - '//lib:truth', - ], + name = "util", + srcs = ["GroupAssert.java"], + deps = [ + "//gerrit-extension-api:api", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:gwtorm", + "//lib:truth", + ], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java index c3c2224..6c301da 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -31,7 +31,7 @@ .that(actual.remove(g)).isTrue(); } assert_().withFailureMessage("unexpected groups: " + actual) - .that((Iterable<?>)actual).isEmpty(); + .that(actual).isEmpty(); } public static void assertGroupInfo(AccountGroup group, GroupInfo info) {
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..ccef13c 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
@@ -17,15 +17,12 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo; import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos; +import static java.util.stream.Collectors.toList; -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; @@ -133,14 +130,14 @@ } @Test - public void testCreateGroup() throws Exception { + public void createGroup() throws Exception { String newGroupName = name("newGroup"); GroupInfo g = gApi.groups().create(newGroupName).get(); assertGroupInfo(getFromCache(newGroupName), g); } @Test - public void testCreateDuplicateInternalGroupCaseSensitiveName_Conflict() + public void createDuplicateInternalGroupCaseSensitiveName_Conflict() throws Exception { String dupGroupName = name("dupGroup"); gApi.groups().create(dupGroupName); @@ -150,7 +147,7 @@ } @Test - public void testCreateDuplicateInternalGroupCaseInsensitiveName() + public void createDuplicateInternalGroupCaseInsensitiveName() throws Exception { String dupGroupName = name("dupGroupA"); String dupGroupNameLowerCase = name("dupGroupA").toLowerCase(); @@ -161,7 +158,7 @@ } @Test - public void testCreateDuplicateSystemGroupCaseSensitiveName_Conflict() + public void createDuplicateSystemGroupCaseSensitiveName_Conflict() throws Exception { String newGroupName = "Registered Users"; exception.expect(ResourceConflictException.class); @@ -170,7 +167,7 @@ } @Test - public void testCreateDuplicateSystemGroupCaseInsensitiveName_Conflict() + public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception { String newGroupName = "registered users"; exception.expect(ResourceConflictException.class); @@ -179,7 +176,7 @@ } @Test - public void testCreateGroupWithProperties() throws Exception { + public void createGroupWithProperties() throws Exception { GroupInput in = new GroupInput(); in.name = name("newGroup"); in.description = "Test description"; @@ -192,14 +189,14 @@ } @Test - public void testCreateGroupWithoutCapability_Forbidden() throws Exception { + public void createGroupWithoutCapability_Forbidden() throws Exception { setApiUser(user); exception.expect(AuthException.class); gApi.groups().create(name("newGroup")); } @Test - public void testGetGroup() throws Exception { + public void getGroup() throws Exception { AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators")); testGetGroup(adminGroup.getGroupUUID().get(), adminGroup); testGetGroup(adminGroup.getName(), adminGroup); @@ -213,7 +210,7 @@ } @Test - public void testGroupName() throws Exception { + public void groupName() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -232,7 +229,7 @@ } @Test - public void testGroupRename() throws Exception { + public void groupRename() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -247,7 +244,7 @@ } @Test - public void testGroupDescription() throws Exception { + public void groupDescription() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -269,7 +266,7 @@ } @Test - public void testGroupOptions() throws Exception { + public void groupOptions() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -284,7 +281,7 @@ } @Test - public void testGroupOwner() throws Exception { + public void groupOwner() throws Exception { String name = name("group"); GroupInfo info = gApi.groups().create(name).get(); String adminUUID = getFromCache("Administrators").getGroupUUID().get(); @@ -398,22 +395,18 @@ } @Test - 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()); + public void listAllGroups() throws Exception { + List<String> expectedGroups = groupCache.all().stream() + .map(a -> a.getName()) + .sorted() + .collect(toList()); assertThat(expectedGroups.size()).isAtLeast(2); assertThat(gApi.groups().list().getAsMap().keySet()) .containsExactlyElementsIn(expectedGroups).inOrder(); } @Test - public void testOnlyVisibleGroupsReturned() throws Exception { + public void onlyVisibleGroupsReturned() throws Exception { String newGroupName = name("newGroup"); GroupInput in = new GroupInput(); in.name = newGroupName; @@ -434,14 +427,14 @@ } @Test - public void testSuggestGroup() throws Exception { + public void suggestGroup() throws Exception { Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap(); assertThat(groups).containsKey("Administrators"); assertThat(groups).hasSize(1); } @Test - public void testAllGroupInfoFieldsSetCorrectly() throws Exception { + public void allGroupInfoFieldsSetCorrectly() throws Exception { AccountGroup adminGroup = getFromCache("Administrators"); Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap(); @@ -510,7 +503,7 @@ throws Exception { assertMembers( gApi.groups().id(group).members(), - TestAccount.names(expectedMembers).toArray(String.class)); + TestAccount.names(expectedMembers).stream().toArray(String[]::new)); assertAccountInfos( Arrays.asList(expectedMembers), gApi.groups().id(group).members()); @@ -518,14 +511,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 +526,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/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD index 4fb65ff..8be3101 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_project', - srcs = glob(['*IT.java']), - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_project", + labels = ["api"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD index e527b9d..4f15ec0 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'api_revision', - srcs = glob(['*IT.java']), - labels = ['api'], + srcs = glob(["*IT.java"]), + group = "api_revision", + labels = ["api"], )
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..ab4daf2 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
@@ -18,19 +18,24 @@ import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT; import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.PATCH; +import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; -import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; +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.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; 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.acceptance.TestProjectInput; 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,57 +43,57 @@ 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; +import com.google.gerrit.extensions.common.ApprovalInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.FileInfo; +import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.RevisionInfo; -import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ETagView; import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSetApproval; 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.gerrit.server.query.change.ChangeData; 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.eclipse.jgit.transport.RefSpec; 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.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; public class RevisionIT extends AbstractDaemonTest { @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,68 +143,166 @@ .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(); + public void postSubmitApproval() throws Exception { 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); + .review(ReviewInput.recommend()); + + String label = "Code-Review"; + ApprovalInfo approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(1); + assertThat(approval.postSubmit).isNull(); + + // Submit by direct push. + git().push() + .setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")) + .call(); assertThat(gApi.changes().id(changeId).get().status) .isEqualTo(ChangeStatus.MERGED); + + approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(1); + assertThat(approval.postSubmit).isNull(); + assertPermitted( + gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), + "Code-Review", 1, 2); + + // Repeating the current label is allowed. Does not flip the postSubmit bit + // due to deduplication codepath. + gApi.changes() + .id(changeId) + .current() + .review(ReviewInput.recommend()); + approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(1); + assertThat(approval.postSubmit).isNull(); + + // Reducing vote is not allowed. + try { + gApi.changes() + .id(changeId) + .current() + .review(ReviewInput.dislike()); + fail("expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage( + "Cannot reduce vote on labels for closed change: Code-Review"); + } + approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(1); + assertThat(approval.postSubmit).isNull(); + + // Increasing vote is allowed. + gApi.changes() + .id(changeId) + .current() + .review(ReviewInput.approve()); + approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(2); + assertThat(approval.postSubmit).isTrue(); + assertPermitted( + gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)), + "Code-Review", 2); + + // Decreasing to previous post-submit vote is still not allowed. + try { + gApi.changes() + .id(changeId) + .current() + .review(ReviewInput.dislike()); + fail("expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage( + "Cannot reduce vote on labels for closed change: Code-Review"); + } + approval = getApproval(changeId, label); + assertThat(approval.value).isEqualTo(2); + assertThat(approval.postSubmit).isTrue(); } @Test - public void submitOnBehalfOfInvalidUser() throws Exception { - allowSubmitOnBehalfOf(); + public void postSubmitApprovalAfterVoteRemoved() throws Exception { PushOneCommit.Result r = createChange(); String changeId = project.get() + "~master~" + r.getChangeId(); + + setApiUser(admin); + revision(r).review(ReviewInput.approve()); + + setApiUser(user); + revision(r).review(ReviewInput.recommend()); + + setApiUser(admin); 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); + .reviewer(user.username) + .deleteVote("Code-Review"); + Optional<ApprovalInfo> crUser = get(changeId, DETAILED_LABELS) + .labels.get("Code-Review").all.stream() + .filter(a -> a._accountId == user.id.get()).findFirst(); + assertThat(crUser.isPresent()).isTrue(); + assertThat(crUser.get().value).isEqualTo(0); + + revision(r).submit(); + + setApiUser(user); + ReviewInput in = new ReviewInput(); + in.label("Code-Review", 0); + in.message = "Still LGTM"; + revision(r).review(in); } @Test - public void submitOnBehalfOfNotPermitted() throws Exception { + public void postSubmitDeleteApprovalNotAllowed() throws Exception { PushOneCommit.Result r = createChange(); + + revision(r).review(ReviewInput.approve()); + revision(r).submit(); + + ReviewInput in = new ReviewInput(); + in.label("Code-Review", 0); + + exception.expect(ResourceConflictException.class); + exception.expectMessage( + "Cannot reduce vote on labels for closed change: Code-Review"); + revision(r).review(in); + } + + @TestProjectInput(submitType = SubmitType.CHERRY_PICK) + @Test + public void approvalCopiedDuringSubmitIsNotPostSubmit() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); gApi.changes() - .id(project.get() + "~master~" + r.getChangeId()) + .id(id.get()) .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()) + .id(id.get()) .current() - .submit(in); + .submit(); + + ChangeData cd = r.getChange(); + assertThat(cd.patchSets()).hasSize(2); + PatchSetApproval psa = Iterators.getOnlyElement( + cd.currentApprovals().stream() + .filter(a -> !a.isLegacySubmit()).iterator()); + assertThat(psa.getPatchSetId().get()).isEqualTo(2); + assertThat(psa.getLabel()).isEqualTo("Code-Review"); + assertThat(psa.getValue()).isEqualTo(2); + assertThat(psa.isPostSubmit()).isFalse(); + } + + @Test + public void voteOnAbandonedChange() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes().id(r.getChangeId()).abandon(); + exception.expect(ResourceConflictException.class); + exception.expectMessage("change is closed"); + gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject()); } @Test @@ -433,6 +536,111 @@ } @Test + public void cherryPickMergeRelativeToDefaultParent() throws Exception { + String parent1FileName = "a.txt"; + String parent2FileName = "b.txt"; + PushOneCommit.Result mergeChangeResult = + createCherryPickableMerge(parent1FileName, parent2FileName); + + String cherryPickBranchName = "branch_for_cherry_pick"; + createBranch(new Branch.NameKey(project, cherryPickBranchName)); + + CherryPickInput cherryPickInput = new CherryPickInput(); + cherryPickInput.destination = cherryPickBranchName; + cherryPickInput.message = "Cherry-pick a merge commit to another branch"; + + ChangeInfo cherryPickedChangeInfo = gApi.changes() + .id(mergeChangeResult.getChangeId()) + .current() + .cherryPick(cherryPickInput) + .get(); + + Map<String, FileInfo> cherryPickedFilesByName = + cherryPickedChangeInfo.revisions + .get(cherryPickedChangeInfo.currentRevision) + .files; + assertThat(cherryPickedFilesByName).containsKey(parent2FileName); + assertThat(cherryPickedFilesByName).doesNotContainKey(parent1FileName); + } + + @Test + public void cherryPickMergeRelativeToSpecificParent() throws Exception { + String parent1FileName = "a.txt"; + String parent2FileName = "b.txt"; + PushOneCommit.Result mergeChangeResult = + createCherryPickableMerge(parent1FileName, parent2FileName); + + String cherryPickBranchName = "branch_for_cherry_pick"; + createBranch(new Branch.NameKey(project, cherryPickBranchName)); + + CherryPickInput cherryPickInput = new CherryPickInput(); + cherryPickInput.destination = cherryPickBranchName; + cherryPickInput.message = "Cherry-pick a merge commit to another branch"; + cherryPickInput.parent = 2; + + ChangeInfo cherryPickedChangeInfo = gApi.changes() + .id(mergeChangeResult.getChangeId()) + .current() + .cherryPick(cherryPickInput) + .get(); + + Map<String, FileInfo> cherryPickedFilesByName = + cherryPickedChangeInfo.revisions + .get(cherryPickedChangeInfo.currentRevision) + .files; + assertThat(cherryPickedFilesByName).containsKey(parent1FileName); + assertThat(cherryPickedFilesByName).doesNotContainKey(parent2FileName); + } + + @Test + public void cherryPickMergeUsingInvalidParent() throws Exception { + String parent1FileName = "a.txt"; + String parent2FileName = "b.txt"; + PushOneCommit.Result mergeChangeResult = + createCherryPickableMerge(parent1FileName, parent2FileName); + + String cherryPickBranchName = "branch_for_cherry_pick"; + createBranch(new Branch.NameKey(project, cherryPickBranchName)); + + CherryPickInput cherryPickInput = new CherryPickInput(); + cherryPickInput.destination = cherryPickBranchName; + cherryPickInput.message = "Cherry-pick a merge commit to another branch"; + cherryPickInput.parent = 0; + + exception.expect(BadRequestException.class); + exception.expectMessage("Cherry Pick: Parent 0 does not exist. Please" + + " specify a parent in range [1, 2]."); + gApi.changes() + .id(mergeChangeResult.getChangeId()) + .current() + .cherryPick(cherryPickInput); + } + + @Test + public void cherryPickMergeUsingNonExistentParent() throws Exception { + String parent1FileName = "a.txt"; + String parent2FileName = "b.txt"; + PushOneCommit.Result mergeChangeResult = + createCherryPickableMerge(parent1FileName, parent2FileName); + + String cherryPickBranchName = "branch_for_cherry_pick"; + createBranch(new Branch.NameKey(project, cherryPickBranchName)); + + CherryPickInput cherryPickInput = new CherryPickInput(); + cherryPickInput.destination = cherryPickBranchName; + cherryPickInput.message = "Cherry-pick a merge commit to another branch"; + cherryPickInput.parent = 3; + + exception.expect(BadRequestException.class); + exception.expectMessage("Cherry Pick: Parent 3 does not exist. Please" + + " specify a parent in range [1, 2]."); + gApi.changes() + .id(mergeChangeResult.getChangeId()) + .current() + .cherryPick(cherryPickInput); + } + + @Test public void canRebase() throws Exception { PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); PushOneCommit.Result r1 = push.to("refs/for/master"); @@ -511,17 +719,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 +740,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 +748,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 +756,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 @@ -608,17 +825,35 @@ } @Test - public void content() throws Exception { + public void description() throws Exception { PushOneCommit.Result r = createChange(); - BinaryResult bin = gApi.changes() + assertThat(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); + .description()).isEqualTo(""); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .description("test"); + assertThat(gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .description()).isEqualTo("test"); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .description(""); + assertThat(gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .description()).isEqualTo(""); + } + + @Test + public void content() throws Exception { + PushOneCommit.Result r = createChange(); + assertContent(r, FILE_NAME, FILE_CONTENT); + assertContent(r, COMMIT_MSG, r.getCommit().getFullMessage()); } @Test @@ -769,14 +1004,32 @@ } @Test + public void patchWithPath() throws Exception { + PushOneCommit.Result r = createChange(); + ChangeApi changeApi = gApi.changes() + .id(r.getChangeId()); + BinaryResult bin = changeApi + .revision(r.getCommit().name()) + .patch(FILE_NAME); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bin.writeTo(os); + String res = new String(os.toByteArray(), UTF_8); + assertThat(res).isEqualTo(PATCH_FILE_ONLY); + + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("File not found: nonexistent-file."); + changeApi.revision(r.getCommit().name()).patch("nonexistent-file"); + } + + @Test public void actions() throws Exception { PushOneCommit.Result r = createChange(); assertThat(current(r).actions().keySet()) - .containsExactly("cherrypick", "rebase"); + .containsExactly("cherrypick", "description", "rebase"); current(r).review(ReviewInput.approve()); assertThat(current(r).actions().keySet()) - .containsExactly("submit", "cherrypick", "rebase"); + .containsExactly("submit", "cherrypick", "description", "rebase"); current(r).submit(); assertThat(current(r).actions().keySet()) @@ -822,4 +1075,107 @@ 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); + } + + private PushOneCommit.Result createCherryPickableMerge(String parent1FileName, + String parent2FileName) throws Exception { + RevCommit initialCommit = getHead(repo()); + + String branchAName = "branchA"; + createBranch(new Branch.NameKey(project, branchAName)); + String branchBName = "branchB"; + createBranch(new Branch.NameKey(project, branchBName)); + + PushOneCommit.Result changeAResult = pushFactory + .create(db, admin.getIdent(), testRepo, "change a", + parent1FileName, "Content of a") + .to("refs/for/" + branchAName); + + testRepo.reset(initialCommit); + PushOneCommit.Result changeBResult = pushFactory + .create(db, admin.getIdent(), testRepo, "change b", + parent2FileName, "Content of b") + .to("refs/for/" + branchBName); + + PushOneCommit pushableMergeCommit = pushFactory.create(db, admin.getIdent(), + testRepo, "merge", ImmutableMap.of(parent1FileName, "Content of a", + parent2FileName, "Content of b")); + pushableMergeCommit.setParents(ImmutableList.of(changeAResult.getCommit(), + changeBResult.getCommit())); + PushOneCommit.Result mergeChangeResult = + pushableMergeCommit.to("refs/for/" + branchAName); + mergeChangeResult.assertOkStatus(); + return mergeChangeResult; + } + + private ApprovalInfo getApproval(String changeId, String label) + throws Exception { + ChangeInfo info = gApi.changes() + .id(changeId) + .get(EnumSet.of(DETAILED_LABELS)); + LabelInfo li = info.labels.get(label); + assertThat(li).isNotNull(); + int accountId = atrScope.get().getUser().getAccountId().get(); + return li.all.stream() + .filter(a -> a._accountId == accountId) + .findFirst() + .get(); + } }
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..0b82a8b --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -0,0 +1,429 @@ +// 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 static com.google.gerrit.extensions.common.RobotCommentInfoSubject.assertThatList; + +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.client.Comment; +import com.google.gerrit.extensions.common.FixReplacementInfo; +import com.google.gerrit.extensions.common.FixSuggestionInfo; +import com.google.gerrit.extensions.common.RobotCommentInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.RestApiException; + +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.Map; + +public class RobotCommentsIT extends AbstractDaemonTest { + private String changeId; + private FixReplacementInfo fixReplacementInfo; + private FixSuggestionInfo fixSuggestionInfo; + private RobotCommentInput withFixRobotCommentInput; + + @Before + public void setUp() throws Exception { + PushOneCommit.Result changeResult = createChange(); + changeId = changeResult.getChangeId(); + + fixReplacementInfo = createFixReplacementInfo(); + fixSuggestionInfo = createFixSuggestionInfo(fixReplacementInfo); + withFixRobotCommentInput = createRobotCommentInput(fixSuggestionInfo); + } + + @Test + public void retrievingRobotCommentsBeforeAddingAnyDoesNotRaiseAnException() + throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + Map<String, List<RobotCommentInfo>> robotComments = gApi.changes() + .id(changeId) + .current() + .robotComments(); + + assertThat(robotComments).isNotNull(); + assertThat(robotComments).isEmpty(); + } + + @Test + public void addedRobotCommentsCanBeRetrieved() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + RobotCommentInput in = createRobotCommentInput(); + addRobotComment(changeId, in); + + Map<String, List<RobotCommentInfo>> out = gApi.changes() + .id(changeId) + .current() + .robotComments(); + + assertThat(out).hasSize(1); + RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path)); + assertRobotComment(comment, in, false); + } + + @Test + public void robotCommentsCanBeRetrievedAsList() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + RobotCommentInput robotCommentInput = createRobotCommentInput(); + addRobotComment(changeId, robotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = gApi.changes() + .id(changeId) + .current() + .robotCommentsAsList(); + + assertThat(robotCommentInfos).hasSize(1); + RobotCommentInfo robotCommentInfo = + Iterables.getOnlyElement(robotCommentInfos); + assertRobotComment(robotCommentInfo, robotCommentInput); + } + + @Test + public void specificRobotCommentCanBeRetrieved() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + RobotCommentInput robotCommentInput = createRobotCommentInput(); + addRobotComment(changeId, robotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + RobotCommentInfo robotCommentInfo = + Iterables.getOnlyElement(robotCommentInfos); + + RobotCommentInfo specificRobotCommentInfo = gApi.changes() + .id(changeId) + .current() + .robotComment(robotCommentInfo.id) + .get(); + assertRobotComment(specificRobotCommentInfo, robotCommentInput); + } + + @Test + public void robotCommentWithoutOptionalFieldsCanBeAdded() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); + addRobotComment(changeId, in); + + Map<String, List<RobotCommentInfo>> out = gApi.changes() + .id(changeId) + .current() + .robotComments(); + assertThat(out).hasSize(1); + RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path)); + assertRobotComment(comment, in, false); + } + + @Test + public void addedFixSuggestionCanBeRetrieved() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement() + .onlyFixSuggestion().isNotNull(); + } + + @Test + public void fixIdIsGeneratedForFixSuggestion() + throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement() + .onlyFixSuggestion().fixId().isNotEmpty(); + assertThatList(robotCommentInfos).onlyElement() + .onlyFixSuggestion().fixId().isNotEqualTo(fixSuggestionInfo.fixId); + } + + @Test + public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion() + .description().isEqualTo(fixSuggestionInfo.description); + } + + @Test + public void descriptionOfFixSuggestionIsMandatory() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixSuggestionInfo.description = null; + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("A description is required for the " + + "suggested fix of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void addedFixReplacementCanBeRetrieved() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion() + .onlyReplacement().isNotNull(); + } + + @Test + public void fixReplacementsAreMandatory() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixSuggestionInfo.replacements = Collections.emptyList(); + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("At least one replacement is required" + + " for the suggested fix of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void pathOfFixReplacementIsAcceptedAsIs() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion() + .onlyReplacement().path().isEqualTo(fixReplacementInfo.path); + } + + @Test + public void pathOfFixReplacementIsMandatory() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixReplacementInfo.path = null; + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("A file path must be given for the " + + "replacement of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void pathOfFixReplacementMustReferToFileOfComment() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixReplacementInfo.path = "anotherFile.txt"; + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("Replacements may only be specified " + + "for the file %s on which the robot comment was added", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion() + .onlyReplacement().range().isEqualTo(fixReplacementInfo.range); + } + + @Test + public void rangeOfFixReplacementIsMandatory() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixReplacementInfo.range = null; + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("A range must be given for the " + + "replacement of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void rangeOfFixReplacementNeedsToBeValid() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixReplacementInfo.range = createRange(13, 9, 5, 10); + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("Range (13:9 - 5:10) is not " + + "valid for the replacement of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void replacementStringOfFixReplacementIsAcceptedAsIs() + throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + assertThatList(robotCommentInfos).onlyElement() + .onlyFixSuggestion().onlyReplacement() + .replacement().isEqualTo(fixReplacementInfo.replacement); + } + + @Test + public void replacementStringOfFixReplacementIsMandatory() + throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + fixReplacementInfo.replacement = null; + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("A content for replacement must be " + + "indicated for the replacement of the robot comment on %s", + withFixRobotCommentInput.path)); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void robotCommentsNotSupportedWithoutNoteDb() throws Exception { + assume().that(notesMigration.enabled()).isFalse(); + + 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(changeId) + .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( + FixSuggestionInfo... fixSuggestionInfos) { + RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); + in.url = "http://www.happy-robot.com"; + in.properties = new HashMap<>(); + in.properties.put("key1", "value1"); + in.properties.put("key2", "value2"); + in.fixSuggestions = Arrays.asList(fixSuggestionInfos); + return in; + } + + private FixSuggestionInfo createFixSuggestionInfo( + FixReplacementInfo... fixReplacementInfos) { + FixSuggestionInfo newFixSuggestionInfo = new FixSuggestionInfo(); + newFixSuggestionInfo.fixId = "An ID which must be overwritten."; + newFixSuggestionInfo.description = "A description for a suggested fix."; + newFixSuggestionInfo.replacements = Arrays.asList(fixReplacementInfos); + return newFixSuggestionInfo; + } + + private FixReplacementInfo createFixReplacementInfo() { + FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo(); + newFixReplacementInfo.path = FILE_NAME; + newFixReplacementInfo.replacement = "some replacement code"; + newFixReplacementInfo.range = createRange(3, 12, 15, 4); + return newFixReplacementInfo; + } + + private Comment.Range createRange(int startLine, int startCharacter, + int endLine, int endCharacter) { + Comment.Range range = new Comment.Range(); + range.startLine = startLine; + range.startCharacter = startCharacter; + range.endLine = endLine; + range.endCharacter = endCharacter; + return range; + } + + private void addRobotComment(String targetChangeId, + RobotCommentInput robotCommentInput) throws Exception { + ReviewInput reviewInput = new ReviewInput(); + reviewInput.robotComments = Collections.singletonMap(robotCommentInput.path, + Collections.singletonList(robotCommentInput)); + reviewInput.message = "robot comment test"; + gApi.changes() + .id(targetChangeId) + .current() + .review(reviewInput); + } + + private List<RobotCommentInfo> getRobotComments() throws RestApiException { + return gApi.changes() + .id(changeId) + .current() + .robotCommentsAsList(); + } + + 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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK index c3274db..313144a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
@@ -4,7 +4,6 @@ group = 'edit', srcs = ['ChangeEditIT.java'], deps = [ - '//lib/commons:codec', '//lib/joda:joda-time', ], labels = ['edit'],
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD index 3fcf2d8..990bad6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUILD
@@ -1,11 +1,10 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'edit', - srcs = ['ChangeEditIT.java'], - deps = [ - '//lib/commons:codec', - '//lib/joda:joda-time', - ], - labels = ['edit'], + srcs = ["ChangeEditIT.java"], + group = "edit", + labels = ["edit"], + deps = [ + "//lib/joda:joda-time", + ], )
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..fab43c0 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
@@ -20,8 +20,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.gerrit.acceptance.AbstractDaemonTest; @@ -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; @@ -63,7 +66,6 @@ import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; -import org.apache.commons.codec.binary.StringUtils; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; @@ -84,6 +86,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; public class ChangeEditIT extends AbstractDaemonTest { @@ -171,8 +174,10 @@ .isEqualTo(RefUpdate.Result.NEW); assertThat( modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED); - editUtil.publish(editUtil.byChange(change).get()); + RawInputUtil.create(CONTENT_NEW2))) + .isEqualTo(RefUpdate.Result.FORCED); + editUtil.publish(editUtil.byChange(change).get(), NotifyHandling.NONE, + ImmutableListMultimap.of()); Optional<ChangeEdit> edit = editUtil.byChange(change); assertThat(edit.isPresent()).isFalse(); assertChangeMessages(change, @@ -202,6 +207,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 +388,8 @@ edit = editUtil.byChange(change); assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg); - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE, + ImmutableListMultimap.of()); assertThat(editUtil.byChange(change).isPresent()).isFalse(); ChangeInfo info = get(changeId, ListChangesOption.CURRENT_COMMIT, @@ -408,7 +432,8 @@ assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage()); } - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE, + ImmutableListMultimap.of()); assertChangeMessages(change, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", @@ -625,16 +650,15 @@ Optional<ChangeEdit> edit = editUtil.byChange(change); assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2))) .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); RestResponse r = adminRestSession.getJsonAccept(urlEditFile()); r.assertOK(); assertThat(readContentFromJson(r)).isEqualTo( - StringUtils.newStringUtf8(CONTENT_NEW2)); + new String(CONTENT_NEW2, UTF_8)); r = adminRestSession.getJsonAccept(urlEditFile(true)); r.assertOK(); assertThat(readContentFromJson(r)).isEqualTo( - StringUtils.newStringUtf8(CONTENT_OLD)); + new String(CONTENT_OLD, UTF_8)); } @Test @@ -711,7 +735,8 @@ assertThat(modifier.modifyMessage(edit.get(), newMsg)) .isEqualTo(RefUpdate.Result.FORCED); edit = editUtil.byChange(change); - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE, + ImmutableListMultimap.of()); ChangeInfo info = get(changeId); assertThat(info.subject).isEqualTo(newSubj); @@ -721,7 +746,7 @@ } @Test - public void testHasEditPredicate() throws Exception { + public void hasEditPredicate() throws Exception { assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); assertThat(queryEdits()).hasSize(1); @@ -738,7 +763,8 @@ editUtil.delete(editUtil.byChange(change).get()); assertThat(queryEdits()).hasSize(1); - editUtil.publish(editUtil.byChange(change2).get()); + editUtil.publish(editUtil.byChange(change2).get(), NotifyHandling.NONE, + ImmutableListMultimap.of()); 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..b9736fd 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,10 +42,13 @@ 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.client.ProjectWatchInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.LabelInfo; +import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -61,6 +66,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 +74,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 +136,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,7 +204,33 @@ } @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 { + // create a user that watches the project + TestAccount user3 = accounts.create("user3", "user3@example.com", "User3"); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); + ProjectWatchInfo pwi = new ProjectWatchInfo(); + pwi.project = project.get(); + pwi.filter = "*"; + pwi.notifyNewChanges = true; + projectsToWatch.add(pwi); + setApiUser(user3); + gApi.accounts().self().setWatchedProjects(projectsToWatch); + TestAccount user2 = accounts.user2(); String pushSpec = "refs/for/master" + "%reviewer=" + user.email @@ -206,7 +260,45 @@ r.assertOkStatus(); assertThat(sender.getMessages()).hasSize(1); m = sender.getMessages().get(0); - assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress); + assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress, + user3.emailAddress); + + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + + user3.email); + r.assertOkStatus(); + assertNotifyTo(user3); + + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + + user3.email); + r.assertOkStatus(); + assertNotifyCc(user3); + + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + + user3.email); + r.assertOkStatus(); + assertNotifyBcc(user3); + + // request that sender gets notified as TO, CC and BCC, email should be sent + // even if the sender is the only recipient + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-to=" + + admin.email); + assertNotifyTo(admin); + + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-cc=" + + admin.email); + r.assertOkStatus(); + assertNotifyCc(admin); + + sender.clear(); + r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE + ",notify-bcc=" + + admin.email); + r.assertOkStatus(); + assertNotifyBcc(admin); } @Test @@ -312,6 +404,45 @@ assertThat(cm.message).isEqualTo( "Uploaded patch set 1.\nmy test message"); } + Collection<RevisionInfo> revisions = ci.revisions.values(); + assertThat(revisions).hasSize(1); + for (RevisionInfo ri : revisions) { + assertThat(ri.description).isEqualTo("my test message"); + } + } + + @Test + public void pushForMasterWithMessageTwiceWithDifferentMessages() + throws Exception { + ProjectConfig config = projectCache.checkedGet(project).getConfig(); + config.getProject() + .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE); + saveProjectConfig(project, config); + + PushOneCommit push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "a.txt", "content"); + PushOneCommit.Result r = push.to("refs/for/master/%m=my_test_message"); + r.assertOkStatus(); + + push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "b.txt", "anotherContent", r.getChangeId()); + r = push.to("refs/for/master/%m=new_test_message"); + r.assertOkStatus(); + + ChangeInfo ci = get(r.getChangeId()); + Collection<RevisionInfo> revisions = ci.revisions.values(); + assertThat(revisions).hasSize(2); + for (RevisionInfo ri: revisions) { + if (ri.isCurrent) { + assertThat(ri.description).isEqualTo("new test message"); + } else { + assertThat(ri.description).isEqualTo("my test message"); + } + } } @Test @@ -611,6 +742,58 @@ assertTwoChangesWithSameRevision(r); } + @Test + public void pushSameCommitTwice() throws Exception { + ProjectConfig config = projectCache.checkedGet(project).getConfig(); + config.getProject() + .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE); + saveProjectConfig(project, config); + + PushOneCommit push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "a.txt", "content"); + PushOneCommit.Result r = push.to("refs/for/master"); + r.assertOkStatus(); + + push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "b.txt", "anotherContent"); + r = push.to("refs/for/master"); + r.assertOkStatus(); + + assertPushRejected(pushHead(testRepo, "refs/for/master", false), + "refs/for/master", "commit(s) already exists (as current patchset)"); + } + + @Test + public void pushSameCommitTwiceWhenIndexFailed() throws Exception { + ProjectConfig config = projectCache.checkedGet(project).getConfig(); + config.getProject() + .setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE); + saveProjectConfig(project, config); + + PushOneCommit push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "a.txt", "content"); + PushOneCommit.Result r = push.to("refs/for/master"); + r.assertOkStatus(); + + push = + pushFactory + .create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "b.txt", "anotherContent"); + r = push.to("refs/for/master"); + r.assertOkStatus(); + + indexer.delete(r.getChange().getId()); + + assertPushRejected(pushHead(testRepo, "refs/for/master", false), + "refs/for/master", "commit(s) already exists (as current patchset)"); + } + private void assertTwoChangesWithSameRevision(PushOneCommit.Result result) throws Exception { List<ChangeInfo> changes = query(result.getCommit().name()); @@ -783,6 +966,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 28ca2dc..2b2759c 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
@@ -19,6 +19,7 @@ import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.SubscribeSection; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.reviewdb.client.Project; @@ -28,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; @@ -59,14 +61,21 @@ return cfg; } - protected static Config submitByCherryPickConifg() { + protected static Config submitByCherryPickConfig() { Config cfg = new Config(); cfg.setBoolean("change", null, "submitWholeTopic", true); cfg.setEnum("project", null, "submitType", SubmitType.CHERRY_PICK); return cfg; } - protected static Config submitByRebaseConifg() { + protected static Config submitByRebaseAlwaysConfig() { + Config cfg = new Config(); + cfg.setBoolean("change", null, "submitWholeTopic", true); + cfg.setEnum("project", null, "submitType", SubmitType.REBASE_ALWAYS); + return cfg; + } + + protected static Config submitByRebaseIfNecessaryConfig() { Config cfg = new Config(); cfg.setBoolean("change", null, "submitWholeTopic", true); cfg.setEnum("project", null, "submitType", SubmitType.REBASE_IF_NECESSARY); @@ -74,13 +83,27 @@ } protected TestRepository<?> createProjectWithPush(String name, + @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); + } + + 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); @@ -128,8 +151,13 @@ Project.NameKey superName = new Project.NameKey(name(superproject)); try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) { md.setMessage("Added superproject subscription"); + SubscribeSection s; ProjectConfig pc = ProjectConfig.read(md); - SubscribeSection s = new SubscribeSection(superName); + if (pc.getSubscribeSections().containsKey(superName)) { + s = pc.getSubscribeSections().get(superName); + } else { + s = new SubscribeSection(superName); + } String refspec; if (superBranch == null) { refspec = subBranch; @@ -296,8 +324,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/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD index db0d8e9..43ec5bc 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUILD
@@ -1,26 +1,28 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'git', - srcs = glob(['*IT.java']), - deps = [ - ':submodule_util', - ':push_for_review', - ], - labels = ['git'], + srcs = glob(["*IT.java"]), + group = "git", + labels = ["git"], + deps = [ + ":push_for_review", + ":submodule_util", + ], ) java_library( - name = 'push_for_review', - srcs = ['AbstractPushForReview.java'], - deps = [ - '//gerrit-acceptance-tests:lib', - '//lib/joda:joda-time', - ], + name = "push_for_review", + testonly = 1, + srcs = ["AbstractPushForReview.java"], + deps = [ + "//gerrit-acceptance-tests:lib", + "//lib/joda:joda-time", + ], ) java_library( - name = 'submodule_util', - srcs = ['AbstractSubmoduleSubscription.java',], - deps = ['//gerrit-acceptance-tests:lib',] + name = "submodule_util", + testonly = 1, + srcs = ["AbstractSubmoduleSubscription.java"], + deps = ["//gerrit-acceptance-tests:lib"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java index 41f47a2..ac5477c 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -33,14 +33,14 @@ } @Test - public void testPushDraftChange_Blocked() throws Exception { + public void pushDraftChange_Blocked() throws Exception { // create draft by pushing to 'refs/drafts/' PushOneCommit.Result r = pushTo("refs/drafts/master"); r.assertErrorStatus("cannot upload drafts"); } @Test - public void testPushDraftChangeMagic_Blocked() throws Exception { + public void pushDraftChangeMagic_Blocked() throws Exception { // create draft by using 'draft' option PushOneCommit.Result r = pushTo("refs/for/master%draft"); r.assertErrorStatus("cannot upload drafts");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java similarity index 62% rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java index fd2385b..521ccc4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -15,6 +15,7 @@ package com.google.gerrit.acceptance.git; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; @@ -34,18 +35,25 @@ import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.edit.ChangeEditModifier; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook; import com.google.gerrit.server.git.SearchingChangeCacheImpl; import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; +import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.Util; +import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.testutil.DisabledReviewDb; +import com.google.gerrit.testutil.TestChanges; import com.google.inject.Inject; import com.google.inject.Provider; +import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; @@ -53,11 +61,12 @@ import org.junit.Test; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @NoHttpd -public class VisibleRefFilterIT extends AbstractDaemonTest { +public class RefAdvertisementIT extends AbstractDaemonTest { @Inject private ChangeEditModifier editModifier; @@ -74,12 +83,23 @@ @Inject private Provider<CurrentUser> userProvider; + @Inject + private ChangeNoteUtil noteUtil; + + @Inject + @AnonymousCowardName + private String anonymousCowardName; + private AccountGroup.UUID admins; - private Change.Id c1; - private Change.Id c2; + private ChangeData c1; + private ChangeData c2; + private ChangeData c3; + private ChangeData c4; private String r1; private String r2; + private String r3; + private String r4; @Before public void setUp() throws Exception { @@ -111,17 +131,31 @@ .branch("branch") .create(new BranchInput()); + // First 2 changes are merged, which means the tags pointing to them are + // visible. allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*"); PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent(), testRepo) .to("refs/for/master%submit"); mr.assertOkStatus(); - c1 = mr.getChange().getId(); - r1 = changeRefPrefix(c1); + c1 = mr.getChange(); + r1 = changeRefPrefix(c1.getId()); PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo) .to("refs/for/branch%submit"); br.assertOkStatus(); - c2 = br.getChange().getId(); - r2 = changeRefPrefix(c2); + c2 = br.getChange(); + r2 = changeRefPrefix(c2.getId()); + + // Second 2 changes are unmerged. + mr = pushFactory.create(db, admin.getIdent(), testRepo) + .to("refs/for/master"); + mr.assertOkStatus(); + c3 = mr.getChange(); + r3 = changeRefPrefix(c3.getId()); + br = pushFactory.create(db, admin.getIdent(), testRepo) + .to("refs/for/branch"); + br.assertOkStatus(); + c4 = br.getChange(); + r4 = changeRefPrefix(c4.getId()); try (Repository repo = repoManager.openRepository(project)) { // master-tag -> master @@ -139,7 +173,7 @@ } @Test - public void allRefsVisibleNoRefsMetaConfig() throws Exception { + public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG); @@ -147,12 +181,16 @@ saveProjectConfig(project, cfg); setApiUser(user); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", r2 + "1", r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/heads/master", "refs/tags/branch-tag", @@ -160,16 +198,20 @@ } @Test - public void allRefsVisibleWithRefsMetaConfig() throws Exception { + public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/*"); allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", r2 + "1", r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/heads/master", RefNames.REFS_CONFIG, @@ -178,28 +220,32 @@ } @Test - public void subsetOfBranchesVisibleIncludingHead() throws Exception { + public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); setApiUser(user); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", + r3 + "1", + r3 + "meta", "refs/heads/master", "refs/tags/master-tag"); } @Test - public void subsetOfBranchesVisibleNotIncludingHead() throws Exception { + public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception { deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); setApiUser(user); - assertRefs( + assertUploadPackRefs( r2 + "1", r2 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/tags/branch-tag", // master branch is not visible but master-tag is reachable from branch @@ -208,12 +254,12 @@ } @Test - public void subsetOfBranchesVisibleWithEdit() throws Exception { + public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); - Change c = notesFactory.createChecked(db, project, c1).getChange(); - PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1)); + Change c = notesFactory.createChecked(db, project, c1.getId()).getChange(); + PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1)); // Admin's edit is not visible. setApiUser(admin); @@ -223,59 +269,64 @@ setApiUser(user); editModifier.createEdit(c, ps1); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", + r3 + "1", + r3 + "meta", "refs/heads/master", "refs/tags/master-tag", - "refs/users/01/1000001/edit-" + c1.get() + "/1"); + "refs/users/01/1000001/edit-" + c1.getId() + "/1"); } @Test - public void subsetOfRefsVisibleWithAccessDatabase() throws Exception { + public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception { allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); try { deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); - Change c = notesFactory.createChecked(db, project, c1).getChange(); - PatchSet ps1 = getPatchSet(new PatchSet.Id(c1, 1)); + PatchSet ps1 = getPatchSet(new PatchSet.Id(c1.getId(), 1)); setApiUser(admin); - editModifier.createEdit(c, ps1); + editModifier.createEdit(c1.change(), ps1); setApiUser(user); - assertRefs( + assertUploadPackRefs( // Change 1 is visible due to accessDatabase capability, even though // refs/heads/master is not. r1 + "1", r1 + "meta", r2 + "1", r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/tags/branch-tag", // See comment in subsetOfBranchesVisibleNotIncludingHead. "refs/tags/master-tag", // All edits are visible due to accessDatabase capability. - "refs/users/00/1000000/edit-" + c1.get() + "/1"); + "refs/users/00/1000000/edit-" + c1.getId() + "/1"); } finally { removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); } } @Test - public void draftRefs() throws Exception { + public void uploadPackDraftRefs() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/*"); PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo) .to("refs/drafts/master"); br.assertOkStatus(); - Change.Id c3 = br.getChange().getId(); - String r3 = changeRefPrefix(c3); + Change.Id c5 = br.getChange().getId(); + String r5 = changeRefPrefix(c5); - // Only admin can see admin's draft change. + // Only admin can see admin's draft change (5). setApiUser(admin); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", @@ -283,6 +334,10 @@ r2 + "meta", r3 + "1", r3 + "meta", + r4 + "1", + r4 + "meta", + r5 + "1", + r5 + "meta", "refs/heads/branch", "refs/heads/master", RefNames.REFS_CONFIG, @@ -291,12 +346,16 @@ // user can't. setApiUser(user); - assertRefs( + assertUploadPackRefs( "HEAD", r1 + "1", r1 + "meta", r2 + "1", r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/heads/master", "refs/tags/branch-tag", @@ -304,7 +363,7 @@ } @Test - public void noSearchingChangeCacheImpl() throws Exception { + public void uploadPackNoSearchingChangeCacheImpl() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/*"); setApiUser(user); @@ -320,6 +379,10 @@ r1 + "meta", r2 + "1", r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", "refs/heads/branch", "refs/heads/master", "refs/tags/branch-tag", @@ -328,7 +391,7 @@ } @Test - public void sequencesWithAccessDatabase() throws Exception { + public void uploadPackSequencesWithAccessDatabase() throws Exception { assume().that(notesMigration.readChangeSequence()).isTrue(); try (Repository repo = repoManager.openRepository(allProjects)) { setApiUser(user); @@ -348,6 +411,82 @@ } } + @Test + public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception { + ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs(); + assertThat(r.allRefs().keySet()).containsExactly( + // meta refs are excluded even when NoteDb is enabled. + "HEAD", + "refs/heads/branch", + "refs/heads/master", + "refs/meta/config", + "refs/tags/branch-tag", + "refs/tags/master-tag"); + assertThat(r.additionalHaves()).containsExactly(obj(c3, 1), obj(c4, 1)); + } + + @Test + public void receivePackRespectsVisibilityOfOpenChanges() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + setApiUser(user); + + assertThat(getReceivePackRefs().additionalHaves()) + .containsExactly(obj(c3, 1)); + } + + @Test + public void receivePackListsOnlyLatestPatchSet() throws Exception { + testRepo.reset(obj(c3, 1)); + PushOneCommit.Result r = amendChange(c3.change().getKey().get()); + r.assertOkStatus(); + c3 = r.getChange(); + assertThat(getReceivePackRefs().additionalHaves()) + .containsExactly(obj(c3, 2), obj(c4, 1)); + } + + @Test + public void receivePackOmitsMissingObject() throws Exception { + // Use the tactic from ConsistencyCheckerIT to insert a new patch set with a + // missing object. + String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + try (Repository repo = repoManager.openRepository(project)) { + TestRepository<?> tr = new TestRepository<>(repo); + String subject = "Subject for missing commit"; + Change c = new Change(c3.change()); + PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2); + c.setCurrentPatchSet(psId, subject, c.getOriginalSubject()); + + PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId()); + db.patchSets().insert(Collections.singleton(ps)); + db.changes().update(Collections.singleton(c)); + + if (notesMigration.commitChangeWrites()) { + PersonIdent committer = serverIdent.get(); + PersonIdent author = noteUtil.newIdent( + accountCache.get(admin.getId()).getAccount(), + committer.getWhen(), + committer, + anonymousCowardName); + tr.branch(RefNames.changeMetaRef(c3.getId())) + .commit() + .author(author) + .committer(committer) + .message( + "Update patch set " + psId.get() + "\n" + + "\n" + + "Patch-set: " + psId.get() + "\n" + + "Commit: " + rev + "\n" + + "Subject: " + subject + "\n") + .create(); + } + indexer.index(db, c.getProject(), c.getId()); + } + + assertThat(getReceivePackRefs().additionalHaves()) + .containsExactly(obj(c4, 1)); + } + /** * Assert that refs seen by a non-admin user match expected. * @@ -356,7 +495,8 @@ * from the expected list before comparing to the actual results. * @throws Exception */ - private void assertRefs(String... expectedWithMeta) throws Exception { + private void assertUploadPackRefs(String... expectedWithMeta) + throws Exception { try (Repository repo = repoManager.openRepository(project)) { assertRefs( repo, @@ -391,6 +531,15 @@ } } + private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs() + throws Exception { + ReceiveCommitsAdvertiseRefsHook hook = + new ReceiveCommitsAdvertiseRefsHook(queryProvider, project); + try (Repository repo = repoManager.openRepository(project)) { + return hook.advertiseRefs(repo.getAllRefs()); + } + } + private ProjectControl projectControl() throws Exception { return projectControlFactory.controlFor(project, userProvider.get()); } @@ -402,4 +551,12 @@ projectControlFactory.controlFor(project, userProvider.get()), db, true); } + + private static ObjectId obj(ChangeData cd, int psNum) throws Exception { + PatchSet.Id psId = new PatchSet.Id(cd.getId(), psNum); + PatchSet ps = cd.patchSet(psId); + assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps) + .isNotNull(); + return ObjectId.fromString(ps.getRevision().get()); + } }
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..7983d0f 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
@@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.assertPushOk; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static java.util.stream.Collectors.toList; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; @@ -77,6 +78,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); @@ -233,6 +235,34 @@ } @Test + public void mergeOnPushToBranchWithOldPatchset() throws Exception { + grant(Permission.PUSH, project, "refs/heads/master"); + PushOneCommit.Result r = pushTo("refs/for/master"); + r.assertOkStatus(); + RevCommit c1 = r.getCommit(); + PatchSet.Id psId1 = r.getPatchSetId(); + String changeId = r.getChangeId(); + assertThat(psId1.get()).isEqualTo(1); + + r = amendChange(changeId); + ChangeData cd = r.getChange(); + PatchSet.Id psId2 = cd.change().currentPatchSetId(); + assertThat(psId2.getParentKey()).isEqualTo(psId1.getParentKey()); + assertThat(psId2.get()).isEqualTo(2); + + testRepo.reset(c1); + assertPushOk( + pushHead(testRepo, "refs/heads/master", false), "refs/heads/master"); + + cd = changeDataFactory.create(db, project, psId1.getParentKey()); + Change c = cd.change(); + assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED); + assertThat(c.currentPatchSetId()).isEqualTo(psId1); + assertThat(cd.patchSets().stream().map(ps -> ps.getId()).collect(toList())) + .containsExactly(psId1, psId2); + } + + @Test public void mergeMultipleOnPushToBranchWithNewPatchset() throws Exception { grant(Permission.PUSH, project, "refs/heads/master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java index 09e498f..9fe1e71 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSectionParserIT.java
@@ -32,7 +32,7 @@ private static final String THIS_SERVER = "http://localhost/"; @Test - public void testFollowMasterBranch() throws Exception { + public void followMasterBranch() throws Exception { Project.NameKey p = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -54,7 +54,7 @@ } @Test - public void testFollowMatchingBranch() throws Exception { + public void followMatchingBranch() throws Exception { Project.NameKey p = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -89,7 +89,7 @@ } @Test - public void testFollowAnotherBranch() throws Exception { + public void followAnotherBranch() throws Exception { Project.NameKey p = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -112,7 +112,7 @@ } @Test - public void testWithAnotherURI() throws Exception { + public void withAnotherURI() throws Exception { Project.NameKey p = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -135,7 +135,7 @@ } @Test - public void testWithSlashesInProjectName() throws Exception { + public void withSlashesInProjectName() throws Exception { Project.NameKey p = createProject("project/with/slashes/a"); Config cfg = new Config(); cfg.fromText("" @@ -158,7 +158,7 @@ } @Test - public void testWithSlashesInPath() throws Exception { + public void withSlashesInPath() throws Exception { Project.NameKey p = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -181,7 +181,7 @@ } @Test - public void testWithMoreSections() throws Exception { + public void withMoreSections() throws Exception { Project.NameKey p1 = createProject("a"); Project.NameKey p2 = createProject("b"); Config cfg = new Config(); @@ -211,7 +211,7 @@ } @Test - public void testWithSubProjectFound() throws Exception { + public void withSubProjectFound() throws Exception { Project.NameKey p1 = createProject("a/b"); Project.NameKey p2 = createProject("b"); Config cfg = new Config(); @@ -241,7 +241,7 @@ } @Test - public void testWithAnInvalidSection() throws Exception { + public void withAnInvalidSection() throws Exception { Project.NameKey p1 = createProject("a"); Project.NameKey p2 = createProject("b"); Project.NameKey p3 = createProject("d"); @@ -285,7 +285,7 @@ } @Test - public void testWithSectionOfNonexistingProject() throws Exception { + public void withSectionOfNonexistingProject() throws Exception { Config cfg = new Config(); cfg.fromText("\n" + "[submodule \"a\"]\n" @@ -304,7 +304,7 @@ } @Test - public void testWithSectionToOtherServer() throws Exception { + public void withSectionToOtherServer() throws Exception { Project.NameKey p1 = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -323,7 +323,7 @@ } @Test - public void testWithRelativeURI() throws Exception { + public void withRelativeURI() throws Exception { Project.NameKey p1 = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -346,7 +346,7 @@ } @Test - public void testWithDeepRelativeURI() throws Exception { + public void withDeepRelativeURI() throws Exception { Project.NameKey p1 = createProject("a"); Config cfg = new Config(); cfg.fromText("" @@ -369,7 +369,7 @@ } @Test - public void testWithOverlyDeepRelativeURI() throws Exception { + public void withOverlyDeepRelativeURI() throws Exception { Project.NameKey p1 = createProject("nested/a"); Config cfg = new Config(); cfg.fromText(""
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java index 6684e85..3b4aa22 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -55,7 +55,7 @@ } @Test - public void testSubscriptionWithoutSpecificSubscription() throws Exception { + public void subscriptionWithoutSpecificSubscription() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -67,7 +67,7 @@ } @Test - public void testSubscriptionToEmptyRepo() throws Exception { + public void subscriptionToEmptyRepo() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -84,7 +84,7 @@ } @Test - public void testSubscriptionToExistingRepo() throws Exception { + public void subscriptionToExistingRepo() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -101,7 +101,7 @@ } @Test - public void testSubscriptionWildcardACLForSingleBranch() throws Exception { + public void subscriptionWildcardACLForSingleBranch() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); // master is allowed to be subscribed to master branch only: @@ -125,7 +125,7 @@ } @Test - public void testSubscriptionWildcardACLForMissingProject() throws Exception { + public void subscriptionWildcardACLForMissingProject() throws Exception { TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*", "not-existing-super-project", "refs/heads/*"); @@ -133,7 +133,7 @@ } @Test - public void testSubscriptionWildcardACLForMissingBranch() throws Exception { + public void subscriptionWildcardACLForMissingBranch() throws Exception { createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*", @@ -142,7 +142,7 @@ } @Test - public void testSubscriptionWildcardACLForMissingGitmodules() throws Exception { + public void subscriptionWildcardACLForMissingGitmodules() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/*", @@ -152,7 +152,7 @@ } @Test - public void testSubscriptionWildcardACLOneOnOneMapping() throws Exception { + public void subscriptionWildcardACLOneOnOneMapping() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); // any branch is allowed to be subscribed to the same superprojects branch: @@ -189,7 +189,7 @@ } @Test - public void testSubscriptionWildcardACLForManyBranches() throws Exception { + public void subscriptionWildcardACLForManyBranches() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -206,7 +206,7 @@ } @Test - public void testSubscriptionWildcardACLOneToManyBranches() throws Exception { + public void subscriptionWildcardACLOneToManyBranches() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -286,7 +286,7 @@ } @Test - public void testSubmoduleCommitMessage() throws Exception { + public void submoduleCommitMessage() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -314,7 +314,7 @@ } @Test - public void testSubscriptionUnsubscribe() throws Exception { + public void subscriptionUnsubscribe() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -340,7 +340,7 @@ } @Test - public void testSubscriptionUnsubscribeByDeletingGitModules() + public void subscriptionUnsubscribeByDeletingGitModules() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -367,7 +367,7 @@ } @Test - public void testSubscriptionToDifferentBranches() throws Exception { + public void subscriptionToDifferentBranches() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/foo", @@ -383,7 +383,7 @@ } @Test - public void testBranchCircularSubscription() throws Exception { + public void branchCircularSubscription() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -407,7 +407,7 @@ } @Test - public void testProjectCircularSubscription() throws Exception { + public void projectCircularSubscription() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -439,7 +439,7 @@ } @Test - public void testSubscriptionFailOnMissingACL() throws Exception { + public void subscriptionFailOnMissingACL() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); @@ -452,7 +452,7 @@ } @Test - public void testSubscriptionFailOnWrongProjectACL() throws Exception { + public void subscriptionFailOnWrongProjectACL() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -467,7 +467,7 @@ } @Test - public void testSubscriptionFailOnWrongBranchACL() throws Exception { + public void subscriptionFailOnWrongBranchACL() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -482,7 +482,7 @@ } @Test - public void testSubscriptionInheritACL() throws Exception { + public void subscriptionInheritACL() throws Exception { createProjectWithPush("config-repo"); createProjectWithPush("config-repo2", new Project.NameKey(name("config-repo"))); @@ -501,7 +501,7 @@ } @Test - public void testAllowedButNotSubscribed() throws Exception { + public void allowedButNotSubscribed() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -526,7 +526,7 @@ } @Test - public void testSubscriptionDeepRelative() throws Exception { + public void subscriptionDeepRelative() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush( "nested/subscribed-to-project");
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..045ed07 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 { @@ -46,16 +53,21 @@ @ConfigSuite.Config public static Config cherryPick() { - return submitByCherryPickConifg(); + return submitByCherryPickConfig(); } @ConfigSuite.Config - public static Config rebase() { - return submitByRebaseConifg(); + public static Config rebaseAlways() { + return submitByRebaseAlwaysConfig(); + } + + @ConfigSuite.Config + public static Config rebaseIfNecessary() { + return submitByRebaseIfNecessaryConfig(); } @Test - public void testSubscriptionUpdateOfManyChanges() throws Exception { + public void subscriptionUpdateOfManyChanges() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project"); allowMatchingSubmoduleSubscription("subscribed-to-project", "refs/heads/master", @@ -101,22 +113,51 @@ 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) + || (getSubmitType() == SubmitType.REBASE_ALWAYS)) { + // 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 subscriptionUpdateIncludingChangeInSuperproject() + 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") @@ -175,7 +216,7 @@ } @Test - public void testUpdateManySubmodules() throws Exception { + public void updateManySubmodules() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub1 = createProjectWithPush("sub1"); TestRepository<?> sub2 = createProjectWithPush("sub2"); @@ -223,7 +264,57 @@ } @Test - public void testDifferentPaths() throws Exception { + public void doNotUseFastForward() 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 useFastForwardWhenNoSubmodule() 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 sameProjectSameBranchDifferentPaths() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub = createProjectWithPush("sub"); @@ -256,7 +347,51 @@ } @Test - public void testNonSubmoduleInSameTopic() throws Exception { + public void sameProjectDifferentBranchDifferentPaths() throws Exception { + TestRepository<?> superRepo = createProjectWithPush("super-project"); + TestRepository<?> sub = createProjectWithPush("sub"); + + allowMatchingSubmoduleSubscription("sub", "refs/heads/master", + "super-project", "refs/heads/master"); + allowMatchingSubmoduleSubscription("sub", "refs/heads/dev", + "super-project", "refs/heads/master"); + + ObjectId devHead = pushChangeTo(sub, "dev"); + Config config = new Config(); + prepareSubmoduleConfigEntry(config, "sub", "sub-master", "master"); + prepareSubmoduleConfigEntry(config, "sub", "sub-dev", "dev"); + pushSubmoduleConfig(superRepo, "master", config); + + ObjectId subMasterId = + pushChangeTo(sub, "refs/for/master", "some message", "b.txt", + "content b", "same-topic"); + + sub.reset(devHead); + ObjectId subDevId = + pushChangeTo(sub, "refs/for/dev", "some message in dev", "b.txt", + "content b", "same-topic"); + + approve(getChangeId(sub, subMasterId).get()); + approve(getChangeId(sub, subDevId).get()); + + ObjectId superPreviousId = pushChangeTo(superRepo, "master"); + + gApi.changes().id(getChangeId(sub, subMasterId).get()).current().submit(); + + expectToHaveSubmoduleState(superRepo, "master", "sub-master", sub, "master"); + expectToHaveSubmoduleState(superRepo, "master", "sub-dev", sub, "dev"); + + superRepo.git().fetch().setRemote("origin").call() + .getAdvertisedRef("refs/heads/master").getObjectId(); + + assertWithMessage("submodule subscription update " + + "should have made one commit") + .that(superRepo.getRepository().resolve("origin/master^")) + .isEqualTo(superPreviousId); + } + + @Test + public void nonSubmoduleInSameTopic() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub = createProjectWithPush("sub"); TestRepository<?> standAlone = createProjectWithPush("standalone"); @@ -296,7 +431,7 @@ } @Test - public void testRecursiveSubmodules() throws Exception { + public void recursiveSubmodules() throws Exception { TestRepository<?> topRepo = createProjectWithPush("top-project"); TestRepository<?> midRepo = createProjectWithPush("mid-project"); TestRepository<?> bottomRepo = createProjectWithPush("bottom-project"); @@ -310,7 +445,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,12 +458,14 @@ 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 - public void testTriangleSubmodules() throws Exception { + public void triangleSubmodules() throws Exception { TestRepository<?> topRepo = createProjectWithPush("top-project"); TestRepository<?> midRepo = createProjectWithPush("mid-project"); TestRepository<?> bottomRepo = createProjectWithPush("bottom-project"); @@ -346,7 +484,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 +497,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,24 +527,32 @@ 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(); - - assertThat(hasSubmodule(midRepo, "master", "bottom-project")).isFalse(); - assertThat(hasSubmodule(topRepo, "master", "mid-project")).isFalse(); + return changeId; } @Test - public void testProjectCircularSubscriptionWholeTopic() throws Exception { + public void branchCircularSubscription() throws Exception { + String changeId = prepareBranchCircularSubscription(); + gApi.changes().id(changeId).current().submit(); + } + + @Test + public void branchCircularSubscriptionPreview() throws Exception { + String changeId = prepareBranchCircularSubscription(); + gApi.changes().id(changeId).current().submitPreview(); + } + + @Test + public void projectCircularSubscriptionWholeTopic() 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"); allowMatchingSubmoduleSubscription("super-project", "refs/heads/dev", "subscribed-to-project", "refs/heads/dev"); @@ -433,4 +583,94 @@ .isFalse(); assertThat(hasSubmodule(subRepo, "dev", "super-project")).isFalse(); } + + @Test + public void projectNoSubscriptionWholeTopic() 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"); + } + + @Test + public void twoProjectsMultipleBranchesWholeTopic() throws Exception { + TestRepository<?> repoA = createProjectWithPush("project-a"); + TestRepository<?> repoB = createProjectWithPush("project-b"); + // bootstrap the dev branch + pushChangeTo(repoA, "dev"); + + // bootstrap the dev branch + ObjectId b0 = pushChangeTo(repoB, "dev"); + + allowMatchingSubmoduleSubscription("project-b", + "refs/heads/master", "project-a", "refs/heads/master"); + allowMatchingSubmoduleSubscription("project-b", "refs/heads/dev", + "project-a", "refs/heads/dev"); + + createSubmoduleSubscription(repoA, "master", "project-b", "master"); + createSubmoduleSubscription(repoA, "dev", "project-b", "dev"); + + + // 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 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(repoB, bHead).get()); + approve(getChangeId(repoB, bDevHead).get()); + gApi.changes().id(getChangeId(repoB, bHead).get()).current().submit(); + + expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master"); + expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev"); + } }
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..f405e19 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
@@ -1,8 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'pgm', - srcs = glob(['*IT.java']), - source_under_test = ['//gerrit-pgm:pgm'], - labels = ['pgm'], + srcs = glob(["*IT.java"]), + group = "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/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD index 558d0a9..ea59d61 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUILD
@@ -1,23 +1,24 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'rest_account', - srcs = glob(['*IT.java']), - deps = [':util'], - labels = ['rest'] + srcs = glob(["*IT.java"]), + group = "rest_account", + labels = ["rest"], + deps = [":util"], ) java_library( - name = 'util', - srcs = [ - 'AccountAssert.java', - 'CapabilityInfo.java', - ], - deps = [ - '//gerrit-acceptance-tests:lib', - '//gerrit-reviewdb:server', - '//lib:gwtorm', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "util", + testonly = 1, + srcs = [ + "AccountAssert.java", + "CapabilityInfo.java", + ], + visibility = ["//visibility:public"], + deps = [ + "//gerrit-acceptance-tests:lib", + "//gerrit-reviewdb:server", + "//lib:gwtorm", + "//lib:junit", + ], )
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..329bf88 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; @@ -39,14 +38,10 @@ public class CapabilitiesIT extends AbstractDaemonTest { @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); - } - }); + public void capabilitiesUser() throws Exception { + Iterable<String> all = Iterables.filter( + GlobalCapability.getAllNames(), + c -> !ADMINISTRATE_SERVER.equals(c) && !PRIORITY.equals(c)); allowGlobalCapabilities(REGISTERED_USERS, all); try { @@ -77,7 +72,7 @@ } @Test - public void testCapabilitiesAdmin() throws Exception { + public void capabilitiesAdmin() throws Exception { RestResponse r = adminRestSession.get("/accounts/self/capabilities"); r.assertOK();
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 6d7a665..36ee6e4 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,10 +20,12 @@ 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.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -31,6 +33,7 @@ import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.api.projects.ProjectInput; @@ -39,9 +42,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; @@ -56,11 +59,13 @@ 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; @@ -68,13 +73,17 @@ 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; +import java.util.stream.Collectors; @NoHttpd public abstract class AbstractSubmit extends AbstractDaemonTest { @@ -113,9 +122,231 @@ @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) + || (getSubmitType() == SubmitType.REBASE_ALWAYS)) { + // 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) + || (getSubmitType() == SubmitType.REBASE_ALWAYS)) { + 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){ + // CherryPick ignores dependencies, thus only change and destination + // branch refs are modified. + assertThat(actual).hasSize(2); + } else if (getSubmitType() == SubmitType.REBASE_ALWAYS) { + // RebaseAlways takes care of dependencies, therefore Change{2,3,4} and + // destination branch will be modified. + assertThat(actual).hasSize(4); + } 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 @@ -225,13 +456,9 @@ // Check that the repo has the expected commits List<RevCommit> log = getRemoteLog(); - List<String> commitsInRepo = Lists.transform(log, - new Function<RevCommit, String>() { - @Override - public String apply(RevCommit input) { - return input.getShortMessage(); - } - }); + List<String> commitsInRepo = log.stream() + .map(c -> c.getShortMessage()) + .collect(Collectors.toList()); int expectedCommitCount = getSubmitType() == SubmitType.MERGE_ALWAYS ? 5 // initial commit + 3 commits + merge commit : 4; // initial commit + 3 commits @@ -346,18 +573,15 @@ 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) { assertThat(last).startsWith( "Change has been successfully cherry-picked as "); + } else if (getSubmitType() == SubmitType.REBASE_ALWAYS) { + assertThat(last).startsWith("Change has been successfully rebased as"); } else { assertThat(last).isEqualTo( "Change has been successfully merged by Administrator"); @@ -419,8 +643,17 @@ assertMerged(change.changeId); } + protected BinaryResult submitPreview(String changeId) throws Exception { + return gApi.changes().id(changeId).current().submitPreview(); + } + + protected BinaryResult submitPreview(String changeId, String format) + throws Exception { + return gApi.changes().id(changeId).current().submitPreview(format); + } + 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/AbstractSubmitByRebase.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java new file mode 100644 index 0000000..492fc05 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -0,0 +1,353 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.GitUtil.getChangeId; +import static com.google.gerrit.acceptance.GitUtil.pushHead; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.extensions.api.changes.SubmitInput; +import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Branch; +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.server.change.Submit.TestSubmitInput; + +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.junit.Test; + +public abstract class AbstractSubmitByRebase extends AbstractSubmit { + + @Override + protected abstract SubmitType getSubmitType(); + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitWithRebase() 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", "b.txt", "other content"); + submit(change2.getChangeId()); + assertRebase(testRepo, false); + RevCommit headAfterSecondSubmit = getRemoteHead(); + assertThat(headAfterSecondSubmit.getParent(0)) + .isEqualTo(headAfterFirstSubmit); + assertApproved(change2.getChangeId()); + assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit); + assertSubmitter(change2.getChangeId(), 1); + assertSubmitter(change2.getChangeId(), 2); + assertPersonEquals(admin.getIdent(), + headAfterSecondSubmit.getAuthorIdent()); + assertPersonEquals(admin.getIdent(), + headAfterSecondSubmit.getCommitterIdent()); + + assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, + headAfterFirstSubmit, headAfterSecondSubmit); + assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(), + change2.getChangeId(), headAfterSecondSubmit.name()); + } + + @Test + public void submitWithRebaseMultipleChanges() throws Exception { + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result change1 = + createChange("Change 1", "a.txt", "content"); + submit(change1.getChangeId()); + RevCommit headAfterFirstSubmit = getRemoteHead(); + if (getSubmitType() == SubmitType.REBASE_ALWAYS) { + assertCurrentRevision(change1.getChangeId(), 2, headAfterFirstSubmit); + } else { + assertThat(headAfterFirstSubmit.name()) + .isEqualTo(change1.getCommit().name()); + } + + testRepo.reset(initialHead); + PushOneCommit.Result change2 = + createChange("Change 2", "b.txt", "other content"); + assertThat(change2.getCommit().getParent(0)) + .isNotEqualTo(change1.getCommit()); + PushOneCommit.Result change3 = + createChange("Change 3", "c.txt", "third content"); + PushOneCommit.Result change4 = + createChange("Change 4", "d.txt", "fourth content"); + approve(change2.getChangeId()); + approve(change3.getChangeId()); + submit(change4.getChangeId()); + + assertRebase(testRepo, false); + assertApproved(change2.getChangeId()); + assertApproved(change3.getChangeId()); + assertApproved(change4.getChangeId()); + + RevCommit headAfterSecondSubmit = parse(getRemoteHead()); + assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4"); + assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit()); + assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit); + + RevCommit parent = parse(headAfterSecondSubmit.getParent(0)); + assertThat(parent.getShortMessage()).isEqualTo("Change 3"); + assertThat(parent).isNotEqualTo(change3.getCommit()); + assertCurrentRevision(change3.getChangeId(), 2, parent); + + RevCommit grandparent = parse(parent.getParent(0)); + assertThat(grandparent).isNotEqualTo(change2.getCommit()); + assertCurrentRevision(change2.getChangeId(), 2, grandparent); + + RevCommit greatgrandparent = parse(grandparent.getParent(0)); + assertThat(greatgrandparent).isEqualTo(headAfterFirstSubmit); + if (getSubmitType() == SubmitType.REBASE_ALWAYS) { + assertCurrentRevision(change1.getChangeId(), 2, greatgrandparent); + } else { + assertCurrentRevision(change1.getChangeId(), 1, greatgrandparent); + } + + + assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, + headAfterFirstSubmit, headAfterSecondSubmit); + assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(), + change2.getChangeId(), headAfterSecondSubmit.name(), + change3.getChangeId(), headAfterSecondSubmit.name(), + change4.getChangeId(), headAfterSecondSubmit.name()); + } + + @Test + public void submitWithRebaseMergeCommit() throws Exception { + /* + * (HEAD, origin/master, origin/HEAD) Merge changes X,Y + |\ + | * Merge branch 'master' into origin/master + | |\ + | | * SHA Added a + | |/ + * | Before + |/ + * Initial empty repository + */ + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result change1 = createChange("Added a", "a.txt", ""); + + PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo, + "Merge to master", "m.txt", ""); + change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit())); + PushOneCommit.Result change2 = change2Push.to("refs/for/master"); + + testRepo.reset(initialHead); + PushOneCommit.Result change3 = createChange("Before", "b.txt", ""); + + approve(change3.getChangeId()); + submit(change3.getChangeId()); + + approve(change1.getChangeId()); + approve(change2.getChangeId()); + submit(change2.getChangeId()); + + RevCommit newHead = getRemoteHead(); + assertThat(newHead.getParentCount()).isEqualTo(2); + + RevCommit headParent1 = parse(newHead.getParent(0).getId()); + RevCommit headParent2 = parse(newHead.getParent(1).getId()); + + if (getSubmitType() == SubmitType.REBASE_ALWAYS){ + assertCurrentRevision(change3.getChangeId(), 2, headParent1.getId()); + } else { + assertThat(change3.getCommit().getId()).isEqualTo(headParent1.getId()); + } + assertThat(headParent1.getParentCount()).isEqualTo(1); + assertThat(headParent1.getParent(0)).isEqualTo(initialHead); + + assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId()); + assertThat(headParent2.getParentCount()).isEqualTo(2); + + RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId()); + RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId()); + + assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId()); + assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId()); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitWithContentMerge_Conflict() 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"); + submitWithConflict(change2.getChangeId(), + "Cannot rebase " + change2.getCommit().name() + + ": The change could not be rebased due to a conflict during merge."); + RevCommit head = getRemoteHead(); + assertThat(head).isEqualTo(headAfterFirstSubmit); + assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit()); + assertNoSubmitter(change2.getChangeId(), 1); + + assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); + assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name()); + } + + @Test + public void repairChangeStateAfterFailure() 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", "b.txt", "other content"); + Change.Id id2 = change2.getChange().getId(); + SubmitInput failAfterRefUpdates = + new TestSubmitInput(new SubmitInput(), true); + submit(change2.getChangeId(), failAfterRefUpdates, + ResourceConflictException.class, "Failing after ref updates"); + RevCommit headAfterFailedSubmit = getRemoteHead(); + + // Bad: ref advanced but change wasn't updated. + PatchSet.Id psId1 = new PatchSet.Id(id2, 1); + PatchSet.Id psId2 = new PatchSet.Id(id2, 2); + ChangeInfo info = gApi.changes().id(id2.get()).get(); + assertThat(info.status).isEqualTo(ChangeStatus.NEW); + assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1); + assertThat(getPatchSet(psId2)).isNull(); + + ObjectId rev2; + try (Repository repo = repoManager.openRepository(project); + RevWalk rw = new RevWalk(repo)) { + ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId(); + assertThat(rev1).isNotNull(); + + rev2 = repo.exactRef(psId2.toRefName()).getObjectId(); + assertThat(rev2).isNotNull(); + assertThat(rev2).isNotEqualTo(rev1); + assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit); + + assertThat(repo.exactRef("refs/heads/master").getObjectId()) + .isEqualTo(rev2); + } + + submit(change2.getChangeId()); + RevCommit headAfterSecondSubmit = getRemoteHead(); + assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit); + + // Change status and patch set entities were updated, and branch tip stayed + // the same. + info = gApi.changes().id(id2.get()).get(); + assertThat(info.status).isEqualTo(ChangeStatus.MERGED); + assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2); + PatchSet ps2 = getPatchSet(psId2); + assertThat(ps2).isNotNull(); + assertThat(ps2.getRevision().get()).isEqualTo(rev2.name()); + assertThat(Iterables.getLast(info.messages).message) + .isEqualTo("Change has been successfully rebased as " + + rev2.name() + " by Administrator"); + + try (Repository repo = repoManager.openRepository(project)) { + assertThat(repo.exactRef("refs/heads/master").getObjectId()) + .isEqualTo(rev2); + } + + assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); + assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(), + change2.getChangeId(), headAfterSecondSubmit.name()); + } + + protected RevCommit parse(ObjectId id) throws Exception { + try (Repository repo = repoManager.openRepository(project); + RevWalk rw = new RevWalk(repo)) { + RevCommit c = rw.parseCommit(id); + rw.parseBody(c); + return c; + } + } + + @Test + public void submitAfterReorderOfCommits() throws Exception { + RevCommit initialHead = getRemoteHead(); + + // Create two commits and push. + RevCommit c1 = commitBuilder() + .add("a.txt", "1") + .message("subject: 1") + .create(); + RevCommit c2 = commitBuilder() + .add("b.txt", "2") + .message("subject: 2") + .create(); + pushHead(testRepo, "refs/for/master", false); + + String id1 = getChangeId(testRepo, c1).get(); + String id2 = getChangeId(testRepo, c2).get(); + + // Swap the order of commits and push again. + testRepo.reset("HEAD~2"); + testRepo.cherryPick(c2); + testRepo.cherryPick(c1); + pushHead(testRepo, "refs/for/master", false); + + approve(id1); + approve(id2); + submit(id1); + RevCommit headAfterSubmit = getRemoteHead(); + + assertRefUpdatedEvents(initialHead, headAfterSubmit); + assertChangeMergedEvents(id2, headAfterSubmit.name(), + id1, headAfterSubmit.name()); + } + + @Test + public void submitChangesAfterBranchOnSecond() throws Exception { + RevCommit initialHead = getRemoteHead(); + + PushOneCommit.Result change = createChange(); + approve(change.getChangeId()); + + PushOneCommit.Result change2 = createChange(); + approve(change2.getChangeId()); + Project.NameKey project = change2.getChange().change().getProject(); + Branch.NameKey branch = new Branch.NameKey(project, "branch"); + createBranchWithRevision(branch, change2.getCommit().getName()); + gApi.changes().id(change2.getChangeId()).current().submit(); + assertMerged(change2.getChangeId()); + assertMerged(change.getChangeId()); + + RevCommit newHead = getRemoteHead(); + assertRefUpdatedEvents(initialHead, newHead); + assertChangeMergedEvents(change.getChangeId(), newHead.name(), + change2.getChangeId(), newHead.name()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java index 880fe89..2c9159f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -15,24 +15,40 @@ package com.google.gerrit.acceptance.rest.change; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS; +import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.extensions.api.changes.ActionVisitor; import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.ActionInfo; -import com.google.gerrit.server.change.GetRevisionActions; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.RevisionInfo; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.change.ChangeJson; +import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.testutil.ConfigSuite; import com.google.inject.Inject; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; public class ActionsIT extends AbstractDaemonTest { @ConfigSuite.Config @@ -41,15 +57,33 @@ } @Inject - private GetRevisionActions getRevisionActions; + private ChangeJson.Factory changeJsonFactory; + + @Inject + private DynamicSet<ActionVisitor> actionVisitors; + + private RegistrationHandle visitorHandle; + + @Before + public void setUp() { + visitorHandle = null; + } + + @After + public void tearDown() { + if (visitorHandle != null) { + visitorHandle.remove(); + } + } @Test public void revisionActionsOneChangePerTopicUnapproved() throws Exception { String changeId = createChangeWithTopic().getChangeId(); Map<String, ActionInfo> actions = getActions(changeId); + assertThat(actions).hasSize(3); assertThat(actions).containsKey("cherrypick"); assertThat(actions).containsKey("rebase"); - assertThat(actions).hasSize(2); + assertThat(actions).containsKey("description"); } @Test @@ -91,16 +125,16 @@ String parent = createChange().getChangeId(); String change = createChangeWithTopic().getChangeId(); approve(change); - String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag1 = getETag(change); approve(parent); - String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag2 = getETag(change); String changeWithSameTopic = createChangeWithTopic().getChangeId(); - String etag3 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag3 = getETag(change); approve(changeWithSameTopic); - String etag4 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag4 = getETag(change); if (isSubmitWholeTopicEnabled()) { assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates(); @@ -117,14 +151,14 @@ approve(change); setApiUser(user); - String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag1 = getETag(change); setApiUser(admin); String draft = createDraftWithTopic().getChangeId(); approve(draft); setApiUser(user); - String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag2 = getETag(change); if (isSubmitWholeTopicEnabled()) { assertThat(etag2).isNotEqualTo(etag1); @@ -140,25 +174,25 @@ approve(change); setApiUserAnonymous(); - String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag1 = getETag(change); setApiUser(admin); approve(parent); setApiUserAnonymous(); - String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag2 = getETag(change); setApiUser(admin); String changeWithSameTopic = createChangeWithTopic().getChangeId(); setApiUserAnonymous(); - String etag3 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag3 = getETag(change); setApiUser(admin); approve(changeWithSameTopic); setApiUserAnonymous(); - String etag4 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag4 = getETag(change); if (isSubmitWholeTopicEnabled()) { assertThat(ImmutableList.of(etag1, etag2, etag3, etag4)).containsNoDuplicates(); @@ -177,13 +211,13 @@ approve(change); setApiUserAnonymous(); - String etag1 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag1 = getETag(change); setApiUser(admin); approve(parent); setApiUserAnonymous(); - String etag2 = getRevisionActions.getETag(parseCurrentRevisionResource(change)); + String etag2 = getETag(change); assertThat(etag2).isEqualTo(etag1); } @@ -277,10 +311,132 @@ } } + @Test + public void changeActionVisitor() throws Exception { + String id = createChange().getChangeId(); + ChangeInfo origChange = + gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)); + + class Visitor implements ActionVisitor { + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo) { + assertThat(changeInfo).isNotNull(); + assertThat(changeInfo._number).isEqualTo(origChange._number); + if (name.equals("followup")) { + return false; + } + if (name.equals("abandon")) { + actionInfo.label = "Abandon All Hope"; + } + return true; + } + + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo, RevisionInfo revisionInfo) { + throw new UnsupportedOperationException(); + } + } + + Map<String, ActionInfo> origActions = origChange.actions; + assertThat(origActions.keySet()).containsAllOf("followup", "abandon"); + assertThat(origActions.get("abandon").label).isEqualTo("Abandon"); + + Visitor v = new Visitor(); + visitorHandle = actionVisitors.add(v); + + Map<String, ActionInfo> newActions = gApi.changes() + .id(id) + .get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)) + .actions; + + Set<String> expectedNames = new TreeSet<>(origActions.keySet()); + expectedNames.remove("followup"); + assertThat(newActions.keySet()).isEqualTo(expectedNames); + + ActionInfo abandon = newActions.get("abandon"); + assertThat(abandon).isNotNull(); + assertThat(abandon.label).isEqualTo("Abandon All Hope"); + } + + @Test + public void revisionActionVisitor() throws Exception { + String id = createChange().getChangeId(); + ChangeInfo origChange = + gApi.changes().id(id).get(EnumSet.of(ListChangesOption.CHANGE_ACTIONS)); + + class Visitor implements ActionVisitor { + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo) { + return true; // Do nothing; implicitly called for CURRENT_ACTIONS. + } + + @Override + public boolean visit(String name, ActionInfo actionInfo, + ChangeInfo changeInfo, RevisionInfo revisionInfo) { + assertThat(changeInfo).isNotNull(); + assertThat(changeInfo._number).isEqualTo(origChange._number); + assertThat(revisionInfo).isNotNull(); + assertThat(revisionInfo._number).isEqualTo(1); + if (name.equals("cherrypick")) { + return false; + } + if (name.equals("rebase")) { + actionInfo.label = "All Your Base"; + } + return true; + } + } + + Map<String, ActionInfo> origActions = + gApi.changes().id(id).current().actions(); + assertThat(origActions.keySet()).containsAllOf("cherrypick", "rebase"); + assertThat(origActions.get("rebase").label).isEqualTo("Rebase"); + + Visitor v = new Visitor(); + visitorHandle = actionVisitors.add(v); + + // Test different codepaths within ActionJson... + // ...via revision API. + visitedRevisionActionsAssertions( + origActions, gApi.changes().id(id).current().actions()); + + // ...via change API with option. + EnumSet<ListChangesOption> opts = + EnumSet.of(CURRENT_ACTIONS, CURRENT_REVISION); + ChangeInfo changeInfo = gApi.changes().id(id).get(opts); + RevisionInfo revisionInfo = + Iterables.getOnlyElement(changeInfo.revisions.values()); + visitedRevisionActionsAssertions(origActions, revisionInfo.actions); + + // ...via ChangeJson directly. + ChangeData cd = changeDataFactory.create( + db, project, new Change.Id(origChange._number)); + revisionInfo = changeJsonFactory.create(opts) + .getRevisionInfo( + cd.changeControl(), Iterables.getOnlyElement(cd.patchSets())); + visitedRevisionActionsAssertions(origActions, revisionInfo.actions); + } + + private void visitedRevisionActionsAssertions( + Map<String, ActionInfo> origActions, Map<String, ActionInfo> newActions) { + assertThat(newActions).isNotNull(); + Set<String> expectedNames = new TreeSet<>(origActions.keySet()); + expectedNames.remove("cherrypick"); + assertThat(newActions.keySet()).isEqualTo(expectedNames); + + ActionInfo rebase = newActions.get("rebase"); + assertThat(rebase).isNotNull(); + assertThat(rebase.label).isEqualTo("All Your Base"); + } + private void commonActionsAssertions(Map<String, ActionInfo> actions) { - assertThat(actions).hasSize(3); + assertThat(actions).hasSize(4); assertThat(actions).containsKey("cherrypick"); assertThat(actions).containsKey("submit"); + assertThat(actions).containsKey("description"); assertThat(actions).containsKey("rebase"); }
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..af64a32 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -0,0 +1,158 @@ +// 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.FakeEmailSender.Message; +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 getNoAssignee() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(getAssignee(r)).isNull(); + } + + @Test + public void addGetAssignee() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(setAssignee(r, user.email)._accountId) + .isEqualTo(user.getId().get()); + assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get()); + + assertThat(sender.getMessages()).hasSize(1); + Message m = sender.getMessages().get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + } + + @Test + public void setNewAssigneeWhenExists() throws Exception { + PushOneCommit.Result r = createChange(); + setAssignee(r, user.email); + assertThat(setAssignee(r, user.email)._accountId) + .isEqualTo(user.getId().get()); + } + + @Test + public void getPastAssignees() 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 assigneeAddedAsReviewer() 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 setAlreadyExistingAssignee() throws Exception { + PushOneCommit.Result r = createChange(); + setAssignee(r, user.email); + assertThat(setAssignee(r, user.email)._accountId) + .isEqualTo(user.getId().get()); + } + + @Test + public void deleteAssignee() 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 deleteAssigneeWhenNoAssignee() 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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK index 04e71eb..654ce29 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -1,10 +1,6 @@ include_defs('//gerrit-acceptance-tests/tests.defs') -SUBMIT_UTIL_SRCS = [ - 'AbstractSubmit.java', - 'AbstractSubmitByMerge.java', -] - +SUBMIT_UTIL_SRCS = glob(['AbstractSubmit*.java']) SUBMIT_TESTS = glob(['Submit*IT.java']) OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD index c06f02f..b7ed2e8 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUILD
@@ -1,36 +1,38 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") -SUBMIT_UTIL_SRCS = [ - 'AbstractSubmit.java', - 'AbstractSubmitByMerge.java', -] +SUBMIT_UTIL_SRCS = glob(["AbstractSubmit*.java"]) -SUBMIT_TESTS = glob(['Submit*IT.java']) -OTHER_TESTS = glob(['*IT.java'], exclude = SUBMIT_TESTS) +SUBMIT_TESTS = glob(["Submit*IT.java"]) -acceptance_tests( - group = 'rest_change_other', - srcs = OTHER_TESTS, - deps = [ - ':submit_util', - '//lib/joda:joda-time', - ], - labels = ['rest'], +OTHER_TESTS = glob( + ["*IT.java"], + exclude = SUBMIT_TESTS, ) acceptance_tests( - group = 'rest_change_submit', - srcs = SUBMIT_TESTS, - deps = [ - ':submit_util', - ], - labels = ['rest'], + srcs = OTHER_TESTS, + group = "rest_change_other", + labels = ["rest"], + deps = [ + ":submit_util", + "//lib/joda:joda-time", + ], +) + +acceptance_tests( + srcs = SUBMIT_TESTS, + group = "rest_change_submit", + labels = ["rest"], + deps = [ + ":submit_util", + ], ) java_library( - name = 'submit_util', - srcs = SUBMIT_UTIL_SRCS, - deps = [ - '//gerrit-acceptance-tests:lib', - ], + name = "submit_util", + testonly = 1, + srcs = SUBMIT_UTIL_SRCS, + deps = [ + "//gerrit-acceptance-tests:lib", + ], )
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..c9cb06f 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,172 @@ } @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 added to CC list. + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + if (notesMigration.readChanges()) { + assertReviewers(c, REVIEWER); + assertReviewers(c, CC, user); + } else { + // If we aren't reading from NoteDb, the user will appear as a + // reviewer. + assertReviewers(c, REVIEWER, user); + 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(); @@ -288,35 +458,22 @@ // Verify emails were sent to added reviewers. List<Message> messages = sender.getMessages(); - assertThat(messages).hasSize(3); - // First email to user. + assertThat(messages).hasSize(2); + Message m = messages.get(0); - if (notesMigration.readChanges()) { - assertThat(m.rcpt()).containsExactly(user.emailAddress); - } else { - assertThat(m.rcpt()).containsExactly( - user.emailAddress, observer.emailAddress); - } + 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"); + + m = messages.get(1); + assertThat(m.rcpt()) + .containsExactly(user.emailAddress, observer.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"); - // Second email to reviewer and observer. - m = messages.get(1); - if (notesMigration.readChanges()) { - assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress); - assertThat(m.body()).contains(admin.fullName + " has uploaded a new change for review."); - } else { - assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress); - assertThat(m.body()).contains("Hello " + observer.fullName + ",\n"); - assertThat(m.body()).contains("I'd like you to do a code review."); - } - - // Third email is review to user and observer. - m = messages.get(2); - 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"); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java index b7f09d1..02cf4f8 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.common.data.Permission; @@ -28,10 +29,13 @@ import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.project.Util; +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.ObjectLoader; import org.eclipse.jgit.revwalk.RevObject; @@ -143,6 +147,36 @@ .isEqualTo(parent.name); } + @Test + public void rejectDoubleInheritance() throws Exception { + setApiUser(admin); + // Create separate projects to test the config + Project.NameKey parent = createProject("projectToInheritFrom"); + Project.NameKey child = createProject("projectWithMalformedConfig"); + + String config = gApi.projects() + .name(child.get()) + .branch(RefNames.REFS_CONFIG).file("project.config").asString(); + + // Append and push malformed project config + String pattern = "[access]\n" + + "\tinheritFrom = " + allProjects.get() + "\n"; + String doubleInherit = pattern + "\tinheritFrom = " + parent.get() + "\n"; + config = config.replace(pattern, doubleInherit); + + TestRepository<InMemoryRepository> childRepo = + cloneProject(child, admin); + // Fetch meta ref + GitUtil.fetch(childRepo, RefNames.REFS_CONFIG + ":cfg"); + childRepo.reset("cfg"); + PushOneCommit push = pushFactory.create( + db, admin.getIdent(), childRepo, "Subject", "project.config", + config); + PushOneCommit.Result res = push.to(RefNames.REFS_CONFIG); + res.assertErrorStatus(); + res.assertMessage("cannot inherit from multiple projects"); + } + private void fetchRefsMetaConfig() throws Exception { git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")) .call();
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/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java index 2a32abe..eb6e433 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -22,20 +22,35 @@ import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.RestSession; +import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.LabelInfo; +import com.google.gerrit.extensions.common.RevisionInfo; +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.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.notedb.PatchSetState; import com.google.gerrit.testutil.ConfigSuite; +import com.google.inject.Inject; import org.eclipse.jgit.lib.Config; import org.junit.Test; import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; public class DraftChangeIT extends AbstractDaemonTest { @ConfigSuite.Config @@ -43,20 +58,8 @@ return allowDraftsDisabledConfig(); } - @Test - public void deleteChange() throws Exception { - PushOneCommit.Result result = createChange(); - result.assertOkStatus(); - String changeId = result.getChangeId(); - String triplet = project.get() + "~master~" + changeId; - ChangeInfo c = get(triplet); - assertThat(c.id).isEqualTo(triplet); - assertThat(c.status).isEqualTo(ChangeStatus.NEW); - RestResponse response = deleteChange(changeId, adminRestSession); - assertThat(response.getEntityContent()) - .isEqualTo("Change is not a draft: " + c._number); - response.assertConflict(); - } + @Inject + private BatchUpdate.Factory updateFactory; @Test public void deleteDraftChange() throws Exception { @@ -75,6 +78,104 @@ } @Test + public void deleteDraftChangeOfAnotherUser() throws Exception { + assume().that(isAllowDrafts()).isTrue(); + PushOneCommit.Result changeResult = createDraftChange(); + changeResult.assertOkStatus(); + String changeId = changeResult.getChangeId(); + Change.Id id = changeResult.getChange().getId(); + + // The user needs to be able to see the draft change (which reviewers can). + gApi.changes() + .id(changeId) + .addReviewer(user.fullName); + + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage(String.format( + "Deleting change %s is not permitted", id)); + gApi.changes() + .id(changeId) + .delete(); + } + + @Test + @TestProjectInput(cloneAs = "user") + public void deleteDraftChangeWhenDraftsNotAllowedAsNormalUser() + throws Exception { + assume().that(isAllowDrafts()).isFalse(); + + setApiUser(user); + // We can't create a draft change while the draft workflow is disabled. + // For this reason, we create a normal change and modify the database. + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + Change.Id id = changeResult.getChange().getId(); + markChangeAsDraft(id); + setDraftStatusOfPatchSetsOfChange(id, true); + + String changeId = changeResult.getChangeId(); + exception.expect(MethodNotAllowedException.class); + exception.expectMessage("Draft workflow is disabled"); + gApi.changes() + .id(changeId) + .delete(); + } + + @Test + @TestProjectInput(cloneAs = "user") + public void deleteDraftChangeWhenDraftsNotAllowedAsAdmin() throws Exception { + assume().that(isAllowDrafts()).isFalse(); + + setApiUser(user); + // We can't create a draft change while the draft workflow is disabled. + // For this reason, we create a normal change and modify the database. + PushOneCommit.Result changeResult = + pushFactory.create(db, user.getIdent(), testRepo) + .to("refs/for/master"); + Change.Id id = changeResult.getChange().getId(); + markChangeAsDraft(id); + setDraftStatusOfPatchSetsOfChange(id, true); + + String changeId = changeResult.getChangeId(); + + // Grant those permissions to admins. + grant(Permission.VIEW_DRAFTS, project, "refs/*"); + grant(Permission.DELETE_DRAFTS, project, "refs/*"); + + try { + setApiUser(admin); + gApi.changes() + .id(changeId) + .delete(); + } finally { + removePermission(Permission.DELETE_DRAFTS, project, "refs/*"); + removePermission(Permission.VIEW_DRAFTS, project, "refs/*"); + } + + setApiUser(user); + assertThat(query(changeId)).isEmpty(); + } + + @Test + public void deleteDraftChangeWithNonDraftPatchSet() throws Exception { + assume().that(isAllowDrafts()).isTrue(); + + PushOneCommit.Result changeResult = createDraftChange(); + Change.Id id = changeResult.getChange().getId(); + setDraftStatusOfPatchSetsOfChange(id, false); + + String changeId = changeResult.getChangeId(); + exception.expect(ResourceConflictException.class); + exception.expectMessage(String.format( + "Cannot delete draft change %s: patch set 1 is not a draft", id)); + gApi.changes() + .id(changeId) + .delete(); + } + + @Test public void publishDraftChange() throws Exception { assume().that(isAllowDrafts()).isTrue(); PushOneCommit.Result result = createDraftChange(); @@ -160,4 +261,90 @@ + patchSet.getRevision().get() + "/publish"); } + + private void markChangeAsDraft(Change.Id id) throws Exception { + try (BatchUpdate batchUpdate = updateFactory + .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdate.addOp(id, new MarkChangeAsDraftUpdateOp()); + batchUpdate.execute(); + } + + ChangeStatus changeStatus = gApi.changes() + .id(id.get()) + .get() + .status; + assertThat(changeStatus).isEqualTo(ChangeStatus.DRAFT); + } + + private void setDraftStatusOfPatchSetsOfChange(Change.Id id, + boolean draftStatus) throws Exception { + try (BatchUpdate batchUpdate = updateFactory + .create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdate.addOp(id, new DraftStatusOfPatchSetsUpdateOp(draftStatus)); + batchUpdate.execute(); + } + + Boolean expectedDraftStatus = draftStatus ? Boolean.TRUE : null; + List<Boolean> patchSetDraftStatuses = getPatchSetDraftStatuses(id); + patchSetDraftStatuses.forEach(status -> + assertThat(status).isEqualTo(expectedDraftStatus)); + } + + private List<Boolean> getPatchSetDraftStatuses(Change.Id id) + throws Exception { + Collection<RevisionInfo> revisionInfos = gApi.changes() + .id(id.get()) + .get(EnumSet.of(ListChangesOption.ALL_REVISIONS)) + .revisions + .values(); + return revisionInfos.stream() + .map(revisionInfo -> revisionInfo.draft) + .collect(Collectors.toList()); + } + + private class MarkChangeAsDraftUpdateOp extends BatchUpdate.Op { + @Override + public boolean updateChange(BatchUpdate.ChangeContext ctx) + throws Exception { + Change change = ctx.getChange(); + + // Change status in database. + change.setStatus(Change.Status.DRAFT); + + // Change status in NoteDb. + PatchSet.Id currentPatchSetId = change.currentPatchSetId(); + ctx.getUpdate(currentPatchSetId).setStatus(Change.Status.DRAFT); + + return true; + } + } + + private class DraftStatusOfPatchSetsUpdateOp extends BatchUpdate.Op { + private final boolean draftStatus; + + DraftStatusOfPatchSetsUpdateOp(boolean draftStatus) { + this.draftStatus = draftStatus; + } + + @Override + public boolean updateChange(BatchUpdate.ChangeContext ctx) + throws Exception { + Collection<PatchSet> patchSets = psUtil.byChange(db, ctx.getNotes()); + + // Change status in database. + patchSets.forEach(patchSet -> patchSet.setDraft(draftStatus)); + db.patchSets().update(patchSets); + + // Change status in NoteDb. + PatchSetState patchSetState = draftStatus ? PatchSetState.DRAFT + : PatchSetState.PUBLISHED; + patchSets.stream() + .map(PatchSet::getId) + .map(ctx::getUpdate) + .forEach(changeUpdate -> + changeUpdate.setPatchSetState(patchSetState)); + + return true; + } + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java index a044772..9997ee6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -52,14 +52,14 @@ } @Test - public void testGetNoHashtags() throws Exception { + public void getNoHashtags() throws Exception { // Get on a change with no hashtags returns an empty list. PushOneCommit.Result r = createChange(); assertThatGet(r).isEmpty(); } @Test - public void testAddSingleHashtag() throws Exception { + public void addSingleHashtag() throws Exception { PushOneCommit.Result r = createChange(); // Adding a single hashtag returns a single hashtag. @@ -75,7 +75,7 @@ } @Test - public void testAddMultipleHashtags() throws Exception { + public void addMultipleHashtags() throws Exception { PushOneCommit.Result r = createChange(); // Adding multiple hashtags returns a sorted list of hashtags. @@ -91,7 +91,7 @@ } @Test - public void testAddAlreadyExistingHashtag() throws Exception { + public void addAlreadyExistingHashtag() throws Exception { // Adding a hashtag that already exists on the change returns a sorted list // of hashtags without duplicates. PushOneCommit.Result r = createChange(); @@ -110,7 +110,7 @@ } @Test - public void testHashtagsWithPrefix() throws Exception { + public void hashtagsWithPrefix() throws Exception { PushOneCommit.Result r = createChange(); // Leading # is stripped from added tag. @@ -150,7 +150,7 @@ } @Test - public void testRemoveSingleHashtag() throws Exception { + public void removeSingleHashtag() throws Exception { // Removing a single tag from a change that only has that tag returns an // empty list. PushOneCommit.Result r = createChange(); @@ -169,7 +169,7 @@ } @Test - public void testRemoveMultipleHashtags() throws Exception { + public void removeMultipleHashtags() throws Exception { // Removing multiple tags from a change that only has those tags returns an // empty list. PushOneCommit.Result r = createChange(); @@ -189,7 +189,7 @@ } @Test - public void testRemoveNotExistingHashtag() throws Exception { + public void removeNotExistingHashtag() throws Exception { // Removing a single hashtag from change that has no hashtags returns an // empty list. PushOneCommit.Result r = createChange(); @@ -216,7 +216,7 @@ } @Test - public void testAddAndRemove() throws Exception { + public void addAndRemove() throws Exception { // Adding and remove hashtags in a single request performs correctly. PushOneCommit.Result r = createChange(); addHashtags(r, "tag1", "tag2"); @@ -238,17 +238,15 @@ } @Test - public void testHashtagWithMixedCase() throws Exception { + public void hashtagWithMixedCase() throws Exception { PushOneCommit.Result r = createChange(); addHashtags(r, "MyHashtag"); assertThatGet(r).containsExactly("MyHashtag"); assertMessage(r, "Hashtag added: MyHashtag"); } - private IterableSubject< - ? extends IterableSubject<?, String, Iterable<String>>, - String, Iterable<String>> - assertThatGet(PushOneCommit.Result r) throws Exception { + private IterableSubject assertThatGet(PushOneCommit.Result r) + throws Exception { return assertThat(gApi.changes() .id(r.getChange().getId().get()) .getHashtags());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java index af43373..0a3b217 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -19,17 +19,23 @@ import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.FooterConstants; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.change.Submit.TestSubmitInput; +import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.strategy.CommitMergeStatus; +import com.google.inject.Inject; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; @@ -40,6 +46,8 @@ import java.util.List; public class SubmitByCherryPickIT extends AbstractSubmit { + @Inject + private DynamicSet<ChangeMessageModifier> changeMessageModifiers; @Override protected SubmitType getSubmitType() { @@ -89,6 +97,31 @@ } @Test + public void changeMessageOnSubmit() throws Exception { + PushOneCommit.Result change = createChange(); + RegistrationHandle handle = + changeMessageModifiers.add(new ChangeMessageModifier() { + @Override + public String onSubmit(String newCommitMessage, RevCommit original, + RevCommit mergeTip, Branch.NameKey destination) { + return newCommitMessage + "Custom: " + destination.get(); + } + }); + try { + submit(change.getChangeId()); + } finally { + handle.remove(); + } + testRepo.git().fetch().setRemote("origin").call(); + ChangeInfo info = get(change.getChangeId()); + RevCommit c = testRepo.getRevWalk() + .parseCommit(ObjectId.fromString(info.currentRevision)); + testRepo.getRevWalk().parseBody(c); + assertThat(c.getFooterLines("Custom")).containsExactly("refs/heads/master"); + assertThat(c.getFooterLines(FooterConstants.REVIEWED_ON)).hasSize(1); + } + + @Test @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) public void submitWithContentMerge() throws Exception { RevCommit initialHead = getRemoteHead();
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..dea5f10 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,28 @@ 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.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; 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.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge { @@ -144,6 +159,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 +179,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 +253,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()); } @@ -360,7 +410,7 @@ } @Test - public void testGerritWorkflow() throws Exception { + public void gerritWorkflow() throws Exception { RevCommit initialHead = getRemoteHead(); // We'll setup a master and a stable branch. @@ -492,4 +542,33 @@ assertRefUpdatedEvents(); assertChangeMergedEvents(); } + + @Test + public void testPreviewSubmitTgz() throws Exception { + Project.NameKey p1 = createProject("project-name"); + + TestRepository<?> repo1 = cloneProject(p1); + PushOneCommit.Result change1 = createChange(repo1, "master", + "test", "a.txt", "1", "topic"); + approve(change1.getChangeId()); + + // get a preview before submitting: + BinaryResult request = submitPreview(change1.getChangeId(), "tgz"); + + assertThat(request.getContentType()).isEqualTo("application/x-gzip"); + File tempfile = File.createTempFile("test", null); + request.writeTo(new FileOutputStream(tempfile)); + + InputStream is = new GZIPInputStream(new FileInputStream(tempfile)); + + List<String> untarredFiles = new LinkedList<>(); + try (TarArchiveInputStream tarInputStream = (TarArchiveInputStream) + new ArchiveStreamFactory().createArchiveInputStream("tar", is)) { + TarArchiveEntry entry = null; + while ((entry = (TarArchiveEntry)tarInputStream.getNextEntry()) != null) { + untarredFiles.add(entry.getName()); + } + } + assertThat(untarredFiles).containsExactly(name("project-name") + ".git"); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java new file mode 100644 index 0000000..0389417 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -0,0 +1,140 @@ +// 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 com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.FooterConstants; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.server.git.ChangeMessageModifier; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +import java.util.List; + +public class SubmitByRebaseAlwaysIT extends AbstractSubmitByRebase { + @Inject + private DynamicSet<ChangeMessageModifier> changeMessageModifiers; + + @Override + protected SubmitType getSubmitType() { + return SubmitType.REBASE_ALWAYS; + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitWithPossibleFastForward() throws Exception { + RevCommit oldHead = getRemoteHead(); + PushOneCommit.Result change = createChange(); + submit(change.getChangeId()); + + RevCommit head = getRemoteHead(); + assertThat(head.getId()).isNotEqualTo(change.getCommit()); + assertThat(head.getParent(0)).isEqualTo(oldHead); + assertApproved(change.getChangeId()); + assertCurrentRevision(change.getChangeId(), 2, head); + assertSubmitter(change.getChangeId(), 1); + assertSubmitter(change.getChangeId(), 2); + assertPersonEquals(admin.getIdent(), head.getAuthorIdent()); + assertPersonEquals(admin.getIdent(), head.getCommitterIdent()); + assertRefUpdatedEvents(oldHead, head); + assertChangeMergedEvents(change.getChangeId(), head.name()); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void alwaysAddFooters() throws Exception { + PushOneCommit.Result change1 = createChange(); + PushOneCommit.Result change2 = createChange(); + + assertThat( + getCurrentCommit(change1).getFooterLines(FooterConstants.REVIEWED_BY)) + .isEmpty(); + assertThat( + getCurrentCommit(change2).getFooterLines(FooterConstants.REVIEWED_BY)) + .isEmpty(); + + // change1 is a fast-forward, but should be rebased in cherry pick style + // anyway, making change2 not a fast-forward, requiring a rebase. + approve(change1.getChangeId()); + submit(change2.getChangeId()); + // ... but both changes should get reviewed-by footers. + assertLatestRevisionHasFooters(change1); + assertLatestRevisionHasFooters(change2); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void changeMessageOnSubmit() throws Exception { + PushOneCommit.Result change1 = createChange(); + PushOneCommit.Result change2 = createChange(); + + RegistrationHandle handle = + changeMessageModifiers.add(new ChangeMessageModifier() { + @Override + public String onSubmit(String newCommitMessage, RevCommit original, + RevCommit mergeTip, Branch.NameKey destination) { + List<String> custom = mergeTip.getFooterLines("Custom"); + if (!custom.isEmpty()) { + newCommitMessage += "Custom-Parent: " + custom.get(0) + "\n"; + } + return newCommitMessage + "Custom: " + destination.get(); + } + }); + try { + // change1 is a fast-forward, but should be rebased in cherry pick style + // anyway, making change2 not a fast-forward, requiring a rebase. + approve(change1.getChangeId()); + submit(change2.getChangeId()); + } finally { + handle.remove(); + } + // ... but both changes should get custom footers. + assertThat(getCurrentCommit(change1).getFooterLines("Custom")) + .containsExactly("refs/heads/master"); + assertThat(getCurrentCommit(change2).getFooterLines("Custom")) + .containsExactly("refs/heads/master"); + assertThat(getCurrentCommit(change2).getFooterLines("Custom-Parent")) + .containsExactly("refs/heads/master"); + } + + private void assertLatestRevisionHasFooters(PushOneCommit.Result change) + throws Exception { + RevCommit c = getCurrentCommit(change); + assertThat(c.getFooterLines(FooterConstants.CHANGE_ID)).isNotEmpty(); + assertThat(c.getFooterLines(FooterConstants.REVIEWED_BY)).isNotEmpty(); + assertThat(c.getFooterLines(FooterConstants.REVIEWED_ON)).isNotEmpty(); + } + + private RevCommit getCurrentCommit(PushOneCommit.Result change) + throws Exception { + testRepo.git().fetch().setRemote("origin").call(); + ChangeInfo info = get(change.getChangeId()); + RevCommit c = testRepo.getRevWalk() + .parseCommit(ObjectId.fromString(info.currentRevision)); + testRepo.getRevWalk().parseBody(c); + return c; + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java index 9b3fd15..431978d2 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -15,32 +15,16 @@ package com.google.gerrit.acceptance.rest.change; import static com.google.common.truth.Truth.assertThat; -import static com.google.gerrit.acceptance.GitUtil.getChangeId; -import static com.google.gerrit.acceptance.GitUtil.pushHead; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; -import com.google.gerrit.extensions.api.changes.SubmitInput; -import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.SubmitType; -import com.google.gerrit.extensions.common.ChangeInfo; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.reviewdb.client.Branch; -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.server.change.Submit.TestSubmitInput; -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.junit.Test; -public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit { +public class SubmitByRebaseIfNecessaryIT extends AbstractSubmitByRebase { @Override protected SubmitType getSubmitType() { @@ -67,143 +51,6 @@ @Test @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) - public void submitWithRebase() 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", "b.txt", "other content"); - submit(change2.getChangeId()); - assertRebase(testRepo, false); - RevCommit headAfterSecondSubmit = getRemoteHead(); - assertThat(headAfterSecondSubmit.getParent(0)) - .isEqualTo(headAfterFirstSubmit); - assertApproved(change2.getChangeId()); - assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit); - assertSubmitter(change2.getChangeId(), 1); - assertSubmitter(change2.getChangeId(), 2); - assertPersonEquals(admin.getIdent(), - headAfterSecondSubmit.getAuthorIdent()); - assertPersonEquals(admin.getIdent(), - headAfterSecondSubmit.getCommitterIdent()); - - assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, - headAfterFirstSubmit, headAfterSecondSubmit); - assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(), - change2.getChangeId(), headAfterSecondSubmit.name()); - } - - @Test - public void submitWithRebaseMultipleChanges() throws Exception { - RevCommit initialHead = getRemoteHead(); - PushOneCommit.Result change1 = - createChange("Change 1", "a.txt", "content"); - submit(change1.getChangeId()); - RevCommit headAfterFirstSubmit = getRemoteHead(); - assertThat(headAfterFirstSubmit.name()) - .isEqualTo(change1.getCommit().name()); - - testRepo.reset(initialHead); - PushOneCommit.Result change2 = - createChange("Change 2", "b.txt", "other content"); - assertThat(change2.getCommit().getParent(0)) - .isNotEqualTo(change1.getCommit()); - PushOneCommit.Result change3 = - createChange("Change 3", "c.txt", "third content"); - PushOneCommit.Result change4 = - createChange("Change 4", "d.txt", "fourth content"); - approve(change2.getChangeId()); - approve(change3.getChangeId()); - submit(change4.getChangeId()); - - assertRebase(testRepo, false); - assertApproved(change2.getChangeId()); - assertApproved(change3.getChangeId()); - assertApproved(change4.getChangeId()); - - RevCommit headAfterSecondSubmit = parse(getRemoteHead()); - assertThat(headAfterSecondSubmit.getShortMessage()).isEqualTo("Change 4"); - assertThat(headAfterSecondSubmit).isNotEqualTo(change4.getCommit()); - assertCurrentRevision(change4.getChangeId(), 2, headAfterSecondSubmit); - - RevCommit parent = parse(headAfterSecondSubmit.getParent(0)); - assertThat(parent.getShortMessage()).isEqualTo("Change 3"); - assertThat(parent).isNotEqualTo(change3.getCommit()); - assertCurrentRevision(change3.getChangeId(), 2, parent); - - RevCommit grandparent = parse(parent.getParent(0)); - assertThat(grandparent).isNotEqualTo(change2.getCommit()); - assertCurrentRevision(change2.getChangeId(), 2, grandparent); - - RevCommit greatgrandparent = parse(grandparent.getParent(0)); - assertThat(greatgrandparent).isEqualTo(change1.getCommit()); - assertCurrentRevision(change1.getChangeId(), 1, greatgrandparent); - - assertRefUpdatedEvents(initialHead, headAfterFirstSubmit, - headAfterFirstSubmit, headAfterSecondSubmit); - assertChangeMergedEvents(change1.getChangeId(), headAfterFirstSubmit.name(), - change2.getChangeId(), headAfterSecondSubmit.name(), - change3.getChangeId(), headAfterSecondSubmit.name(), - change4.getChangeId(), headAfterSecondSubmit.name()); - } - - @Test - public void submitWithRebaseMergeCommit() throws Exception { - /* - * (HEAD, origin/master, origin/HEAD) Merge changes X,Y - |\ - | * Merge branch 'master' into origin/master - | |\ - | | * SHA Added a - | |/ - * | Before - |/ - * Initial empty repository - */ - RevCommit initialHead = getRemoteHead(); - PushOneCommit.Result change1 = createChange("Added a", "a.txt", ""); - - PushOneCommit change2Push = pushFactory.create(db, admin.getIdent(), testRepo, - "Merge to master", "m.txt", ""); - change2Push.setParents(ImmutableList.of(initialHead, change1.getCommit())); - PushOneCommit.Result change2 = change2Push.to("refs/for/master"); - - testRepo.reset(initialHead); - PushOneCommit.Result change3 = createChange("Before", "b.txt", ""); - - approve(change3.getChangeId()); - submit(change3.getChangeId()); - - approve(change1.getChangeId()); - approve(change2.getChangeId()); - submit(change2.getChangeId()); - - RevCommit newHead = getRemoteHead(); - assertThat(newHead.getParentCount()).isEqualTo(2); - - RevCommit headParent1 = parse(newHead.getParent(0).getId()); - RevCommit headParent2 = parse(newHead.getParent(1).getId()); - - assertThat(headParent1.getId()).isEqualTo(change3.getCommit().getId()); - assertThat(headParent1.getParentCount()).isEqualTo(1); - assertThat(headParent1.getParent(0)).isEqualTo(initialHead); - - assertThat(headParent2.getId()).isEqualTo(change2.getCommit().getId()); - assertThat(headParent2.getParentCount()).isEqualTo(2); - - RevCommit headGrandparent1 = parse(headParent2.getParent(0).getId()); - RevCommit headGrandparent2 = parse(headParent2.getParent(1).getId()); - - assertThat(headGrandparent1.getId()).isEqualTo(initialHead.getId()); - assertThat(headGrandparent2.getId()).isEqualTo(change1.getCommit().getId()); - } - - @Test - @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) public void submitWithContentMerge() throws Exception { RevCommit initialHead = getRemoteHead(); PushOneCommit.Result change = @@ -235,160 +82,4 @@ change2.getChangeId(), headAfterSecondSubmit.name(), change3.getChangeId(), headAfterThirdSubmit.name()); } - - @Test - @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) - public void submitWithContentMerge_Conflict() 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"); - submitWithConflict(change2.getChangeId(), - "Cannot rebase " + change2.getCommit().name() - + ": The change could not be rebased due to a conflict during merge."); - RevCommit head = getRemoteHead(); - assertThat(head).isEqualTo(headAfterFirstSubmit); - assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit()); - assertNoSubmitter(change2.getChangeId(), 1); - - assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); - assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name()); - } - - @Test - public void repairChangeStateAfterFailure() 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", "b.txt", "other content"); - Change.Id id2 = change2.getChange().getId(); - SubmitInput failAfterRefUpdates = - new TestSubmitInput(new SubmitInput(), true); - submit(change2.getChangeId(), failAfterRefUpdates, - ResourceConflictException.class, "Failing after ref updates"); - RevCommit headAfterFailedSubmit = getRemoteHead(); - - // Bad: ref advanced but change wasn't updated. - PatchSet.Id psId1 = new PatchSet.Id(id2, 1); - PatchSet.Id psId2 = new PatchSet.Id(id2, 2); - ChangeInfo info = gApi.changes().id(id2.get()).get(); - assertThat(info.status).isEqualTo(ChangeStatus.NEW); - assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(1); - assertThat(getPatchSet(psId2)).isNull(); - - ObjectId rev2; - try (Repository repo = repoManager.openRepository(project); - RevWalk rw = new RevWalk(repo)) { - ObjectId rev1 = repo.exactRef(psId1.toRefName()).getObjectId(); - assertThat(rev1).isNotNull(); - - rev2 = repo.exactRef(psId2.toRefName()).getObjectId(); - assertThat(rev2).isNotNull(); - assertThat(rev2).isNotEqualTo(rev1); - assertThat(rw.parseCommit(rev2).getParent(0)).isEqualTo(headAfterFirstSubmit); - - assertThat(repo.exactRef("refs/heads/master").getObjectId()) - .isEqualTo(rev2); - } - - submit(change2.getChangeId()); - RevCommit headAfterSecondSubmit = getRemoteHead(); - assertThat(headAfterSecondSubmit).isEqualTo(headAfterFailedSubmit); - - // Change status and patch set entities were updated, and branch tip stayed - // the same. - info = gApi.changes().id(id2.get()).get(); - assertThat(info.status).isEqualTo(ChangeStatus.MERGED); - assertThat(info.revisions.get(info.currentRevision)._number).isEqualTo(2); - PatchSet ps2 = getPatchSet(psId2); - assertThat(ps2).isNotNull(); - assertThat(ps2.getRevision().get()).isEqualTo(rev2.name()); - assertThat(Iterables.getLast(info.messages).message) - .isEqualTo("Change has been successfully rebased as " - + rev2.name() + " by Administrator"); - - try (Repository repo = repoManager.openRepository(project)) { - assertThat(repo.exactRef("refs/heads/master").getObjectId()) - .isEqualTo(rev2); - } - - assertRefUpdatedEvents(initialHead, headAfterFirstSubmit); - assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name(), - change2.getChangeId(), headAfterSecondSubmit.name()); - } - - private RevCommit parse(ObjectId id) throws Exception { - try (Repository repo = repoManager.openRepository(project); - RevWalk rw = new RevWalk(repo)) { - RevCommit c = rw.parseCommit(id); - rw.parseBody(c); - return c; - } - } - - @Test - public void submitAfterReorderOfCommits() throws Exception { - RevCommit initialHead = getRemoteHead(); - - // Create two commits and push. - RevCommit c1 = commitBuilder() - .add("a.txt", "1") - .message("subject: 1") - .create(); - RevCommit c2 = commitBuilder() - .add("b.txt", "2") - .message("subject: 2") - .create(); - pushHead(testRepo, "refs/for/master", false); - - String id1 = getChangeId(testRepo, c1).get(); - String id2 = getChangeId(testRepo, c2).get(); - - // Swap the order of commits and push again. - testRepo.reset("HEAD~2"); - testRepo.cherryPick(c2); - testRepo.cherryPick(c1); - pushHead(testRepo, "refs/for/master", false); - - approve(id1); - approve(id2); - submit(id1); - RevCommit headAfterSubmit = getRemoteHead(); - - assertRefUpdatedEvents(initialHead, headAfterSubmit); - assertChangeMergedEvents(id2, headAfterSubmit.name(), - id1, headAfterSubmit.name()); - } - - @Test - public void submitChangesAfterBranchOnSecond() throws Exception { - RevCommit initialHead = getRemoteHead(); - - PushOneCommit.Result change = createChange(); - approve(change.getChangeId()); - - PushOneCommit.Result change2 = createChange(); - approve(change2.getChangeId()); - Project.NameKey project = change2.getChange().change().getProject(); - Branch.NameKey branch = new Branch.NameKey(project, "branch"); - createBranchWithRevision(branch, change2.getCommit().getName()); - gApi.changes().id(change2.getChangeId()).current().submit(); - assertMerged(change2.getChangeId()); - assertMerged(change.getChangeId()); - - RevCommit newHead = getRemoteHead(); - assertRefUpdatedEvents(initialHead, newHead); - assertChangeMergedEvents(change.getChangeId(), newHead.name(), - change2.getChangeId(), newHead.name()); - } }
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 7fab6b1..d92c002 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,23 +16,26 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; +import static java.util.stream.Collectors.toList; -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.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.GerritConfig; -import com.google.gerrit.acceptance.GerritConfigs; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescriptions; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.common.ChangeInput; import com.google.gerrit.extensions.common.GroupInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.group.CreateGroup; import com.google.gerrit.server.group.GroupsCollection; import com.google.inject.Inject; @@ -42,6 +45,7 @@ import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; @Sandboxed public class SuggestReviewersIT extends AbstractDaemonTest { @@ -82,10 +86,8 @@ } @Test - @GerritConfigs( - {@GerritConfig(name = "suggest.from", value = "1"), - @GerritConfig(name = "accounts.visibility", value = "NONE") - }) + @GerritConfig(name = "suggest.from", value = "1") + @GerritConfig(name = "accounts.visibility", value = "NONE") public void suggestReviewersNoResult2() throws Exception { String changeId = createChange().getChangeId(); List<SuggestedReviewerInfo> reviewers = @@ -94,24 +96,29 @@ } @Test - @GerritConfig(name = "suggest.from", value = "2") - public void suggestReviewersNoResult3() throws Exception { - String changeId = createChange().getChangeId(); - List<SuggestedReviewerInfo> reviewers = - suggestReviewers(changeId, name("").substring(0, 1), 6); - assertThat(reviewers).isEmpty(); - } - - @Test public void suggestReviewersChange() throws Exception { String changeId = createChange().getChangeId(); List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, name("u"), 6); - assertThat(reviewers).hasSize(6); + assertReviewers(reviewers, ImmutableList.of(user1, user2, user3), + ImmutableList.of(group1, group2, group3)); + reviewers = suggestReviewers(changeId, name("u"), 5); - assertThat(reviewers).hasSize(5); + assertReviewers(reviewers, ImmutableList.of(user1, user2, user3), + ImmutableList.of(group1, group2)); + reviewers = suggestReviewers(changeId, group3.getName(), 10); + assertReviewers(reviewers, ImmutableList.of(), ImmutableList.of(group3)); + + // Suggested accounts are ordered by activity. All users have no activity, + // hence we don't know which of the matching accounts we get when the query + // is limited to 1. + reviewers = suggestReviewers(changeId, name("u"), 1); assertThat(reviewers).hasSize(1); + assertThat(reviewers.get(0).account).isNotNull(); + assertThat(ImmutableList.of(reviewers.get(0).account._accountId)) + .containsAnyIn(ImmutableList.of(user1, user2, user3).stream() + .map(u -> u.id.get()).collect(toList())); } @Test @@ -221,7 +228,7 @@ assertThat(reviewers).hasSize(1); reviewers = suggestReviewers(changeId, "example.com"); - assertThat(reviewers).hasSize(6); + assertThat(reviewers).hasSize(5); reviewers = suggestReviewers(changeId, user1.email); assertThat(reviewers).hasSize(1); @@ -246,10 +253,8 @@ } @Test - @GerritConfigs({ - @GerritConfig(name = "addreviewer.maxAllowed", value="2"), - @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value="1"), - }) + @GerritConfig(name = "addreviewer.maxAllowed", value="2") + @GerritConfig(name = "addreviewer.maxWithoutConfirmation", value="1") public void suggestReviewersGroupSizeConsiderations() throws Exception { AccountGroup largeGroup = group("large"); AccountGroup mediumGroup = group("medium"); @@ -284,6 +289,158 @@ assertThat(reviewer.confirm).isTrue(); } + @Test + public void defaultReviewerSuggestion() throws Exception{ + TestAccount user1 = user("customuser1", "User1"); + TestAccount reviewer1 = user("customuser2", "User2"); + TestAccount reviewer2 = user("customuser3", "User3"); + + setApiUser(user1); + String changeId1 = createChangeFromApi(); + + setApiUser(reviewer1); + reviewChange(changeId1); + + setApiUser(user1); + String changeId2 = createChangeFromApi(); + + setApiUser(reviewer1); + reviewChange(changeId2); + + setApiUser(reviewer2); + reviewChange(changeId2); + + setApiUser(user1); + String changeId3 = createChangeFromApi(); + List<SuggestedReviewerInfo> reviewers = + suggestReviewers(changeId3, null, 4); + assertThat( + reviewers.stream() + .map(r -> r.account._accountId) + .collect(Collectors.toList())) + .containsExactly( + reviewer1.id.get(), + reviewer2.id.get()) + .inOrder(); + + // check that existing reviewers are filtered out + gApi.changes().id(changeId3).addReviewer(reviewer1.email); + reviewers = + suggestReviewers(changeId3, null, 4); + assertThat( + reviewers.stream() + .map(r -> r.account._accountId) + .collect(Collectors.toList())) + .containsExactly( + reviewer2.id.get()) + .inOrder(); + } + + @Test + public void defaultReviewerSuggestionOnFirstChange() throws Exception{ + TestAccount user1 = user("customuser1", "User1"); + setApiUser(user1); + List<SuggestedReviewerInfo> reviewers = + suggestReviewers(createChange().getChangeId(), "", 4); + assertThat(reviewers).isEmpty(); + } + + @Test + @GerritConfig(name = "suggest.maxSuggestedReviewers", value = "10") + public void reviewerRanking() throws Exception{ + // Assert that user are ranked by the number of times they have applied a + // a label to a change (highest), added comments (medium) or owned a + // change (low). + String fullName = "Primum Finalis"; + TestAccount userWhoOwns = user("customuser1", fullName); + TestAccount reviewer1 = user("customuser2", fullName); + TestAccount reviewer2 = user("customuser3", fullName); + TestAccount userWhoComments = user("customuser4", fullName); + TestAccount userWhoLooksForSuggestions = user("customuser5", fullName); + + // Create a change as userWhoOwns and add some reviews + setApiUser(userWhoOwns); + String changeId1 = createChangeFromApi(); + + setApiUser(reviewer1); + reviewChange(changeId1); + + setApiUser(user1); + String changeId2 = createChangeFromApi(); + + setApiUser(reviewer1); + reviewChange(changeId2); + + setApiUser(reviewer2); + reviewChange(changeId2); + + // Create a comment as a different user + setApiUser(userWhoComments); + ReviewInput ri = new ReviewInput(); + ri.message = "Test"; + gApi.changes().id(changeId1).revision(1).review(ri); + + // Create a change as a new user to assert that we receive the correct + // ranking + + setApiUser(userWhoLooksForSuggestions); + List<SuggestedReviewerInfo> reviewers = + suggestReviewers(createChangeFromApi(), "Pri", 4); + assertThat( + reviewers.stream() + .map(r -> r.account._accountId) + .collect(Collectors.toList())) + .containsExactly( + reviewer1.id.get(), + reviewer2.id.get(), + userWhoOwns.id.get(), + userWhoComments.id.get()) + .inOrder(); + } + + @Test + public void reviewerRankingProjectIsolation() throws Exception{ + // Create new project + Project.NameKey newProject = createProject("test"); + + // Create users who review changes in both the default and the new project + String fullName = "Primum Finalis"; + TestAccount userWhoOwns = user("customuser1", fullName); + TestAccount reviewer1 = user("customuser2", fullName); + TestAccount reviewer2 = user("customuser3", fullName); + + setApiUser(userWhoOwns); + String changeId1 = createChangeFromApi(); + + setApiUser(reviewer1); + reviewChange(changeId1); + + setApiUser(userWhoOwns); + String changeId2 = createChangeFromApi(newProject); + + setApiUser(reviewer2); + reviewChange(changeId2); + + setApiUser(userWhoOwns); + String changeId3 = createChangeFromApi(newProject); + + setApiUser(reviewer2); + reviewChange(changeId3); + + setApiUser(userWhoOwns); + List<SuggestedReviewerInfo> reviewers = + suggestReviewers(createChangeFromApi(), "Prim", 4); + + // Assert that reviewer1 is on top, even though reviewer2 has more reviews + // in other projects + assertThat( + reviewers.stream() + .map(r -> r.account._accountId) + .collect(Collectors.toList())) + .containsExactly(reviewer1.id.get(), reviewer2.id.get()) + .inOrder(); + } + private List<SuggestedReviewerInfo> suggestReviewers(String changeId, String query) throws Exception { return gApi.changes() @@ -310,13 +467,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); } @@ -325,4 +478,45 @@ throws Exception { return user(name, fullName, name, groups); } + + private void reviewChange(String changeId) throws RestApiException { + ReviewInput ri = new ReviewInput(); + ri.label("Code-Review", 1); + gApi.changes().id(changeId).current().review(ri); + } + + private String createChangeFromApi() throws RestApiException{ + return createChangeFromApi(project); + } + + private String createChangeFromApi(Project.NameKey project) + throws RestApiException{ + ChangeInput ci = new ChangeInput(); + ci.project = project.get(); + ci.subject = "Test change at" + System.nanoTime(); + ci.branch = "master"; + return gApi.changes().create(ci).get().changeId; + } + + private void assertReviewers(List<SuggestedReviewerInfo> actual, + List<TestAccount> expectedUsers, List<AccountGroup> expectedGroups) { + List<Integer> actualAccountIds = actual.stream() + .filter(i -> i.account != null) + .map(i -> i.account._accountId) + .collect(toList()); + assertThat(actualAccountIds) + .containsExactlyElementsIn( + expectedUsers.stream().map(u -> u.id.get()).collect(toList())); + + List<String> actualGroupIds = actual.stream() + .filter(i -> i.group != null) + .map(i -> i.group.id) + .collect(toList()); + assertThat(actualGroupIds) + .containsExactlyElementsIn( + expectedGroups.stream() + .map(g -> g.getGroupUUID().get()) + .collect(toList())) + .inOrder(); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD index b9d3ffb..6becf0f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'rest_config', - srcs = glob(['*IT.java']), - labels = ['rest'] + srcs = glob(["*IT.java"]), + group = "rest_config", + labels = ["rest"], )
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..cac293c 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
@@ -19,67 +19,66 @@ 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 - @GerritConfigs({ - // auth - @GerritConfig(name = "auth.type", value = "HTTP"), - @GerritConfig(name = "auth.contributorAgreements", value = "true"), - @GerritConfig(name = "auth.loginUrl", value = "https://example.com/login"), - @GerritConfig(name = "auth.loginText", value = "LOGIN"), - @GerritConfig(name = "auth.switchAccountUrl", value = "https://example.com/switch"), + // auth + @GerritConfig(name = "auth.type", value = "HTTP") + @GerritConfig(name = "auth.contributorAgreements", value = "true") + @GerritConfig(name = "auth.loginUrl", value = "https://example.com/login") + @GerritConfig(name = "auth.loginText", value = "LOGIN") + @GerritConfig(name = "auth.switchAccountUrl", value = "https://example.com/switch") - // auth fields ignored when auth == HTTP - @GerritConfig(name = "auth.registerUrl", value = "https://example.com/register"), - @GerritConfig(name = "auth.registerText", value = "REGISTER"), - @GerritConfig(name = "auth.editFullNameUrl", value = "https://example.com/editname"), - @GerritConfig(name = "auth.httpPasswordUrl", value = "https://example.com/password"), + // auth fields ignored when auth == HTTP + @GerritConfig(name = "auth.registerUrl", value = "https://example.com/register") + @GerritConfig(name = "auth.registerText", value = "REGISTER") + @GerritConfig(name = "auth.editFullNameUrl", value = "https://example.com/editname") + @GerritConfig(name = "auth.httpPasswordUrl", value = "https://example.com/password") - // change - @GerritConfig(name = "change.allowDrafts", value = "false"), - @GerritConfig(name = "change.largeChange", value = "300"), - @GerritConfig(name = "change.replyTooltip", value = "Publish votes and draft comments"), - @GerritConfig(name = "change.replyLabel", value = "Vote"), - @GerritConfig(name = "change.updateDelay", value = "50s"), + // change + @GerritConfig(name = "change.allowDrafts", value = "false") + @GerritConfig(name = "change.largeChange", value = "300") + @GerritConfig(name = "change.replyTooltip", value = "Publish votes and draft comments") + @GerritConfig(name = "change.replyLabel", value = "Vote") + @GerritConfig(name = "change.updateDelay", value = "50s") - // download - @GerritConfig(name = "download.archive", values = {"tar", - "tbz2", "tgz", "txz"}), + // download + @GerritConfig(name = "download.archive", values = {"tar", + "tbz2", "tgz", "txz"}) - // 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.reportBugText", value = "REPORT BUG"), + // gerrit + @GerritConfig(name = "gerrit.allProjects", value = "Root") + @GerritConfig(name = "gerrit.allUsers", value = "Users") + @GerritConfig(name = "gerrit.enableGwtUi", 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"), + // suggest + @GerritConfig(name = "suggest.from", value = "3") - // user - @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"), - }) + // user + @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 +106,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 +123,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 +136,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 +144,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 +191,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/group/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD index d9a400c..b3672ee 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUILD
@@ -1,8 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'rest_group', - srcs = glob(['*IT.java']), - labels = ['rest'] + srcs = glob(["*IT.java"]), + group = "rest_group", + labels = ["rest"], ) -
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK index d53e69a..2d3e2da 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -4,16 +4,16 @@ group = 'rest_project', srcs = glob(['*IT.java']), deps = [ - ':branch', ':project', + ':refassert', ], labels = ['rest'], ) java_library( - name = 'branch', + name = 'refassert', srcs = [ - 'BranchAssert.java', + 'RefAssert.java', ], deps = [ '//lib:truth',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD index 579171f..3266be8 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
@@ -1,37 +1,37 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'rest_project', - srcs = glob(['*IT.java']), - deps = [ - ':branch', - ':project', - ], - labels = ['rest'], + srcs = glob(["*IT.java"]), + group = "rest_project", + labels = ["rest"], + deps = [ + ":project", + ":refassert", + ], ) java_library( - name = 'branch', - srcs = [ - 'BranchAssert.java', - ], - deps = [ - '//lib:truth', - '//gerrit-extension-api:api', - '//gerrit-server:server', - ], + name = "refassert", + srcs = [ + "RefAssert.java", + ], + deps = [ + "//gerrit-extension-api:api", + "//gerrit-server:server", + "//lib:truth", + ], ) java_library( - name = 'project', - srcs = [ - 'ProjectAssert.java', - ], - deps = [ - '//gerrit-extension-api:api', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:gwtorm', - '//lib:truth', - ], + name = "project", + srcs = [ + "ProjectAssert.java", + ], + deps = [ + "//gerrit-extension-api:api", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:gwtorm", + "//lib:truth", + ], )
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/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java index b8a0e4b..767207a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -52,7 +52,7 @@ public class CreateProjectIT extends AbstractDaemonTest { @Test - public void testCreateProjectHttp() throws Exception { + public void createProjectHttp() throws Exception { String newProjectName = name("newProject"); RestResponse r = adminRestSession.put("/projects/" + newProjectName); r.assertCreated(); @@ -65,7 +65,7 @@ } @Test - public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict() + public void createProjectHttpWhenProjectAlreadyExists_Conflict() throws Exception { adminRestSession .put("/projects/" + allProjects.get()) @@ -73,7 +73,7 @@ } @Test - public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed() + public void createProjectHttpWhenProjectAlreadyExists_PreconditionFailed() throws Exception { adminRestSession .putWithHeader("/projects/" + allProjects.get(), @@ -91,7 +91,7 @@ } @Test - public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception { + public void createProjectHttpWithNameMismatch_BadRequest() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("otherName"); adminRestSession @@ -100,7 +100,7 @@ } @Test - public void testCreateProjectHttpWithInvalidRefName_BadRequest() + public void createProjectHttpWithInvalidRefName_BadRequest() throws Exception { ProjectInput in = new ProjectInput(); in.branches = Collections.singletonList(name("invalid ref name")); @@ -110,7 +110,7 @@ } @Test - public void testCreateProject() throws Exception { + public void createProject() throws Exception { String newProjectName = name("newProject"); ProjectInfo p = gApi.projects().create(newProjectName).get(); assertThat(p.name).isEqualTo(newProjectName); @@ -121,7 +121,7 @@ } @Test - public void testCreateProjectWithGitSuffix() throws Exception { + public void createProjectWithGitSuffix() throws Exception { String newProjectName = name("newProject"); ProjectInfo p = gApi.projects().create(newProjectName + ".git").get(); assertThat(p.name).isEqualTo(newProjectName); @@ -132,7 +132,7 @@ } @Test - public void testCreateProjectWithProperties() throws Exception { + public void createProjectWithProperties() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -155,7 +155,7 @@ } @Test - public void testCreateChildProject() throws Exception { + public void createChildProject() throws Exception { String parentName = name("parent"); ProjectInput in = new ProjectInput(); in.name = parentName; @@ -171,7 +171,7 @@ } @Test - public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity() + public void createChildProjectUnderNonExistingParent_UnprocessableEntity() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("newProjectName"); @@ -180,7 +180,7 @@ } @Test - public void testCreateProjectWithOwner() throws Exception { + public void createProjectWithOwner() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -199,7 +199,7 @@ } @Test - public void testCreateProjectWithNonExistingOwner_UnprocessableEntity() + public void createProjectWithNonExistingOwner_UnprocessableEntity() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("newProjectName"); @@ -208,7 +208,7 @@ } @Test - public void testCreatePermissionOnlyProject() throws Exception { + public void createPermissionOnlyProject() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -218,7 +218,7 @@ } @Test - public void testCreateProjectWithEmptyCommit() throws Exception { + public void createProjectWithEmptyCommit() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -228,7 +228,7 @@ } @Test - public void testCreateProjectWithBranches() throws Exception { + public void createProjectWithBranches() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -244,7 +244,7 @@ } @Test - public void testCreateProjectWithoutCapability_Forbidden() throws Exception { + public void createProjectWithoutCapability_Forbidden() throws Exception { setApiUser(user); ProjectInput in = new ProjectInput(); in.name = name("newProject"); @@ -252,7 +252,7 @@ } @Test - public void testCreateProjectWhenProjectAlreadyExists_Conflict() + public void createProjectWhenProjectAlreadyExists_Conflict() throws Exception { ProjectInput in = new ProjectInput(); in.name = allProjects.get();
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/DeleteBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java index af1383b..3f0c43e 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -15,7 +15,7 @@ package com.google.gerrit.acceptance.rest.project; import static com.google.common.truth.Truth.assertThat; -import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames; +import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -25,6 +25,7 @@ import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; import com.google.gerrit.extensions.api.projects.ProjectApi; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.reviewdb.client.RefNames; @@ -107,6 +108,31 @@ assertBranchesDeleted(); } + @Test + public void missingInput() throws Exception { + DeleteBranchesInput input = null; + exception.expect(BadRequestException.class); + exception.expectMessage("branches must be specified"); + project().deleteBranches(input); + } + + @Test + public void missingBranchList() throws Exception { + DeleteBranchesInput input = new DeleteBranchesInput(); + exception.expect(BadRequestException.class); + exception.expectMessage("branches must be specified"); + project().deleteBranches(input); + } + + @Test + public void emptyBranchList() throws Exception { + DeleteBranchesInput input = new DeleteBranchesInput(); + input.branches = Lists.newArrayList(); + exception.expect(BadRequestException.class); + exception.expectMessage("branches must be specified"); + project().deleteBranches(input); + } + private String errorMessageForBranches(List<String> branches) { StringBuilder message = new StringBuilder(); for (String branch : branches) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java new file mode 100644 index 0000000..491171d --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -0,0 +1,124 @@ +// 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.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.projects.TagApi; +import com.google.gerrit.extensions.api.projects.TagInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; + +import org.junit.Before; +import org.junit.Test; + +@NoHttpd +public class DeleteTagIT extends AbstractDaemonTest { + private final String TAG = "refs/tags/test"; + + @Before + public void setUp() throws Exception { + tag().create(new TagInput()); + } + + @Test + public void deleteTag_Forbidden() throws Exception { + setApiUser(user); + assertDeleteForbidden(); + } + + @Test + public void deleteTagByAdmin() throws Exception { + assertDeleteSucceeds(); + } + + @Test + public void deleteTagByProjectOwner() throws Exception { + grantOwner(); + setApiUser(user); + assertDeleteSucceeds(); + } + + @Test + public void deleteTagByAdminForcePushBlocked() throws Exception { + blockForcePush(); + assertDeleteSucceeds(); + } + + @Test + public void deleteTagByProjectOwnerForcePushBlocked_Forbidden() + throws Exception { + grantOwner(); + blockForcePush(); + setApiUser(user); + assertDeleteForbidden(); + } + + @Test + public void deleteTagByUserWithForcePushPermission() throws Exception { + grantForcePush(); + setApiUser(user); + assertDeleteSucceeds(); + } + + @Test + public void deleteTagByUserWithDeletePermission() throws Exception { + grantDelete(); + setApiUser(user); + assertDeleteSucceeds(); + } + + private void blockForcePush() throws Exception { + block(Permission.PUSH, ANONYMOUS_USERS, "refs/tags/*").setForce(true); + } + + private void grantForcePush() throws Exception { + grant(Permission.PUSH, project, "refs/tags/*", true, ANONYMOUS_USERS); + } + + private void grantDelete() throws Exception { + allow(Permission.DELETE, ANONYMOUS_USERS, "refs/tags/*"); + } + + private void grantOwner() throws Exception { + allow(Permission.OWNER, REGISTERED_USERS, "refs/tags/*"); + } + + private TagApi tag() throws Exception { + return gApi.projects() + .name(project.get()) + .tag(TAG); + } + + private void assertDeleteSucceeds() throws Exception { + String tagRev = tag().get().revision; + tag().delete(); + eventRecorder.assertRefUpdatedEvents(project.get(), TAG, + null, tagRev, + tagRev, null); + exception.expect(ResourceNotFoundException.class); + tag().get(); + } + + private void assertDeleteForbidden() throws Exception { + exception.expect(AuthException.class); + exception.expectMessage("Cannot delete tag"); + tag().delete(); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.java new file mode 100644 index 0000000..4516fb3 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagsIT.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.project; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.extensions.api.projects.DeleteTagsInput; +import com.google.gerrit.extensions.api.projects.ProjectApi; +import com.google.gerrit.extensions.api.projects.TagInfo; +import com.google.gerrit.extensions.api.projects.TagInput; +import com.google.gerrit.extensions.restapi.ResourceConflictException; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; + +@NoHttpd +public class DeleteTagsIT extends AbstractDaemonTest { + private static final List<String> TAGS = ImmutableList.of( + "refs/tags/test-1", "refs/tags/test-2", "refs/tags/test-3"); + + @Before + public void setUp() throws Exception { + for (String name : TAGS) { + project().tag(name).create(new TagInput()); + } + assertTags(TAGS); + } + + @Test + public void deleteTags() throws Exception { + HashMap<String, RevCommit> initialRevisions = initialRevisions(TAGS); + DeleteTagsInput input = new DeleteTagsInput(); + input.tags = TAGS; + project().deleteTags(input); + assertTagsDeleted(); + assertRefUpdatedEvents(initialRevisions); + } + + @Test + public void deleteTagsForbidden() throws Exception { + DeleteTagsInput input = new DeleteTagsInput(); + input.tags = TAGS; + setApiUser(user); + try { + project().deleteTags(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForTags(TAGS)); + } + setApiUser(admin); + assertTags(TAGS); + } + + @Test + public void deleteTagsNotFound() throws Exception { + DeleteTagsInput input = new DeleteTagsInput(); + List<String> tags = Lists.newArrayList(TAGS); + tags.add("refs/tags/does-not-exist"); + input.tags = tags; + try { + project().deleteTags(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForTags( + ImmutableList.of("refs/tags/does-not-exist"))); + } + assertTagsDeleted(); + } + + @Test + public void deleteTagsNotFoundContinue() throws Exception { + // If it fails on the first tag in the input, it should still + // continue to process the remaining tags. + DeleteTagsInput input = new DeleteTagsInput(); + List<String> tags = Lists.newArrayList("refs/tags/does-not-exist"); + tags.addAll(TAGS); + input.tags = tags; + try { + project().deleteTags(input); + fail("Expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessage(errorMessageForTags( + ImmutableList.of("refs/tags/does-not-exist"))); + } + assertTagsDeleted(); + } + + private String errorMessageForTags(List<String> tags) { + StringBuilder message = new StringBuilder(); + for (String tag : tags) { + message.append("Cannot delete ") + .append(tag) + .append(": it doesn't exist or you do not have permission ") + .append("to delete it\n"); + } + return message.toString(); + } + + private HashMap<String, RevCommit> initialRevisions(List<String> tags) + throws Exception { + HashMap<String, RevCommit> result = new HashMap<>(); + for (String tag : tags) { + result.put(tag, getRemoteHead(project, tag)); + } + return result; + } + + private void assertRefUpdatedEvents(HashMap<String, RevCommit> revisions) + throws Exception { + for (String tag : revisions.keySet()) { + RevCommit revision = revisions.get(tag); + eventRecorder.assertRefUpdatedEvents(project.get(), tag, + null, revision, + revision, null); + } + } + + private ProjectApi project() throws Exception { + return gApi.projects().name(project.get()); + } + + private void assertTags(List<String> expected) throws Exception { + List<TagInfo> actualTags = project().tags().get(); + Iterable<String> actualNames = Iterables.transform(actualTags, b -> b.ref); + assertThat(actualNames).containsExactlyElementsIn(expected).inOrder(); + } + + private void assertTagsDeleted() throws Exception { + assertTags(ImmutableList.<String>of()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java index 8522a4d..4667cc6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -37,12 +37,12 @@ } @Test - public void testGcNonExistingProject_NotFound() throws Exception { + public void gcNonExistingProject_NotFound() throws Exception { POST("/projects/non-existing/gc").assertNotFound(); } @Test - public void testGcNotAllowed_Forbidden() throws Exception { + public void gcNotAllowed_Forbidden() throws Exception { userRestSession .post("/projects/" + allProjects.get() + "/gc") .assertForbidden();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java index 7c98188..5728217 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,8 +14,8 @@ package com.google.gerrit.acceptance.rest.project; -import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches; -import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertRefNames; +import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefNames; +import static com.google.gerrit.acceptance.rest.project.RefAssert.assertRefs; import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; @@ -47,7 +47,7 @@ @Test @TestProjectInput(createEmptyCommit = false) public void listBranchesOfEmptyProject() throws Exception { - assertBranches(ImmutableList.of( + assertRefs(ImmutableList.of( branch("HEAD", null, false), branch(RefNames.REFS_CONFIG, null, false)), list().get()); @@ -57,7 +57,7 @@ public void listBranches() throws Exception { String master = pushTo("refs/heads/master").getCommit().name(); String dev = pushTo("refs/heads/dev").getCommit().name(); - assertBranches(ImmutableList.of( + assertRefs(ImmutableList.of( branch("HEAD", "master", false), branch(RefNames.REFS_CONFIG, null, false), branch("refs/heads/dev", dev, true), @@ -72,7 +72,7 @@ pushTo("refs/heads/dev"); setApiUser(user); // refs/meta/config is hidden since user is no project owner - assertBranches(ImmutableList.of( + assertRefs(ImmutableList.of( branch("HEAD", "master", false), branch("refs/heads/master", master, false)), list().get()); @@ -85,7 +85,7 @@ String dev = pushTo("refs/heads/dev").getCommit().name(); setApiUser(user); // refs/meta/config is hidden since user is no project owner - assertBranches(ImmutableList.of(branch("refs/heads/dev", dev, false)), + assertRefs(ImmutableList.of(branch("refs/heads/dev", dev, false)), list().get()); }
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..e3104bb 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; @@ -32,12 +31,8 @@ import java.util.Set; public class ProjectAssert { - public static IterableSubject< - ? extends IterableSubject< - ?, Project.NameKey, Iterable<Project.NameKey>>, - Project.NameKey, - Iterable<Project.NameKey>> - assertThatNameList(Iterable<ProjectInfo> actualIt) { + public static IterableSubject assertThatNameList( + Iterable<ProjectInfo> actualIt) { List<ProjectInfo> actual = ImmutableList.copyOf(actualIt); for (ProjectInfo info : actual) { assertWithMessage("missing project name").that(info.name).isNotNull(); @@ -45,13 +40,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/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java similarity index 62% rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java index c860bf0..0cbf79a 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/RefAssert.java
@@ -16,28 +16,27 @@ 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; +import com.google.gerrit.extensions.api.projects.RefInfo; import java.util.List; -public class BranchAssert { - public static void assertBranches(List<BranchInfo> expectedBranches, - List<BranchInfo> actualBranches) { - assertRefNames(refs(expectedBranches), actualBranches); - for (int i = 0; i < expectedBranches.size(); i++) { - assertBranchInfo(expectedBranches.get(i), actualBranches.get(i)); +public class RefAssert { + public static void assertRefs(List<? extends RefInfo> expectedRefs, + List<? extends RefInfo> actualRefs) { + assertRefNames(refs(expectedRefs), actualRefs); + for (int i = 0; i < expectedRefs.size(); i++) { + assertRefInfo(expectedRefs.get(i), actualRefs.get(i)); } } public static void assertRefNames(Iterable<String> expectedRefs, - Iterable<BranchInfo> actualBranches) { - Iterable<String> actualNames = refs(actualBranches); + Iterable<? extends RefInfo> actualRefs) { + Iterable<String> actualNames = refs(actualRefs); assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder(); } - public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) { + public static void assertRefInfo(RefInfo expected, RefInfo actual) { assertThat(actual.ref).isEqualTo(expected.ref); if (expected.revision != null) { assertThat(actual.revision).named("revision of " + actual.ref) @@ -47,13 +46,8 @@ .isEqualTo(toBoolean(expected.canDelete)); } - 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; - } - }); + private static Iterable<String> refs(Iterable<? extends RefInfo> infos) { + 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/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/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD index a5e6d36..ac32b02 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'server_change', - srcs = glob(['*IT.java']), - labels = ['server'], + srcs = glob(["*IT.java"]), + group = "server_change", + labels = ["server"], )
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..f0f4566 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 @@ -361,7 +397,7 @@ setApiUser(admin); Map<String, List<CommentInfo>> actual = gApi.changes().id(r1.getChangeId()).drafts(); - assertThat((Iterable<?>) actual.keySet()).containsExactly(FILE_NAME); + assertThat(actual.keySet()).containsExactly(FILE_NAME); List<CommentInfo> comments = actual.get(FILE_NAME); assertThat(comments).hasSize(2); @@ -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(); @@ -519,10 +554,12 @@ + url + "#/c/" + c + "/1/a.txt\n" + "File a.txt:\n" + "\n" + + url + "#/c/12/1/a.txt@a2\n" + "PS1, Line 2: \n" + "what happened to this?\n" + "\n" + "\n" + + url + "#/c/12/1/a.txt@1\n" + "PS1, Line 1: ew\n" + "nit: trailing whitespace\n" + "\n" @@ -530,20 +567,25 @@ + url + "#/c/" + c + "/2/a.txt\n" + "File a.txt:\n" + "\n" + + url + "#/c/12/2/a.txt@a1\n" + "PS2, Line 1: \n" + "comment 1 on base\n" + "\n" + "\n" + + url + "#/c/12/2/a.txt@a2\n" + "PS2, Line 2: \n" + "comment 2 on base\n" + "\n" + "\n" + + url + "#/c/12/2/a.txt@1\n" + "PS2, Line 1: ew\n" + "join lines\n" + "\n" + "\n" + + url + "#/c/12/2/a.txt@2\n" + "PS2, Line 2: nten\n" + "typo: content\n" + + "\n" + "\n"); } @@ -687,29 +729,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..a39f300 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
@@ -21,7 +21,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.common.RawInputUtil; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.common.CommitInfo; @@ -646,6 +648,40 @@ changeAndCommit(psId1_1, c1_1, 1)); } + @Test + @GerritConfig(name = "index.testReindexAfterUpdate", value = "false") + 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 +690,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..257b92f 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() @@ -240,7 +300,7 @@ } @Test - public void testTopicChaining() throws Exception { + public void topicChaining() throws Exception { RevCommit initialHead = getRemoteHead(); // Create two independent commits and push. RevCommit c1_1 = commitBuilder() @@ -277,7 +337,7 @@ } @Test - public void testNewBranchTwoChangesTogether() throws Exception { + public void newBranchTwoChangesTogether() throws Exception { Project.NameKey p1 = createProject("a-new-project", null, false); TestRepository<?> repo1 = cloneProject(p1); @@ -319,7 +379,7 @@ } @Test - public void testSubmissionIdSavedOnMergeInOneProject() throws Exception { + public void submissionIdSavedOnMergeInOneProject() throws Exception { // Create two commits and push. RevCommit c1_1 = commitBuilder() .add("a.txt", "1")
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD index ff0c51b..3804bea 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUILD
@@ -1,7 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'server_event', - srcs = glob(['*IT.java']), - labels = ['server'], + srcs = glob(["*IT.java"]), + group = "server_event", + labels = ["server"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK new file mode 100644 index 0000000..c642154 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUCK
@@ -0,0 +1,10 @@ +include_defs('//gerrit-acceptance-tests/tests.defs') + +acceptance_tests( + group = 'server_mail', + srcs = glob(['*IT.java']), + labels = ['server'], + deps = [ + '//lib/joda:joda-time', + ], +)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD new file mode 100644 index 0000000..2f70adc --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/BUILD
@@ -0,0 +1,12 @@ +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") + +acceptance_tests( + srcs = glob(["*IT.java"]), + group = "server_mail", + labels = ["server"], + deps = [ + "//lib/greenmail", + "//lib/joda:joda-time", + "//lib/mail", + ], +)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java new file mode 100644 index 0000000..8d93bbf --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailIT.java
@@ -0,0 +1,104 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.server.mail; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.server.mail.receive.MailReceiver; +import com.google.gerrit.testutil.ConfigSuite; +import com.google.inject.Inject; + +import com.icegreen.greenmail.junit.GreenMailRule; +import com.icegreen.greenmail.user.GreenMailUser; +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.GreenMailUtil; +import com.icegreen.greenmail.util.ServerSetupTest; + +import org.eclipse.jgit.lib.Config; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.mail.internet.MimeMessage; + +@NoHttpd +@RunWith(ConfigSuite.class) +public class MailIT extends AbstractDaemonTest { + private static final String RECEIVEEMAIL = "receiveemail"; + private static final String HOST = "localhost"; + private static final String USERNAME = "user@domain.com"; + private static final String PASSWORD = "password"; + + @Inject + private MailReceiver mailReceiver; + + @Inject + private GreenMail greenMail; + + @Rule + public final GreenMailRule mockPop3Server = new GreenMailRule( + ServerSetupTest.SMTP_POP3_IMAP); + + @ConfigSuite.Default + public static Config pop3Config() { + Config cfg = new Config(); + cfg.setString(RECEIVEEMAIL, null, "host", HOST); + cfg.setString(RECEIVEEMAIL, null, "port", "3110"); + cfg.setString(RECEIVEEMAIL, null, "username", USERNAME); + cfg.setString(RECEIVEEMAIL, null, "password", PASSWORD); + cfg.setString(RECEIVEEMAIL, null, "protocol", "POP3"); + cfg.setString(RECEIVEEMAIL, null, "fetchInterval", "99"); + return cfg; + } + + @ConfigSuite.Config + public static Config imapConfig() { + Config cfg = new Config(); + cfg.setString(RECEIVEEMAIL, null, "host", HOST); + cfg.setString(RECEIVEEMAIL, null, "port", "3143"); + cfg.setString(RECEIVEEMAIL, null, "username", USERNAME); + cfg.setString(RECEIVEEMAIL, null, "password", PASSWORD); + cfg.setString(RECEIVEEMAIL, null, "protocol", "IMAP"); + cfg.setString(RECEIVEEMAIL, null, "fetchInterval", "99"); + return cfg; + } + + @Test + public void delete() throws Exception { + GreenMailUser user = mockPop3Server.setUser(USERNAME, USERNAME, PASSWORD); + user.deliver(createSimpleMessage()); + assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1); + // Let Gerrit handle emails + mailReceiver.handleEmails(false); + // Check that the message is still present + assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1); + // Mark the message for deletion + mailReceiver.requestDeletion( + mockPop3Server.getReceivedMessages()[0].getMessageID()); + // Let Gerrit handle emails + mailReceiver.handleEmails(false); + // Check that the message was deleted + assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(0); + } + + private MimeMessage createSimpleMessage() { + return GreenMailUtil + .createTextEmail(USERNAME, "from@localhost.com", "subject", + "body", + greenMail.getImap().getServerSetup()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java new file mode 100644 index 0000000..7d59fb1 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -0,0 +1,165 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.server.mail; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; + +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.common.ChangeMessageInfo; +import com.google.gerrit.server.mail.MailUtil; +import com.google.gerrit.server.mail.send.EmailHeader; +import com.google.gerrit.testutil.FakeEmailSender; +import com.google.gerrit.testutil.TestTimeUtil; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Timestamp; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Tests the presence of required metadata in email headers, text and html. */ +public class MailMetadataIT extends AbstractDaemonTest { + private String systemTimeZone; + + @Before + public void setTimeForTesting() { + systemTimeZone = System.setProperty("user.timezone", "US/Eastern"); + TestTimeUtil.resetWithClockStep(1, SECONDS); + } + + @After + public void resetTime() { + TestTimeUtil.useSystemTime(); + System.setProperty("user.timezone", systemTimeZone); + } + + @Test + public void metadataOnNewChange() throws Exception { + PushOneCommit.Result newChange = createChange(); + gApi.changes() + .id(newChange.getChangeId()) + .addReviewer(user.getId().toString()); + + List<FakeEmailSender.Message> emails = sender.getMessages(); + assertThat(emails).hasSize(1); + FakeEmailSender.Message message = emails.get(0); + + String changeURL = "<" + canonicalWebUrl.get() + + newChange.getChange().getId().get() + ">"; + + Map<String, Object> expectedHeaders = new HashMap<>(); + expectedHeaders.put("Gerrit-PatchSet", "1"); + expectedHeaders.put("Gerrit-Change-Id", newChange.getChangeId()); + expectedHeaders.put("Gerrit-MessageType", "newchange"); + expectedHeaders.put("Gerrit-Commit", newChange.getCommit().getId().name()); + expectedHeaders.put("Gerrit-ChangeURL", changeURL); + + assertHeaders(message.headers(), expectedHeaders); + + // Remove metadata that is not present in email + expectedHeaders.remove("Gerrit-ChangeURL"); + expectedHeaders.remove("Gerrit-Commit"); + assertTextFooter(message.body(), expectedHeaders); + } + + @Test + public void metadataOnNewComment() throws Exception { + PushOneCommit.Result newChange = createChange(); + gApi.changes() + .id(newChange.getChangeId()) + .addReviewer(user.getId().toString()); + sender.clear(); + + // Review change + ReviewInput input = new ReviewInput(); + input.message = "Test"; + revision(newChange).review(input); + setApiUser(user); + Collection<ChangeMessageInfo> result = + gApi.changes().id(newChange.getChangeId()).get().messages; + assertThat(result).isNotEmpty(); + + List<FakeEmailSender.Message> emails = sender.getMessages(); + assertThat(emails).hasSize(1); + FakeEmailSender.Message message = emails.get(0); + + String changeURL = "<" + canonicalWebUrl.get() + + newChange.getChange().getId().get() + ">"; + Map<String, Object> expectedHeaders = new HashMap<>(); + expectedHeaders.put("Gerrit-PatchSet", "1"); + expectedHeaders.put("Gerrit-Change-Id", newChange.getChangeId()); + expectedHeaders.put("Gerrit-MessageType", "comment"); + expectedHeaders.put("Gerrit-Commit", + newChange.getCommit().getId().name()); + expectedHeaders.put("Gerrit-ChangeURL", changeURL); + expectedHeaders.put("Gerrit-Comment-Date", + Iterables.getLast(result).date); + + assertHeaders(message.headers(), expectedHeaders); + + // Remove metadata that is not present in email + expectedHeaders.remove("Gerrit-ChangeURL"); + expectedHeaders.remove("Gerrit-Commit"); + assertTextFooter(message.body(), expectedHeaders); + } + + private static void assertHeaders(Map<String, EmailHeader> have, + Map<String, Object> want) throws Exception { + for (Map.Entry<String, Object> entry : want.entrySet()) { + if (entry.getValue() instanceof String) { + assertThat(have).containsEntry("X-" + entry.getKey(), + new EmailHeader.String((String) entry.getValue())); + } else if (entry.getValue() instanceof Date) { + assertThat(have).containsEntry("X-" + entry.getKey(), + new EmailHeader.Date((Date) entry.getValue())); + } else { + throw new Exception("Object has unsupported type: " + + entry.getValue().getClass().getName() + + " must be java.util.Date or java.lang.String for key " + + entry.getKey()); + } + } + } + + private static void assertTextFooter(String body, + Map<String, Object> want) throws Exception { + for (Map.Entry<String, Object> entry : want.entrySet()) { + if (entry.getValue() instanceof String) { + assertThat(body).contains(entry.getKey() + ": " + entry.getValue()); + } else if (entry.getValue() instanceof Timestamp) { + assertThat(body).contains(entry.getKey() + ": " + + MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + ((Timestamp) entry.getValue()).toInstant(), + ZoneId.of("UTC")))); + } else { + throw new Exception("Object has unsupported type: " + + entry.getValue().getClass().getName() + + " must be java.util.Date or java.lang.String for key " + + entry.getKey()); + } + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java new file mode 100644 index 0000000..0f6f3db --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -0,0 +1,305 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.server.mail; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +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.CommentInput; +import com.google.gerrit.extensions.client.Comment; +import com.google.gerrit.extensions.client.Side; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.ChangeMessageInfo; +import com.google.gerrit.extensions.common.CommentInfo; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.MailUtil; +import com.google.gerrit.server.mail.receive.MailMessage; +import com.google.gerrit.server.mail.receive.MailProcessor; +import com.google.inject.Inject; + +import org.joda.time.DateTime; +import org.junit.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +public class MailProcessorIT extends AbstractDaemonTest { + @Inject + private MailProcessor mailProcessor; + + @Test + public void parseAndPersistChangeMessage() throws Exception { + String changeId = createChangeWithReview(); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + List<CommentInfo> comments = gApi.changes().id(changeId) + .current().commentsAsList(); + String ts = MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + comments.get(0).updated.toInstant(), + ZoneId.of("UTC"))); + + // Build Message + MailMessage.Builder b = messageBuilderWithDefaultFields(); + String txt = newPlaintextBody(canonicalWebUrl.get() + "#/c/" + + changeInfo._number + "/1", "Test Message", null, null, null); + b.textContent(txt + textFooterForChange(changeId, ts)); + + mailProcessor.process(b.build()); + + Collection<ChangeMessageInfo> messages = + gApi.changes().id(changeId).get().messages; + assertThat(messages).hasSize(3); + assertThat(Iterables.getLast(messages).message) + .isEqualTo("Patch Set 1:\nTest Message"); + assertThat(Iterables.getLast(messages).tag) + .isEqualTo("mailMessageId=some id"); + } + + @Test + public void parseAndPersistInlineComment() throws Exception { + String changeId = createChangeWithReview(); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + List<CommentInfo> comments = gApi.changes().id(changeId) + .current().commentsAsList(); + String ts = MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + comments.get(0).updated.toInstant(), + ZoneId.of("UTC"))); + + // Build Message + MailMessage.Builder b = messageBuilderWithDefaultFields(); + String txt = newPlaintextBody(canonicalWebUrl.get() + "#/c/" + + changeInfo._number + "/1", null, "Some Inline Comment", null, null); + b.textContent(txt + textFooterForChange(changeId, ts)); + + mailProcessor.process(b.build()); + + // Assert messages + Collection<ChangeMessageInfo> messages = + gApi.changes().id(changeId).get().messages; + assertThat(messages).hasSize(3); + assertThat(Iterables.getLast(messages).message) + .isEqualTo("Patch Set 1:\n(1 comment)"); + assertThat(Iterables.getLast(messages).tag) + .isEqualTo("mailMessageId=some id"); + + // Assert comment + comments = gApi.changes().id(changeId).current().commentsAsList(); + assertThat(comments).hasSize(3); + assertThat(comments.get(2).message) + .isEqualTo("Some Inline Comment"); + assertThat(comments.get(2).tag) + .isEqualTo("mailMessageId=some id"); + assertThat(comments.get(2).inReplyTo) + .isEqualTo(comments.get(1).id); + } + + @Test + public void parseAndPersistFileComment() throws Exception { + String changeId = createChangeWithReview(); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + List<CommentInfo> comments = gApi.changes().id(changeId) + .current().commentsAsList(); + String ts = MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + comments.get(0).updated.toInstant(), + ZoneId.of("UTC"))); + + // Build Message + MailMessage.Builder b = messageBuilderWithDefaultFields(); + String txt = newPlaintextBody(canonicalWebUrl.get() + "#/c/" + + changeInfo._number + "/1", null, null, "Some Comment on File 1", null); + b.textContent(txt + textFooterForChange(changeId, ts)); + + mailProcessor.process(b.build()); + + // Assert messages + Collection<ChangeMessageInfo> messages = + gApi.changes().id(changeId).get().messages; + assertThat(messages).hasSize(3); + assertThat(Iterables.getLast(messages).message) + .isEqualTo("Patch Set 1:\n(1 comment)"); + assertThat(Iterables.getLast(messages).tag) + .isEqualTo("mailMessageId=some id"); + + // Assert comment + comments = gApi.changes().id(changeId).current().commentsAsList(); + assertThat(comments).hasSize(3); + assertThat(comments.get(0).message).isEqualTo("Some Comment on File 1"); + assertThat(comments.get(0).inReplyTo).isNull(); + assertThat(comments.get(0).tag).isEqualTo("mailMessageId=some id"); + assertThat(comments.get(0).path).isEqualTo("gerrit-server/test.txt"); + } + + @Test + public void parseAndPersistMessageTwice() throws Exception { + String changeId = createChangeWithReview(); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + List<CommentInfo> comments = gApi.changes().id(changeId) + .current().commentsAsList(); + String ts = MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + comments.get(0).updated.toInstant(), + ZoneId.of("UTC"))); + + // Build Message + MailMessage.Builder b = messageBuilderWithDefaultFields(); + String txt = newPlaintextBody(canonicalWebUrl.get() + "#/c/" + + changeInfo._number + "/1", null, "Some Inline Comment", null, null); + b.textContent(txt + textFooterForChange(changeId, ts)); + + mailProcessor.process(b.build()); + comments = gApi.changes().id(changeId).current().commentsAsList(); + assertThat(comments).hasSize(3); + + // Check that the comment has not been persisted a second time + mailProcessor.process(b.build()); + comments = gApi.changes().id(changeId).current().commentsAsList(); + assertThat(comments).hasSize(3); + } + + @Test + public void parseAndPersistMessageFromInactiveAccount() throws Exception { + String changeId = createChangeWithReview(); + ChangeInfo changeInfo = gApi.changes().id(changeId).get(); + List<CommentInfo> comments = gApi.changes().id(changeId) + .current().commentsAsList(); + String ts = MailUtil.rfcDateformatter.format(ZonedDateTime.ofInstant( + comments.get(0).updated.toInstant(), + ZoneId.of("UTC"))); + assertThat(comments).hasSize(2); + + // Build Message + MailMessage.Builder b = messageBuilderWithDefaultFields(); + String txt = newPlaintextBody(canonicalWebUrl.get() + "#/c/" + + changeInfo._number + "/1", null, "Some Inline Comment", null, null); + b.textContent(txt + textFooterForChange(changeId, ts)); + + // Set account state to inactive + gApi.accounts().id("user").setActive(false); + + mailProcessor.process(b.build()); + comments = gApi.changes().id(changeId).current().commentsAsList(); + + // Check that comment size has not changed + assertThat(comments).hasSize(2); + + // Reset + gApi.accounts().id("user").setActive(true); + } + + private static CommentInput newComment(String path, Side side, int line, + String message) { + CommentInput c = new CommentInput(); + c.path = path; + c.side = side; + c.line = line != 0 ? line : null; + c.message = message; + if (line != 0) { + Comment.Range range = new Comment.Range(); + range.startLine = line; + range.startCharacter = 1; + range.endLine = line; + range.endCharacter = 5; + c.range = range; + } + return c; + } + + /** + * Create a plaintext message body with the specified comments. + * + * @param changeMessage + * @param c1 Comment in reply to first inline comment. + * @param f1 Comment on file one. + * @param fc1 Comment in reply to a comment of file 1. + * @return A string with all inline comments and the original quoted email. + */ + private static String newPlaintextBody(String changeURL, String changeMessage, + String c1, String f1, String fc1) { + return (changeMessage == null ? "" : changeMessage + "\n") + + "> Foo Bar has posted comments on this change. ( \n" + + "> " + changeURL +" )\n" + + "> \n" + + "> Change subject: Test change\n" + + "> ...............................................................\n" + + "> \n" + + "> \n" + + "> Patch Set 1: Code-Review+1\n" + + "> \n" + + "> (3 comments)\n" + + "> \n" + + "> " + changeURL + "/gerrit-server/test.txt\n" + + "> File \n" + + "> gerrit-server/test.txt:\n" + + (f1 == null ? "" : f1 + "\n") + + "> \n" + + "> Patch Set #4:\n" + + "> " + changeURL + "/gerrit-server/test.txt\n" + + "> \n" + + "> Some comment" + + "> \n" + + (fc1 == null ? "" : fc1 + "\n") + + "> " + changeURL + "/gerrit-server/test.txt@2\n" + + "> PS1, Line 2: throw new Exception(\"Object has unsupported: \" +\n" + + "> : entry.getValue() +\n" + + "> : \" must be java.util.Date\");\n" + + "> Should entry.getKey() be included in this message?\n" + + "> \n" + + (c1 == null ? "" : c1 + "\n") + + "> \n"; + } + + private static String textFooterForChange(String changeId, String timestamp) { + return "Gerrit-Change-Id: " + changeId + "\n" + + "Gerrit-PatchSet: 1\n" + + "Gerrit-MessageType: comment\n" + + "Gerrit-Comment-Date: " + timestamp + "\n"; + } + + private MailMessage.Builder messageBuilderWithDefaultFields() { + MailMessage.Builder b = MailMessage.builder(); + b.id("some id"); + Address address = new Address(user.fullName, user.email); + b.from(address); + b.addTo(address); + b.subject(""); + b.dateReceived(new DateTime()); + return b; + } + + private String createChangeWithReview() throws Exception { + // Create change + String file = "gerrit-server/test.txt"; + String contents = "contents \nlorem \nipsum \nlorem"; + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, + "first subject", file, contents); + PushOneCommit.Result r = push.to("refs/for/master"); + String changeId = r.getChangeId(); + + // Review it + ReviewInput input = new ReviewInput(); + input.message = "I have two comments"; + input.comments = new HashMap<>(); + CommentInput c1 = newComment(file, Side.REVISION, 0, "comment on file"); + CommentInput c2 = newComment(file, Side.REVISION, 2, "inline comment"); + input.comments.put(c1.path, ImmutableList.of(c1, c2)); + revision(r).review(input); + return changeId; + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD index 17c4cdc..d314f16 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUILD
@@ -1,7 +1,10 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'server_notedb', - srcs = glob(['*IT.java']), - labels = ['notedb', 'server'], + srcs = glob(["*IT.java"]), + group = "server_notedb", + labels = [ + "notedb", + "server", + ], )
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..15e5e78 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 java.util.stream.Collectors.toList; 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.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.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,22 @@ 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.NoteDbChangeState.PrimaryStorage; 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.patch.PatchSetInfoFactory; +import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.project.Util; +import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.NoteDbChecker; import com.google.gerrit.testutil.NoteDbMode; @@ -72,6 +85,9 @@ 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.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -85,7 +101,9 @@ import java.sql.Timestamp; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; public class ChangeRebuilderIT extends AbstractDaemonTest { @@ -93,6 +111,13 @@ public static Config defaultConfig() { Config cfg = new Config(); cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true); + + // Disable async reindex-if-stale check after index update. This avoids + // unintentional auto-rebuilding of the change in NoteDb during the read + // path of the reindex-if-stale check. For the purposes of this test, we + // want precise control over when auto-rebuilding happens. + cfg.setBoolean("index", null, "testReindexAfterUpdate", false); + return cfg; } @@ -109,7 +134,7 @@ private Provider<ReviewDb> dbProvider; @Inject - private PatchLineCommentsUtil plcUtil; + private CommentsUtil commentsUtil; @Inject private Provider<PostReview> postReview; @@ -123,6 +148,12 @@ @Inject private Sequences seq; + @Inject + private ChangeBundleReader bundleReader; + + @Inject + private PatchSetInfoFactory patchSetInfoFactory; + @Before public void setUp() throws Exception { assume().that(NoteDbMode.readWrite()).isFalse(); @@ -387,8 +418,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(); } @@ -400,10 +431,16 @@ final Change.Id id = r.getPatchSetId().getParentKey(); assertChangeUpToDate(true, id); - // Make a ReviewDb change behind NoteDb's back and ensure it's detected. - setNotesMigration(false, false); - gApi.changes().id(id.get()).topic(name("a-topic")); - setInvalidNoteDbState(id); + // Update ReviewDb and NoteDb, then revert the corresponding NoteDb change + // to simulate it failing. + NoteDbChangeState oldState = + NoteDbChangeState.parse(getUnwrappedDb().changes().get(id)); + String topic = name("a-topic"); + gApi.changes().id(id.get()).topic(topic); + try (Repository repo = repoManager.openRepository(project)) { + new TestRepository<>(repo) + .update(RefNames.changeMetaRef(id), oldState.getChangeMetaId()); + } assertChangeUpToDate(false, id); // Next NoteDb read comes inside the transaction started by BatchUpdate. In @@ -411,7 +448,6 @@ // the change is parsed by ChangesCollection and when the BatchUpdate // executes. We simulate it here by using BatchUpdate directly and not going // through an API handler. - setNotesMigration(true, true); final String msg = "message from BatchUpdate"; try (BatchUpdate bu = batchUpdateFactory.create(db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) { @@ -428,29 +464,33 @@ return true; } }); - bu.execute(); + try { + bu.execute(); + fail("expected update to fail"); + } catch (UpdateException e) { + assertThat(e.getMessage()).contains("cannot copy ChangeNotesState"); + } } - // As an implementation detail, change wasn't actually rebuilt inside the - // BatchUpdate transaction, but it was rebuilt during read for the - // subsequent reindex. Thus it's impossible to actually observe an - // out-of-date state in the caller. - assertChangeUpToDate(true, id); - // 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); - assertThat(actual.differencesFrom(expected)).isEmpty(); - assertThat( - Iterables.transform( - notes.getChangeMessages(), - new Function<ChangeMessage, String>() { - @Override - public String apply(ChangeMessage in) { - return in.getMessage(); - } - })) - .contains(msg); + // TODO(dborowitz): Re-enable these assertions once we fix auto-rebuilding + // in the BatchUpdate path. + //// As an implementation detail, change wasn't actually rebuilt inside the + //// BatchUpdate transaction, but it was rebuilt during read for the + //// subsequent reindex. Thus it's impossible to actually observe an + //// out-of-date state in the caller. + //assertChangeUpToDate(true, id); + + //// Check that the bundles are equal. + //ChangeNotes notes = notesFactory.create(dbProvider.get(), project, id); + //ChangeBundle actual = ChangeBundle.fromNotes(commentsUtil, notes); + //ChangeBundle expected = bundleReader.fromReviewDb(getUnwrappedDb(), id); + //assertThat(actual.differencesFrom(expected)).isEmpty(); + //assertThat( + // Iterables.transform( + // notes.getChangeMessages(), + // ChangeMessage::getMessage)) + // .contains(msg); + //assertThat(actual.getChange().getTopic()).isEqualTo(topic); } @Test @@ -477,8 +517,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 +547,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 +589,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 @@ -581,11 +621,15 @@ ReviewDb db = getUnwrappedDb(); Change c = db.changes().get(id); // Leave change meta ID alone so DraftCommentNotes does the rebuild. + ObjectId badSha = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); NoteDbChangeState bogusState = new NoteDbChangeState( - id, NoteDbChangeState.parse(c).getChangeMetaId(), - ImmutableMap.<Account.Id, ObjectId>of( - user.getId(), - ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))); + id, + PrimaryStorage.REVIEW_DB, + Optional.of( + NoteDbChangeState.RefState.create( + NoteDbChangeState.parse(c).getChangeMetaId(), + ImmutableMap.of(user.getId(), badSha)))); c.setNoteDbState(bogusState.toString()); db.changes().update(Collections.singleton(c)); @@ -604,8 +648,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 +755,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); @@ -808,28 +854,6 @@ } @Test - public void skipPatchSetsGreaterThanCurrentPatchSet() throws Exception { - PushOneCommit.Result r = createChange(); - Change change = r.getChange().change(); - Change.Id id = change.getId(); - - PatchSet badPs = - new PatchSet(new PatchSet.Id(id, change.currentPatchSetId().get() + 1)); - badPs.setCreatedOn(TimeUtil.nowTs()); - badPs.setUploader(new Account.Id(12345)); - badPs.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); - db.patchSets().insert(Collections.singleton(badPs)); - indexer.index(db, change.getProject(), id); - - checker.rebuildAndCheckChanges(id); - - setNotesMigration(true, true); - ChangeNotes notes = notesFactory.create(db, project, id); - assertThat(notes.getPatchSets().keySet()) - .containsExactly(change.currentPatchSetId()); - } - - @Test public void leadingSpacesInSubject() throws Exception { String subj = " " + PushOneCommit.SUBJECT; PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, @@ -971,6 +995,173 @@ 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); + } + + @Test + public void laterEventsDependingOnEarlierPatchSetDontIntefereWithOtherPatchSets() + throws Exception { + PushOneCommit.Result r1 = createChange(); + ChangeData cd = r1.getChange(); + Change.Id id = cd.getId(); + amendChange(cd.change().getKey().get()); + TestTimeUtil.incrementClock(90, TimeUnit.DAYS); + + ReviewInput rin = ReviewInput.approve(); + rin.message = "Some very late message on PS1"; + gApi.changes().id(id.get()).revision(1).review(rin); + + checker.rebuildAndCheckChanges(id); + } + + @Test + public void ignoreChangeMessageBeyondCurrentPatchSet() throws Exception { + PushOneCommit.Result r = createChange(); + PatchSet.Id psId1 = r.getPatchSetId(); + Change.Id id = psId1.getParentKey(); + gApi.changes().id(id.get()).current().review(ReviewInput.recommend()); + + r = amendChange(r.getChangeId()); + PatchSet.Id psId2 = r.getPatchSetId(); + + assertThat(db.patchSets().byChange(id)).hasSize(2); + assertThat(db.changeMessages().byPatchSet(psId2)).hasSize(1); + db.patchSets().deleteKeys(Collections.singleton(psId2)); + + checker.rebuildAndCheckChanges(psId2.getParentKey()); + setNotesMigration(true, true); + + ChangeData cd = changeDataFactory.create(db, project, id); + assertThat(cd.change().currentPatchSetId()).isEqualTo(psId1); + assertThat(cd.patchSets().stream().map(ps -> ps.getId()).collect(toList())) + .containsExactly(psId1); + PatchSet ps = cd.currentPatchSet(); + assertThat(ps).isNotNull(); + assertThat(ps.getId()).isEqualTo(psId1); + } + + @Test + public void highestNumberedPatchSetIsNotCurrent() throws Exception { + PushOneCommit.Result r1 = createChange(); + PatchSet.Id psId1 = r1.getPatchSetId(); + Change.Id id = psId1.getParentKey(); + PushOneCommit.Result r2 = amendChange(r1.getChangeId()); + PatchSet.Id psId2 = r2.getPatchSetId(); + + try (BatchUpdate bu = batchUpdateFactory.create(db, project, + identifiedUserFactory.create(user.getId()), TimeUtil.nowTs())) { + bu.addOp(id, new BatchUpdate.Op() { + @Override + public boolean updateChange(ChangeContext ctx) + throws PatchSetInfoNotAvailableException { + ctx.getChange().setCurrentPatchSet( + patchSetInfoFactory.get(ctx.getDb(), ctx.getNotes(), psId1)); + return true; + } + }); + bu.execute(); + } + ChangeNotes notes = notesFactory.create(db, project, id); + assertThat(psUtil.byChangeAsMap(db, notes).keySet()) + .containsExactly(psId1, psId2); + assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1); + + assertThat(db.changes().get(id).currentPatchSetId()).isEqualTo(psId1); + + checker.rebuildAndCheckChanges(id); + setNotesMigration(true, true); + + notes = notesFactory.create(db, project, id); + assertThat(psUtil.byChangeAsMap(db, notes).keySet()) + .containsExactly(psId1, psId2); + assertThat(notes.getChange().currentPatchSetId()).isEqualTo(psId1); + } + private void assertChangesReadOnly(RestApiException e) throws Exception { Throwable cause = e.getCause(); assertThat(cause).isInstanceOf(UpdateException.class); @@ -1086,4 +1277,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/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java new file mode 100644 index 0000000..ba04366 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -0,0 +1,193 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.server.notedb; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; +import static java.util.stream.Collectors.toList; + +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.DraftInput; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.common.ApprovalInfo; +import com.google.gerrit.extensions.common.ChangeInfo; +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.server.ReviewDbUtil; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.RepoRefCache; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.testutil.NoteDbMode; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class NoteDbPrimaryIT extends AbstractDaemonTest { + @Inject + private AllUsersName allUsers; + + @Before + public void setUp() throws Exception { + assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.READ_WRITE); + db = ReviewDbUtil.unwrapDb(db); + } + + @Test + public void updateChange() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + + gApi.changes().id(id.get()).current().review(ReviewInput.approve()); + gApi.changes().id(id.get()).current().submit(); + + ChangeInfo info = gApi.changes().id(id.get()).get(); + assertThat(info.status).isEqualTo(ChangeStatus.MERGED); + ApprovalInfo approval = + Iterables.getOnlyElement(info.labels.get("Code-Review").all); + assertThat(approval._accountId).isEqualTo(admin.id.get()); + assertThat(approval.value).isEqualTo(2); + assertThat(info.messages).hasSize(3); + assertThat(Iterables.getLast(info.messages).message) + .isEqualTo("Change has been successfully merged by " + admin.fullName); + + ChangeNotes notes = notesFactory.create(db, project, id); + assertThat(notes.getChange().getStatus()).isEqualTo(Change.Status.MERGED); + assertThat(notes.getChange().getNoteDbState()) + .isEqualTo(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + + // Writes weren't reflected in ReviewDb. + assertThat(db.changes().get(id).getStatus()).isEqualTo(Change.Status.NEW); + assertThat(db.patchSetApprovals().byChange(id)).isEmpty(); + assertThat(db.changeMessages().byChange(id)).hasSize(1); + } + + @Test + public void deleteDraftComment() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + + DraftInput din = new DraftInput(); + din.path = PushOneCommit.FILE_NAME; + din.line = 1; + din.message = "A comment"; + gApi.changes().id(id.get()).current().createDraft(din); + + CommentInfo di = Iterables.getOnlyElement( + gApi.changes().id(id.get()).current().drafts() + .get(PushOneCommit.FILE_NAME)); + assertThat(di.message).isEqualTo(din.message); + + assertThat( + db.patchComments().draftByChangeFileAuthor(id, din.path, admin.id)) + .isEmpty(); + + gApi.changes().id(id.get()).current().draft(di.id).delete(); + assertThat(gApi.changes().id(id.get()).current().drafts()).isEmpty(); + } + + @Test + public void deleteVote() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + + gApi.changes().id(id.get()).current().review(ReviewInput.approve()); + List<ApprovalInfo> approvals = + gApi.changes().id(id.get()).get().labels.get("Code-Review").all; + assertThat(approvals).hasSize(1); + assertThat(approvals.get(0).value).isEqualTo(2); + + gApi.changes().id(id.get()).reviewer(admin.id.toString()) + .deleteVote("Code-Review"); + + approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all; + assertThat(approvals).hasSize(1); + assertThat(approvals.get(0).value).isEqualTo(0); + } + + @Test + public void deleteVoteViaReview() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + + gApi.changes().id(id.get()).current().review(ReviewInput.approve()); + List<ApprovalInfo> approvals = + gApi.changes().id(id.get()).get().labels.get("Code-Review").all; + assertThat(approvals).hasSize(1); + assertThat(approvals.get(0).value).isEqualTo(2); + + gApi.changes().id(id.get()).current().review(ReviewInput.noScore()); + + approvals = gApi.changes().id(id.get()).get().labels.get("Code-Review").all; + assertThat(approvals).hasSize(1); + assertThat(approvals.get(0).value).isEqualTo(0); + } + + @Test + public void deleteReviewer() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + + gApi.changes().id(id.get()).addReviewer(user.id.toString()); + assertThat(getReviewers(id)).containsExactly(user.id); + gApi.changes().id(id.get()).reviewer(user.id.toString()).remove(); + assertThat(getReviewers(id)).isEmpty(); + } + + private void setNoteDbPrimary(Change.Id id) throws Exception { + Change c = db.changes().get(id); + assertThat(c).named("change " + id).isNotNull(); + NoteDbChangeState state = NoteDbChangeState.parse(c); + assertThat(state.getPrimaryStorage()) + .named("storage of " + id) + .isEqualTo(REVIEW_DB); + + try (Repository changeRepo = repoManager.openRepository(c.getProject()); + Repository allUsersRepo = repoManager.openRepository(allUsers)) { + assertThat( + state.isUpToDate( + new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo))) + .named("change " + id + " up to date") + .isTrue(); + } + + c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + db.changes().update(Collections.singleton(c)); + } + + private List<Account.Id> getReviewers(Change.Id id) throws Exception { + return gApi.changes().id(id.get()).get() + .reviewers.values().stream() + .flatMap(Collection::stream) + .map(a -> new Account.Id(a._accountId)) + .collect(toList()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD index bcf9c9f..622caf7 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUILD
@@ -1,16 +1,7 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') - -FLAKY_TEST_CASES=['ProjectWatchIT.java'] +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'server_project', - srcs = glob(['*IT.java'], exclude=FLAKY_TEST_CASES), - labels = ['server'], -) - -acceptance_tests( - group = 'server_project_flaky', - flaky = 1, - srcs = FLAKY_TEST_CASES, - labels = ['server', 'flaky'], + srcs = glob(["*IT.java"]), + group = "server_project", + labels = ["server"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java index 6f4cc45..8b1690f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -26,11 +26,13 @@ import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.events.CommentAddedListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.group.SystemGroupBackend; @@ -175,6 +177,34 @@ assertThat(q.blocking).isTrue(); } + @Test + public void customLabel_DisallowPostSubmit() throws Exception { + label.setFunctionName("NoOp"); + label.setAllowPostSubmit(false); + P.setFunctionName("NoOp"); + saveLabelConfig(); + + PushOneCommit.Result r = createChange(); + revision(r).review(ReviewInput.approve()); + revision(r).submit(); + + ChangeInfo info = get(r.getChangeId(), ListChangesOption.DETAILED_LABELS); + assertPermitted(info, "Code-Review", 2); + assertPermitted(info, P.getName(), 0, 1); + assertPermitted(info, label.getName()); + + ReviewInput in = new ReviewInput(); + in.label(P.getName(), P.getMax().getValue()); + revision(r).review(in); + + in = new ReviewInput(); + in.label(label.getName(), label.getMax().getValue()); + exception.expect(ResourceConflictException.class); + exception.expectMessage( + "Voting on labels disallowed after submit: " + label.getName()); + revision(r).review(in); + } + private void saveLabelConfig() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); cfg.getLabelSections().put(label.getName(), label);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java index abd84ef..e6efc0a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -19,6 +19,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.restapi.RestApiException; @@ -38,6 +39,7 @@ import java.util.List; @NoHttpd +@Sandboxed public class ProjectWatchIT extends AbstractDaemonTest { @Test public void newPatchSetsNotifyConfig() throws Exception { @@ -161,6 +163,156 @@ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); } + @Test + public void watchKeyword() throws Exception { + String watchedProject = createProject("watchedProject").get(); + setApiUser(user); + + // watch keyword in project as user + watch(watchedProject, "multimaster"); + + // push a change with keyword -> should trigger email notification + setApiUser(admin); + TestRepository<InMemoryRepository> watchedRepo = + cloneProject(new Project.NameKey(watchedProject), admin); + PushOneCommit.Result r = pushFactory + .create(db, admin.getIdent(), watchedRepo, + "Document multimaster setup", "a.txt", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification for user + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()) + .contains("Change subject: Document multimaster setup\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + sender.clear(); + + // push a change without keyword -> should not trigger email notification + r = pushFactory.create(db, admin.getIdent(), watchedRepo, + "Cleanup cache implementation", "b.txt", "b1").to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification + assertThat(sender.getMessages()).hasSize(0); + } + + @Test + public void watchAllProjects() throws Exception { + String anyProject = createProject("anyProject").get(); + setApiUser(user); + + // watch the All-Projects project to watch all projects + watch(allProjects.get(), null); + + // push a change to any project -> should trigger email notification + setApiUser(admin); + TestRepository<InMemoryRepository> anyRepo = + cloneProject(new Project.NameKey(anyProject), admin); + PushOneCommit.Result r = pushFactory + .create(db, admin.getIdent(), anyRepo, "TRIGGER", "a", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()).contains("Change subject: TRIGGER\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + } + + @Test + public void watchFileAllProjects() throws Exception { + String anyProject = createProject("anyProject").get(); + setApiUser(user); + + // watch file in All-Projects project as user to watch the file in all + // projects + watch(allProjects.get(), "file:a.txt"); + + // push a change to watched file in any project -> should trigger email + // notification for user + setApiUser(admin); + TestRepository<InMemoryRepository> anyRepo = + cloneProject(new Project.NameKey(anyProject), admin); + PushOneCommit.Result r = pushFactory + .create(db, admin.getIdent(), anyRepo, "TRIGGER", "a.txt", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification for user + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()).contains("Change subject: TRIGGER\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + sender.clear(); + + // watch project as user2 + TestAccount user2 = accounts.create("user2", "user2@test.com", "User2"); + setApiUser(user2); + watch(anyProject, null); + + // push a change to non-watched file in any project -> should not trigger + // email notification for user, only for user2 + r = pushFactory.create(db, admin.getIdent(), anyRepo, + "TRIGGER_USER2", "b.txt", "b1").to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification + messages = sender.getMessages(); + assertThat(messages).hasSize(1); + m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user2.emailAddress); + assertThat(m.body()).contains("Change subject: TRIGGER_USER2\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + } + + @Test + public void watchKeywordAllProjects() throws Exception { + String anyProject = createProject("anyProject").get(); + setApiUser(user); + + // watch keyword in project as user + watch(allProjects.get(), "multimaster"); + + // push a change with keyword to any project -> should trigger email + // notification + setApiUser(admin); + TestRepository<InMemoryRepository> anyRepo = + cloneProject(new Project.NameKey(anyProject), admin); + PushOneCommit.Result r = pushFactory + .create(db, admin.getIdent(), anyRepo, + "Document multimaster setup", "a.txt", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification for user + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()) + .contains("Change subject: Document multimaster setup\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + sender.clear(); + + // push a change without keyword to any project -> should not trigger email + // notification + r = pushFactory.create(db, admin.getIdent(), anyRepo, + "Cleanup cache implementation", "b.txt", "b1").to("refs/for/master"); + r.assertOkStatus(); + + // assert email notification + assertThat(sender.getMessages()).hasSize(0); + } + private void watch(String project, String filter) throws RestApiException { List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD index 3c91aa1..91d8d71 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUILD
@@ -1,8 +1,8 @@ -load('//gerrit-acceptance-tests:tests.bzl', 'acceptance_tests') +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") acceptance_tests( - group = 'ssh', - srcs = glob(['*IT.java']), - deps = ['//lib/commons:compress'], - labels = ['ssh'], + srcs = glob(["*IT.java"]), + group = "ssh", + labels = ["ssh"], + deps = ["//lib/commons:compress"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java index 7176254..7864cf6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -78,7 +78,7 @@ } @Test - public void testGcWithoutCapability_Error() throws Exception { + public void gcWithoutCapability_Error() throws Exception { userSshSession.exec("gerrit gc --all"); assertThat(userSshSession.hasError()).isTrue(); String error = userSshSession.getError();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java index 2865ff87..5c8d166 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -41,7 +41,7 @@ private static Gson gson = new Gson(); @Test - public void testBasicQueryJSON() throws Exception { + public void basicQueryJSON() throws Exception { String changeId1 = createChange().getChangeId(); String changeId2 = createChange().getChangeId(); @@ -68,7 +68,7 @@ } @Test - public void testAllApprovalsOptionJSON() throws Exception { + public void allApprovalsOptionJSON() throws Exception { String changeId = createChange().getChangeId(); gApi.changes().id(changeId).current().review(ReviewInput.approve()); List<ChangeAttribute> changes = executeSuccessfulQuery(changeId); @@ -83,7 +83,7 @@ } @Test - public void testAllReviewersOptionJSON() throws Exception { + public void allReviewersOptionJSON() throws Exception { String changeId = createChange().getChangeId(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; @@ -100,7 +100,7 @@ } @Test - public void testCommitMessageOptionJSON() throws Exception { + public void commitMessageOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = executeSuccessfulQuery("--commit-message " + changeId); @@ -110,7 +110,7 @@ } @Test - public void testCurrentPatchSetOptionJSON() throws Exception { + public void currentPatchSetOptionJSON() throws Exception { String changeId = createChange().getChangeId(); amendChange(changeId); @@ -133,7 +133,7 @@ } @Test - public void testPatchSetsOptionJSON() throws Exception { + public void patchSetsOptionJSON() throws Exception { String changeId = createChange().getChangeId(); amendChange(changeId); amendChange(changeId); @@ -159,7 +159,7 @@ } @Test - public void testFileOptionJSON() throws Exception { + public void fileOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = @@ -185,7 +185,7 @@ } @Test - public void testCommentOptionJSON() throws Exception { + public void commentOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = executeSuccessfulQuery(changeId); @@ -199,7 +199,7 @@ } @Test - public void testCommentOptionsInCurrentPatchSetJSON() throws Exception { + public void commentOptionsInCurrentPatchSetJSON() throws Exception { String changeId = createChange().getChangeId(); ReviewInput review = new ReviewInput(); @@ -224,7 +224,7 @@ } @Test - public void testCommentOptionInPatchSetsJSON() throws Exception { + public void commentOptionInPatchSetsJSON() throws Exception { String changeId = createChange().getChangeId(); ReviewInput review = new ReviewInput(); @@ -268,7 +268,7 @@ } @Test - public void testDependenciesOptionJSON() throws Exception { + public void dependenciesOptionJSON() throws Exception { String changeId1 = createChange().getChangeId(); String changeId2 = createChange().getChangeId(); List<ChangeAttribute> changes = executeSuccessfulQuery(changeId1); @@ -290,7 +290,7 @@ } @Test - public void testSubmitRecordsOptionJSON() throws Exception { + public void submitRecordsOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = executeSuccessfulQuery(changeId); assertThat(changes.size()).isEqualTo(1); @@ -303,7 +303,7 @@ } @Test - public void testQueryWithNonVisibleCurrentPatchSet() throws Exception { + public void queryWithNonVisibleCurrentPatchSet() throws Exception { String changeId = createChange().getChangeId(); amendChangeAsDraft(changeId); String query = "--current-patch-set --patch-sets " + changeId;
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl index ff2562d..1fd47ae 100644 --- a/gerrit-acceptance-tests/tests.bzl +++ b/gerrit-acceptance-tests/tests.bzl
@@ -1,22 +1,18 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") BOUNCYCASTLE = [ - '//lib/bouncycastle:bcpkix-without-neverlink', - '//lib/bouncycastle:bcpg-without-neverlink', + "//lib/bouncycastle:bcpkix-without-neverlink", + "//lib/bouncycastle:bcpg-without-neverlink", ] def acceptance_tests( group, - srcs, - flaky = 0, deps = [], labels = [], - source_under_test = [], #unused - vm_args = ['-Xmx256m']): + vm_args = ['-Xmx256m'], + **kwargs): junit_tests( name = group, - srcs = srcs, - flaky = flaky, deps = deps + BOUNCYCASTLE + [ '//gerrit-acceptance-tests:lib', ], @@ -24,5 +20,7 @@ 'acceptance', 'slow', ], + size = "large", jvm_flags = vm_args, + **kwargs )
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-antlr/BUILD b/gerrit-antlr/BUILD index c955ab1..8337152 100644 --- a/gerrit-antlr/BUILD +++ b/gerrit-antlr/BUILD
@@ -1,32 +1,32 @@ -load('//tools/bzl:genrule2.bzl', 'genrule2') +load("//tools/bzl:genrule2.bzl", "genrule2") java_library( - name = 'query_exception', - srcs = ['src/main/java/com/google/gerrit/server/query/QueryParseException.java'], - visibility = ['//visibility:public'], + name = "query_exception", + srcs = ["src/main/java/com/google/gerrit/server/query/QueryParseException.java"], + visibility = ["//visibility:public"], ) genrule2( - name = 'query_antlr', - srcs = ['src/main/antlr3/com/google/gerrit/server/query/Query.g'], - cmd = ' && '.join([ - '$(location //lib/antlr:antlr-tool) -o $$TMP $<', - 'cd $$TMP', - '$$ROOT/$(location @bazel_tools//tools/zip:zipper) cC $$ROOT/$@ $$(find .)' - ]), - tools = [ - '@bazel_tools//tools/zip:zipper', - '//lib/antlr:antlr-tool', - ], - out = 'query_antlr.srcjar', + name = "query_antlr", + srcs = ["src/main/antlr3/com/google/gerrit/server/query/Query.g"], + outs = ["query_antlr.srcjar"], + cmd = " && ".join([ + "$(location //lib/antlr:antlr-tool) -o $$TMP $<", + "cd $$TMP", + "$$ROOT/$(location @bazel_tools//tools/zip:zipper) cC $$ROOT/$@ $$(find .)", + ]), + tools = [ + "//lib/antlr:antlr-tool", + "@bazel_tools//tools/zip:zipper", + ], ) java_library( - name = 'query_parser', - srcs = [':query_antlr'], - deps = [ - ':query_exception', - '//lib/antlr:java_runtime', - ], - visibility = ['//visibility:public'], + name = "query_parser", + srcs = [":query_antlr"], + visibility = ["//visibility:public"], + deps = [ + ":query_exception", + "//lib/antlr:java_runtime", + ], )
diff --git a/gerrit-cache-h2/BUILD b/gerrit-cache-h2/BUILD index a70393d..45cf416 100644 --- a/gerrit-cache-h2/BUILD +++ b/gerrit-cache-h2/BUILD
@@ -1,30 +1,30 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") java_library( - name = 'cache-h2', - srcs = glob(['src/main/java/**/*.java']), - deps = [ - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-server:server', - '//lib:guava', - '//lib:h2', - '//lib/guice:guice', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', - ], - visibility = ['//visibility:public'], + name = "cache-h2", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-server:server", + "//lib:guava", + "//lib:h2", + "//lib/guice", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + ], ) junit_tests( - name = 'tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':cache-h2', - '//gerrit-server:server', - '//lib:guava', - '//lib:h2', - '//lib/guice:guice', - '//lib:junit', - ], + name = "tests", + srcs = glob(["src/test/java/**/*.java"]), + deps = [ + ":cache-h2", + "//gerrit-server:server", + "//lib:guava", + "//lib:h2", + "//lib:junit", + "//lib/guice", + ], )
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..b2ae8b0 100644 --- a/gerrit-common/BUCK +++ b/gerrit-common/BUCK
@@ -61,8 +61,8 @@ ':client', '//lib:guava', '//lib:junit', + '//lib:truth', ], - source_under_test = [':client'], ) java_test(
diff --git a/gerrit-common/BUILD b/gerrit-common/BUILD index 86ba087..4389080 100644 --- a/gerrit-common/BUILD +++ b/gerrit-common/BUILD
@@ -1,77 +1,86 @@ -load('//tools/bzl:gwt.bzl', 'gwt_module') -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:gwt.bzl", "gwt_module") +load("//tools/bzl:junit.bzl", "junit_tests") -SRC = 'src/main/java/com/google/gerrit/' +SRC = "src/main/java/com/google/gerrit/" ANNOTATIONS = [ - SRC + x for x in [ - 'common/Nullable.java', - 'common/audit/Audit.java', - 'common/auth/SignInRequired.java', - ] + SRC + x + for x in [ + "common/Nullable.java", + "common/audit/Audit.java", + "common/auth/SignInRequired.java", + ] ] java_library( - name = 'annotations', - srcs = ANNOTATIONS, - visibility = ['//visibility:public'], + name = "annotations", + srcs = ANNOTATIONS, + visibility = ["//visibility:public"], ) gwt_module( - name = 'client', - srcs = glob([SRC + 'common/**/*.java']), - gwt_xml = SRC + 'Common.gwt.xml', - exported_deps = [ - '//gerrit-extension-api:api', - '//gerrit-prettify:client', - '//lib:guava', - '//lib:gwtorm_client', - '//lib:servlet-api-3_1', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/joda:joda-time', - '//lib/log:api', - ], - visibility = ['//visibility:public'], + name = "client", + srcs = glob([SRC + "common/**/*.java"]), + exported_deps = [ + "//gerrit-extension-api:api", + "//gerrit-prettify:client", + "//lib:guava", + "//lib:gwtorm_client", + "//lib:servlet-api-3_1", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/joda:joda-time", + "//lib/log:api", + ], + gwt_xml = SRC + "Common.gwt.xml", + visibility = ["//visibility:public"], ) java_library( - name = 'server', - srcs = glob([SRC + 'common/**/*.java'], exclude = ANNOTATIONS), - deps = [ - ':annotations', - '//gerrit-extension-api:api', - '//gerrit-patch-jgit:server', - '//gerrit-prettify:server', - '//gerrit-reviewdb:server', - '//lib:guava', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:servlet-api-3_1', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/joda:joda-time', - '//lib/log:api', - ], - visibility = ['//visibility:public'], + name = "server", + srcs = glob( + [SRC + "common/**/*.java"], + exclude = ANNOTATIONS, + ), + visibility = ["//visibility:public"], + deps = [ + ":annotations", + "//gerrit-extension-api:api", + "//gerrit-patch-jgit:server", + "//gerrit-prettify:server", + "//gerrit-reviewdb:server", + "//lib:guava", + "//lib:gwtjsonrpc", + "//lib:gwtorm", + "//lib:servlet-api-3_1", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/joda:joda-time", + "//lib/log:api", + ], ) -TEST = 'src/test/java/com/google/gerrit/common/' -AUTO_VALUE_TEST_SRCS = [TEST + 'AutoValueTest.java'] +TEST = "src/test/java/com/google/gerrit/common/" + +AUTO_VALUE_TEST_SRCS = [TEST + "AutoValueTest.java"] junit_tests( - name = 'client_tests', - srcs = glob(['src/test/java/**/*.java'], exclude = AUTO_VALUE_TEST_SRCS), - deps = [ - ':client', - '//lib:guava', - '//lib:junit', - ], + name = "client_tests", + srcs = glob( + ["src/test/java/**/*.java"], + exclude = AUTO_VALUE_TEST_SRCS, + ), + deps = [ + ":client", + "//lib:guava", + "//lib:junit", + "//lib:truth", + ], ) junit_tests( - name = 'auto_value_tests', - srcs = AUTO_VALUE_TEST_SRCS, - deps = [ - '//lib:truth', - '//lib/auto:auto-value', - ], + name = "auto_value_tests", + srcs = AUTO_VALUE_TEST_SRCS, + deps = [ + "//lib:truth", + "//lib/auto:auto-value", + ], )
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/FilenameComparator.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java new file mode 100644 index 0000000..535130a1 --- /dev/null +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java
@@ -0,0 +1,64 @@ +// 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.common.data; + +import com.google.gerrit.reviewdb.client.Patch; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; + +public class FilenameComparator implements Comparator<String> { + public static final FilenameComparator INSTANCE = new FilenameComparator(); + + private static final Set<String> cppHeaderSuffixes = new HashSet<>( + Arrays.asList(".h", ".hxx", ".hpp")); + + private FilenameComparator() {} + + @Override + public int compare(final String path1, final String path2) { + if (Patch.COMMIT_MSG.equals(path1) && Patch.COMMIT_MSG.equals(path2)) { + return 0; + } else if (Patch.COMMIT_MSG.equals(path1)) { + return -1; + } else if (Patch.COMMIT_MSG.equals(path2)) { + return 1; + } + if (Patch.MERGE_LIST.equals(path1) && Patch.MERGE_LIST.equals(path2)) { + return 0; + } else if (Patch.MERGE_LIST.equals(path1)) { + return -1; + } else if (Patch.MERGE_LIST.equals(path2)) { + return 1; + } + + int s1 = path1.lastIndexOf('.'); + int s2 = path2.lastIndexOf('.'); + if (s1 > 0 && s2 > 0 && + path1.substring(0, s1).equals(path2.substring(0, s2))) { + String suffixA = path1.substring(s1); + String suffixB = path2.substring(s2); + // C++ and C: give priority to header files (.h/.hpp/...) + if (cppHeaderSuffixes.contains(suffixA)) { + return -1; + } else if (cppHeaderSuffixes.contains(suffixB)) { + return 1; + } + } + return path1.compareTo(path2); + } +}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java index b1e1243..7a8ac77 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -25,6 +25,7 @@ import java.util.Map; public class LabelType { + public static final boolean DEF_ALLOW_POST_SUBMIT = true; public static final boolean DEF_CAN_OVERRIDE = true; public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CHANGE = true; public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false; @@ -104,6 +105,7 @@ protected boolean copyAllScoresOnTrivialRebase; protected boolean copyAllScoresIfNoCodeChange; protected boolean copyAllScoresIfNoChange; + protected boolean allowPostSubmit; protected short defaultValue; protected List<LabelValue> values; @@ -144,6 +146,7 @@ DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE); setCopyMaxScore(DEF_COPY_MAX_SCORE); setCopyMinScore(DEF_COPY_MIN_SCORE); + setAllowPostSubmit(DEF_ALLOW_POST_SUBMIT); } public String getName() { @@ -174,6 +177,14 @@ this.canOverride = canOverride; } + public boolean allowPostSubmit() { + return allowPostSubmit; + } + + public void setAllowPostSubmit(boolean allowPostSubmit) { + this.allowPostSubmit = allowPostSubmit; + } + public void setRefPatterns(List<String> refPatterns) { this.refPatterns = refPatterns; } @@ -193,8 +204,7 @@ if (values.isEmpty()) { return null; } - final LabelValue v = values.get(values.size() - 1); - return v.getValue() > 0 ? v : null; + return values.get(values.size() - 1); } public short getDefaultValue() {
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/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java index 9dccf0c..3dc41fe 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -16,14 +16,27 @@ import com.google.gerrit.reviewdb.client.Account; +import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * Describes the state required to submit a change. */ public class SubmitRecord { + public static Optional<SubmitRecord> findOkRecord( + Collection<SubmitRecord> in) { + if (in == null) { + return Optional.empty(); + } + return in.stream().filter(r -> r.status == Status.OK).findFirst(); + } + public enum Status { + // NOTE: These values are persisted in the index, so deleting or changing + // the name of any values requires a schema upgrade. + /** The change is ready for submission. */ OK, @@ -50,6 +63,9 @@ public static class Label { public enum Status { + // NOTE: These values are persisted in the index, so deleting or changing + // the name of any values requires a schema upgrade. + /** * This label provides what is necessary for submission. * <p>
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-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java index ea3721e..9c78390 100644 --- a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java +++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -20,12 +20,12 @@ public class EncodePathSeparatorTest { @Test - public void testDefaultBehaviour() { + public void defaultBehaviour() { assertEquals("a/b", new GitwebType().replacePathSeparator("a/b")); } @Test - public void testExclamationMark() { + public void exclamationMark() { GitwebType gitwebType = new GitwebType(); gitwebType.setPathSeparator('!'); assertEquals("a!b", gitwebType.replacePathSeparator("a/b"));
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/FilenameComparatorTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/FilenameComparatorTest.java new file mode 100644 index 0000000..ef8f0a9 --- /dev/null +++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/FilenameComparatorTest.java
@@ -0,0 +1,65 @@ +// 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.common.data; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class FilenameComparatorTest { + private FilenameComparator comparator = FilenameComparator.INSTANCE; + + @Test + public void basicPaths() { + assertThat(comparator.compare( + "abc/xyz/FileOne.java", "xyz/abc/FileTwo.java")).isLessThan(0); + assertThat(comparator.compare( + "abc/xyz/FileOne.java", "abc/xyz/FileOne.java")).isEqualTo(0); + assertThat(comparator.compare( + "zzz/yyy/FileOne.java", "abc/xyz/FileOne.java")).isGreaterThan(0); + } + + @Test + public void specialPaths() { + assertThat(comparator.compare( + "ABC/xyz/FileOne.java", "/COMMIT_MSG")).isGreaterThan(0); + assertThat(comparator.compare( + "/COMMIT_MSG", "ABC/xyz/FileOne.java")).isLessThan(0); + + assertThat(comparator.compare( + "ABC/xyz/FileOne.java", "/MERGE_LIST")).isGreaterThan(0); + assertThat(comparator.compare( + "/MERGE_LIST", "ABC/xyz/FileOne.java")).isLessThan(0); + + assertThat(comparator.compare( + "/COMMIT_MSG", "/MERGE_LIST")).isLessThan(0); + assertThat(comparator.compare( + "/MERGE_LIST", "/COMMIT_MSG")).isGreaterThan(0); + + assertThat(comparator.compare( + "/COMMIT_MSG", "/COMMIT_MSG")).isEqualTo(0); + assertThat(comparator.compare( + "/MERGE_LIST", "/MERGE_LIST")).isEqualTo(0); + } + + @Test + public void cppExtensions() { + assertThat(comparator.compare("abc/file.h", "abc/file.cc")).isLessThan(0); + assertThat(comparator.compare("abc/file.c", "abc/file.hpp")) + .isGreaterThan(0); + assertThat(comparator.compare("abc..xyz.file.h", "abc.xyz.file.cc")) + .isLessThan(0); + } +} \ No newline at end of file
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java index b350a27..b7fb17f 100644 --- a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java +++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -27,7 +27,7 @@ public class ParameterizedStringTest { @Test - public void testEmptyString() { + public void emptyString() { final ParameterizedString p = new ParameterizedString(""); assertEquals("", p.getPattern()); assertEquals("", p.getRawPattern()); @@ -40,7 +40,7 @@ } @Test - public void testAsis1() { + public void asis1() { final ParameterizedString p = ParameterizedString.asis("${bar}c"); assertEquals("${bar}c", p.getPattern()); assertEquals("${bar}c", p.getRawPattern()); @@ -54,7 +54,7 @@ } @Test - public void testReplace1() { + public void replace1() { final ParameterizedString p = new ParameterizedString("${bar}c"); assertEquals("${bar}c", p.getPattern()); assertEquals("{0}c", p.getRawPattern()); @@ -70,7 +70,7 @@ } @Test - public void testReplace2() { + public void replace2() { final ParameterizedString p = new ParameterizedString("a${bar}c"); assertEquals("a${bar}c", p.getPattern()); assertEquals("a{0}c", p.getRawPattern()); @@ -86,7 +86,7 @@ } @Test - public void testReplace3() { + public void replace3() { final ParameterizedString p = new ParameterizedString("a${bar}"); assertEquals("a${bar}", p.getPattern()); assertEquals("a{0}", p.getRawPattern()); @@ -102,7 +102,7 @@ } @Test - public void testReplace4() { + public void replace4() { final ParameterizedString p = new ParameterizedString("a${bar}c"); assertEquals("a${bar}c", p.getPattern()); assertEquals("a{0}c", p.getRawPattern()); @@ -117,7 +117,7 @@ } @Test - public void testReplaceToLowerCase() { + public void replaceToLowerCase() { final ParameterizedString p = new ParameterizedString("${a.toLowerCase}"); assertEquals(1, p.getParameterNames().size()); assertTrue(p.getParameterNames().contains("a")); @@ -138,7 +138,7 @@ } @Test - public void testReplaceToUpperCase() { + public void replaceToUpperCase() { final ParameterizedString p = new ParameterizedString("${a.toUpperCase}"); assertEquals(1, p.getParameterNames().size()); assertTrue(p.getParameterNames().contains("a")); @@ -159,7 +159,7 @@ } @Test - public void testReplaceLocalName() { + public void replaceLocalName() { final ParameterizedString p = new ParameterizedString("${a.localPart}"); assertEquals(1, p.getParameterNames().size()); assertTrue(p.getParameterNames().contains("a")); @@ -180,7 +180,7 @@ } @Test - public void testUndefinedFunctionName() { + public void undefinedFunctionName() { ParameterizedString p = new ParameterizedString( "hi, ${userName.toUpperCase},your eamil address is '${email.toLowerCase.localPart}'.right?"); @@ -200,7 +200,7 @@ } @Test - public void testReplaceToUpperCaseToLowerCase() { + public void replaceToUpperCaseToLowerCase() { final ParameterizedString p = new ParameterizedString("${a.toUpperCase.toLowerCase}"); assertEquals(1, p.getParameterNames().size()); @@ -222,7 +222,7 @@ } @Test - public void testReplaceToUpperCaseLocalName() { + public void replaceToUpperCaseLocalName() { final ParameterizedString p = new ParameterizedString("${a.toUpperCase.localPart}"); assertEquals(1, p.getParameterNames().size()); @@ -244,7 +244,7 @@ } @Test - public void testReplaceToUpperCaseAnUndefinedMethod() { + public void replaceToUpperCaseAnUndefinedMethod() { final ParameterizedString p = new ParameterizedString("${a.toUpperCase.anUndefinedMethod}"); assertEquals(1, p.getParameterNames().size()); @@ -266,7 +266,7 @@ } @Test - public void testReplaceLocalNameToUpperCase() { + public void replaceLocalNameToUpperCase() { final ParameterizedString p = new ParameterizedString("${a.localPart.toUpperCase}"); assertEquals(1, p.getParameterNames().size()); @@ -288,7 +288,7 @@ } @Test - public void testReplaceLocalNameToLowerCase() { + public void replaceLocalNameToLowerCase() { final ParameterizedString p = new ParameterizedString("${a.localPart.toLowerCase}"); assertEquals(1, p.getParameterNames().size()); @@ -310,7 +310,7 @@ } @Test - public void testReplaceLocalNameAnUndefinedMethod() { + public void replaceLocalNameAnUndefinedMethod() { final ParameterizedString p = new ParameterizedString("${a.localPart.anUndefinedMethod}"); assertEquals(1, p.getParameterNames().size()); @@ -332,7 +332,7 @@ } @Test - public void testReplaceToLowerCaseToUpperCase() { + public void replaceToLowerCaseToUpperCase() { final ParameterizedString p = new ParameterizedString("${a.toLowerCase.toUpperCase}"); assertEquals(1, p.getParameterNames().size()); @@ -354,7 +354,7 @@ } @Test - public void testReplaceToLowerCaseLocalName() { + public void replaceToLowerCaseLocalName() { final ParameterizedString p = new ParameterizedString("${a.toLowerCase.localPart}"); assertEquals(1, p.getParameterNames().size()); @@ -376,7 +376,7 @@ } @Test - public void testReplaceToLowerCaseAnUndefinedMethod() { + public void replaceToLowerCaseAnUndefinedMethod() { final ParameterizedString p = new ParameterizedString("${a.toLowerCase.anUndefinedMethod}"); assertEquals(1, p.getParameterNames().size()); @@ -398,7 +398,7 @@ } @Test - public void testReplaceSubmitTooltipWithVariables() { + public void replaceSubmitTooltipWithVariables() { ParameterizedString p = new ParameterizedString( "Submit patch set ${patchSet} into ${branch}"); assertEquals(2, p.getParameterNames().size()); @@ -415,7 +415,7 @@ } @Test - public void testReplaceSubmitTooltipWithoutVariables() { + public void replaceSubmitTooltipWithoutVariables() { ParameterizedString p = new ParameterizedString( "Submit patch set 40 into master"); Map<String, String> params = ImmutableMap.of(
diff --git a/gerrit-elasticsearch/BUCK b/gerrit-elasticsearch/BUCK new file mode 100644 index 0000000..f2dc3de --- /dev/null +++ b/gerrit-elasticsearch/BUCK
@@ -0,0 +1,50 @@ +java_library( + name = 'elasticsearch', + srcs = glob(['src/main/java/**/*.java']), + deps = [ + '//gerrit-antlr:query_exception', + '//gerrit-extension-api:api', + '//gerrit-reviewdb:client', + '//gerrit-reviewdb:server', + '//gerrit-server:server', + '//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', 'flaky'], + srcs = glob(['src/test/java/**/*.java']), + deps = [ + ':elasticsearch', + '//gerrit-extension-api:api', + '//gerrit-reviewdb:server', + '//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..c0ffd5b --- /dev/null +++ b/gerrit-elasticsearch/BUILD
@@ -0,0 +1,69 @@ +java_library( + name = "elasticsearch", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-antlr:query_exception", + "//gerrit-extension-api:api", + "//gerrit-reviewdb:client", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:gson", + "//lib:guava", + "//lib:gwtorm", + "//lib:protobuf", + "//lib/commons:codec", + "//lib/commons:lang", + "//lib/elasticsearch", + "//lib/elasticsearch:jest", + "//lib/elasticsearch:jest-common", + "//lib/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", + ], +) + +load("//tools/bzl:junit.bzl", "junit_tests") + +java_library( + name = "elasticsearch_test_utils", + testonly = 1, + srcs = glob(["src/test/java/**/ElasticTestUtils.java"]), + deps = [ + ":elasticsearch", + "//gerrit-extension-api:api", + "//gerrit-server:server", + "//lib:gson", + "//lib:guava", + "//lib:junit", + "//lib:truth", + "//lib/elasticsearch", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/jgit/org.eclipse.jgit.junit:junit", + ], +) + +junit_tests( + name = "elasticsearch_tests", + size = "large", + srcs = glob(["src/test/java/**/*Test.java"]), + flaky = 1, + tags = [ + "elastic", + "flaky", + ], + deps = [ + ":elasticsearch", + ":elasticsearch_test_utils", + "//gerrit-server:query_tests_code", + "//gerrit-server:server", + "//gerrit-server:testutil", + "//lib/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..917238b --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -0,0 +1,220 @@ +// 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 org.apache.commons.codec.binary.Base64.decodeBase64; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; + +import com.google.common.base.Strings; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Iterables; +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.IndexUtils; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.Schema.Values; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gwtorm.protobuf.ProtobufCodec; + +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.List; +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> { + protected static <T> List<T> decodeProtos(JsonObject doc, String fieldName, + ProtobufCodec<T> codec) { + JsonArray field = doc.getAsJsonArray(fieldName); + if (field == null) { + return null; + } + return FluentIterable.from(field) + .transform(i -> codec.decode(decodeBase64(i.toString()))) + .toList(); + } + + 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; + + AbstractElasticIndex(@GerritServerConfig Config cfg, + FillArgs fillArgs, + SitePaths sitePaths, + Schema<V> schema, + String indexName) { + 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 = String.format("%s%s%04d", + Strings.nullToEmpty(cfg.getString("index", null, "prefix")), + indexName, + schema.getVersion()); + + // 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/ElasticAccountIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java new file mode 100644 index 0000000..8b0ea41 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -0,0 +1,234 @@ +// 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 static com.google.gerrit.server.index.account.AccountField.ID; +import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Account.Id; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +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.IndexUtils; +import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.account.AccountField; +import com.google.gerrit.server.index.account.AccountIndex; +import com.google.gerrit.server.query.DataSource; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +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.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +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; + +class ElasticAccountIndex extends AbstractElasticIndex<Account.Id, AccountState> + implements AccountIndex { + static class AccountMapping { + MappingProperties accounts; + + AccountMapping(Schema<AccountState> schema) { + this.accounts = ElasticMapping.createMapping(schema); + } + } + + static final String ACCOUNTS = "accounts"; + static final String ACCOUNTS_PREFIX = ACCOUNTS + "_"; + + private static final Logger log = + LoggerFactory.getLogger(ElasticAccountIndex.class); + + private final Gson gson; + private final AccountMapping mapping; + private final AccountCache accountCache; + private final ElasticQueryBuilder queryBuilder; + + @AssistedInject + ElasticAccountIndex( + @GerritServerConfig Config cfg, + FillArgs fillArgs, + SitePaths sitePaths, + AccountCache accountCache, + @Assisted Schema<AccountState> schema) { + super(cfg, fillArgs, sitePaths, schema, ACCOUNTS_PREFIX); + this.accountCache = accountCache; + this.mapping = new AccountMapping(schema); + this.queryBuilder = new ElasticQueryBuilder(); + this.gson = new GsonBuilder() + .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create(); + } + + @Override + public void replace(AccountState as) throws IOException { + Bulk bulk = new Bulk.Builder() + .defaultIndex(indexName) + .defaultType(ACCOUNTS) + .addAction(insert(ACCOUNTS, as)) + .refresh(refresh) + .build(); + JestResult result = client.execute(bulk); + if (!result.isSucceeded()) { + throw new IOException( + String.format("Failed to replace account %s in index %s: %s", + as.getAccount().getId(), indexName, result.getErrorMessage())); + } + } + + @Override + public DataSource<AccountState> getSource(Predicate<AccountState> p, + QueryOptions opts) throws QueryParseException { + return new QuerySource(p, opts); + } + + @Override + protected Builder addActions(Builder builder, Id c) { + return builder.addAction(delete(ACCOUNTS, c)); + } + + @Override + protected String getMappings() { + ImmutableMap<String, AccountMapping> mappings = + ImmutableMap.of("mappings", mapping); + return gson.toJson(mappings); + } + + @Override + protected String getId(AccountState as) { + return as.getAccount().getId().toString(); + } + + private class QuerySource implements DataSource<AccountState> { + private final Search search; + private final Set<String> fields; + + QuerySource(Predicate<AccountState> p, QueryOptions opts) + throws QueryParseException { + QueryBuilder qb = queryBuilder.toQueryBuilder(p); + fields = IndexUtils.accountFields(opts); + SearchSourceBuilder searchSource = new SearchSourceBuilder() + .query(qb) + .from(opts.start()) + .size(opts.limit()) + .fields(Lists.newArrayList(fields)); + + Sort sort = new Sort(AccountField.ID.getName(), Sorting.ASC); + sort.setIgnoreUnmapped(); + + search = new Search.Builder(searchSource.toString()) + .addType(ACCOUNTS) + .addIndex(indexName) + .addSort(ImmutableList.of(sort)) + .build(); + } + + @Override + public int getCardinality() { + return 10; + } + + @Override + public ResultSet<AccountState> read() throws OrmException { + try { + List<AccountState> 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<AccountState> r = Collections.unmodifiableList(results); + return new ResultSet<AccountState>() { + @Override + public Iterator<AccountState> iterator() { + return r.iterator(); + } + + @Override + public List<AccountState> toList() { + return r; + } + + @Override + public void close() { + // Do nothing. + } + }; + } catch (IOException e) { + throw new OrmException(e); + } + } + + @Override + public String toString() { + return search.toString(); + } + + private AccountState toChangeData(JsonElement json) { + JsonElement source = json.getAsJsonObject().get("_source"); + if (source == null) { + source = json.getAsJsonObject().get("fields"); + } + return toAccountState(source); + } + + private AccountState toAccountState(JsonElement element) { + Account.Id id = new Account.Id( + element.getAsJsonObject().get(ID.getName()).getAsInt()); + // Use the AccountCache rather than depending on any stored fields in the + // document (of which there shouldn't be any. The most expensive part to + // compute anyway is the effective group IDs, and we don't have a good way + // to reindex when those change. + return accountCache.get(id); + } + } +}
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..13d9ad6 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -0,0 +1,398 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.codec.binary.Base64.decodeBase64; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; +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.FillArgs; +import com.google.gerrit.server.index.IndexUtils; +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.project.SubmitRuleOptions; +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.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) { + MappingProperties mapping = ElasticMapping.createMapping(schema); + this.openChanges = mapping; + this.closedChanges = mapping; + } + } + + static final String CHANGES_PREFIX = "changes_"; + 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, CHANGES_PREFIX); + this.db = db; + this.changeDataFactory = changeDataFactory; + mapping = new ChangeMapping(schema); + + this.queryBuilder = new ElasticQueryBuilder(); + this.gson = new GsonBuilder() + .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create(); + } + + @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.changeFields(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()); + } + + decodeSubmitRecords(source, + ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(), + ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd); + decodeSubmitRecords(source, + ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(), + ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd); + + if (source.get(ChangeField.REF_STATE.getName()) != null) { + JsonArray refStates = + source.get(ChangeField.REF_STATE.getName()).getAsJsonArray(); + cd.setRefStates( + Iterables.transform( + refStates, e -> Base64.decodeBase64(e.getAsString()))); + } + if (source.get(ChangeField.REF_STATE_PATTERN.getName()) != null) { + JsonArray refStatePatterns = source.get( + ChangeField.REF_STATE_PATTERN.getName()).getAsJsonArray(); + cd.setRefStatePatterns( + Iterables.transform( + refStatePatterns, e -> Base64.decodeBase64(e.getAsString()))); + } + + return cd; + } + + private void decodeSubmitRecords(JsonObject doc, String fieldName, + SubmitRuleOptions opts, ChangeData out) { + JsonArray records = doc.getAsJsonArray(fieldName); + if (records == null) { + return; + } + ChangeField.parseSubmitRecords( + FluentIterable.from(records) + .transform(i -> new String(decodeBase64(i.toString()), UTF_8)) + .toList(), + opts, out); + } + } +}
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..3762368 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -0,0 +1,70 @@ +// 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.lifecycle.LifecycleModule; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.index.IndexConfig; +import com.google.gerrit.server.index.IndexModule; +import com.google.gerrit.server.index.SingleVersionModule; +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() + .implement(AccountIndex.class, ElasticAccountIndex.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..45f686f --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticMapping.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.elasticsearch; + +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.server.index.FieldDef; +import com.google.gerrit.server.index.FieldType; +import com.google.gerrit.server.index.Schema; + +import java.util.Map; + +class ElasticMapping { + static MappingProperties createMapping(Schema<?> schema) { + ElasticMapping.Builder mapping = new ElasticMapping.Builder(); + for (FieldDef<?, ?> field : schema.getFields().values()) { + String name = field.getName(); + FieldType<?> fieldType = field.getType(); + if (fieldType == FieldType.EXACT) { + mapping.addExactField(name); + } else if (fieldType == FieldType.TIMESTAMP) { + mapping.addTimestamp(name); + } else if (fieldType == FieldType.INTEGER + || fieldType == FieldType.INTEGER_RANGE + || fieldType == FieldType.LONG) { + mapping.addNumber(name); + } else if (fieldType == FieldType.PREFIX + || fieldType == FieldType.FULL_TEXT + || fieldType == FieldType.STORED_ONLY) { + mapping.addString(name); + } else { + throw new IllegalStateException( + "Unsupported field type: " + fieldType.getName()); + } + } + return mapping.build(); + } + + 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..22f3d76 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticQueryBuilder.java
@@ -0,0 +1,182 @@ +// 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 java.time.Instant; + +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(Instant.ofEpochMilli(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(Instant.ofEpochMilli(r.getMinTimestamp().getTime())); + } + return QueryBuilders.rangeQuery(r.getField().getName()) + .gte(Instant.ofEpochMilli(r.getMinTimestamp().getTime())) + .lte(Instant.ofEpochMilli(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/ElasticQueryAccountsTest.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java new file mode 100644 index 0000000..68759df --- /dev/null +++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.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.elasticsearch; + +import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo; +import com.google.gerrit.server.query.account.AbstractQueryAccountsTest; +import com.google.gerrit.testutil.InMemoryModule; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.util.concurrent.ExecutionException; + +public class ElasticQueryAccountsTest extends AbstractQueryAccountsTest { + private static ElasticNodeInfo nodeInfo; + + @BeforeClass + public static void startIndexService() + throws InterruptedException, ExecutionException { + if (nodeInfo != null) { + // do not start Elasticsearch twice + return; + } + nodeInfo = ElasticTestUtils.startElasticsearchNode(); + ElasticTestUtils.createAllIndexes(nodeInfo); + } + + @AfterClass + public static void stopElasticsearchServer() { + if (nodeInfo != null) { + nodeInfo.node.close(); + nodeInfo.elasticDir.delete(); + nodeInfo = null; + } + } + + @After + public void cleanupIndex() { + if (nodeInfo != null) { + ElasticTestUtils.deleteAllIndexes(nodeInfo); + ElasticTestUtils.createAllIndexes(nodeInfo); + } + } + + @Override + protected Injector createInjector() { + Config elasticsearchConfig = new Config(config); + InMemoryModule.setDefaults(elasticsearchConfig); + ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port); + return Guice.createInjector( + new InMemoryModule(elasticsearchConfig, notesMigration)); + } +}
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..95dbe5b --- /dev/null +++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -0,0 +1,82 @@ +// 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.elasticsearch.ElasticTestUtils.ElasticNodeInfo; +import com.google.gerrit.server.query.change.AbstractQueryChangesTest; +import com.google.gerrit.testutil.InMemoryModule; +import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; + +public class ElasticQueryChangesTest extends AbstractQueryChangesTest { + private static ElasticNodeInfo nodeInfo; + + @BeforeClass + public static void startIndexService() + throws InterruptedException, ExecutionException { + if (nodeInfo != null) { + // do not start Elasticsearch twice + return; + } + nodeInfo = ElasticTestUtils.startElasticsearchNode(); + + ElasticTestUtils.createAllIndexes(nodeInfo); + } + + @After + public void cleanupIndex() { + if (nodeInfo != null) { + ElasticTestUtils.deleteAllIndexes(nodeInfo); + ElasticTestUtils.createAllIndexes(nodeInfo); + } + } + + @AfterClass + public static void stopElasticsearchServer() { + if (nodeInfo != null) { + nodeInfo.node.close(); + nodeInfo.elasticDir.delete(); + nodeInfo = null; + } + } + + @Override + protected Injector createInjector() { + Config elasticsearchConfig = new Config(config); + InMemoryModule.setDefaults(elasticsearchConfig); + ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port); + return Guice.createInjector( + new InMemoryModule(elasticsearchConfig, notesMigration)); + } + + @Test + public void byOwnerInvalidQuery() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + insert(repo, newChange(repo), userId); + String nameEmail = user.asIdentifiedUser().getNameEmail(); + assertQuery("owner: \"" + nameEmail + "\"\\"); + } + +}
diff --git a/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java new file mode 100644 index 0000000..9d6c808 --- /dev/null +++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -0,0 +1,188 @@ +// 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 static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.elasticsearch.ElasticAccountIndex.ACCOUNTS_PREFIX; +import static com.google.gerrit.elasticsearch.ElasticChangeIndex.CHANGES_PREFIX; +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.ElasticAccountIndex.AccountMapping; +import com.google.gerrit.elasticsearch.ElasticChangeIndex.ChangeMapping; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.index.IndexModule.IndexType; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.account.AccountSchemaDefinitions; +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +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 java.io.File; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +final class ElasticTestUtils { + static final Gson gson = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + + static class ElasticNodeInfo { + final Node node; + final String port; + final File elasticDir; + + private ElasticNodeInfo(Node node, File rootDir, String port) { + this.node = node; + this.port = port; + this.elasticDir = rootDir; + } + } + + static void configure(Config config, String port) { + config.setEnum("index", null, "type", IndexType.ELASTICSEARCH); + config.setString("index", null, "protocol", "http"); + config.setString("index", null, "hostname", "localhost"); + config.setString("index", null, "port", port); + config.setBoolean("index", "elasticsearch", "test", true); + } + + static ElasticNodeInfo startElasticsearchNode() + throws InterruptedException, ExecutionException { + File 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()) + .put("transport.tcp.connect_timeout", "60s") + .build(); + + // Start the node + Node node = NodeBuilder.nodeBuilder() + .settings(settings) + .node(); + + // Wait for it to be ready + node.client() + .admin() + .cluster() + .prepareHealth() + .setWaitForYellowStatus() + .execute() + .actionGet(); + + assertThat(node.isClosed()).isFalse(); + return new ElasticNodeInfo(node, elasticDir, getHttpPort(node)); + } + + static void deleteAllIndexes(ElasticNodeInfo nodeInfo) { + nodeInfo.node.client().admin().indices().prepareDelete("_all").execute(); + } + + static class NodeInfo { + String httpAddress; + } + + static class Info { + Map<String, NodeInfo> nodes; + } + + static void createAllIndexes(ElasticNodeInfo nodeInfo) { + Schema<ChangeData> changeSchema = + ChangeSchemaDefinitions.INSTANCE.getLatest(); + ChangeMapping openChangesMapping = new ChangeMapping(changeSchema); + ChangeMapping closedChangesMapping = new ChangeMapping(changeSchema); + openChangesMapping.closedChanges = null; + closedChangesMapping.openChanges = null; + nodeInfo.node + .client() + .admin() + .indices() + .prepareCreate( + String.format("%s%04d", CHANGES_PREFIX, changeSchema.getVersion())) + .addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping)) + .addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping)) + .execute() + .actionGet(); + + Schema<AccountState> accountSchema = + AccountSchemaDefinitions.INSTANCE.getLatest(); + AccountMapping accountMapping = new AccountMapping(accountSchema); + nodeInfo.node + .client() + .admin() + .indices() + .prepareCreate( + String.format( + "%s%04d", ACCOUNTS_PREFIX, accountSchema.getVersion())) + .addMapping(ElasticAccountIndex.ACCOUNTS, gson.toJson(accountMapping)) + .execute() + .actionGet(); + } + + private static String getHttpPort(Node node) + 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); + if (info.nodes == null || info.nodes.size() != 1) { + throw new RuntimeException( + "Cannot extract local Elasticsearch http port"); + } + Iterator<NodeInfo> values = info.nodes.values().iterator(); + String httpAddress = values.next().httpAddress; + if (Strings.isNullOrEmpty(httpAddress)) { + throw new RuntimeException( + "Cannot extract local Elasticsearch http port"); + } + if (httpAddress.indexOf(':') < 0) { + throw new RuntimeException( + "Seems that port is not included in Elasticsearch http_address"); + } + return httpAddress.substring(httpAddress.indexOf(':') + 1, + httpAddress.length()); + } + + private ElasticTestUtils() { + // hide default constructor + } +}
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK index 61cd406..19c1781 100644 --- a/gerrit-extension-api/BUCK +++ b/gerrit-extension-api/BUCK
@@ -61,20 +61,20 @@ java_test( name = 'api_tests', - srcs = glob(['src/test/java/**/*.java']), + srcs = glob(['src/test/java/**/*Test.java']), deps = [ ':api', + '//gerrit-test-util:test_util', '//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..2c59108 100644 --- a/gerrit-extension-api/BUILD +++ b/gerrit-extension-api/BUILD
@@ -1,46 +1,75 @@ -load('//tools/bzl:gwt.bzl', 'gwt_module') +load("//lib:guava.bzl", "GUAVA_DOC_URL") +load("//lib/jgit:jgit.bzl", "JGIT_DOC_URL") +load("//tools/bzl:gwt.bzl", "gwt_module") +load("//tools/bzl:junit.bzl", "junit_tests") -SRC = 'src/main/java/com/google/gerrit/extensions/' -SRCS = glob([SRC + '**/*.java']) +SRC = "src/main/java/com/google/gerrit/extensions/" -EXT_API_SRCS = glob([SRC + 'client/*.java']) +SRCS = glob([SRC + "**/*.java"]) + +EXT_API_SRCS = glob([SRC + "client/*.java"]) gwt_module( - name = 'client', - srcs = EXT_API_SRCS, - gwt_xml = SRC + 'Extensions.gwt.xml', - visibility = ['//visibility:public'], + name = "client", + srcs = EXT_API_SRCS, + gwt_xml = SRC + "Extensions.gwt.xml", + visibility = ["//visibility:public"], ) java_binary( - name = 'extension-api', - main_class = 'Dummy', - runtime_deps = [':lib'], - visibility = ['//visibility:public'], + name = "extension-api", + main_class = "Dummy", + visibility = ["//visibility:public"], + runtime_deps = [":lib"], ) java_library( - name = 'lib', - exports = [ - ':api', - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib:servlet-api-3_1', - ], - visibility = ['//visibility:public'], + name = "lib", + visibility = ["//visibility:public"], + exports = [ + ":api", + "//lib:guava", + "//lib:servlet-api-3_1", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + ], ) #TODO(davido): There is no provided_deps argument to java_library rule java_library( - name = 'api', - srcs = glob([SRC + '**/*.java']), - deps = [ - '//gerrit-common:annotations', - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - ], - visibility = ['//visibility:public'], + name = "api", + srcs = glob([SRC + "**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-common:annotations", + "//lib:guava", + "//lib/guice", + "//lib/guice:guice-assistedinject", + ], +) + +junit_tests( + name = "api_tests", + srcs = glob(["src/test/java/**/*Test.java"]), + deps = [ + ":api", + "//gerrit-test-util:test_util", + "//lib:truth", + "//lib/guice", + ], +) + +load("//tools/bzl:javadoc.bzl", "java_doc") + +java_doc( + name = "extension-api-javadoc", + external_docs = [ + JGIT_DOC_URL, + GUAVA_DOC_URL, + ], + libs = [":api"], + pkgs = ["com.google.gerrit.extensions"], + title = "Gerrit Review Extension API Documentation", + visibility = ["//visibility:public"], )
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml index 6844ec8..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.4</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 8f7f93c..63ac914 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; @@ -87,6 +90,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/AbandonInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java index 34726a8..62829f0 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java
@@ -16,9 +16,12 @@ import com.google.gerrit.extensions.restapi.DefaultInput; +import java.util.Map; + public class AbandonInput { @DefaultInput public String message; public NotifyHandling notify = NotifyHandling.ALL; + public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ActionVisitor.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ActionVisitor.java new file mode 100644 index 0000000..6aa7f0c --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ActionVisitor.java
@@ -0,0 +1,65 @@ +// 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.annotations.ExtensionPoint; +import com.google.gerrit.extensions.common.ActionInfo; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.RevisionInfo; + +/** + * Extension point called during population of {@link ActionInfo} maps. + * <p> + * Each visitor may mutate the input {@link ActionInfo}, or filter it out of the + * map entirely. When multiple extensions are registered, the order in which + * they are executed is undefined. + */ +@ExtensionPoint +public interface ActionVisitor { + /** + * Visit a change-level action. + * <p> + * Callers may mutate the input {@link ActionInfo}, or return false to omit + * the action from the map entirely. Inputs other than the {@link ActionInfo} + * should be considered immutable. + * + * @param name name of the action, as a key into the {@link ActionInfo} map + * returned by the REST API. + * @param actionInfo action being visited; caller may mutate. + * @param changeInfo information about the change to which this action + * belongs; caller should treat as immutable. + * @return true if the action should remain in the map, or false to omit it. + */ + boolean visit(String name, ActionInfo actionInfo, ChangeInfo changeInfo); + + /** + * Visit a revision-level action. + * <p> + * Callers may mutate the input {@link ActionInfo}, or return false to omit + * the action from the map entirely. Inputs other than the {@link ActionInfo} + * should be considered immutable. + * + * @param name name of the action, as a key into the {@link ActionInfo} map + * returned by the REST API. + * @param actionInfo action being visited; caller may mutate. + * @param changeInfo information about the change to which this action + * belongs; caller should treat as immutable. + * @param revisionInfo information about the revision to which this action + * belongs; caller should treat as immutable. + * @return true if the action should remain in the map, or false to omit it. + */ + boolean visit(String name, ActionInfo actionInfo, ChangeInfo changeInfo, + RevisionInfo revisionInfo); +}
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..a042757e 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
@@ -19,11 +19,15 @@ import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.restapi.DefaultInput; +import java.util.Map; + public class AddReviewerInput { @DefaultInput public String reviewer; public Boolean confirmed; public ReviewerState state; + public NotifyHandling notify; + public Map<RecipientType, NotifyInfo> notifyDetails; public boolean confirmed() { return (confirmed != null) ? confirmed : false;
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java similarity index 70% rename from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java index a0fed9e..61b5b85 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.extensions.api.changes; -import com.google.inject.servlet.ServletModule; +import com.google.gerrit.extensions.restapi.DefaultInput; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO - } +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..a545cad 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,9 +15,11 @@ 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; +import com.google.gerrit.extensions.common.MergePatchSetInput; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -94,9 +96,15 @@ */ ChangeApi revert(RevertInput in) throws RestApiException; + /** Create a merge patch set for the change. */ + ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException; + 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. @@ -104,7 +112,7 @@ void publish() throws RestApiException; /** - * Deletes a draft change. + * Deletes a change. */ void delete() throws RestApiException; @@ -139,6 +147,28 @@ 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. + * + * @return the assignee that was deleted, or null if there was no assignee. + */ + 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 +356,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 +411,18 @@ EnumSet<SubmittedTogetherOption> options) throws RestApiException { throw new NotImplementedException(); } + + @Override + public SubmittedTogetherInfo submittedTogether( + EnumSet<ListChangesOption> a, + EnumSet<SubmittedTogetherOption> b) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public ChangeInfo createMergePatchSet(MergePatchSetInput in) + throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java index 7ae7ef1..2e1bb13 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -17,4 +17,5 @@ public class CherryPickInput { public String message; public String destination; + public Integer parent; }
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..bb942c7 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.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.api.changes; + +import java.util.Map; + +/** Input passed to {@code DELETE /changes/[id]/reviewers/[id]}. */ +public class DeleteReviewerInput { + /** Who to send email notifications to after the reviewer is deleted. */ + public NotifyHandling notify = NotifyHandling.ALL; + public Map<RecipientType, NotifyInfo> notifyDetails; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java index 671f43e..ee5463b 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
@@ -16,6 +16,8 @@ import com.google.gerrit.extensions.restapi.DefaultInput; +import java.util.Map; + /** Input passed to {@code DELETE /changes/[id]/reviewers/[id]/votes/[label]}. */ public class DeleteVoteInput { @DefaultInput @@ -23,4 +25,5 @@ /** Who to send email notifications to after vote is deleted. */ public NotifyHandling notify = NotifyHandling.ALL; + public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java similarity index 62% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java index a0fed9e..ef49651 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.extensions.api.changes; -import com.google.inject.servlet.ServletModule; +import java.util.List; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +/** Detailed information about who should be notified about an update. */ +public class NotifyInfo { + public List<String> accounts; + + public NotifyInfo(List<String> accounts) { + this.accounts = accounts; } }
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..6623281 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.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.api.changes; + +import java.util.Map; + +/** 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; + public Map<RecipientType, NotifyInfo> notifyDetails; +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java similarity index 92% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java index ea0def0..e3b8a53 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.api.changes; public enum RecipientType { TO, CC, BCC
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..c8bfed2 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
@@ -18,6 +18,7 @@ import com.google.gerrit.extensions.client.Comment; import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.common.FixSuggestionInfo; import com.google.gerrit.extensions.restapi.DefaultInput; import java.util.ArrayList; @@ -34,6 +35,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,11 +50,15 @@ /** * 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; + public Map<RecipientType, NotifyInfo> notifyDetails; /** * If true check to make sure that the comments being posted aren't already @@ -94,6 +100,14 @@ 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 List<FixSuggestionInfo> fixSuggestions; + } + 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..b934a18 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; @@ -31,10 +33,15 @@ public interface RevisionApi { void delete() throws RestApiException; + String description() throws RestApiException; + void description(String description) throws RestApiException; + void review(ReviewInput in) throws RestApiException; void submit() throws RestApiException; void submit(SubmitInput in) throws RestApiException; + BinaryResult submitPreview() throws RestApiException; + BinaryResult submitPreview(String format) throws RestApiException; void publish() throws RestApiException; ChangeApi cherryPick(CherryPickInput in) throws RestApiException; ChangeApi rebase() throws RestApiException; @@ -52,26 +59,59 @@ 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; + + String etag() throws RestApiException; /** * Returns patch of revision. */ BinaryResult patch() throws RestApiException; + BinaryResult patch(String path) throws RestApiException; Map<String, ActionInfo> actions() throws RestApiException; 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 +208,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 +224,12 @@ } @Override + public List<RobotCommentInfo> robotCommentsAsList() + throws RestApiException { + throw new NotImplementedException(); + } + + @Override public Map<String, List<CommentInfo>> drafts() throws RestApiException { throw new NotImplementedException(); } @@ -198,11 +250,21 @@ } @Override + public RobotCommentApi robotComment(String id) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public BinaryResult patch() throws RestApiException { throw new NotImplementedException(); } @Override + public BinaryResult patch(String path) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public Map<String, ActionInfo> actions() throws RestApiException { throw new NotImplementedException(); } @@ -213,9 +275,39 @@ } @Override + public BinaryResult submitPreview() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public BinaryResult submitPreview(String format) 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(); + } + + @Override + public void description(String description) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public String description() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public String etag() 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/SubmitInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java index e415acb..b1ad6e1 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
@@ -14,6 +14,8 @@ package com.google.gerrit.extensions.api.changes; +import java.util.Map; + public class SubmitInput { /** Not used anymore, kept for backward compatibility */ @Deprecated @@ -22,4 +24,5 @@ public String onBehalfOf; public NotifyHandling notify = NotifyHandling.ALL; + public Map<RecipientType, NotifyInfo> notifyDetails; }
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/api/projects/BranchInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java index 77513a2..8ef1b8e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchInfo.java
@@ -21,7 +21,6 @@ import java.util.Map; public class BranchInfo extends RefInfo { - public Boolean canDelete; public Map<String, ActionInfo> actions; public List<WebLinkInfo> webLinks; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.java similarity index 74% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.java index ea0def0..b933624 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.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. @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.api.projects; -public enum RecipientType { - TO, CC, BCC +import java.util.List; + +public class DeleteTagsInput { + public List<String> tags; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index e111291..99450de 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -40,6 +40,7 @@ ListRefsRequest<TagInfo> tags(); void deleteBranches(DeleteBranchesInput in) throws RestApiException; + void deleteTags(DeleteTagsInput in) throws RestApiException; abstract class ListRefsRequest<T extends RefInfo> { protected int limit; @@ -205,5 +206,10 @@ public void deleteBranches(DeleteBranchesInput in) throws RestApiException { throw new NotImplementedException(); } + + @Override + public void deleteTags(DeleteTagsInput in) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java index 1844a76..c573600 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/RefInfo.java
@@ -17,4 +17,5 @@ public class RefInfo { public String ref; public String revision; + public Boolean canDelete; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java index 4348daf..bce4a7c 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -22,6 +22,8 @@ TagInfo get() throws RestApiException; + void delete() throws RestApiException; + /** * A default implementation which allows source compatibility * when adding new methods to the interface. @@ -36,5 +38,10 @@ public TagInfo get() throws RestApiException { throw new NotImplementedException(); } + + @Override + public void delete() throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java similarity index 75% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java index ea0def0..07d9f37 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.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. @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.client; -public enum RecipientType { - TO, CC, BCC -} +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..24e6094 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
@@ -34,6 +34,7 @@ public String inReplyTo; public Timestamp updated; public String message; + public boolean unresolved; public static class Range { public int startLine; @@ -41,6 +42,15 @@ public int endLine; public int endCharacter; + public boolean isValid() { + return startLine >= 0 + && startCharacter >= 0 + && endLine >= 0 + && endCharacter >= 0 + && startLine <= endLine + && (startLine != endLine || startCharacter <= endCharacter); + } + @Override public boolean equals(Object o) { if (o instanceof Range) { @@ -57,6 +67,23 @@ public int hashCode() { return Objects.hash(startLine, startCharacter, endLine, endCharacter); } + + @Override + public String toString() { + return "Range{" + + "startLine=" + startLine + + ", startCharacter=" + startCharacter + + ", endLine=" + endLine + + ", endCharacter=" + endCharacter + + '}'; + } + } + + public short side() { + if (side == Side.PARENT) { + return (short) (parent == null ? 0 : -parent.shortValue()); + } + return 1; } @Override @@ -75,7 +102,8 @@ && Objects.equals(range, c.range) && Objects.equals(inReplyTo, c.inReplyTo) && Objects.equals(updated, c.updated) - && Objects.equals(message, c.message); + && Objects.equals(message, c.message) + && Objects.equals(unresolved, c.unresolved); } return false; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java index d246996..3f5fa31 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -22,6 +22,9 @@ /** Default tab size. */ public static final int DEFAULT_TAB_SIZE = 8; + /** Default font size. */ + public static final int DEFAULT_FONT_SIZE = 12; + /** Default line length. */ public static final int DEFAULT_LINE_LENGTH = 100; @@ -41,6 +44,7 @@ public Integer context; public Integer tabSize; + public Integer fontSize; public Integer lineLength; public Integer cursorBlinkRate; public Boolean expandAllComments; @@ -68,6 +72,7 @@ DiffPreferencesInfo i = new DiffPreferencesInfo(); i.context = DEFAULT_CONTEXT; i.tabSize = DEFAULT_TAB_SIZE; + i.fontSize = DEFAULT_FONT_SIZE; i.lineLength = DEFAULT_LINE_LENGTH; i.cursorBlinkRate = 0; i.ignoreWhitespace = Whitespace.IGNORE_NONE;
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..77d79fe 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,8 @@ public DownloadCommand downloadCommand; public DateFormat dateFormat; public TimeFormat timeFormat; + public Boolean expandInlineDiffs; + public Boolean highlightAssigneeInChangeTable; public Boolean relativeDateInChangeTable; public DiffView diffView; public Boolean sizeBarInChangeTable; @@ -121,8 +142,10 @@ public Boolean muteCommonPathPrefixes; public Boolean signedOffBy; public List<MenuItem> my; + public List<String> changeTable; public Map<String, String> urlAliases; public EmailStrategy emailStrategy; + public DefaultBase defaultBaseForMerges; public boolean isShowInfoInReviewCategory() { return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE; @@ -174,12 +197,15 @@ p.downloadCommand = DownloadCommand.CHECKOUT; p.dateFormat = DateFormat.STD; p.timeFormat = TimeFormat.HHMM_12; + p.expandInlineDiffs = false; + 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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java similarity index 77% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.java index ea0def0..6450b0d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GitBasicAuthPolicy.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. @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.client; -public enum RecipientType { - TO, CC, BCC +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/SubmitType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java index fcfeb01..b52e89a 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
@@ -18,6 +18,7 @@ FAST_FORWARD_ONLY, MERGE_IF_NECESSARY, REBASE_IF_NECESSARY, + REBASE_ALWAYS, MERGE_ALWAYS, CHERRY_PICK }
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/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java index 6d28dbc..9125bfd 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -20,6 +20,8 @@ public String tag; public Integer value; public Timestamp date; + public Boolean postSubmit; + public VotingRangeInfo permittedVotingRange; public ApprovalInfo(Integer id) { super(id);
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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java similarity index 68% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java index ea0def0..180e2d2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.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. @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java similarity index 62% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java index ea0def0..0e8ad65 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.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. @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.java similarity index 67% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.java index ea0def0..9e5890e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.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. @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC +import com.google.gerrit.extensions.client.Comment; + +public class FixReplacementInfo { + public String path; + public Comment.Range range; + public String replacement; }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java similarity index 68% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java index a0fed9e..7ba7fcc 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.extensions.common; -import com.google.inject.servlet.ServletModule; +import java.util.List; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO - } +public class FixSuggestionInfo { + public String fixId; + public String description; + public List<FixReplacementInfo> replacements; }
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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java similarity index 71% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java index ea0def0..263b6c4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.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. @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC +public class MergePatchSetInput { + public String subject; + public boolean inheritParent; + public MergeInput merge; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java similarity index 70% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java index ea0def0..845f7cb7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.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. @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +import java.util.List; + +public class PluginConfigInfo { + public Boolean hasAvatars; + public List<String> jsResourcePaths; +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java similarity index 76% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java index ea0def0..e66c242 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.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. @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +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/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java index 34a1e63..5242c7e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -33,4 +33,5 @@ public Map<String, ActionInfo> actions; public String commitWithFooters; public PushCertificateInfo pushCertificate; + public String description; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java similarity index 60% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java index ea0def0..8d8731f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.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. @@ -12,8 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC +import java.util.List; +import java.util.Map; + +public class RobotCommentInfo extends CommentInfo { + public String robotId; + public String robotRunId; + public String url; + public Map<String, String> properties; + public List<FixSuggestionInfo> fixSuggestions; }
diff --git a/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-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java similarity index 79% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java index ea0def0..98d650c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.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. @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +public class SshdInfo { +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java similarity index 78% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java index ea0def0..5b0dcbe 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.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. @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +public class SuggestInfo { + public int from; +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java similarity index 76% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java index ea0def0..5010689 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.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. @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC -} +public class UserConfigInfo { + public String anonymousCowardName; +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java similarity index 68% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java index ea0def0..5c35a49 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.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. @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.extensions.common; -public enum RecipientType { - TO, CC, BCC +public class VotingRangeInfo { + public int min; + public int max; + + public VotingRangeInfo(int min, int max) { + this.min = min; + this.max = max; + } }
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/BadRequestException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java index d5a9c1f..6e79c3a 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
@@ -22,4 +22,12 @@ public BadRequestException(String msg) { super(msg); } + + /** + * @param msg error text for client describing how request is bad. + * @param cause cause of this exception. + */ + public BadRequestException(String msg, Throwable cause) { + super(msg, cause); + } }
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-extension-api/src/test/java/com/google/gerrit/extensions/client/RangeTest.java b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/client/RangeTest.java new file mode 100644 index 0000000..1bb7711 --- /dev/null +++ b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/client/RangeTest.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.extensions.client; + + +import static com.google.gerrit.extensions.client.RangeSubject.assertThat; + +import org.junit.Test; + +public class RangeTest { + + @Test + public void rangeOverMultipleLinesWithSmallerEndCharacterIsValid() { + Comment.Range range = createRange(13, 31, 19, 10); + assertThat(range).isValid(); + } + + @Test + public void rangeInOneLineIsValid() { + Comment.Range range = createRange(13, 2, 13, 10); + assertThat(range).isValid(); + } + + @Test + public void startPositionEqualToEndPositionIsValidRange() { + Comment.Range range = createRange(13, 11, 13, 11); + assertThat(range).isValid(); + } + + @Test + public void negativeStartLineResultsInInvalidRange() { + Comment.Range range = createRange(-1, 2, 19, 10); + assertThat(range).isInvalid(); + } + + @Test + public void negativeEndLineResultsInInvalidRange() { + Comment.Range range = createRange(13, 2, -1, 10); + assertThat(range).isInvalid(); + } + + @Test + public void negativeStartCharacterResultsInInvalidRange() { + Comment.Range range = createRange(13, -1, 19, 10); + assertThat(range).isInvalid(); + } + + @Test + public void negativeEndCharacterResultsInInvalidRange() { + Comment.Range range = createRange(13, 2, 19, -1); + assertThat(range).isInvalid(); + } + + @Test + public void startLineGreaterThanEndLineResultsInInvalidRange() { + Comment.Range range = createRange(20, 2, 19, 10); + assertThat(range).isInvalid(); + } + + @Test + public void startCharGreaterThanEndCharForSameLineResultsInInvalidRange() { + Comment.Range range = createRange(13, 11, 13, 10); + assertThat(range).isInvalid(); + } + + private Comment.Range createRange(int startLine, int startCharacter, + int endLine, int endCharacter) { + Comment.Range range = new Comment.Range(); + range.startLine = startLine; + range.startCharacter = startCharacter; + range.endLine = endLine; + range.endCharacter = endCharacter; + return range; + } +}
diff --git a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java index 299b9b0..f18cdf3 100644 --- a/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java +++ b/gerrit-extension-api/src/test/java/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -33,13 +33,13 @@ // {@code assertThat(ds.contains(...)).isFalse() @} instead. @Test - public void testContainsWithEmpty() throws Exception { + public void containsWithEmpty() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); assertThat(ds.contains(2)).isFalse(); //See above comment about ds.contains } @Test - public void testContainsTrueWithSingleElement() throws Exception { + public void containsTrueWithSingleElement() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); ds.add(2); @@ -47,7 +47,7 @@ } @Test - public void testContainsFalseWithSingleElement() throws Exception { + public void containsFalseWithSingleElement() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); ds.add(2); @@ -55,7 +55,7 @@ } @Test - public void testContainsTrueWithTwoElements() throws Exception { + public void containsTrueWithTwoElements() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); ds.add(2); ds.add(4); @@ -64,7 +64,7 @@ } @Test - public void testContainsFalseWithTwoElements() throws Exception { + public void containsFalseWithTwoElements() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); ds.add(2); ds.add(4); @@ -73,7 +73,7 @@ } @Test - public void testContainsDynamic() throws Exception { + public void containsDynamic() throws Exception { DynamicSet<Integer> ds = new DynamicSet<>(); ds.add(2);
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/BUILD b/gerrit-gpg/BUILD index 79f50b1..dcaf442 100644 --- a/gerrit-gpg/BUILD +++ b/gerrit-gpg/BUILD
@@ -1,58 +1,59 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") DEPS = [ - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:guava', - '//lib:gwtorm', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:guava", + "//lib:gwtorm", + "//lib/guice:guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", ] java_library( - name = 'gpg', - srcs = glob(['src/main/java/**/*.java']), - deps = DEPS + [ - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcprov', - ], - visibility = ['//visibility:public'], + name = "gpg", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = DEPS + [ + "//lib/bouncycastle:bcpg", + "//lib/bouncycastle:bcprov", + ], ) -TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java']) +TESTUTIL_SRCS = glob(["src/test/**/testutil/**/*.java"]) java_library( - name = 'testutil', - srcs = TESTUTIL_SRCS, - deps = DEPS + [ - ':gpg', - '//lib/bouncycastle:bcpg-without-neverlink', - '//lib/bouncycastle:bcprov-without-neverlink', - ], - visibility = ['//visibility:public'], + name = "testutil", + testonly = 1, + srcs = TESTUTIL_SRCS, + visibility = ["//visibility:public"], + deps = DEPS + [ + ":gpg", + "//lib/bouncycastle:bcpg-without-neverlink", + "//lib/bouncycastle:bcprov-without-neverlink", + ], ) junit_tests( - name = 'gpg_tests', - srcs = glob( - ['src/test/java/**/*.java'], - exclude = TESTUTIL_SRCS, - ), - deps = DEPS + [ - ':gpg', - ':testutil', - '//gerrit-cache-h2:cache-h2', - '//gerrit-lucene:lucene', - '//gerrit-server:testutil', - '//lib:truth', - '//lib/jgit/org.eclipse.jgit.junit:junit', - '//lib/bouncycastle:bcpg-without-neverlink', - '//lib/bouncycastle:bcprov-without-neverlink', - ], - visibility = ['//visibility:public'], + name = "gpg_tests", + srcs = glob( + ["src/test/java/**/*.java"], + exclude = TESTUTIL_SRCS, + ), + visibility = ["//visibility:public"], + deps = DEPS + [ + ":gpg", + ":testutil", + "//gerrit-cache-h2:cache-h2", + "//gerrit-lucene:lucene", + "//gerrit-server:testutil", + "//lib:truth", + "//lib/jgit/org.eclipse.jgit.junit:junit", + "//lib/bouncycastle:bcpg-without-neverlink", + "//lib/bouncycastle:bcprov-without-neverlink", + ], )
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..c65b114 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
@@ -18,11 +18,8 @@ import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY; 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; @@ -227,12 +224,12 @@ return false; } - @SuppressWarnings("unchecked") private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key, String userId) { - return MoreObjects.firstNonNull( - key.getSignaturesForID(userId), - Collections.emptyIterator()); + Iterator<PGPSignature> result = key.getSignaturesForID(userId); + return result != null + ? result + : Collections.emptyIterator(); } private Set<String> getAllowedUserIds(IdentifiedUser user) { @@ -274,9 +271,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/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java index e4c81df..66e810c 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -291,7 +291,8 @@ return null; } - return new RevocationKey(sub.isCritical(), sub.getData()); + return new RevocationKey(sub.isCritical(), sub.isLongLength(), + sub.getData()); } private void checkRevocations(PGPPublicKey key, @@ -341,7 +342,8 @@ if (sub == null) { return null; } - return new RevocationReason(sub.isCritical(), sub.getData()); + return new RevocationReason(sub.isCritical(), sub.isLongLength(), + sub.getData()); } private static String reasonToString(RevocationReason reason) { @@ -405,7 +407,6 @@ // Don't check the timestamp of these certifications. This allows admins // to correct untrusted keys by signing them with a trusted key, such that // older signatures created by those keys retroactively appear valid. - @SuppressWarnings("unchecked") Iterator<PGPSignature> sigs = key.getSignaturesForID(userId); while (sigs.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..d2ccb88 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; @@ -49,7 +48,7 @@ import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.index.account.AccountIndexCollection; -import com.google.gerrit.server.mail.AddKeySender; +import com.google.gerrit.server.mail.send.AddKeySender; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -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-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java index 11e9768..3df1154 100644 --- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java +++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -85,7 +85,7 @@ } @Test - public void testGet() throws Exception { + public void get() throws Exception { TestKey key1 = validKeyWithoutExpiration(); tr.branch(REFS_GPG_KEYS) .commit() @@ -104,7 +104,7 @@ } @Test - public void testGetMultiple() throws Exception { + public void getMultiple() throws Exception { TestKey key1 = validKeyWithoutExpiration(); TestKey key2 = validKeyWithExpiration(); tr.branch(REFS_GPG_KEYS)
diff --git a/gerrit-gwtdebug/BUILD b/gerrit-gwtdebug/BUILD new file mode 100644 index 0000000..115c6b9 --- /dev/null +++ b/gerrit-gwtdebug/BUILD
@@ -0,0 +1,17 @@ +java_library( + name = "gwtdebug", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-pgm:daemon", + "//gerrit-pgm:pgm", + "//gerrit-pgm:util", + "//gerrit-util-cli:cli", + "//lib/gwt:dev", + "//lib/jetty:server", + "//lib/jetty:servlet", + "//lib/jetty:servlets", + "//lib/log:api", + "//lib/log:log4j", + ], +)
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..a9a2e48 100644 --- a/gerrit-gwtexpui/BUILD +++ b/gerrit-gwtexpui/BUILD
@@ -1,114 +1,118 @@ -load('//tools/bzl:gwt.bzl', 'gwt_module') -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:gwt.bzl", "gwt_module") +load("//tools/bzl:junit.bzl", "junit_tests") -SRC = 'src/main/java/com/google/gwtexpui/' +SRC = "src/main/java/com/google/gwtexpui/" gwt_module( - name = 'Clippy', - srcs = glob([SRC + 'clippy/client/*.java']), - gwt_xml = SRC + 'clippy/Clippy.gwt.xml', - resources = [ - SRC + 'clippy/client/clippy.css', - SRC + 'clippy/client/clippy.swf', - SRC + 'clippy/client/page_white_copy.png', - SRC + 'clippy/client/CopyableLabelText.properties', - ], - deps = [ - ':SafeHtml', - ':UserAgent', - '//lib/gwt:user', - ], - visibility = ['//visibility:public'], + name = "Clippy", + srcs = glob([SRC + "clippy/client/*.java"]), + data = [ + "//lib:LICENSE-clippy", + "//lib:LICENSE-silk_icons", + ], + gwt_xml = SRC + "clippy/Clippy.gwt.xml", + resources = [ + SRC + "clippy/client/clippy.css", + SRC + "clippy/client/clippy.swf", + SRC + "clippy/client/page_white_copy.png", + SRC + "clippy/client/CopyableLabelText.properties", + ], + visibility = ["//visibility:public"], + deps = [ + ":SafeHtml", + ":UserAgent", + "//lib/gwt:user-neverlink", + ], ) java_library( - name = 'CSS', - srcs = glob([SRC + 'css/rebind/*.java']), - resources = [SRC + 'css/CSS.gwt.xml'], - deps = ['//lib/gwt:dev'], - visibility = ['//visibility:public'], + name = "CSS", + srcs = glob([SRC + "css/rebind/*.java"]), + resources = [SRC + "css/CSS.gwt.xml"], + visibility = ["//visibility:public"], + deps = ["//lib/gwt:dev"], ) gwt_module( - name = 'GlobalKey', - srcs = glob([SRC + 'globalkey/client/*.java']), - gwt_xml = SRC + 'globalkey/GlobalKey.gwt.xml', - resources = [ - SRC + 'globalkey/client/KeyConstants.properties', - SRC + 'globalkey/client/key.css', - ], - deps = [ - ':SafeHtml', - ':UserAgent', - '//lib/gwt:user', - ], - visibility = ['//visibility:public'], + name = "GlobalKey", + srcs = glob([SRC + "globalkey/client/*.java"]), + gwt_xml = SRC + "globalkey/GlobalKey.gwt.xml", + resources = [ + SRC + "globalkey/client/KeyConstants.properties", + SRC + "globalkey/client/key.css", + ], + visibility = ["//visibility:public"], + deps = [ + ":SafeHtml", + ":UserAgent", + "//lib/gwt:user", + ], ) java_library( - name = 'linker_server', - srcs = glob([SRC + 'linker/server/*.java']), - deps = ['//lib:servlet-api-3_1'], - visibility = ['//visibility:public'], + name = "linker_server", + srcs = glob([SRC + "linker/server/*.java"]), + visibility = ["//visibility:public"], + deps = ["//lib:servlet-api-3_1"], ) gwt_module( - name = 'Progress', - srcs = glob([SRC + 'progress/client/*.java']), - gwt_xml = SRC + 'progress/Progress.gwt.xml', - resources = [SRC + 'progress/client/progress.css'], - deps = ['//lib/gwt:user'], - visibility = ['//visibility:public'], + name = "Progress", + srcs = glob([SRC + "progress/client/*.java"]), + gwt_xml = SRC + "progress/Progress.gwt.xml", + resources = [SRC + "progress/client/progress.css"], + visibility = ["//visibility:public"], + deps = ["//lib/gwt:user"], ) gwt_module( - name = 'SafeHtml', - srcs = glob([SRC + 'safehtml/client/*.java']), - gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml', - resources = [SRC + 'safehtml/client/safehtml.css'], - deps = ['//lib/gwt:user'], - visibility = ['//visibility:public'], + name = "SafeHtml", + srcs = glob([SRC + "safehtml/client/*.java"]), + gwt_xml = SRC + "safehtml/SafeHtml.gwt.xml", + resources = [SRC + "safehtml/client/safehtml.css"], + visibility = ["//visibility:public"], + deps = ["//lib/gwt:user"], ) junit_tests( - name = 'SafeHtml_tests', - srcs = glob([ - 'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java', - ]), - deps = [ - ':SafeHtml', - '//lib:truth', - '//lib/gwt:user', - '//lib/gwt:dev', - ], + name = "SafeHtml_tests", + srcs = glob([ + "src/test/java/com/google/gwtexpui/safehtml/client/**/*.java", + ]), + deps = [ + ":SafeHtml", + "//lib:truth", + "//lib/gwt:dev", + "//lib/gwt:user", + ], ) gwt_module( - name = 'UserAgent', - srcs = glob([SRC + 'user/client/*.java']), - gwt_xml = SRC + 'user/User.gwt.xml', - resources = [SRC + 'user/client/tooltip.css'], - deps = ['//lib/gwt:user'], - visibility = ['//visibility:public'], + name = "UserAgent", + srcs = glob([SRC + "user/client/*.java"]), + gwt_xml = SRC + "user/User.gwt.xml", + resources = [SRC + "user/client/tooltip.css"], + visibility = ["//visibility:public"], + deps = ["//lib/gwt:user"], ) java_library( - name = 'server', - srcs = glob([SRC + 'server/*.java']), - deps = ['//lib:servlet-api-3_1'], - visibility = ['//visibility:public'], + name = "server", + srcs = glob([SRC + "server/*.java"]), + visibility = ["//visibility:public"], + deps = ["//lib:servlet-api-3_1"], ) java_library( - name = 'client-src-lib', - srcs = [], - resources = glob( - [SRC + n for n in [ - 'clippy/**/*', - 'globalkey/**/*', - 'safehtml/**/*', - 'user/**/*', - ]] - ), - visibility = ['//visibility:public'], + name = "client-src-lib", + srcs = [], + resources = glob( + [SRC + n for n in [ + "clippy/**/*", + "globalkey/**/*", + "safehtml/**/*", + "user/**/*", + ]], + ), + visibility = ["//visibility:public"], )
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java index cf5a445..525a837 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -43,7 +43,7 @@ } @Override - public final void requestSuggestions(final Request request, final Callback cb) { + public final void requestSuggestions(Request request, Callback cb) { onRequestSuggestions(request, new Callback() { @Override public void onSuggestionsReady(final Request request, @@ -88,27 +88,28 @@ ds = escape(ds); } - StringBuilder pattern = new StringBuilder(); - for (String qterm : splitQuery(qstr)) { - qterm = escape(qterm); - // We now surround qstr by <strong>. But the chosen approach is not too - // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in - // escapes (e.g.: "Tim <email@example.org>"). Those escapes will - // get <strong>-ed as well (e.g.: "<" -> "&<strong>l</strong>t;"). But - // as repairing those mangled escapes is easier than not mangling them in - // the first place, we repair them afterwards. - - if (pattern.length() > 0) { - pattern.append("|"); + if (qstr != null && !qstr.isEmpty()) { + StringBuilder pattern = new StringBuilder(); + for (String qterm : splitQuery(qstr)) { + qterm = escape(qterm); + // We now surround qstr by <strong>. But the chosen approach is not too + // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in + // escapes (e.g.: "Tim <email@example.org>"). Those escapes will + // get <strong>-ed as well (e.g.: "<" -> "&<strong>l</strong>t;"). But + // as repairing those mangled escapes is easier than not mangling them in + // the first place, we repair them afterwards. + if (pattern.length() > 0) { + pattern.append("|"); + } + pattern.append(qterm); } - pattern.append(qterm); + + ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>"); + + // Repairing <strong>-ed escapes. + ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3"); } - ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>"); - - // Repairing <strong>-ed escapes. - ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3"); - displayString = ds; }
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java index 554315e..964a7d5 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/LinkFindReplaceTest.java
@@ -15,7 +15,6 @@ package com.google.gwtexpui.safehtml.client; import static com.google.common.truth.Truth.assertThat; -import static com.google.gwtexpui.safehtml.client.LinkFindReplace.hasValidScheme; import org.junit.Rule; import org.junit.Test; @@ -26,7 +25,7 @@ public ExpectedException exception = ExpectedException.none(); @Test - public void testNoEscaping() { + public void noEscaping() { String find = "find"; String link = "link"; LinkFindReplace a = new LinkFindReplace(find, link); @@ -36,7 +35,7 @@ } @Test - public void testBackreference() { + public void backreference() { LinkFindReplace l = new LinkFindReplace( "(bug|issue)\\s*([0-9]+)", "/bug?id=$2"); assertThat(l.replace("issue 123")) @@ -44,39 +43,39 @@ } @Test - public void testHasValidScheme() { - assertThat(hasValidScheme("/absolute/path")).isTrue(); - assertThat(hasValidScheme("relative/path")).isTrue(); - assertThat(hasValidScheme("http://url/")).isTrue(); - assertThat(hasValidScheme("HTTP://url/")).isTrue(); - assertThat(hasValidScheme("https://url/")).isTrue(); - assertThat(hasValidScheme("mailto://url/")).isTrue(); - assertThat(hasValidScheme("ftp://url/")).isFalse(); - assertThat(hasValidScheme("data:evil")).isFalse(); - assertThat(hasValidScheme("javascript:alert(1)")).isFalse(); + public void hasValidScheme() { + assertThat(LinkFindReplace.hasValidScheme("/absolute/path")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("relative/path")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("http://url/")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("HTTP://url/")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("https://url/")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("mailto://url/")).isTrue(); + assertThat(LinkFindReplace.hasValidScheme("ftp://url/")).isFalse(); + assertThat(LinkFindReplace.hasValidScheme("data:evil")).isFalse(); + assertThat(LinkFindReplace.hasValidScheme("javascript:alert(1)")).isFalse(); } @Test - public void testInvalidSchemeInReplace() { + public void invalidSchemeInReplace() { exception.expect(IllegalArgumentException.class); new LinkFindReplace("find", "javascript:alert(1)").replace("find"); } @Test - public void testInvalidSchemeWithBackreference() { + public void invalidSchemeWithBackreference() { exception.expect(IllegalArgumentException.class); new LinkFindReplace(".*(script:[^;]*)", "java$1") .replace("Look at this script: alert(1);"); } @Test - public void testReplaceEscaping() { + public void replaceEscaping() { assertThat(new LinkFindReplace("find", "a\"&'<>b").replace("find")) .isEqualTo("<a href=\"a"&'<>b\">find</a>"); } @Test - public void testHtmlInFind() { + public void htmlInFind() { String rawFind = "<b>"bold"</b>"; LinkFindReplace a = new LinkFindReplace(rawFind, "/bold"); assertThat(a.pattern().getSource()).isEqualTo(rawFind);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java index 0f124c0..3b5e769 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -20,7 +20,7 @@ public class RawFindReplaceTest { @Test - public void testFindReplace() { + public void findReplace() { final String find = "find"; final String replace = "replace"; final RawFindReplace a = new RawFindReplace(find, replace);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java index 0862711..ff34a3f 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -25,7 +25,7 @@ public ExpectedException exception = ExpectedException.none(); @Test - public void testEmpty() { + public void empty() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b.isEmpty()).isTrue(); assertThat(b.hasContent()).isFalse(); @@ -37,7 +37,7 @@ } @Test - public void testToSafeHtml() { + public void toSafeHtml() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); b.append(1); @@ -49,7 +49,7 @@ } @Test - public void testAppend_boolean() { + public void append_boolean() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append(true)); assertThat(b).isSameAs(b.append(false)); @@ -57,7 +57,7 @@ } @Test - public void testAppend_char() { + public void append_char() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append('a')); assertThat(b).isSameAs(b.append('b')); @@ -65,7 +65,7 @@ } @Test - public void testAppend_int() { + public void append_int() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append(4)); assertThat(b).isSameAs(b.append(2)); @@ -74,7 +74,7 @@ } @Test - public void testAppend_long() { + public void append_long() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append(4L)); assertThat(b).isSameAs(b.append(2L)); @@ -82,21 +82,21 @@ } @Test - public void testAppend_float() { + public void append_float() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append(0.0f)); assertThat(b.asString()).isEqualTo("0.0"); } @Test - public void testAppend_double() { + public void append_double() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append(0.0)); assertThat(b.asString()).isEqualTo("0.0"); } @Test - public void testAppend_String() { + public void append_String() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((String) null)); assertThat(b.asString()).isEmpty(); @@ -106,7 +106,7 @@ } @Test - public void testAppend_StringBuilder() { + public void append_StringBuilder() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((StringBuilder) null)); assertThat(b.asString()).isEmpty(); @@ -116,7 +116,7 @@ } @Test - public void testAppend_StringBuffer() { + public void append_StringBuffer() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((StringBuffer) null)); assertThat(b.asString()).isEmpty(); @@ -126,7 +126,7 @@ } @Test - public void testAppend_Object() { + public void append_Object() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((Object) null)); assertThat(b.asString()).isEmpty(); @@ -140,7 +140,7 @@ } @Test - public void testAppend_CharSequence() { + public void append_CharSequence() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((CharSequence) null)); assertThat(b.asString()).isEmpty(); @@ -150,7 +150,7 @@ } @Test - public void testAppend_SafeHtml() { + public void append_SafeHtml() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.append((SafeHtml) null)); assertThat(b.asString()).isEmpty(); @@ -160,7 +160,7 @@ } @Test - public void testHtmlSpecialCharacters() { + public void htmlSpecialCharacters() { assertThat(escape("&")).isEqualTo("&"); assertThat(escape("<")).isEqualTo("<"); assertThat(escape(">")).isEqualTo(">"); @@ -178,21 +178,21 @@ } @Test - public void testEntityNbsp() { + public void entityNbsp() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.nbsp()); assertThat(b.asString()).isEqualTo(" "); } @Test - public void testTagBr() { + public void tagBr() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.br()); assertThat(b.asString()).isEqualTo("<br />"); } @Test - public void testTagTableTrTd() { + public void tagTableTrTd() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.openElement("table")); assertThat(b).isSameAs(b.openTr()); @@ -205,7 +205,7 @@ } @Test - public void testTagDiv() { + public void tagDiv() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.openDiv()); assertThat(b).isSameAs(b.append("d<a>ta")); @@ -214,7 +214,7 @@ } @Test - public void testTagAnchor() { + public void tagAnchor() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.openAnchor()); @@ -234,7 +234,7 @@ } @Test - public void testTagHeightWidth() { + public void tagHeightWidth() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.openElement("img")); assertThat(b).isSameAs(b.setHeight(100)); @@ -244,7 +244,7 @@ } @Test - public void testStyleName() { + public void styleName() { final SafeHtmlBuilder b = new SafeHtmlBuilder(); assertThat(b).isSameAs(b.openSpan()); assertThat(b).isSameAs(b.setStyleName("foo")); @@ -255,7 +255,7 @@ } @Test - public void testRejectJavaScript_AnchorHref() { + public void rejectJavaScript_AnchorHref() { final String href = "javascript:window.close();"; exception.expect(RuntimeException.class); exception.expectMessage("javascript unsafe in href: " + href); @@ -263,7 +263,7 @@ } @Test - public void testRejectJavaScript_ImgSrc() { + public void rejectJavaScript_ImgSrc() { final String href = "javascript:window.close();"; exception.expect(RuntimeException.class); exception.expectMessage("javascript unsafe in href: " + href); @@ -271,7 +271,7 @@ } @Test - public void testRejectJavaScript_FormAction() { + public void rejectJavaScript_FormAction() { final String href = "javascript:window.close();"; exception.expect(RuntimeException.class); exception.expectMessage("javascript unsafe in href: " + href);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java index 8fe743e..9d310c6 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -20,7 +20,7 @@ public class SafeHtml_LinkifyTest { @Test - public void testLinkify_SimpleHttp1() { + public void linkify_SimpleHttp1() { final SafeHtml o = html("A http://go.here/ B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -30,7 +30,7 @@ } @Test - public void testLinkify_SimpleHttps2() { + public void linkify_SimpleHttps2() { final SafeHtml o = html("A https://go.here/ B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -40,7 +40,7 @@ } @Test - public void testLinkify_Parens1() { + public void linkify_Parens1() { final SafeHtml o = html("A (http://go.here/) B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -50,7 +50,7 @@ } @Test - public void testLinkify_Parens() { + public void linkify_Parens() { final SafeHtml o = html("A http://go.here/#m() B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -60,7 +60,7 @@ } @Test - public void testLinkify_AngleBrackets1() { + public void linkify_AngleBrackets1() { final SafeHtml o = html("A <http://go.here/> B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -70,7 +70,7 @@ } @Test - public void testLinkify_TrailingPlainLetter() { + public void linkify_TrailingPlainLetter() { final SafeHtml o = html("A http://go.here/foo B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -80,7 +80,7 @@ } @Test - public void testLinkify_TrailingDot() { + public void linkify_TrailingDot() { final SafeHtml o = html("A http://go.here/. B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -90,7 +90,7 @@ } @Test - public void testLinkify_TrailingComma() { + public void linkify_TrailingComma() { final SafeHtml o = html("A http://go.here/, B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n); @@ -100,7 +100,7 @@ } @Test - public void testLinkify_TrailingDotDot() { + public void linkify_TrailingDotDot() { final SafeHtml o = html("A http://go.here/.. B"); final SafeHtml n = o.linkify(); assertThat(o).isNotSameAs(n);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java index 0401c9e..65a13a7 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -24,14 +24,14 @@ public class SafeHtml_ReplaceTest { @Test - public void testReplaceEmpty() { + public void replaceEmpty() { SafeHtml o = html("A\nissue42\nB"); assertThat(o.replaceAll(null)).isSameAs(o); assertThat(o.replaceAll(Collections.<FindReplace> emptyList())).isSameAs(o); } @Test - public void testReplaceOneLink() { + public void replaceOneLink() { SafeHtml o = html("A\nissue 42\nB"); SafeHtml n = o.replaceAll(repls( new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>"))); @@ -41,7 +41,7 @@ } @Test - public void testReplaceNoLeadingOrTrailingText() { + public void replaceNoLeadingOrTrailingText() { SafeHtml o = html("issue 42"); SafeHtml n = o.replaceAll(repls( new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>"))); @@ -51,7 +51,7 @@ } @Test - public void testReplaceTwoLinks() { + public void replaceTwoLinks() { SafeHtml o = html("A\nissue 42\nissue 9918\nB"); SafeHtml n = o.replaceAll(repls( new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>"))); @@ -64,7 +64,7 @@ } @Test - public void testReplaceInOrder() { + public void replaceInOrder() { SafeHtml o = html("A\nissue 42\nReally GWTEXPUI-9918 is better\nB"); SafeHtml n = o.replaceAll(repls( new RawFindReplace("(GWTEXPUI-(\\d+))", @@ -80,7 +80,7 @@ } @Test - public void testReplaceOverlappingAfterFirstChar() { + public void replaceOverlappingAfterFirstChar() { SafeHtml o = html("abcd"); RawFindReplace ab = new RawFindReplace("ab", "AB"); RawFindReplace bc = new RawFindReplace("bc", "23"); @@ -92,7 +92,7 @@ } @Test - public void testReplaceOverlappingAtFirstCharLongestMatch() { + public void replaceOverlappingAtFirstCharLongestMatch() { SafeHtml o = html("abcd"); RawFindReplace ab = new RawFindReplace("ab", "AB"); RawFindReplace abc = new RawFindReplace("[^d][^d][^d]", "234"); @@ -102,7 +102,7 @@ } @Test - public void testReplaceOverlappingAtFirstCharFirstMatch() { + public void replaceOverlappingAtFirstCharFirstMatch() { SafeHtml o = html("abcd"); RawFindReplace ab1 = new RawFindReplace("ab", "AB"); RawFindReplace ab2 = new RawFindReplace("[^cd][^cd]", "12"); @@ -112,7 +112,7 @@ } @Test - public void testFailedSanitization() { + public void failedSanitization() { SafeHtml o = html("abcd"); LinkFindReplace evil = new LinkFindReplace("(b)", "javascript:alert('$1')"); LinkFindReplace ok = new LinkFindReplace("(b)", "/$1");
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java index 9a7108d..eb7d038 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -27,7 +27,7 @@ } @Test - public void testBulletList1() { + public void bulletList1() { final SafeHtml o = html("A\n\n* line 1\n* 2nd line"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -40,7 +40,7 @@ } @Test - public void testBulletList2() { + public void bulletList2() { final SafeHtml o = html("A\n\n* line 1\n* 2nd line\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -54,7 +54,7 @@ } @Test - public void testBulletList3() { + public void bulletList3() { final SafeHtml o = html("* line 1\n* 2nd line\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -67,7 +67,7 @@ } @Test - public void testBulletList4() { + public void bulletList4() { final SafeHtml o = html("To see this bug, you have to:\n" // + "* Be on IMAP or EAS (not on POP)\n"// + "* Be very unlucky\n"); @@ -82,7 +82,7 @@ } @Test - public void testBulletList5() { + public void bulletList5() { final SafeHtml o = html("To see this bug,\n" // + "you have to:\n" // + "* Be on IMAP or EAS (not on POP)\n"// @@ -98,7 +98,7 @@ } @Test - public void testDashList1() { + public void dashList1() { final SafeHtml o = html("A\n\n- line 1\n- 2nd line"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -111,7 +111,7 @@ } @Test - public void testDashList2() { + public void dashList2() { final SafeHtml o = html("A\n\n- line 1\n- 2nd line\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -125,7 +125,7 @@ } @Test - public void testDashList3() { + public void dashList3() { final SafeHtml o = html("- line 1\n- 2nd line\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java index 8085cac..897cf40 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -27,7 +27,7 @@ } @Test - public void testPreformat1() { + public void preformat1() { final SafeHtml o = html("A\n\n This is pre\n formatted"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -40,7 +40,7 @@ } @Test - public void testPreformat2() { + public void preformat2() { final SafeHtml o = html("A\n\n This is pre\n formatted\n\nbut this is not"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -54,7 +54,7 @@ } @Test - public void testPreformat3() { + public void preformat3() { final SafeHtml o = html("A\n\n Q\n <R>\n S\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -69,7 +69,7 @@ } @Test - public void testPreformat4() { + public void preformat4() { final SafeHtml o = html(" Q\n <R>\n S\n\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java index 766760f..d0c3ad2 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
@@ -27,7 +27,7 @@ } @Test - public void testQuote1() { + public void quote1() { final SafeHtml o = html("> I'm happy\n > with quotes!\n\nSee above."); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -37,7 +37,7 @@ } @Test - public void testQuote2() { + public void quote2() { final SafeHtml o = html("See this said:\n\n > a quoted\n > string block\n\nOK?"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -48,7 +48,7 @@ } @Test - public void testNestedQuotes1() { + public void nestedQuotes1() { final SafeHtml o = html(" > > prior\n > \n > next\n"); final SafeHtml n = o.wikify(); assertThat(n.asString()).isEqualTo(quote(quote("prior") + "next\n"));
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java index 8f6ff8d..c8341f4 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -20,7 +20,7 @@ public class SafeHtml_WikifyTest { @Test - public void testWikify_OneLine1() { + public void wikify_OneLine1() { final SafeHtml o = html("A B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -28,7 +28,7 @@ } @Test - public void testWikify_OneLine2() { + public void wikify_OneLine2() { final SafeHtml o = html("A B\n"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -36,7 +36,7 @@ } @Test - public void testWikify_OneParagraph1() { + public void wikify_OneParagraph1() { final SafeHtml o = html("A\nB"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -44,7 +44,7 @@ } @Test - public void testWikify_OneParagraph2() { + public void wikify_OneParagraph2() { final SafeHtml o = html("A\nB\n"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -52,7 +52,7 @@ } @Test - public void testWikify_TwoParagraphs() { + public void wikify_TwoParagraphs() { final SafeHtml o = html("A\nB\n\nC\nD"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -60,7 +60,7 @@ } @Test - public void testLinkify_SimpleHttp1() { + public void linkify_SimpleHttp1() { final SafeHtml o = html("A http://go.here/ B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -70,7 +70,7 @@ } @Test - public void testLinkify_SimpleHttps2() { + public void linkify_SimpleHttps2() { final SafeHtml o = html("A https://go.here/ B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -80,7 +80,7 @@ } @Test - public void testLinkify_Parens1() { + public void linkify_Parens1() { final SafeHtml o = html("A (http://go.here/) B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -90,7 +90,7 @@ } @Test - public void testLinkify_Parens() { + public void linkify_Parens() { final SafeHtml o = html("A http://go.here/#m() B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n); @@ -100,7 +100,7 @@ } @Test - public void testLinkify_AngleBrackets1() { + public void linkify_AngleBrackets1() { final SafeHtml o = html("A <http://go.here/> B"); final SafeHtml n = o.wikify(); assertThat(o).isNotSameAs(n);
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK index ef78d98..729b7e7 100644 --- a/gerrit-gwtui-common/BUCK +++ b/gerrit-gwtui-common/BUCK
@@ -8,7 +8,6 @@ ] 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', @@ -36,9 +35,9 @@ visibility = ['PUBLIC'], ) -prebuilt_jar( +java_library( name = 'diffy_logo', - binary_jar = ':diffy_image_files_ln', + resources = glob(['src/main/resources/com/google/gerrit/client/diffy*.png']), deps = [ '//lib:LICENSE-diffy', '//lib:LICENSE-CC-BY3.0-unported', @@ -46,17 +45,6 @@ visibility = ['PUBLIC'], ) -genrule( - name = 'diffy_image_files_ln', - cmd = 'ln -s $(location :diffy_image_files) $OUT', - out = 'diffy_images.jar', -) - -java_library( - name = 'diffy_image_files', - resources = DIFFY, -) - java_test( name = 'client_tests', srcs = glob(['src/test/java/**/*.java']), @@ -66,7 +54,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..46262d6 --- /dev/null +++ b/gerrit-gwtui-common/BUILD
@@ -0,0 +1,62 @@ +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-neverlink"] + +SRC = "src/main/java/com/google/gerrit/" + +gwt_module( + name = "client", + srcs = glob(["src/main/**/*.java"]), + exported_deps = EXPORTED_DEPS, + gwt_xml = SRC + "GerritGwtUICommon.gwt.xml", + resources = glob( + ["src/main/**/*"], + exclude = [SRC + "client/**/*.java"] + [ + SRC + "GerritGwtUICommon.gwt.xml", + ], + ), + visibility = ["//visibility:public"], + deps = DEPS, +) + +java_library2( + name = "client-lib", + srcs = glob(["src/main/**/*.java"]), + exported_deps = EXPORTED_DEPS, + resources = glob(["src/main/**/*"]), + visibility = ["//visibility:public"], + deps = DEPS, +) + +java_library( + name = "diffy_logo", + data = [ + "//lib:LICENSE-CC-BY3.0-unported", + "//lib:LICENSE-diffy", + ], + resources = glob(["src/main/resources/com/google/gerrit/client/diffy*.png"]), + visibility = ["//visibility:public"], +) + +junit_tests( + name = "client_tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":client", + "//lib:junit", + "//lib/gwt:dev", + "//lib/jgit/org.eclipse.jgit:jgit", + ], +)
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..054bfdd 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; }-*/; @@ -304,10 +305,20 @@ public final native boolean hasValue() /*-{ return this.hasOwnProperty('value'); }-*/; public final native short value() /*-{ return this.value || 0; }-*/; + public final native VotingRangeInfo permittedVotingRange() /*-{ return this.permitted_voting_range; }-*/; + protected ApprovalInfo() { } } + public static class VotingRangeInfo extends AccountInfo { + public final native short min() /*-{ return this.min || 0; }-*/; + public final native short max() /*-{ return this.max || 0; }-*/; + + protected VotingRangeInfo() { + } + } + public static class EditInfo extends JavaScriptObject { public final native String name() /*-{ return this.name; }-*/; public final native String setName(String n) /*-{ this.name = n; }-*/; @@ -414,6 +425,10 @@ return PatchSet.Id.toId(_number()); } + public final boolean isMerge() { + return commit().parents().length() > 1; + } + protected RevisionInfo () { } } @@ -458,6 +473,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..e557470 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
@@ -15,6 +15,7 @@ package com.google.gerrit.client.info; import com.google.gerrit.client.rpc.Natives; +import com.google.gerrit.common.data.FilenameComparator; import com.google.gerrit.reviewdb.client.Patch; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; @@ -30,7 +31,6 @@ public final native boolean binary() /*-{ return this.binary || false; }-*/; public final native String status() /*-{ return this.status; }-*/; - // JSNI methods cannot have 'long' as a parameter type or a return type and // it's suggested to use double in this case: // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html#important @@ -48,37 +48,20 @@ public final native void _row(int r) /*-{ this._row = r }-*/; public static void sortFileInfoByPath(JsArray<FileInfo> list) { - Collections.sort(Natives.asList(list), new Comparator<FileInfo>() { - @Override - public int compare(FileInfo a, FileInfo b) { - if (Patch.COMMIT_MSG.equals(a.path())) { - return -1; - } else if (Patch.COMMIT_MSG.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('.'); - if (s1 > 0 && s2 > 0 && - a.path().substring(0, s1).equals(b.path().substring(0, s2))) { - String suffixA = a.path().substring(s1); - String suffixB = b.path().substring(s2); - // C++ and C: give priority to header files (.h/.hpp/...) - if (suffixA.indexOf(".h") == 0) { - return -1; - } else if (suffixB.indexOf(".h") == 0) { - return 1; - } - } - return a.path().compareTo(b.path()); - } - }); + Collections.sort(Natives.asList(list), + Comparator.comparing(FileInfo::path, FilenameComparator.INSTANCE)); } 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..9c751ed 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/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java index cf7e1d8..5a6918a 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
@@ -14,6 +14,7 @@ package com.google.gerrit.client.ui; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.SuggestOracle; /** @@ -31,6 +32,10 @@ private final SuggestOracle oracle; private Query query; private String last; + private Timer requestRetentionTimer; + private boolean cancelOutstandingRequest; + + private boolean serveSuggestions; public RemoteSuggestOracle(SuggestOracle src) { oracle = src; @@ -42,13 +47,33 @@ @Override public void requestSuggestions(Request req, Callback cb) { - Query q = new Query(req, cb); - if (query == null) { - query = q; - q.start(); - } else { - query = q; + if (!serveSuggestions){ + return; } + + // Use a timer for key stroke retention, such that we don't query the + // backend for each and every keystroke we receive. + if (requestRetentionTimer != null) { + requestRetentionTimer.cancel(); + } + requestRetentionTimer = new Timer() { + @Override + public void run() { + Query q = new Query(req, cb); + if (query == null) { + query = q; + q.start(); + } else { + query = q; + } + } + }; + requestRetentionTimer.schedule(200); + } + + @Override + public void requestDefaultSuggestions(Request req, Callback cb) { + requestSuggestions(req, cb); } @Override @@ -56,6 +81,19 @@ return oracle.isDisplayStringHTML(); } + public void cancelOutstandingRequest() { + if (requestRetentionTimer != null) { + requestRetentionTimer.cancel(); + } + if (query != null) { + cancelOutstandingRequest = true; + } + } + + public void setServeSuggestions(boolean serveSuggestions) { + this.serveSuggestions = serveSuggestions; + } + private class Query implements Callback { final Request request; final Callback callback; @@ -71,7 +109,11 @@ @Override public void onSuggestionsReady(Request req, Response res) { - if (query == this) { + if (cancelOutstandingRequest || !serveSuggestions) { + // If cancelOutstandingRequest() was called, we ignore this response + cancelOutstandingRequest = false; + query = null; + } else if (query == this) { // No new request was started while this query was running. // Propose this request's response as the suggestions. query = null;
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-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java index 6705e51..23183f2 100644 --- a/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java +++ b/gerrit-gwtui-common/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
@@ -36,19 +36,19 @@ } @Test - public void testFuture() { + public void future() { assertFormat(-100, YEAR_IN_MILLIS, "in the future"); assertFormat(-1, SECOND_IN_MILLIS, "in the future"); } @Test - public void testFormatSeconds() { + public void formatSeconds() { assertFormat(1, SECOND_IN_MILLIS, "1 seconds ago"); assertFormat(89, SECOND_IN_MILLIS, "89 seconds ago"); } @Test - public void testFormatMinutes() { + public void formatMinutes() { assertFormat(90, SECOND_IN_MILLIS, "2 minutes ago"); assertFormat(3, MINUTE_IN_MILLIS, "3 minutes ago"); assertFormat(60, MINUTE_IN_MILLIS, "60 minutes ago"); @@ -56,33 +56,33 @@ } @Test - public void testFormatHours() { + public void formatHours() { assertFormat(90, MINUTE_IN_MILLIS, "2 hours ago"); assertFormat(149, MINUTE_IN_MILLIS, "2 hours ago"); assertFormat(35, HOUR_IN_MILLIS, "35 hours ago"); } @Test - public void testFormatDays() { + public void formatDays() { assertFormat(36, HOUR_IN_MILLIS, "2 days ago"); assertFormat(13, DAY_IN_MILLIS, "13 days ago"); } @Test - public void testFormatWeeks() { + public void formatWeeks() { assertFormat(14, DAY_IN_MILLIS, "2 weeks ago"); assertFormat(69, DAY_IN_MILLIS, "10 weeks ago"); } @Test - public void testFormatMonths() { + public void formatMonths() { assertFormat(70, DAY_IN_MILLIS, "2 months ago"); assertFormat(75, DAY_IN_MILLIS, "3 months ago"); assertFormat(364, DAY_IN_MILLIS, "12 months ago"); } @Test - public void testFormatYearsMonths() { + public void formatYearsMonths() { assertFormat(366, DAY_IN_MILLIS, "1 year ago"); assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago"); assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago"); @@ -91,7 +91,7 @@ } @Test - public void testFormatYears() { + public void formatYears() { assertFormat(5, YEAR_IN_MILLIS, "5 years ago"); assertFormat(60, YEAR_IN_MILLIS, "60 years ago"); }
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..721b646 --- /dev/null +++ b/gerrit-gwtui/BUILD
@@ -0,0 +1,40 @@ +load( + "//tools/bzl:gwt.bzl", + "gwt_genrule", + "gen_ui_module", + "gwt_user_agent_permutations", +) +load("//tools/bzl:license.bzl", "license_test") +load("//tools/bzl:junit.bzl", "junit_tests") + +gwt_genrule() + +gwt_genrule("_r") + +gen_ui_module(name = "ui_module") + +gen_ui_module( + name = "ui_module", + suffix = "_r", +) + +gwt_user_agent_permutations() + +license_test( + name = "ui_module_license_test", + target = ":ui_module", +) + +junit_tests( + name = "ui_tests", + srcs = glob(["src/test/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + ":ui_module", + "//gerrit-common:client", + "//gerrit-extension-api:client", + "//lib:junit", + "//lib/gwt:dev", + "//lib/gwt:user", + ], +)
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs index cd8fa74..85553f2 100644 --- a/gerrit-gwtui/gwt.defs +++ b/gerrit-gwtui/gwt.defs
@@ -18,14 +18,14 @@ 'firefox', 'gecko1_8', 'safari', - 'msie', 'ie8', 'ie9', 'ie10', 'ie11', + 'msie', 'ie8', 'ie9', 'ie10', 'edge', ] ALIASES = { 'chrome': 'safari', 'firefox': 'gecko1_8', - 'msie': 'ie11', - 'edge': 'edge', + 'msie': 'ie10', + 'edge': 'gecko1_8', } MODULE = 'com.google.gerrit.GerritGwtUI' CPU_COUNT = cpu_count() @@ -124,7 +124,6 @@ prebuilt_jar( name = '%s_gwtxml_lib' % gwt_name, binary_jar = ':%s_gwtxml_gen' % gwt_name, - gwt_jar = ':%s_gwtxml_gen' % gwt_name, ) gwt_binary( name = gwt_name,
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/MessageOfTheDayBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java index 054cdb3..de1bd93 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
@@ -46,10 +46,10 @@ initWidget(uiBinder.createAndBindUi(this)); SafeHtmlBuilder b = new SafeHtmlBuilder(); - if (motd.size() == 1) { - b.append(SafeHtml.asis(motd.get(0).html)); + if (this.motd.size() == 1) { + b.append(SafeHtml.asis(this.motd.get(0).html)); } else { - for (HostPageData.Message m : motd) { + for (HostPageData.Message m : this.motd) { b.openDiv(); b.append(SafeHtml.asis(m.html)); b.openElement("hr");
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..066464a 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
@@ -27,11 +27,11 @@ public class SearchSuggestOracle extends HighlightSuggestOracle { private static final List<ParamSuggester> paramSuggester = Arrays.asList( - new ParamSuggester(Arrays.asList("project:", "parentproject:"), + new ParamSuggester(Arrays.asList("project:", "p:", "parentproject:"), new ProjectNameSuggestOracle()), new ParamSuggester(Arrays.asList( - "owner:", "reviewer:", "commentby:", "reviewedby:", "author:", - "committer:", "from:"), + "owner:", "o:", "reviewer:", "r:", "commentby:", "reviewedby:", + "author:", "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/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java index 7cfb1fc..ae93a83 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -19,6 +19,7 @@ public class ProjectAccessInfo extends JavaScriptObject { public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/; public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/; + public final native boolean configVisible() /*-{ return this.config_visible ? true : false; }-*/; protected ProjectAccessInfo() { }
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..e7fa14c 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.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java index 984c5a3..322af1b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -77,6 +77,7 @@ String projectSubmitType_MERGE_ALWAYS(); String projectSubmitType_MERGE_IF_NECESSARY(); String projectSubmitType_REBASE_IF_NECESSARY(); + String projectSubmitType_REBASE_ALWAYS(); String projectSubmitType_CHERRY_PICK(); String headingProjectState();
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..1ae3a16 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
@@ -54,6 +54,7 @@ headingProjectSubmitType = Submit Type projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only projectSubmitType_MERGE_IF_NECESSARY = Merge if Necessary +projectSubmitType_REBASE_ALWAYS = Rebase Always projectSubmitType_REBASE_IF_NECESSARY = Rebase if Necessary projectSubmitType_MERGE_ALWAYS = Always Merge projectSubmitType_CHERRY_PICK = Cherry Pick @@ -123,7 +124,11 @@ abandon, \ addPatchSet, \ create, \ + createTag, \ + createSignedTag, \ + delete, \ deleteDrafts, \ + editAssignee, \ editHashtags, \ editTopicName, \ forgeAuthor, \ @@ -133,8 +138,6 @@ publishDrafts, \ push, \ pushMerge, \ - pushTag, \ - pushSignedTag, \ read, \ rebase, \ removeReviewer, \ @@ -145,7 +148,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 +162,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/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java index e1cfa90..24c2da7 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -72,6 +72,7 @@ public class ProjectInfoScreen extends ProjectScreen { private boolean isOwner; + private boolean configVisible; private LabeledWidgetsGrid grid; private Panel pluginOptionsPanel; @@ -154,6 +155,7 @@ @Override public void onSuccess(ProjectAccessInfo result) { isOwner = result.isOwner(); + configVisible = result.configVisible(); enableForm(); saveProject.setVisible(isOwner); } @@ -625,7 +627,7 @@ actionsPanel.add(createChangeAction()); } - if (isOwner) { + if (isOwner && configVisible) { actionsPanel.add(createEditConfigAction()); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java index 81286ea..3a46203 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -43,6 +43,8 @@ return C.projectSubmitType_MERGE_IF_NECESSARY(); case REBASE_IF_NECESSARY: return C.projectSubmitType_REBASE_IF_NECESSARY(); + case REBASE_ALWAYS: + return C.projectSubmitType_REBASE_ALWAYS(); case MERGE_ALWAYS: return C.projectSubmitType_MERGE_ALWAYS(); case CHERRY_PICK:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java index 36107ee..779c32b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -36,8 +36,20 @@ class Actions extends Composite { private static final String[] CORE = { - "abandon", "cherrypick", "followup", "hashtags", "publish", - "rebase", "restore", "revert", "submit", "topic", "/",}; + "abandon", + "assignee", + "cherrypick", + "description", + "followup", + "hashtags", + "publish", + "rebase", + "restore", + "revert", + "submit", + "topic", + "/", + }; interface Binder extends UiBinder<FlowPanel, Actions> {} private static final Binder uiBinder = GWT.create(Binder.class);
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..7d6b1c3 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -0,0 +1,224 @@ +// 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()); + 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); + if (currentAssignee != null) { + suggestBox.setText(FormatUtil.nameEmail(currentAssignee)); + suggestBox.selectAll(); + } else { + suggestBox.setText(""); + } + } + + 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.trim().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..47d7541 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.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.client.change; + +import com.google.gerrit.client.account.AccountApi; +import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.Natives; +import com.google.gerrit.client.ui.AccountSuggestOracle.AccountSuggestion; +import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle; +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 { + @Override + protected void _onRequestSuggestions(Request req, Callback cb) { + AccountApi.suggest(req.getQuery(), req.getLimit(), + new GerritCallback<JsArray<AccountInfo>>() { + @Override + public void onSuccess(JsArray<AccountInfo> result) { + List<AccountSuggestion> r = new ArrayList<>(result.length()); + for (AccountInfo reviewer : Natives.asList(result)) { + r.add(new AccountSuggestion(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/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java index 63de389..99f3b9f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -51,6 +51,6 @@ String abandoned(); String deleteChangeEdit(); - String deleteDraftChange(); + String deleteChange(); String deleteDraftRevision(); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties index 5b4f18f..dd4760d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
@@ -34,5 +34,5 @@ deleteChangeEdit = Delete Change Edit?\n\ \n\ All changes made in the edit revision will be lost. -deleteDraftChange = Delete Draft Change? +deleteChange = Delete Change? deleteDraftRevision = Delete Draft Revision?
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..794425e 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, @@ -435,7 +462,8 @@ } private void gotoSibling(int offset) { - if (offset > 0 && changeInfo.currentRevision().equals(revision)) { + if (offset > 0 && changeInfo.currentRevision() != null + && changeInfo.currentRevision().equals(revision)) { return; } @@ -468,15 +496,13 @@ } private void initChangeAction(ChangeInfo info) { - if (info.status() == Status.DRAFT) { - NativeMap<ActionInfo> actions = info.hasActions() - ? info.actions() - : NativeMap.<ActionInfo> create(); - actions.copyKeysIntoChildren("id"); - if (actions.containsKey("/")) { - deleteChange.setVisible(true); - deleteChange.setTitle(actions.get("/").title()); - } + NativeMap<ActionInfo> actions = info.hasActions() + ? info.actions() + : NativeMap.create(); + actions.copyKeysIntoChildren("id"); + if (actions.containsKey("/")) { + deleteChange.setVisible(true); + deleteChange.setTitle(actions.get("/").title()); } } @@ -569,35 +595,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 +647,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); + if (Window.confirm(Resources.C.deleteChange())) { + DraftActions.delete(changeId, publish, deleteRevision, deleteChange); } } @@ -884,7 +917,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 +970,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 +993,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 +1031,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 +1062,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 +1085,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 +1107,7 @@ @Override public void onFailure(Throwable caught) { + files.showError(caught); } })); } @@ -1224,6 +1320,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 +1499,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 +1512,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 4eacc8c..d7ae1af 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; @@ -181,7 +182,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( @@ -209,4 +210,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/LocalComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java index f6022f9..44316bc 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -216,7 +216,7 @@ } else { line = Integer.parseInt(elements[offset + 4]); } - CommentInfo info = CommentInfo.create(path, side, line, range); + CommentInfo info = CommentInfo.create(path, side, line, range, false); info.message(storage.getItem(key)); if (key.startsWith("patchReply-")) { info.inReplyTo(elements[1]);
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..0d0dba7 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,12 @@ 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() != null + && 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..0eea695 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
@@ -304,6 +304,9 @@ for (String id : names) { JsArrayString p = permitted.get(id); if (p != null) { + if (!all.containsKey(id)) { + continue; + } Set<Short> a = new TreeSet<>(); for (int i = 0; i < p.length(); i++) { a.add(LabelInfo.parseValue(p.get(i))); @@ -422,12 +425,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..6f518b1 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,27 +16,28 @@ 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; -import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle; import com.google.gerrit.reviewdb.client.Change; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; +import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** REST API based suggestion Oracle for reviewers. */ -public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle { +public class ReviewerSuggestOracle extends HighlightSuggestOracle { private Change.Id changeId; @Override - protected void _onRequestSuggestions(final Request req, final Callback cb) { - ChangeApi.suggestReviewers(changeId.get(), req.getQuery(), req.getLimit()) + protected void onRequestSuggestions(final Request req, final Callback cb) { + ChangeApi + .suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), false) .get(new GerritCallback<JsArray<SuggestReviewerInfo>>() { @Override public void onSuccess(JsArray<SuggestReviewerInfo> result) { @@ -55,11 +56,16 @@ }); } + @Override + public void requestDefaultSuggestions(final Request req, final Callback cb) { + requestSuggestions(req, cb); + } + public void setChange(Change.Id changeId) { 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..bebbaaa 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
@@ -81,6 +81,7 @@ Reviewers() { reviewerSuggestOracle = new ReviewerSuggestOracle(); suggestBox = new RemoteSuggestBox(reviewerSuggestOracle); + suggestBox.enableDefaultSuggestions(); suggestBox.setVisibleLength(55); suggestBox.setHintText(Util.C.approvalTableAddReviewerHint()); suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() { @@ -123,6 +124,7 @@ UIObject.setVisible(form, true); UIObject.setVisible(error, false); addReviewerIcon.setVisible(false); + suggestBox.setServeSuggestionsOnOracle(true); suggestBox.setFocus(true); } @@ -143,6 +145,7 @@ UIObject.setVisible(form, false); suggestBox.setFocus(false); suggestBox.setText(""); + suggestBox.setServeSuggestionsOnOracle(false); } private void addReviewer(final String reviewer, boolean confirmed) { @@ -198,7 +201,7 @@ }); } - private void updateReviewerList() { + void updateReviewerList() { ChangeApi.detail(changeId.get(), new GerritCallback<ChangeInfo>() { @Override @@ -252,6 +255,9 @@ Map<Integer, VotableInfo> d = new HashMap<>(); for (String name : change.labels()) { LabelInfo label = change.label(name); + Short labelMaxValue = label.valueSet().isEmpty() + ? null + : LabelInfo.parseValue(label.maxValue()); if (label.all() != null) { for (ApprovalInfo ai : Natives.asList(label.all())) { int id = ai._accountId(); @@ -260,7 +266,11 @@ ad = new VotableInfo(); d.put(id, ad); } - if (ai.hasValue()) { + if (labelMaxValue != null + && ai.permittedVotingRange() != null + && ai.permittedVotingRange().max() == labelMaxValue) { + ad.votable(name + " (" + label.maxValue() + ") "); + } else if (ai.hasValue()) { ad.votable(name); } }
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/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java index 62c14cb..fb66570 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -95,11 +95,13 @@ } private static String queryIncoming(String who) { - return "is:open reviewer:" + who + " -owner:" + who + " -star:ignore"; + return "is:open ((reviewer:" + who + " -owner:" + who + + " -star:ignore) OR assignee:" + who + ")"; } private static String queryClosed(String who) { - return "is:closed (owner:" + who + " OR reviewer:" + who + ")"; + return "is:closed (owner:" + who + " OR reviewer:" + who + " OR assignee:" + + who + ")"; } @Override
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..84c2403 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(input, cb); + } + public static RestApi comments(int id) { return call(id, "comments"); } @@ -157,10 +170,14 @@ return change(id).view("reviewers"); } - public static RestApi suggestReviewers(int id, String q, int n) { - return change(id).view("suggest_reviewers") - .addParameter("q", q) - .addParameter("n", n); + public static RestApi suggestReviewers(int id, String q, int n, boolean e) { + RestApi api = change(id).view("suggest_reviewers") + .addParameter("n", n) + .addParameter("e", e); + if (q != null) { + api.addParameter("q", q); + } + return api; } public static RestApi vote(int id, int reviewer, String vote) { @@ -178,12 +195,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 +218,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 +237,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 +260,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 +309,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 +321,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/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java index d42c344..2800b0b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -24,12 +24,12 @@ public class CommentInfo extends JavaScriptObject { public static CommentInfo create(String path, Side side, - int line, CommentRange range) { - return create(path, side, 0, line, range); + int line, CommentRange range, Boolean unresolved) { + return create(path, side, 0, line, range, unresolved); } public static CommentInfo create(String path, Side side, int parent, - int line, CommentRange range) { + int line, CommentRange range, boolean unresolved) { CommentInfo n = createObject().cast(); n.path(path); n.side(side); @@ -40,6 +40,7 @@ } else if (line > 0) { n.line(line); } + n.unresolved(unresolved); return n; } @@ -55,6 +56,7 @@ } else if (r.hasLine()) { n.line(r.line()); } + n.unresolved(r.unresolved()); return n; } @@ -72,6 +74,7 @@ } else if (s.hasLine()) { n.line(s.line()); } + n.unresolved(s.unresolved()); return n; } @@ -81,6 +84,7 @@ public final native void range(CommentRange r) /*-{ this.range = r }-*/; public final native void inReplyTo(String i) /*-{ this.in_reply_to = i }-*/; public final native void message(String m) /*-{ this.message = m }-*/; + public final native void unresolved(boolean b) /*-{ this.unresolved = b }-*/; public final void side(Side side) { sideRaw(side.toString()); @@ -93,6 +97,7 @@ public final native String id() /*-{ return this.id }-*/; public final native String inReplyTo() /*-{ return this.in_reply_to }-*/; public final native int patchSet() /*-{ return this.patch_set }-*/; + public final native boolean unresolved() /*-{ return this.unresolved }-*/; public final Side side() { String s = sideRaw();
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..1d1ead9 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; } @@ -194,7 +196,8 @@ getStoredSideFromDisplaySide(side), getParentNumFromDisplaySide(side), line, - null)).setEdit(true); + null, + false)).setEdit(true); } }
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..1981ed0 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) { @@ -86,7 +87,8 @@ getStoredSideFromDisplaySide(cm.side()), getParentNumFromDisplaySide(cm.side()), line, - CommentRange.create(fromTo))).setEdit(true); + CommentRange.create(fromTo), + false)).setEdit(true); cm.setCursor(fromTo.to()); cm.setSelection(cm.getCursor()); } else {
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..cc23aca 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) { @@ -175,7 +176,8 @@ getPath(), getStoredSideFromDisplaySide(side), to.line() + 1, - CommentRange.create(fromTo))).setEdit(true); + CommentRange.create(fromTo), + false)).setEdit(true); cm.setCursor(Pos.create(host.getCmLine(to.line(), side), to.ch())); cm.setSelection(cm.getCursor()); } else {
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/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java index 3702e68..e43a24e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -45,7 +45,7 @@ public static class AccountSuggestion implements SuggestOracle.Suggestion { private final String suggestion; - AccountSuggestion(AccountInfo info, String query) { + public AccountSuggestion(AccountInfo info, String query) { this.suggestion = format(info, query); } @@ -61,7 +61,8 @@ public static String format(AccountInfo info, String query) { String s = FormatUtil.nameEmail(info); - if (!containsQuery(s, query) && info.secondaryEmails() != null) { + if (query != null && !containsQuery(s, query) && + info.secondaryEmails() != null) { for (String email : Natives.asList(info.secondaryEmails())) { AccountInfo info2 = AccountInfo.create(info._accountId(), info.name(), email, info.username());
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..57cd849 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
@@ -13,6 +13,8 @@ // limitations under the License. package com.google.gerrit.client.ui; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; @@ -42,6 +44,7 @@ public RemoteSuggestBox(SuggestOracle oracle) { remoteSuggestOracle = new RemoteSuggestOracle(oracle); + remoteSuggestOracle.setServeSuggestions(true); display = new DefaultSuggestionDisplay(); textBox = new HintTextBox(); @@ -49,7 +52,6 @@ @Override public void onKeyDown(KeyDownEvent e) { submitOnSelection = false; - if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { CloseEvent.fire(RemoteSuggestBox.this, RemoteSuggestBox.this); } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) { @@ -70,10 +72,11 @@ suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() { @Override public void onSelection(SelectionEvent<Suggestion> event) { - textBox.setFocus(true); if (submitOnSelection) { SelectionEvent.fire(RemoteSuggestBox.this, getText()); } + remoteSuggestOracle.cancelOutstandingRequest(); + display.hideSuggestions(); } }); initWidget(suggestBox); @@ -134,4 +137,23 @@ public HandlerRegistration addCloseHandler(CloseHandler<RemoteSuggestBox> h) { return addHandler(h, CloseEvent.getType()); } + + public void selectAll() { + suggestBox.getValueBox().selectAll(); + } + + public void enableDefaultSuggestions() { + textBox.addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent focusEvent) { + if (textBox.getText().equals("")) { + suggestBox.showSuggestionList(); + } + } + }); + } + + public void setServeSuggestionsOnOracle(boolean serveSuggestions) { + remoteSuggestOracle.setServeSuggestions(serveSuggestions); + } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java index 2d7736b..cab29da 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
@@ -29,7 +29,8 @@ @Override protected void onRequestSuggestions(Request req, Callback cb) { - if (req.getQuery().length() >= Gerrit.info().suggest().from()) { + if (req.getQuery() != null + && req.getQuery().length() >= Gerrit.info().suggest().from()) { _onRequestSuggestions(req, cb); } else { List<Suggestion> none = Collections.emptyList();
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-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java index 25c8270..8e59b10 100644 --- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java +++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
@@ -24,7 +24,7 @@ public class LineMapperTest { @Test - public void testAppendCommon() { + public void appendCommon() { LineMapper mapper = new LineMapper(); mapper.appendCommon(10); assertEquals(10, mapper.getLineA()); @@ -32,7 +32,7 @@ } @Test - public void testAppendInsert() { + public void appendInsert() { LineMapper mapper = new LineMapper(); mapper.appendInsert(10); assertEquals(0, mapper.getLineA()); @@ -40,7 +40,7 @@ } @Test - public void testAppendDelete() { + public void appendDelete() { LineMapper mapper = new LineMapper(); mapper.appendDelete(10); assertEquals(10, mapper.getLineA()); @@ -48,7 +48,7 @@ } @Test - public void testFindInCommon() { + public void findInCommon() { LineMapper mapper = new LineMapper(); mapper.appendCommon(10); assertEquals(new LineOnOtherInfo(9, true), @@ -58,7 +58,7 @@ } @Test - public void testFindAfterCommon() { + public void findAfterCommon() { LineMapper mapper = new LineMapper(); mapper.appendCommon(10); assertEquals(new LineOnOtherInfo(10, true), @@ -68,7 +68,7 @@ } @Test - public void testFindInInsertGap() { + public void findInInsertGap() { LineMapper mapper = new LineMapper(); mapper.appendInsert(10); assertEquals(new LineOnOtherInfo(-1, false), @@ -76,7 +76,7 @@ } @Test - public void testFindAfterInsertGap() { + public void findAfterInsertGap() { LineMapper mapper = new LineMapper(); mapper.appendInsert(10); assertEquals(new LineOnOtherInfo(0, true), @@ -86,7 +86,7 @@ } @Test - public void testFindInDeleteGap() { + public void findInDeleteGap() { LineMapper mapper = new LineMapper(); mapper.appendDelete(10); assertEquals(new LineOnOtherInfo(-1, false), @@ -94,7 +94,7 @@ } @Test - public void testFindAfterDeleteGap() { + public void findAfterDeleteGap() { LineMapper mapper = new LineMapper(); mapper.appendDelete(10); assertEquals(new LineOnOtherInfo(0, true), @@ -104,7 +104,7 @@ } @Test - public void testReplaceWithInsertInB() { + public void replaceWithInsertInB() { // 0 c c // 1 a b // 2 a b
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/BUILD b/gerrit-httpd/BUILD index 1341ad1..6244b5b 100644 --- a/gerrit-httpd/BUILD +++ b/gerrit-httpd/BUILD
@@ -1,72 +1,77 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +package( + default_visibility = ["//visibility:public"], +) + +load("//tools/bzl:junit.bzl", "junit_tests") SRCS = glob( - ['src/main/java/**/*.java'], + ["src/main/java/**/*.java"], ) -RESOURCES = glob(['src/main/resources/**/*']) + +RESOURCES = glob(["src/main/resources/**/*"]) java_library( - name = 'httpd', - srcs = SRCS, - resources = RESOURCES, - deps = [ - '//gerrit-antlr:query_exception', - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-gwtexpui:linker_server', - '//gerrit-gwtexpui:server', - '//gerrit-launcher:launcher', - '//gerrit-patch-jgit:server', - '//gerrit-prettify:server', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//gerrit-util-cli:cli', - '//gerrit-util-http:http', - '//lib:args4j', - '//lib:gson', - '//lib:guava', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:jsch', - '//lib:mime-util', - '//lib:servlet-api-3_1', - '//lib/auto:auto-value', - '//lib/commons:codec', - '//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/log:api', - '//lib/lucene:lucene-core-and-backward-codecs', - ], - visibility = ['//visibility:public'], + name = "httpd", + srcs = SRCS, + resources = RESOURCES, + visibility = ["//visibility:public"], + deps = [ + "//gerrit-antlr:query_exception", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-gwtexpui:linker_server", + "//gerrit-gwtexpui:server", + "//gerrit-launcher:launcher", + "//gerrit-patch-jgit:server", + "//gerrit-prettify:server", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//gerrit-util-cli:cli", + "//gerrit-util-http:http", + "//lib:args4j", + "//lib:gson", + "//lib:guava", + "//lib:gwtjsonrpc", + "//lib:gwtorm", + "//lib:jsch", + "//lib:mime-util", + "//lib:servlet-api-3_1", + "//lib/auto:auto-value", + "//lib/commons:codec", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + "//lib/lucene:lucene-core-and-backward-codecs", + ], ) junit_tests( - name = 'httpd_tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':httpd', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//gerrit-util-http:http', - '//gerrit-util-http:testutil', - '//lib:jimfs', - '//lib:junit', - '//lib:gson', - '//lib:gwtorm', - '//lib:guava', - '//lib:servlet-api-3_1-without-neverlink', - '//lib:truth', - '//lib/easymock:easymock', - '//lib/guice:guice', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.junit:junit', - '//lib/joda:joda-time', - ], + name = "httpd_tests", + srcs = glob(["src/test/java/**/*.java"]), + deps = [ + ":httpd", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//gerrit-util-http:http", + "//gerrit-util-http:testutil", + "//lib:gson", + "//lib:guava", + "//lib:gwtorm", + "//lib:jimfs", + "//lib:junit", + "//lib:servlet-api-3_1-without-neverlink", + "//lib:truth", + "//lib/easymock", + "//lib/guice", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/jgit/org.eclipse.jgit.junit:junit", + "//lib/joda:joda-time", + ], )
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/HttpServletResponseRecorder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java new file mode 100644 index 0000000..3f419ed --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpServletResponseRecorder.java
@@ -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. + +package com.google.gerrit.httpd; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * HttpServletResponse wrapper to allow response status code override. + * + * Differently from the normal HttpServletResponse, this class allows multiple + * filters to override the response http status code. + */ +public class HttpServletResponseRecorder extends HttpServletResponseWrapper { + private static final Logger log = LoggerFactory + .getLogger(HttpServletResponseWrapper.class); + private static final String LOCATION_HEADER = "Location"; + + private int status; + private String statusMsg = ""; + private Map<String, String> headers = new HashMap<>(); + + /** + * Constructs a response recorder wrapping the given response. + * + * @param response the response to be wrapped + */ + public HttpServletResponseRecorder(HttpServletResponse response) { + super(response); + } + + @Override + public void sendError(int sc) throws IOException { + this.status = sc; + } + + @Override + public void sendError(int sc, String msg) throws IOException { + this.status = sc; + this.statusMsg = msg; + } + + @Override + public void sendRedirect(String location) throws IOException { + this.status = SC_MOVED_TEMPORARILY; + setHeader(LOCATION_HEADER, location); + } + + @Override + public void setHeader(String name, String value) { + super.setHeader(name, value); + headers.put(name, value); + } + + @SuppressWarnings("all") + // @Override is omitted for backwards compatibility with servlet-api 2.5 + // TODO: Remove @SuppressWarnings and add @Override when Google upgrades + // to servlet-api 3.1 + public int getStatus() { + return status; + } + + void play() throws IOException { + if (status != 0) { + log.debug("Replaying {} {}", status, statusMsg); + + if (status == SC_MOVED_TEMPORARILY) { + super.sendRedirect(headers.get(LOCATION_HEADER)); + } else { + super.sendError(status, statusMsg); + } + } + } +}
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/RemoteUserUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RemoteUserUtil.java index 479a5e5..e99838a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RemoteUserUtil.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RemoteUserUtil.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Strings.emptyToNull; import static com.google.common.net.HttpHeaders.AUTHORIZATION; +import static java.nio.charset.StandardCharsets.UTF_8; import org.eclipse.jgit.util.Base64; @@ -72,7 +73,7 @@ } else if (auth.startsWith("Basic ")) { auth = auth.substring("Basic ".length()); - auth = new String(Base64.decode(auth)); + auth = new String(Base64.decode(auth), UTF_8); final int c = auth.indexOf(':'); return c > 0 ? auth.substring(0, c) : null;
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/UniversalWebLoginFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java new file mode 100644 index 0000000..f6efb61 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
@@ -0,0 +1,104 @@ +// 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.httpd; + +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.servlet.ServletModule; + +import java.io.IOException; +import java.util.Optional; + +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.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class UniversalWebLoginFilter implements Filter { + private final DynamicItem<WebSession> session; + private final DynamicSet<WebLoginListener> webLoginListeners; + private final Provider<CurrentUser> userProvider; + + public static ServletModule module() { + return new ServletModule() { + @Override + protected void configureServlets() { + filter("/login*", "/logout*").through(UniversalWebLoginFilter.class); + bind(UniversalWebLoginFilter.class).in(Singleton.class); + + DynamicSet.setOf(binder(), WebLoginListener.class); + } + }; + } + + @Inject + public UniversalWebLoginFilter(DynamicItem<WebSession> session, + DynamicSet<WebLoginListener> webLoginListeners, + Provider<CurrentUser> userProvider) { + this.session = session; + this.webLoginListeners = webLoginListeners; + this.userProvider = userProvider; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponseRecorder wrappedResponse = + new HttpServletResponseRecorder((HttpServletResponse) response); + + Optional<IdentifiedUser> loggedInUserBefore = loggedInUser(); + chain.doFilter(request, wrappedResponse); + Optional<IdentifiedUser> loggedInUserAfter = loggedInUser(); + + if (!loggedInUserBefore.isPresent() && loggedInUserAfter.isPresent()) { + for (WebLoginListener loginListener : webLoginListeners) { + loginListener.onLogin(loggedInUserAfter.get(), httpRequest, + wrappedResponse); + } + } else if (loggedInUserBefore.isPresent() && !loggedInUserAfter.isPresent()) { + for (WebLoginListener loginListener : webLoginListeners) { + loginListener.onLogout(loggedInUserBefore.get(), httpRequest, + wrappedResponse); + } + } + + wrappedResponse.play(); + } + + private Optional<IdentifiedUser> loggedInUser() { + return session.get().isSignedIn() ? + Optional.of(userProvider.get().asIdentifiedUser()) : + Optional.empty(); + } + + @Override + public void destroy() { + } + +}
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..842c575 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()); @@ -90,13 +92,23 @@ serve("/starred").with(query("is:starred")); serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS)); - serveRegex("^/register/?$").with(screen(PageLinks.REGISTER + "/")); + serveRegex("^/register(/.*)?$").with(registerScreen()); serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById()); serveRegex("^/p/(.*)$").with(queryProjectNew()); 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); @@ -231,6 +243,18 @@ return srv; } + private Key<HttpServlet> registerScreen() { + return key(new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, + final HttpServletResponse rsp) throws IOException { + toGerrit("/register" + req.getPathInfo(), req, rsp); + } + }); + } + static void toGerrit(final String target, final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { final StringBuilder url = new StringBuilder();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebLoginListener.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebLoginListener.java new file mode 100644 index 0000000..55e927b --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebLoginListener.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.httpd; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.server.IdentifiedUser; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Allows to listen and override the reponse to login/logout web actions. + * + * Allows to intercept and act when a Gerrit user logs in or logs out of + * the Web interface to perform actions or to override the output response + * status code. + * + * Typical use can be multi-factor authentication (on login) or global sign-out + * from SSO systems (on logout). + * + */ +@ExtensionPoint +public interface WebLoginListener { + + /** + * Invoked after a user's web login. + * + * @param userId logged in user + * @param request request of the latest login action + * @param response response of the latest login action + */ + void onLogin(IdentifiedUser userId, HttpServletRequest request, + HttpServletResponse response) throws IOException; + + /** + * Invoked after a user's web logout. + * + * @param userId logged out user + * @param request request of the latest logout action + * @param response response of the latest logout action + */ + void onLogout(IdentifiedUser userId, HttpServletRequest request, + HttpServletResponse response) throws IOException; +}
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..48ba60e 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)); @@ -77,6 +76,8 @@ bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.class); listener().toInstance(registerInParentInjectors()); + + install(UniversalWebLoginFilter.module()); } private void installAuthModule() {
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 79d298c..2d981f5 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
@@ -42,6 +42,7 @@ import com.google.gerrit.server.config.GitwebCgiConfig; import com.google.gerrit.server.config.GitwebConfig; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.LocalDiskRepositoryManager; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; @@ -49,6 +50,7 @@ import com.google.gwtexpui.server.CacheHeaders; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.ProvisionException; import com.google.inject.Singleton; import org.eclipse.jgit.errors.RepositoryNotFoundException; @@ -100,7 +102,7 @@ private final EnvList _env; @Inject - GitwebServlet(LocalDiskRepositoryManager repoManager, + GitwebServlet(GitRepositoryManager repoManager, ProjectControl.Factory projectControl, Provider<AnonymousUser> anonymousUserProvider, Provider<CurrentUser> userProvider, @@ -110,7 +112,11 @@ GitwebConfig gitwebConfig, GitwebCgiConfig gitwebCgiConfig) throws IOException { - this.repoManager = repoManager; + if (!(repoManager instanceof LocalDiskRepositoryManager)) { + throw new ProvisionException( + "Gitweb can only be used with LocalDiskRepositoryManager"); + } + this.repoManager = (LocalDiskRepositoryManager)repoManager; this.projectControl = projectControl; this.anonymousUserProvider = anonymousUserProvider; this.userProvider = userProvider; @@ -612,45 +618,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..3812fa11 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,12 @@ 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; @@ -73,7 +71,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; 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-")) { @@ -438,7 +433,8 @@ appendPluginInfoTable(md, scanner.getManifest().getMainAttributes()); if (about != null) { - InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about)); + InputStreamReader isr = new InputStreamReader( + scanner.getInputStream(about), UTF_8); StringBuilder aboutContent = new StringBuilder(); try (BufferedReader reader = new BufferedReader(isr)) { String line; @@ -561,7 +557,7 @@ int d = file.lastIndexOf('.'); return scanner.getEntry(file.substring(0, d) + ".md"); } - return Optional.absent(); + return Optional.empty(); } private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java index 04e49c9..60ceeb9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
@@ -39,10 +39,9 @@ return null; } - @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public Enumeration getInitParameterNames() { - return Collections.enumeration(Collections.emptyList()); + public Enumeration<String> getInitParameterNames() { + return Collections.emptyEnumeration(); } @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BazelBuild.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BazelBuild.java new file mode 100644 index 0000000..594d209 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BazelBuild.java
@@ -0,0 +1,70 @@ +// 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.httpd.raw; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Properties; + +public class BazelBuild extends BuildSystem { + public BazelBuild(Path sourceRoot) { + super(sourceRoot); + } + + @Override + protected ProcessBuilder newBuildProcess(Label label) throws IOException { + Properties properties = loadBuildProperties( + sourceRoot.resolve(".primary_build_tool")); + String buck = firstNonNull(properties.getProperty("bazel"), "bazel"); + ProcessBuilder proc = new ProcessBuilder(buck, "build", label.fullName()); + if (properties.containsKey("PATH")) { + proc.environment().put("PATH", properties.getProperty("PATH")); + } + return proc; + } + + @Override + public String buildCommand(Label l) { + return "bazel build " + l.toString(); + } + + @Override + public Path targetPath(Label l) { + return sourceRoot.resolve("bazel-bin").resolve(l.pkg).resolve(l.name); + } + + @Override + public Label gwtZipLabel(String agent) { + return new Label("gerrit-gwtui", "ui_" + agent + ".zip"); + } + + @Override + public Label polygerritComponents() { + return new Label("polygerrit-ui", + "polygerrit_components.bower_components.zip"); + } + + @Override + public Label fontZipLabel() { + return new Label("polygerrit-ui", "fonts.zip"); + } + + @Override + public String name() { + return "bazel"; + } +}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsDevServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsDevServlet.java new file mode 100644 index 0000000..027a04e --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsDevServlet.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.raw; + +import com.google.common.cache.Cache; +import com.google.gerrit.httpd.raw.BuildSystem.Label; +import com.google.gerrit.launcher.GerritLauncher; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; + +/* Bower component servlet only used in development mode */ +class BowerComponentsDevServlet extends ResourceServlet { + private static final long serialVersionUID = 1L; + + private final Path bowerComponents; + private final Path zip; + + BowerComponentsDevServlet(Cache<Path, Resource> cache, + BuildSystem builder) throws IOException { + super(cache, true); + + Objects.requireNonNull(builder); + Label label = builder.polygerritComponents(); + try { + builder.build(label); + } catch (BuildSystem.BuildFailureException e) { + throw new IOException(e); + } + + zip = builder.targetPath(label); + bowerComponents = GerritLauncher + .newZipFileSystem(zip) + .getPath("/"); + } + + @Override + protected Path getResourcePath(String pathInfo) throws IOException { + return bowerComponents.resolve(pathInfo); + } +}
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 deleted file mode 100644 index ef55e34..0000000 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java +++ /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. - -package com.google.gerrit.httpd.raw; - -import com.google.common.cache.Cache; -import com.google.gerrit.launcher.GerritLauncher; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -class BowerComponentsServlet extends ResourceServlet { - private static final long serialVersionUID = 1L; - - private final Path zip; - private final Path bowerComponents; - - BowerComponentsServlet(Cache<Path, Resource> cache, Path buckOut) - throws IOException { - super(cache, true); - zip = getZipPath(buckOut); - if (zip == null || !Files.exists(zip)) { - bowerComponents = null; - } else { - bowerComponents = GerritLauncher - .newZipFileSystem(zip) - .getPath("bower_components/"); - } - } - - @Override - protected Path getResourcePath(String pathInfo) throws IOException { - if (bowerComponents == null) { - throw new IOException("No polymer components found: " + zip - + ". Run `buck build //polygerrit-ui:polygerrit_components`?"); - } - return bowerComponents.resolve(pathInfo); - } - - private static Path getZipPath(Path buckOut) { - if (buckOut == null) { - return null; - } - return buckOut.resolve("gen") - .resolve("polygerrit-ui") - .resolve("polygerrit_components") - .resolve("polygerrit_components.bower_components.zip"); - } -}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java index 0b4a02e..7d85877 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java
@@ -15,104 +15,65 @@ package com.google.gerrit.httpd.raw; import static com.google.common.base.MoreObjects.firstNonNull; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.escape.Escaper; -import com.google.common.html.HtmlEscapers; -import com.google.common.io.ByteStreams; -import com.google.gerrit.common.TimeUtil; -import com.google.gwtexpui.server.CacheHeaders; - -import org.eclipse.jgit.util.RawParseUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Properties; -import javax.servlet.http.HttpServletResponse; +class BuckUtils extends BuildSystem { + BuckUtils(Path sourceRoot) { + super(sourceRoot); + } -class BuckUtils { - private static final Logger log = - LoggerFactory.getLogger(BuckUtils.class); - - static void build(Path root, Path gen, String target) - throws IOException, BuildFailureException { - log.info("buck build " + target); - Properties properties = loadBuckProperties(gen); + @Override + protected ProcessBuilder newBuildProcess(Label label) throws IOException { + Properties properties = loadBuildProperties( + sourceRoot.resolve("buck-out/gen/tools/buck/buck.properties")); String buck = firstNonNull(properties.getProperty("buck"), "buck"); - ProcessBuilder proc = new ProcessBuilder(buck, "build", target) - .directory(root.toFile()) - .redirectErrorStream(true); + ProcessBuilder proc = new ProcessBuilder(buck, "build", label.fullName()); if (properties.containsKey("PATH")) { proc.environment().put("PATH", properties.getProperty("PATH")); } - long start = TimeUtil.nowMs(); - Process rebuild = proc.start(); - byte[] out; - try (InputStream in = rebuild.getInputStream()) { - out = ByteStreams.toByteArray(in); - } finally { - rebuild.getOutputStream().close(); - } - - int status; - try { - status = rebuild.waitFor(); - } catch (InterruptedException e) { - throw new InterruptedIOException("interrupted waiting for " + buck); - } - if (status != 0) { - throw new BuildFailureException(out); - } - - long time = TimeUtil.nowMs() - start; - log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0)); + return proc; } - private static Properties loadBuckProperties(Path gen) throws IOException { - Properties properties = new Properties(); - Path p = gen.resolve(Paths.get("tools/buck/buck.properties")); - try (InputStream in = Files.newInputStream(p)) { - properties.load(in); - } catch (NoSuchFileException e) { - // Ignore; will be run from PATH, with a descriptive error if it fails. - } - return properties; + @Override + public Path targetPath(Label label) { + return sourceRoot.resolve("buck-out") + .resolve("gen").resolve(label.artifact); } - static void displayFailure(String rule, byte[] why, HttpServletResponse res) - throws IOException { - res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - res.setContentType("text/html"); - res.setCharacterEncoding(UTF_8.name()); - CacheHeaders.setNotCacheable(res); - - Escaper html = HtmlEscapers.htmlEscaper(); - try (PrintWriter w = res.getWriter()) { - w.write("<html><title>BUILD FAILED</title><body>"); - w.format("<h1>%s FAILED</h1>", html.escape(rule)); - w.write("<pre>"); - w.write(html.escape(RawParseUtils.decode(why))); - w.write("</pre>"); - w.write("</body></html>"); - } + @Override + public String buildCommand(Label l) { + return "buck build " + l.toString(); } - static class BuildFailureException extends Exception { - private static final long serialVersionUID = 1L; + @Override + public Label gwtZipLabel(String agent) { + // TODO(davido): instead of assuming specific Buck's internal + // target directory for gwt_binary() artifacts, ask Buck for + // the location of user agent permutation GWT zip, e. g.: + // $ buck targets --show_output //gerrit-gwtui:ui_safari \ + // | awk '{print $2}' + String t = "ui_" + agent; + return new BuildSystem.Label("gerrit-gwtui", t, + String.format("gerrit-gwtui/__gwt_binary_%s__/%s.zip", t, t)); + } - final byte[] why; + @Override + public Label polygerritComponents() { + return new Label("polygerrit-ui", "polygerrit_components", + "polygerrit-ui/polygerrit_components/" + + "polygerrit_components.bower_components.zip"); + } - BuildFailureException(byte[] why) { - this.why = why; - } + @Override + public Label fontZipLabel() { + return new Label("polygerrit-ui", "fonts", "polygerrit-ui/fonts/fonts.zip"); + } + + @Override + public String name() { + return "buck"; } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuildSystem.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuildSystem.java new file mode 100644 index 0000000..76d3110 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuildSystem.java
@@ -0,0 +1,176 @@ +// 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.httpd.raw; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.escape.Escaper; +import com.google.common.html.HtmlEscapers; +import com.google.common.io.ByteStreams; +import com.google.gerrit.common.TimeUtil; +import com.google.gwtexpui.server.CacheHeaders; + +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Properties; + +import javax.servlet.http.HttpServletResponse; + +public abstract class BuildSystem { + private static final Logger log = + LoggerFactory.getLogger(BuildSystem.class); + + protected final Path sourceRoot; + + public BuildSystem(Path sourceRoot) { + this.sourceRoot = sourceRoot; + } + + protected abstract ProcessBuilder newBuildProcess(Label l) throws IOException; + + protected static Properties loadBuildProperties(Path propPath) + throws IOException { + Properties properties = new Properties(); + try (InputStream in = Files.newInputStream(propPath)) { + properties.load(in); + } catch (NoSuchFileException e) { + // Ignore; will be run from PATH, with a descriptive error if it fails. + } + return properties; + } + + // builds the given label. + public void build(Label label) + throws IOException, BuildFailureException { + ProcessBuilder proc = newBuildProcess(label); + proc.directory(sourceRoot.toFile()) + .redirectErrorStream(true); + log.info("building [" + name() + "] " + label.fullName()); + long start = TimeUtil.nowMs(); + Process rebuild = proc.start(); + byte[] out; + try (InputStream in = rebuild.getInputStream()) { + out = ByteStreams.toByteArray(in); + } finally { + rebuild.getOutputStream().close(); + } + + int status; + try { + status = rebuild.waitFor(); + } catch (InterruptedException e) { + throw new InterruptedIOException("interrupted waiting for " + proc.toString()); + } + if (status != 0) { + log.warn("build failed: " + new String(out)); + throw new BuildFailureException(out); + } + + long time = TimeUtil.nowMs() - start; + log.info(String.format("UPDATED %s in %.3fs", label.fullName(), + time / 1000.0)); + } + + // Represents a label in either buck or bazel. + class Label { + protected final String pkg; + protected final String name; + + // Regrettably, buck confounds rule names and artifact names, + // and so we have to lug this along. Non-null only for Buck; in that case, + // holds the path relative to buck-out/gen/ + protected final String artifact; + + public String fullName() { + return "//" + pkg + ":" + name; + } + + @Override + public String toString() { + String s = fullName(); + if (!name.equals(artifact)) { + s += "(" + artifact + ")"; + } + return s; + } + + // Label in Buck style. + Label(String pkg, String name, String artifact) { + this.name = name; + this.pkg = pkg; + this.artifact = artifact; + } + + // Label in Bazel style. + Label(String pkg, String name) { + this(pkg, name, name); + } + } + + class BuildFailureException extends Exception { + private static final long serialVersionUID = 1L; + + final byte[] why; + + BuildFailureException(byte[] why) { + this.why = why; + } + + public void display(String rule, HttpServletResponse res) + throws IOException { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + res.setContentType("text/html"); + res.setCharacterEncoding(UTF_8.name()); + CacheHeaders.setNotCacheable(res); + + Escaper html = HtmlEscapers.htmlEscaper(); + try (PrintWriter w = res.getWriter()) { + w.write("<html><title>BUILD FAILED</title><body>"); + w.format("<h1>%s FAILED</h1>", html.escape(rule)); + w.write("<pre>"); + w.write(html.escape(RawParseUtils.decode(why))); + w.write("</pre>"); + w.write("</body></html>"); + } + } + } + + /** returns the command to build given target */ + abstract String buildCommand(Label l); + + /** returns the root relative path to the artifact for the given label */ + abstract Path targetPath(Label l); + + /** Label for the agent specific GWT zip. */ + abstract Label gwtZipLabel(String agent); + + /** Label for the polygerrit component zip. */ + abstract Label polygerritComponents(); + + /** Label for the fonts zip file. */ + abstract Label fontZipLabel(); + + /** Build system name. */ + abstract String name(); +}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java index 4047279..42b5e7e 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -14,7 +14,6 @@ package com.google.gerrit.httpd.raw; -import com.google.common.base.Optional; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Change; @@ -33,6 +32,7 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsDevServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsDevServlet.java new file mode 100644 index 0000000..b7b650a --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsDevServlet.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.httpd.raw; + +import com.google.common.cache.Cache; +import com.google.gerrit.launcher.GerritLauncher; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; + +/* Font servlet only used in development mode */ +class FontsDevServlet extends ResourceServlet { + private static final long serialVersionUID = 1L; + + private final Path fonts; + + FontsDevServlet(Cache<Path, Resource> cache, BuildSystem builder) + throws IOException { + super(cache, true); + Objects.requireNonNull(builder); + + BuildSystem.Label zipLabel = builder.fontZipLabel(); + try { + builder.build(zipLabel); + } catch (BuildSystem.BuildFailureException e) { + throw new IOException(e); + } + + Path zip = builder.targetPath(zipLabel); + Objects.requireNonNull(zip); + + fonts = GerritLauncher.newZipFileSystem(zip).getPath("/"); + } + + @Override + protected Path getResourcePath(String pathInfo) throws IOException { + return fonts.resolve(pathInfo); + } +}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java deleted file mode 100644 index 3a8c8cb..0000000 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java +++ /dev/null
@@ -1,61 +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.httpd.raw; - -import com.google.common.cache.Cache; -import com.google.gerrit.launcher.GerritLauncher; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -class FontsServlet extends ResourceServlet { - private static final long serialVersionUID = 1L; - - private final Path zip; - private final Path fonts; - - FontsServlet(Cache<Path, Resource> cache, Path buckOut) - throws IOException { - super(cache, true); - zip = getZipPath(buckOut); - if (zip == null || !Files.exists(zip)) { - fonts = null; - } else { - fonts = GerritLauncher - .newZipFileSystem(zip) - .getPath("/"); - } - } - - @Override - protected Path getResourcePath(String pathInfo) throws IOException { - if (fonts == null) { - throw new IOException("No fonts found: " + zip - + ". Run `buck build //polygerrit-ui:fonts`?"); - } - return fonts.resolve(pathInfo); - } - - private static Path getZipPath(Path buckOut) { - if (buckOut == null) { - return null; - } - return buckOut.resolve("gen") - .resolve("polygerrit-ui") - .resolve("fonts") - .resolve("fonts.zip"); - } -}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java index 1984cbb..c36d257 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -14,7 +14,7 @@ package com.google.gerrit.httpd.raw; -import com.google.gerrit.httpd.raw.BuckUtils.BuildFailureException; +import com.google.gerrit.httpd.raw.BuildSystem.Label; import com.google.gwtexpui.linker.server.UserAgentRule; import java.io.File; @@ -43,48 +43,40 @@ private final UserAgentRule rule = new UserAgentRule(); private final Set<String> uaInitialized = new HashSet<>(); private final Path unpackedWar; - private final Path gen; - private final Path root; + private final BuildSystem builder; - private String lastTarget; + private String lastAgent; private long lastTime; - RecompileGwtUiFilter(Path buckOut, Path unpackedWar) { + RecompileGwtUiFilter(BuildSystem builder, Path unpackedWar) { + this.builder = builder; this.unpackedWar = unpackedWar; - gen = buckOut.resolve("gen"); - root = buckOut.getParent(); } @Override public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain) throws IOException, ServletException { - String pkg = "gerrit-gwtui"; - String target = "ui_" + rule.select((HttpServletRequest) request); - if (gwtuiRecompile || !uaInitialized.contains(target)) { - String rule = "//" + pkg + ":" + target; - // TODO(davido): instead of assuming specific Buck's internal - // target directory for gwt_binary() artifacts, ask Buck for - // the location of user agent permutation GWT zip, e. g.: - // $ buck targets --show_output //gerrit-gwtui:ui_safari \ - // | awk '{print $2}' - String child = String.format("%s/__gwt_binary_%s__", pkg, target); - File zip = gen.resolve(child).resolve(target + ".zip").toFile(); + String agent = rule.select((HttpServletRequest) request); + if (unpackedWar != null + && (gwtuiRecompile || !uaInitialized.contains(agent))) { + Label label = builder.gwtZipLabel(agent); + File zip = builder.targetPath(label).toFile(); synchronized (this) { try { - BuckUtils.build(root, gen, rule); - } catch (BuildFailureException e) { - BuckUtils.displayFailure(rule, e.why, (HttpServletResponse) res); + builder.build(label); + } catch (BuildSystem.BuildFailureException e) { + e.display(label.toString(), (HttpServletResponse) res); return; } - if (!target.equals(lastTarget) || lastTime != zip.lastModified()) { - lastTarget = target; + if (!agent.equals(lastAgent) || lastTime != zip.lastModified()) { + lastAgent = agent; lastTime = zip.lastModified(); unpack(zip, unpackedWar.toFile()); } } - uaInitialized.add(target); + uaInitialized.add(agent); } chain.doFilter(request, res); }
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..e4d3339 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
@@ -87,6 +87,8 @@ .put("tif", "image/tiff") .put("tiff", "image/tiff") .put("txt", "text/plain") + .put("woff", "font/woff") + .put("woff2", "font/woff2") .build(); protected static String contentType(String name) { @@ -294,17 +296,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..d1070fc 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,19 @@ 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.common.Nullable; +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 +49,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 +66,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 +110,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 +120,11 @@ this.options = options; } + @Provides + @Singleton private Paths getPaths() { if (paths == null) { - paths = new Paths(); + paths = new Paths(options); } return paths; } @@ -104,11 +141,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()); } } @@ -182,8 +221,7 @@ if (p.unpackedWar != null) { return p.unpackedWar.resolve(name); } - return p.buckOut.resolveSibling("gerrit-war").resolve("src") - .resolve("main").resolve("webapp").resolve(name); + return p.sourceRoot.resolve("gerrit-war/src/main/webapp/" + name); } } @@ -194,7 +232,7 @@ .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET))); Paths p = getPaths(); if (p.isDev()) { - filter("/").through(new RecompileGwtUiFilter(p.buckOut, p.unpackedWar)); + filter("/").through(new RecompileGwtUiFilter(p.builder, p.unpackedWar)); } } @@ -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 @@ -252,27 +282,31 @@ @Provides @Singleton - BowerComponentsServlet getBowerComponentsServlet( + BowerComponentsDevServlet getBowerComponentsServlet( @Named(CACHE) Cache<Path, Resource> cache) throws IOException { - return new BowerComponentsServlet(cache, getPaths().buckOut); + return getPaths().isDev() + ? new BowerComponentsDevServlet(cache, getPaths().builder) + : null; } @Provides @Singleton - FontsServlet getFontsServlet( + FontsDevServlet getFontsServlet( @Named(CACHE) Cache<Path, Resource> cache) throws IOException { - return new FontsServlet(cache, getPaths().buckOut); + return getPaths().isDev() + ? new FontsDevServlet(cache, getPaths().builder) + : null; } private Path polyGerritBasePath() { Paths p = getPaths(); if (options.forcePolyGerritDev()) { - checkArgument(p.buckOut != null, - "no buck-out directory found for PolyGerrit developer mode"); + checkArgument(p.sourceRoot != null, + "no source root directory found for PolyGerrit developer mode"); } if (p.isDev()) { - return p.buckOut.getParent().resolve("polygerrit-ui").resolve("app"); + return p.sourceRoot.resolve("polygerrit-ui").resolve("app"); } return p.warFs != null @@ -281,13 +315,14 @@ } } - private class Paths { + private static class Paths { private final FileSystem warFs; - private final Path buckOut; + private final BuildSystem builder; + private final Path sourceRoot; private final Path unpackedWar; private final boolean development; - private Paths() { + private Paths(GerritOptions options) { try { File launcherLoadedFrom = getLauncherLoadedFrom(); if (launcherLoadedFrom != null @@ -303,28 +338,42 @@ .getParentFile() .getParentFile() .toURI()); - buckOut = null; + sourceRoot = null; development = false; + builder = null; return; } warFs = getDistributionArchive(launcherLoadedFrom); if (warFs == null) { - buckOut = getDeveloperBuckOut(); unpackedWar = makeWarTempDir(); development = true; } else if (options.forcePolyGerritDev()) { - buckOut = getDeveloperBuckOut(); unpackedWar = null; development = true; } else { - buckOut = null; unpackedWar = null; development = false; + sourceRoot = null; + builder = null; + return; } } catch (IOException e) { throw new ProvisionException( "Error initializing static content paths", e); } + + sourceRoot = getSourseRootOrNull(); + builder = GerritLauncher.isBazel() + ? new BazelBuild(sourceRoot) + : new BuckUtils(sourceRoot); + } + + private static Path getSourseRootOrNull() { + try { + return GerritLauncher.resolveInSourceRoot("."); + } catch (FileNotFoundException e) { + return null; + } } private FileSystem getDistributionArchive(File war) throws IOException { @@ -355,14 +404,6 @@ return development; } - private Path getDeveloperBuckOut() { - try { - return GerritLauncher.getDeveloperBuckOut(); - } catch (FileNotFoundException e) { - return null; - } - } - private Path makeWarTempDir() { // Obtain our local temporary directory, but it comes back as a file // so we have to switch it to be a directory post creation. @@ -393,4 +434,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 BowerComponentsDevServlet bowerComponentServlet; + private final FontsDevServlet fontServlet; + + @Inject + PolyGerritFilter(GerritOptions options, + Paths paths, + @Named(POLYGERRIT_INDEX_SERVLET) HttpServlet polyGerritIndex, + PolyGerritUiServlet polygerritUI, + @Nullable BowerComponentsDevServlet bowerComponentServlet, + @Nullable FontsDevServlet 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/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java index 3f471bf..b39e2a2 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -19,7 +19,6 @@ import com.google.gerrit.common.data.ProjectAccess; 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.account.GroupBackend; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; @@ -76,13 +75,14 @@ } @Override - protected ProjectAccess updateProjectConfig(CurrentUser user, + protected ProjectAccess updateProjectConfig(ProjectControl projectControl, ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate) throws IOException, NoSuchProjectException, ConfigInvalidException { RevCommit commit = config.commit(md); gitRefUpdated.fire(config.getProject().getNameKey(), RefNames.REFS_CONFIG, - base, commit.getId(), user.asIdentifiedUser().getAccount()); + base, commit.getId(), + projectControl.getUser().asIdentifiedUser().getAccount()); projectCache.evict(config.getProject()); return projectAccessFactory.create(projectName).call();
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..adfd528 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,8 +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; import com.google.gerrit.common.data.GroupDescription; @@ -207,8 +205,8 @@ detail.setLocal(local); detail.setOwnerOf(ownerOf); - detail.setCanUpload(pc.isOwner() - || (metaConfigControl.isVisible() && metaConfigControl.canUpload())); + detail.setCanUpload(metaConfigControl.isVisible() + && (pc.isOwner() || metaConfigControl.canUpload())); detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible()); detail.setGroupInfo(buildGroupInfo(local)); detail.setLabelTypes(pc.getLabelTypes()); @@ -217,10 +215,10 @@ } private List<WebLinkInfoCommon> getConfigFileLogLinks(String projectName) { - FluentIterable<WebLinkInfoCommon> links = - webLinks.getFileHistoryLinksCommon(projectName, RefNames.REFS_CONFIG, + List<WebLinkInfoCommon> links = + webLinks.getFileHistoryLinks(projectName, RefNames.REFS_CONFIG, ProjectConfig.PROJECT_CONFIG); - return links.isEmpty() ? null : links.toList(); + return links.isEmpty() ? null : links; } private Map<AccountGroup.UUID, GroupInfo> buildGroupInfo(List<AccessSection> local) { @@ -238,14 +236,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/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java index 111dfc9..4c7d257 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -31,7 +31,6 @@ import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.config.AllProjectsName; @@ -163,17 +162,17 @@ md.setMessage("Modify access rules\n"); } - return updateProjectConfig(projectControl.getUser(), config, md, + return updateProjectConfig(projectControl, config, md, parentProjectUpdate); } catch (RepositoryNotFoundException notFound) { throw new NoSuchProjectException(projectName); } } - protected abstract T updateProjectConfig(CurrentUser user, + protected abstract T updateProjectConfig(ProjectControl projectControl, ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate) throws IOException, NoSuchProjectException, ConfigInvalidException, - OrmException; + OrmException, PermissionDeniedException; private void replace(ProjectConfig config, Set<String> toDelete, AccessSection section) throws NoSuchGroupException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java index 9260e01..966cd88 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -19,6 +19,7 @@ import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -27,7 +28,6 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.change.ChangeInserter; @@ -43,6 +43,7 @@ import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.project.SetParent; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -106,9 +107,20 @@ } @Override - protected Change.Id updateProjectConfig(CurrentUser user, + protected Change.Id updateProjectConfig(ProjectControl projectControl, ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate) - throws IOException, OrmException { + throws IOException, OrmException, PermissionDeniedException { + RefControl refsMetaConfigControl = + projectControl.controlForRef(RefNames.REFS_CONFIG); + if (!refsMetaConfigControl.isVisible()) { + throw new PermissionDeniedException( + RefNames.REFS_CONFIG + " not visible"); + } + if (!projectControl.isOwner() && !refsMetaConfigControl.canUpload()) { + throw new PermissionDeniedException( + "cannot upload to " + RefNames.REFS_CONFIG); + } + md.setInsertChangeId(true); Change.Id changeId = new Change.Id(seq.nextChangeId()); RevCommit commit = @@ -120,9 +132,9 @@ try (RevWalk rw = new RevWalk(md.getRepository()); ObjectInserter objInserter = md.getRepository().newObjectInserter(); - BatchUpdate bu = updateFactory.create( - db, config.getProject().getNameKey(), user, - TimeUtil.nowTs())) { + BatchUpdate bu = + updateFactory.create(db, config.getProject().getNameKey(), + projectControl.getUser(), TimeUtil.nowTs())) { bu.setRepository(md.getRepository(), rw, objInserter); bu.insertChange( changeInserterFactory.create(changeId, commit, RefNames.REFS_CONFIG)
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..f19d1d8 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; @@ -86,7 +85,7 @@ } @Test - public void testNoFilters() throws Exception { + public void noFilters() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.class); @@ -108,7 +107,7 @@ } @Test - public void testSingleFilterNoBubbling() throws Exception { + public void singleFilterNoBubbling() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock("config", FilterConfig.class); @@ -135,7 +134,7 @@ } @Test - public void testSingleFilterBubbling() throws Exception { + public void singleFilterBubbling() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.class); @@ -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); @@ -167,7 +166,7 @@ } @Test - public void testTwoFiltersNoBubbling() throws Exception { + public void twoFiltersNoBubbling() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.class); @@ -200,7 +199,7 @@ } @Test - public void testTwoFiltersBubbling() throws Exception { + public void twoFiltersBubbling() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.class); @@ -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); @@ -240,7 +239,7 @@ } @Test - public void testPostponedLoading() throws Exception { + public void postponedLoading() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.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); @@ -292,7 +291,7 @@ } @Test - public void testDynamicUnloading() throws Exception { + public void dynamicUnloading() throws Exception { EasyMockSupport ems = new EasyMockSupport(); FilterConfig config = ems.createMock(FilterConfig.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-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java index 9559e13..dbf9904 100644 --- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
@@ -29,7 +29,7 @@ private static final String RESOURCE = "my-resource"; @Test - public void testUnauthorized() throws Exception { + public void unauthorized() throws Exception { ContextMapper classUnderTest = new ContextMapper(CONTEXT); HttpServletRequest originalRequest = @@ -47,7 +47,7 @@ } @Test - public void testAuthorized() throws Exception { + public void authorized() throws Exception { ContextMapper classUnderTest = new ContextMapper(CONTEXT); HttpServletRequest originalRequest =
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java index af90585..29f982e 100644 --- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -27,7 +27,7 @@ public class ParameterParserTest { @Test - public void testConvertFormToJson() throws BadRequestException { + public void convertFormToJson() throws BadRequestException { JsonObject obj = ParameterParser.formToJson( ImmutableMap.of( "message", new String[]{"this.is.text"},
diff --git a/gerrit-launcher/BUILD b/gerrit-launcher/BUILD index ced3447..33b779e 100644 --- a/gerrit-launcher/BUILD +++ b/gerrit-launcher/BUILD
@@ -1,7 +1,19 @@ # NOTE: GerritLauncher must be a single, self-contained class. Do not add any # additional srcs or deps to this rule. java_library( - name = 'launcher', - srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'], - visibility = ['//visibility:public'], + name = "launcher", + srcs = ["src/main/java/com/google/gerrit/launcher/GerritLauncher.java"], + resources = [":workspace-root.txt"], + visibility = ["//visibility:public"], +) + +# The root of the workspace is non-hermetic, but we need it for +# on-the-fly GWT recompiles and PolyGerrit updates. +genrule( + name = "gen_root", + outs = ["workspace-root.txt"], + cmd = ("cat bazel-out/stable-status.txt | " + + "grep STABLE_WORKSPACE_ROOT | cut -d ' ' -f 2 > $@"), + stamp = 1, + visibility = ["//visibility:public"], )
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..32e4dd8 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
@@ -14,6 +14,7 @@ package com.google.gerrit.launcher; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -42,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Scanner; import java.util.SortedMap; import java.util.TreeMap; import java.util.jar.Attributes; @@ -55,6 +57,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 +106,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); } @@ -575,25 +617,77 @@ /** * Locate the path of the {@code eclipse-out} directory in a source tree. * + * @return local path of the {@code eclipse-out} directory in a source tree. * @throws FileNotFoundException if the directory cannot be found. */ public static Path getDeveloperEclipseOut() throws FileNotFoundException { return resolveInSourceRoot("eclipse-out"); } - /** - * Locate the path of the {@code buck-out} directory in a source tree. - * - * @throws FileNotFoundException if the directory cannot be found. - */ - public static Path getDeveloperBuckOut() throws FileNotFoundException { - return resolveInSourceRoot("buck-out"); + static String SOURCE_ROOT_RESOURCE = "/gerrit-launcher/workspace-root.txt"; + static String PRIMARY_BUILD_TOOL = ".primary_build_tool"; + + /** returns whether we're running out of a bazel build. */ + public static boolean isBazel() { + Class<GerritLauncher> self = GerritLauncher.class; + URL rootURL = self.getResource(SOURCE_ROOT_RESOURCE); + if (rootURL != null) { + return true; + } + + Path p = null; + try { + p = resolveInSourceRoot("eclipse-out"); + if (!Files.exists(p)) { + p = resolveInSourceRoot("bazel-out"); + } + Path path = p.getParent().resolve(PRIMARY_BUILD_TOOL); + if (Files.exists(path)) { + String content = new String(Files.readAllBytes(path)); + if (content.toLowerCase().contains("bazel")) { + return true; + } + } + } catch (IOException e) { + // Ignore + } + + // Not Bazel then + return false; } - private static Path resolveInSourceRoot(String name) + /** + * Locate a path in the source tree. + * + * @return local path of the {@code name} directory in a source tree. + * @throws FileNotFoundException if the directory cannot be found. + */ + public static Path resolveInSourceRoot(String name) throws FileNotFoundException { + // Find ourselves in the classpath, as a loose class file or jar. Class<GerritLauncher> self = GerritLauncher.class; + + // If the build system provides us with a source root, use that. + try (InputStream stream = self.getResourceAsStream(SOURCE_ROOT_RESOURCE)) { + System.err.println("URL: " + stream); + if (stream != null) { + try (Scanner scan = + new Scanner(stream, UTF_8.name()).useDelimiter("\n")) { + if (scan.hasNext()) { + Path p = Paths.get(scan.next()); + if (!Files.exists(p)) { + throw new FileNotFoundException( + "source root not found: " + p); + } + return p; + } + } + } + } catch (IOException e) { + // not Bazel, then. + } + URL u = self.getResource(self.getSimpleName() + ".class"); if (u == null) { throw new FileNotFoundException("Cannot find class " + self.getName()); @@ -614,7 +708,8 @@ // Pop up to the top-level source folder by looking for .buckconfig. Path dir = Paths.get(u.getPath()); - while (!Files.isRegularFile(dir.resolve(".buckconfig"))) { + while (!Files.isRegularFile(dir.resolve(".buckconfig")) + && !Files.isRegularFile(dir.resolve("WORKSPACE"))) { Path parent = dir.getParent(); if (parent == null) { throw new FileNotFoundException("Cannot find source root from " + u); @@ -632,7 +727,7 @@ private static ClassLoader useDevClasspath() throws MalformedURLException, FileNotFoundException { - Path out = getDeveloperEclipseOut(); + Path out = resolveInSourceRoot("eclipse-out"); List<URL> dirs = new ArrayList<>(); dirs.add(out.resolve("classes").toUri().toURL()); ClassLoader cl = GerritLauncher.class.getClassLoader();
diff --git a/gerrit-lucene/BUILD b/gerrit-lucene/BUILD index 2f1cba7..5f87b4e 100644 --- a/gerrit-lucene/BUILD +++ b/gerrit-lucene/BUILD
@@ -1,41 +1,44 @@ QUERY_BUILDER = [ - 'src/main/java/com/google/gerrit/lucene/QueryBuilder.java', + "src/main/java/com/google/gerrit/lucene/QueryBuilder.java", ] java_library( - name = 'query_builder', - srcs = QUERY_BUILDER, - deps = [ - '//gerrit-antlr:query_exception', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:gwtorm', - '//lib:guava', - '//lib/lucene:lucene-core-and-backward-codecs', - ], - visibility = ['//visibility:public'], + name = "query_builder", + srcs = QUERY_BUILDER, + visibility = ["//visibility:public"], + deps = [ + "//gerrit-antlr:query_exception", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:guava", + "//lib:gwtorm", + "//lib/lucene:lucene-core-and-backward-codecs", + ], ) java_library( - name = 'lucene', - srcs = glob(['src/main/java/**/*.java'], exclude = QUERY_BUILDER), - deps = [ - ':query_builder', - '//gerrit-antlr:query_exception', - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:guava', - '//lib:gwtorm', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', - '//lib/lucene:lucene-analyzers-common', - '//lib/lucene:lucene-core-and-backward-codecs', - '//lib/lucene:lucene-misc', - ], - visibility = ['//visibility:public'], + name = "lucene", + srcs = glob( + ["src/main/java/**/*.java"], + exclude = QUERY_BUILDER, + ), + visibility = ["//visibility:public"], + deps = [ + ":query_builder", + "//gerrit-antlr:query_exception", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:guava", + "//lib:gwtorm", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + "//lib/lucene:lucene-analyzers-common", + "//lib/lucene:lucene-core-and-backward-codecs", + "//lib/lucene:lucene-misc", + ], )
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..6237a61 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
@@ -30,6 +30,7 @@ import com.google.gerrit.server.index.FieldDef.FillArgs; import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.Index; +import com.google.gerrit.server.index.IndexUtils; import com.google.gerrit.server.index.Schema; import com.google.gerrit.server.index.Schema.Values; @@ -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/LuceneAccountIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java index 78c0185..87f1608 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -16,13 +16,12 @@ import static com.google.gerrit.server.index.account.AccountField.ID; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.index.IndexUtils; import com.google.gerrit.server.index.QueryOptions; import com.google.gerrit.server.index.Schema; import com.google.gerrit.server.index.account.AccountIndex; @@ -56,7 +55,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Set; import java.util.concurrent.ExecutionException; public class LuceneAccountIndex @@ -164,7 +162,7 @@ List<AccountState> result = new ArrayList<>(docs.scoreDocs.length); for (int i = opts.start(); i < docs.scoreDocs.length; i++) { ScoreDoc sd = docs.scoreDocs[i]; - Document doc = searcher.doc(sd.doc, fields(opts)); + Document doc = searcher.doc(sd.doc, IndexUtils.accountFields(opts)); result.add(toAccountState(doc)); } final List<AccountState> r = Collections.unmodifiableList(result); @@ -198,13 +196,6 @@ } } - private Set<String> fields(QueryOptions opts) { - Set<String> fs = opts.fields(); - return fs.contains(ID.getName()) - ? fs - : Sets.union(fs, ImmutableSet.of(ID.getName())); - } - private AccountState toAccountState(Document doc) { Account.Id id = new Account.Id(doc.getField(ID.getName()).numericValue().intValue());
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..e6d395c 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
@@ -16,19 +16,17 @@ import static com.google.common.base.Preconditions.checkNotNull; 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 static java.util.stream.Collectors.toList; -import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Collections2; 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; @@ -46,6 +44,7 @@ import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.index.FieldDef.FillArgs; import com.google.gerrit.server.index.IndexExecutor; +import com.google.gerrit.server.index.IndexUtils; import com.google.gerrit.server.index.QueryOptions; import com.google.gerrit.server.index.Schema; import com.google.gerrit.server.index.change.ChangeField; @@ -54,6 +53,7 @@ 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.project.SubmitRuleOptions; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.change.ChangeData; @@ -109,28 +109,31 @@ private static final Logger log = LoggerFactory.getLogger(LuceneChangeIndex.class); - public static final String CHANGES_OPEN = "open"; - public static final String CHANGES_CLOSED = "closed"; + static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED); + static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID); - static final String UPDATED_SORT_FIELD = - sortFieldName(ChangeField.UPDATED); - static final String ID_SORT_FIELD = - sortFieldName(ChangeField.LEGACY_ID); - + private static final String CHANGES_PREFIX = "changes_"; + private static final String CHANGES_OPEN = "open"; + private static final String CHANGES_CLOSED = "closed"; private static final String ADDED_FIELD = ChangeField.ADDED.getName(); private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName(); private static final String CHANGE_FIELD = ChangeField.CHANGE.getName(); private static final String DELETED_FIELD = ChangeField.DELETED.getName(); private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName(); private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName(); + private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName(); + private static final String REF_STATE_PATTERN_FIELD = + ChangeField.REF_STATE_PATTERN.getName(); private static final String REVIEWEDBY_FIELD = ChangeField.REVIEWEDBY.getName(); private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName(); private static final String 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(); + private static final String SUBMIT_RECORD_LENIENT_FIELD = + ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(); + private static final String SUBMIT_RECORD_STRICT_FIELD = + ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(); static Term idTerm(ChangeData cd) { return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get()); @@ -320,7 +323,7 @@ throw new OrmException("interrupted"); } - final Set<String> fields = fields(opts); + final Set<String> fields = IndexUtils.changeFields(opts); return new ChangeDataResults( executor.submit(new Callable<List<Document>>() { @Override @@ -397,7 +400,7 @@ close(); throw new OrmRuntimeException(e); } catch (ExecutionException e) { - Throwables.propagateIfPossible(e.getCause()); + Throwables.throwIfUnchecked(e.getCause()); throw new OrmRuntimeException(e.getCause()); } } @@ -408,31 +411,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,15 +466,22 @@ if (fields.contains(HASHTAG_FIELD)) { decodeHashtags(doc, cd); } - if (fields.contains(STARREDBY_FIELD)) { - decodeStarredBy(doc, cd); - } if (fields.contains(STAR_FIELD)) { decodeStar(doc, cd); } if (fields.contains(REVIEWER_FIELD)) { decodeReviewers(doc, cd); } + decodeSubmitRecords(doc, SUBMIT_RECORD_STRICT_FIELD, + ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd); + decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD, + ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd); + if (fields.contains(REF_STATE_FIELD)) { + decodeRefStates(doc, cd); + } + if (fields.contains(REF_STATE_PATTERN_FIELD)) { + decodeRefStatePatterns(doc, cd); + } return cd; } @@ -568,17 +553,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 +566,30 @@ 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 void decodeSubmitRecords(Multimap<String, IndexableField> doc, + String field, SubmitRuleOptions opts, ChangeData cd) { + ChangeField.parseSubmitRecords( + Collections2.transform( + doc.get(field), f -> f.binaryValue().utf8ToString()), + opts, cd); + } + + private void decodeRefStates(Multimap<String, IndexableField> doc, + ChangeData cd) { + cd.setRefStates(copyAsBytes(doc.get(REF_STATE_FIELD))); + } + + private void decodeRefStatePatterns(Multimap<String, IndexableField> doc, + ChangeData cd) { + cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD))); } private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc, @@ -619,4 +606,16 @@ } return result; } + + private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) { + return fields.stream() + .map( + f -> { + BytesRef ref = f.binaryValue(); + byte[] b = new byte[ref.length]; + System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length); + return b; + }) + .collect(toList()); + } }
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..d23c5bb 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.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.SingleVersionModule; 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..e95a1fb 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
@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.index.GerritIndexStatus; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexCollection; import com.google.gerrit.server.index.IndexDefinition; @@ -51,8 +52,6 @@ private static final Logger log = LoggerFactory .getLogger(LuceneVersionManager.class); - static final String CHANGES_PREFIX = "changes_"; - private static class Version<V> { private final Schema<V> schema; private final int version;
diff --git a/gerrit-main/BUCK b/gerrit-main/BUCK index 388126e..da39eec 100644 --- a/gerrit-main/BUCK +++ b/gerrit-main/BUCK
@@ -9,7 +9,5 @@ name = 'main_lib', srcs = ['src/main/java/Main.java'], deps = ['//gerrit-launcher:launcher'], - source = '1.2', - target = '1.2', visibility = ['//tools/eclipse:classpath'], )
diff --git a/gerrit-main/BUILD b/gerrit-main/BUILD new file mode 100644 index 0000000..243a70b --- /dev/null +++ b/gerrit-main/BUILD
@@ -0,0 +1,13 @@ +java_binary( + name = "main_bin", + main_class = "Main", + visibility = ["//visibility:public"], + runtime_deps = [":main_lib"], +) + +java_library( + name = "main_lib", + srcs = ["src/main/java/Main.java"], + visibility = ["//visibility:public"], + deps = ["//gerrit-launcher:launcher"], +)
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-oauth/BUILD b/gerrit-oauth/BUILD index b2cf17b..b459c70 100644 --- a/gerrit-oauth/BUILD +++ b/gerrit-oauth/BUILD
@@ -1,26 +1,27 @@ SRCS = glob( - ['src/main/java/**/*.java'], + ["src/main/java/**/*.java"], ) -RESOURCES = glob(['src/main/resources/**/*']) + +RESOURCES = glob(["src/main/resources/**/*"]) java_library( - name = 'oauth', - srcs = SRCS, - resources = RESOURCES, - deps = [ - '//gerrit-common:annotations', - '//gerrit-extension-api:api', - '//gerrit-httpd:httpd', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:gson', - '//lib:guava', - '//lib:gwtorm', - '//lib/commons:codec', - '//lib/guice:guice', - '//lib/guice:guice-servlet', - '//lib/log:api', - '//lib:servlet-api-3_1', - ], - visibility = ['//visibility:public'], + name = "oauth", + srcs = SRCS, + resources = RESOURCES, + visibility = ["//visibility:public"], + deps = [ + "//gerrit-common:annotations", + "//gerrit-extension-api:api", + "//gerrit-httpd:httpd", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:gson", + "//lib:guava", + "//lib:gwtorm", + "//lib:servlet-api-3_1", + "//lib/commons:codec", + "//lib/guice", + "//lib/guice:guice-servlet", + "//lib/log:api", + ], )
diff --git a/gerrit-openid/BUILD b/gerrit-openid/BUILD index b5ae049..7b0d2b1 100644 --- a/gerrit-openid/BUILD +++ b/gerrit-openid/BUILD
@@ -1,24 +1,25 @@ java_library( - name = 'openid', - srcs = glob(['src/main/java/**/*.java']), - resources = glob(['src/main/resources/**/*']), - deps = [ # We want all these deps to be provided_deps - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-gwtexpui:server', - '//gerrit-httpd:httpd', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//lib:guava', - '//lib:gwtorm', - '//lib:servlet-api-3_1', - '//lib/commons:codec', - '//lib/guice:guice', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', - '//lib/openid:consumer', - ], - visibility = ['//visibility:public'], + name = "openid", + srcs = glob(["src/main/java/**/*.java"]), + resources = glob(["src/main/resources/**/*"]), + visibility = ["//visibility:public"], + deps = [ + # We want all these deps to be provided_deps + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-gwtexpui:server", + "//gerrit-httpd:httpd", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//lib:guava", + "//lib:gwtorm", + "//lib:servlet-api-3_1", + "//lib/commons:codec", + "//lib/guice", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + "//lib/openid:consumer", + ], )
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-commonsnet/BUILD b/gerrit-patch-commonsnet/BUILD index c5e541d..7524bfe 100644 --- a/gerrit-patch-commonsnet/BUILD +++ b/gerrit-patch-commonsnet/BUILD
@@ -1,11 +1,11 @@ java_library( - name = 'commons-net', - srcs = glob(['src/main/java/org/apache/commons/net/**/*.java']), - deps = [ - '//gerrit-util-ssl:ssl', - '//lib/commons:codec', - '//lib/commons:net', - '//lib/log:api', - ], - visibility = ['//visibility:public'], + name = "commons-net", + srcs = glob(["src/main/java/org/apache/commons/net/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-util-ssl:ssl", + "//lib/commons:codec", + "//lib/commons:net", + "//lib/log:api", + ], )
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-patch-jgit/BUILD b/gerrit-patch-jgit/BUILD index 13a2fe0..e3d3cbd 100644 --- a/gerrit-patch-jgit/BUILD +++ b/gerrit-patch-jgit/BUILD
@@ -1,66 +1,67 @@ -load('//tools/bzl:genrule2.bzl', 'genrule2') -load('//tools/bzl:gwt.bzl', 'gwt_module') +load("//tools/bzl:genrule2.bzl", "genrule2") +load("//tools/bzl:gwt.bzl", "gwt_module") -SRC = 'src/main/java/org/eclipse/jgit/' +SRC = "src/main/java/org/eclipse/jgit/" gwt_module( - name = 'client', - srcs = [ - SRC + 'diff/Edit_JsonSerializer.java', - SRC + 'diff/ReplaceEdit.java', - ], - gwt_xml = SRC + 'JGit.gwt.xml', - deps = [ - ':Edit', - '//lib/gwt:user', - '//lib:gwtjsonrpc', - ], - visibility = ['//visibility:public'], + name = "client", + srcs = [ + SRC + "diff/Edit_JsonSerializer.java", + SRC + "diff/ReplaceEdit.java", + ], + gwt_xml = SRC + "JGit.gwt.xml", + visibility = ["//visibility:public"], + deps = [ + ":Edit", + "//lib:gwtjsonrpc", + "//lib/gwt:user", + ], ) gwt_module( - name = 'Edit', - srcs = [':jgit_edit_src'], - visibility = ['//visibility:public'], + name = "Edit", + srcs = [":jgit_edit_src"], + visibility = ["//visibility:public"], ) genrule2( - name = 'jgit_edit_src', - cmd = ' && '.join([ - 'unzip -qd $$TMP $(location @jgit_src//file) ' + - 'org/eclipse/jgit/diff/Edit.java', - 'cd $$TMP', - 'zip -Dq $$ROOT/$@ org/eclipse/jgit/diff/Edit.java', - ]), - tools = ['@jgit_src//file'], - out = 'edit.srcjar', + name = "jgit_edit_src", + outs = ["edit.srcjar"], + cmd = " && ".join([ + "unzip -qd $$TMP $(location @jgit//jar:src) " + + "org/eclipse/jgit/diff/Edit.java", + "cd $$TMP", + "zip -Dq $$ROOT/$@ org/eclipse/jgit/diff/Edit.java", + ]), + tools = ["@jgit//jar:src"], ) java_library( - name = 'server', - srcs = [ - SRC + x for x in [ - 'diff/EditDeserializer.java', - 'diff/ReplaceEdit.java', - 'internal/storage/file/WindowCacheStatAccessor.java', - 'lib/ObjectIdSerialization.java', - ] - ], - deps = [ - '//lib:gson', - '//lib/jgit/org.eclipse.jgit:jgit', - ], - visibility = ['//visibility:public'], + name = "server", + srcs = [ + SRC + x + for x in [ + "diff/EditDeserializer.java", + "diff/ReplaceEdit.java", + "internal/storage/file/WindowCacheStatAccessor.java", + "lib/ObjectIdSerialization.java", + ] + ], + visibility = ["//visibility:public"], + deps = [ + "//lib:gson", + "//lib/jgit/org.eclipse.jgit:jgit", + ], ) java_test( - name = 'jgit_patch_tests', - test_class = 'org.eclipse.jgit.diff.EditDeserializerTest', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':server', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "jgit_patch_tests", + srcs = glob(["src/test/java/**/*.java"]), + test_class = "org.eclipse.jgit.diff.EditDeserializerTest", + visibility = ["//visibility:public"], + deps = [ + ":server", + "//lib:junit", + "//lib/jgit/org.eclipse.jgit:jgit", + ], )
diff --git a/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java b/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java index a2c3dae..c431715 100644 --- a/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java +++ b/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java
@@ -20,7 +20,7 @@ public class EditDeserializerTest { @Test - public void testDiffDeserializer() { + public void diffDeserializer() { assertNotNull("edit deserializer", new EditDeserializer()); } }
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK index 4be941c..1d0c752 100644 --- a/gerrit-pgm/BUCK +++ b/gerrit-pgm/BUCK
@@ -47,7 +47,6 @@ ':init-api', ':util', '//gerrit-common:annotations', - '//gerrit-lucene:lucene', '//lib:args4j', '//lib:derby', '//lib:gwtjsonrpc', @@ -66,6 +65,7 @@ REST_UTIL_DEPS = [ '//gerrit-cache-h2:cache-h2', + '//gerrit-elasticsearch:elasticsearch', '//gerrit-util-cli:cli', '//lib:args4j', '//lib:gwtorm', @@ -120,6 +120,7 @@ ':init-api', ':util', '//gerrit-cache-h2:cache-h2', + '//gerrit-elasticsearch:elasticsearch', '//gerrit-gpg:gpg', '//gerrit-lucene:lucene', '//gerrit-oauth:oauth', @@ -128,7 +129,6 @@ '//lib:gwtorm', '//lib:protobuf', '//lib:servlet-api-3_1', - '//lib/auto:auto-value', '//lib/prolog:cafeteria', '//lib/prolog:compiler', '//lib/prolog:runtime', @@ -180,5 +180,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..a382fbf 100644 --- a/gerrit-pgm/BUILD +++ b/gerrit-pgm/BUILD
@@ -1,161 +1,175 @@ -load('//tools/bzl:java.bzl', 'java_library2') -load('//tools/bzl:junit.bzl', 'junit_tests') +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/' +SRCS = "src/main/java/com/google/gerrit/pgm/" -INIT_API_SRCS = glob([SRCS + 'init/api/*.java']) +RSRCS = "src/main/resources/com/google/gerrit/pgm/" + +INIT_API_SRCS = glob([SRCS + "init/api/*.java"]) BASE_JETTY_DEPS = [ - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-gwtexpui:linker_server', - '//gerrit-gwtexpui:server', - '//gerrit-httpd:httpd', - '//gerrit-server:server', - '//gerrit-sshd:sshd', - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', - '//lib/log:log4j', + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-gwtexpui:linker_server", + "//gerrit-gwtexpui:server", + "//gerrit-httpd:httpd", + "//gerrit-server:server", + "//gerrit-sshd:sshd", + "//lib:guava", + "//lib/guice:guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/joda:joda-time", + "//lib/log:api", + "//lib/log:log4j", ] DEPS = BASE_JETTY_DEPS + [ - '//gerrit-reviewdb:server', - '//lib/log:jsonevent-layout', + "//gerrit-reviewdb:server", + "//lib/log:jsonevent-layout", ] java_library( - name = 'init-api', - srcs = INIT_API_SRCS, - deps = DEPS + ['//gerrit-common:annotations'], - visibility = ['//visibility:public'], + name = "init-api", + srcs = INIT_API_SRCS, + visibility = ["//visibility:public"], + deps = DEPS + ["//gerrit-common:annotations"], ) java_library( - name = 'init', - srcs = glob([SRCS + 'init/*.java']), - resources = glob([RSRCS + 'init/*']), - deps = DEPS + [ - ':init-api', - ':util', - '//gerrit-common:annotations', - '//gerrit-launcher:launcher', # We want this dep to be provided_deps - '//gerrit-lucene:lucene', - '//lib:args4j', - '//lib:derby', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:h2', - '//lib/commons:validator', - '//lib/mina:sshd', - ], - visibility = ['//visibility:public'], + name = "init", + srcs = glob([SRCS + "init/*.java"]), + resources = glob([RSRCS + "init/*"]), + visibility = ["//visibility:public"], + deps = DEPS + [ + ":init-api", + ":util", + "//gerrit-common:annotations", + "//gerrit-launcher:launcher", # We want this dep to be provided_deps + "//gerrit-lucene:lucene", + "//lib:args4j", + "//lib:derby", + "//lib:gwtjsonrpc", + "//lib:gwtorm", + "//lib:h2", + "//lib/commons:validator", + "//lib/mina:sshd", + ], ) REST_UTIL_DEPS = [ - '//gerrit-cache-h2:cache-h2', - '//gerrit-util-cli:cli', - '//lib:args4j', - '//lib:gwtorm', - '//lib/commons:dbcp', + "//gerrit-cache-h2:cache-h2", + "//gerrit-util-cli:cli", + "//lib:args4j", + "//lib:gwtorm", + "//lib/commons:dbcp", ] java_library( - name = 'util', - exports = [':util-nodep'], - runtime_deps = DEPS + REST_UTIL_DEPS, - visibility = ['//visibility:public'], + name = "util", + visibility = ["//visibility:public"], + exports = [":util-nodep"], + runtime_deps = DEPS + REST_UTIL_DEPS, ) java_library( - name = 'util-nodep', - srcs = glob([SRCS + 'util/*.java']), - deps = DEPS + REST_UTIL_DEPS, # We want all these deps to be provided_deps - visibility = ['//visibility:public'], + name = "util-nodep", + srcs = glob([SRCS + "util/*.java"]), + visibility = ["//visibility:public"], + deps = DEPS + REST_UTIL_DEPS, # We want all these deps to be provided_deps ) JETTY_DEPS = [ - '//lib/jetty:jmx', - '//lib/jetty:server', - '//lib/jetty:servlet', + "//lib/jetty:jmx", + "//lib/jetty:server", + "//lib/jetty:servlet", ] java_library( - name = 'http', - runtime_deps = DEPS + JETTY_DEPS, - exports = [':http-jetty'], - visibility = ['//visibility:public'], + name = "http", + visibility = ["//visibility:public"], + exports = [":http-jetty"], + runtime_deps = DEPS + JETTY_DEPS, ) java_library( - name = 'http-jetty', - srcs = glob([SRCS + 'http/jetty/*.java']), - deps = JETTY_DEPS + BASE_JETTY_DEPS + [ # We want all these deps to be provided_deps - '//gerrit-launcher:launcher', - '//gerrit-reviewdb:client', - '//lib:servlet-api-3_1', - ], - visibility = ['//visibility:public'], + name = "http-jetty", + srcs = glob([SRCS + "http/jetty/*.java"]), + visibility = ["//visibility:public"], + deps = JETTY_DEPS + BASE_JETTY_DEPS + [ + # We want all these deps to be provided_deps + "//gerrit-launcher:launcher", + "//gerrit-reviewdb:client", + "//lib:servlet-api-3_1", + ], ) REST_PGM_DEPS = [ - ':http', - ':init', - ':init-api', - ':util', - '//gerrit-cache-h2:cache-h2', - '//gerrit-gpg:gpg', - '//gerrit-lucene:lucene', - '//gerrit-oauth:oauth', - '//gerrit-openid:openid', - '//lib:args4j', - '//lib:gwtorm', - '//lib:protobuf', - '//lib:servlet-api-3_1-without-neverlink', - '//lib/auto:auto-value', - '//lib/prolog:cafeteria', - '//lib/prolog:compiler', - '//lib/prolog:runtime', + ":http", + ":init", + ":init-api", + ":util", + "//gerrit-cache-h2:cache-h2", + "//gerrit-elasticsearch:elasticsearch", + "//gerrit-gpg:gpg", + "//gerrit-lucene:lucene", + "//gerrit-oauth:oauth", + "//gerrit-openid:openid", + "//lib:args4j", + "//lib:gwtorm", + "//lib:protobuf", + "//lib:servlet-api-3_1-without-neverlink", + "//lib/prolog:cafeteria", + "//lib/prolog:compiler", + "//lib/prolog:runtime", ] java_library( - name = 'pgm', - resources = glob([RSRCS + '*']), - runtime_deps = DEPS + REST_PGM_DEPS + [ - ':daemon', - ], - visibility = ['//visibility:public'], + name = "pgm", + resources = glob([RSRCS + "*"]), + visibility = ["//visibility:public"], + runtime_deps = DEPS + REST_PGM_DEPS + [ + ":daemon", + ], ) # no transitive deps, used for gerrit-acceptance-framework java_library( - name = 'daemon', - srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']), - resources = glob([RSRCS + '*']), - deps = DEPS + REST_PGM_DEPS + [ # We want all these deps to be provided_deps - '//gerrit-launcher:launcher', - ], - visibility = ['//visibility:public'], + name = "daemon", + srcs = glob([ + SRCS + "*.java", + SRCS + "rules/*.java", + ]), + resources = glob([RSRCS + "*"]), + visibility = ["//visibility:public"], + deps = DEPS + REST_PGM_DEPS + [ + # We want all these deps to be provided_deps + "//gerrit-launcher:launcher", + "//lib/auto:auto-value", + ], ) junit_tests( - name = 'pgm_tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':init', - ':init-api', - ':pgm', - '//gerrit-common:server', - '//gerrit-server:server', - '//lib:guava', - '//lib:junit', - '//lib/easymock:easymock', - '//lib/guice:guice', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.junit:junit', - ], + name = "pgm_tests", + srcs = glob(["src/test/java/**/*.java"]), + deps = [ + ":init", + ":init-api", + ":pgm", + "//gerrit-common:server", + "//gerrit-server:server", + "//lib:guava", + "//lib:junit", + "//lib/easymock", + "//lib/guice", + "//lib/jgit/org.eclipse.jgit:jgit", + "//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..a0ab714 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; @@ -67,7 +68,8 @@ import com.google.gerrit.server.index.IndexModule; import com.google.gerrit.server.index.IndexModule.IndexType; import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier; -import com.google.gerrit.server.mail.SmtpEmailSender; +import com.google.gerrit.server.mail.receive.MailReceiver; +import com.google.gerrit.server.mail.send.SmtpEmailSender; import com.google.gerrit.server.mime.MimeUtil2Module; import com.google.gerrit.server.patch.DiffExecutorModule; import com.google.gerrit.server.plugins.PluginGuiceEnvironment; @@ -125,7 +127,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 +153,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 +176,8 @@ } @VisibleForTesting - public Daemon(Runnable serverStarted) { + public Daemon(Runnable serverStarted, Path sitePath) { + super (sitePath); this.serverStarted = serverStarted; } @@ -181,6 +187,10 @@ @Override public int run() throws Exception { + if (stopOnly) { + RuntimeShutdown.manualShutdown(); + return 0; + } if (doInit) { try { new Init(getSitePath()).run(); @@ -214,14 +224,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 +316,13 @@ @VisibleForTesting public void stop() { + if (runId != null) { + try { + Files.delete(runFile); + } catch (IOException err) { + log.warn("failed to delete " + runFile, err); + } + } manager.stop(); } @@ -353,6 +363,7 @@ modules.add(new SearchingChangeCacheImpl.Module(slave)); modules.add(new InternalAccountDirectory.Module()); modules.add(new DefaultCacheFactory.Module()); + modules.add(cfgInjector.getInstance(MailReceiver.Module.class)); if (emailModule != null) { modules.add(emailModule); } else { @@ -401,15 +412,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 +433,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..755ab1b 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
@@ -14,10 +14,10 @@ package com.google.gerrit.pgm; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb; import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; -import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.base.Stopwatch; import com.google.common.collect.ArrayListMultimap; @@ -29,6 +29,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.common.FormatUtil; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; import com.google.gerrit.extensions.registration.DynamicSet; @@ -43,12 +44,16 @@ import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.ChainedReceiveCommands; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.index.DummyIndexModule; import com.google.gerrit.server.index.change.ReindexAfterUpdate; -import com.google.gerrit.server.notedb.ChangeRebuilder; +import com.google.gerrit.server.notedb.ChangeBundleReader; +import com.google.gerrit.server.notedb.NoteDbUpdateManager; import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; @@ -58,9 +63,12 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.kohsuke.args4j.Option; @@ -68,6 +76,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -108,6 +117,9 @@ private GitRepositoryManager repoManager; @Inject + private NoteDbUpdateManager.Factory updateManagerFactory; + + @Inject private NotesMigration notesMigration; @Inject @@ -116,6 +128,9 @@ @Inject private WorkQueue workQueue; + @Inject + private ChangeBundleReader bundleReader; + @Override public int run() throws Exception { mustHaveValidSite(); @@ -154,7 +169,7 @@ @Override public Boolean call() { try (ReviewDb db = unwrapDb(schemaFactory.open())) { - return rebuilder.rebuildProject( + return rebuildProject( db, changesByProject, project, allUsersRepo); } catch (Exception e) { log.error("Error rebuilding project " + project, e); @@ -234,13 +249,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()); } @@ -263,4 +273,37 @@ return ImmutableMultimap.copyOf(changesByProject); } } + + private boolean rebuildProject(ReviewDb db, + ImmutableMultimap<Project.NameKey, Change.Id> allChanges, + Project.NameKey project, Repository allUsersRepo) + throws IOException, OrmException { + checkArgument(allChanges.containsKey(project)); + boolean ok = true; + ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(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 { + rebuilder.buildUpdates( + manager, bundleReader.fromReviewDb(db, changeId)); + } catch (NoPatchSetsException e) { + log.warn(e.getMessage()); + } catch (Throwable t) { + log.error("Failed to rebuild change " + changeId, t); + ok = false; + } + pm.update(1); + } + manager.execute(); + } finally { + pm.endTask(); + } + return ok; + } }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/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/InitDev.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDev.java new file mode 100644 index 0000000..5500da8 --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDev.java
@@ -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. + +package com.google.gerrit.pgm.init; + +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.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class InitDev implements InitStep { + private final InitFlags flags; + private final Section plugins; + + @Inject + InitDev(InitFlags flags, + Section.Factory sections) { + this.flags = flags; + this.plugins = sections.get("plugins", null); + } + + @Override + public void run() throws Exception { + if (!flags.dev) { + return; + } + plugins.set("allowRemoteAdmin", "true"); + } +}
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..c9a9b5c 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,7 @@ 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.pgm.init.api.ConsoleUI; import com.google.gerrit.pgm.init.api.InitFlags; import com.google.gerrit.pgm.init.api.InitStep; @@ -23,6 +23,7 @@ import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.index.IndexModule; import com.google.gerrit.server.index.IndexModule.IndexType; +import com.google.gerrit.server.index.IndexUtils; import com.google.gerrit.server.index.SchemaDefinitions; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -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/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java index b5aa625..a442f29 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -64,6 +64,7 @@ step().to(InitHttpd.class); step().to(InitCache.class); step().to(InitPlugins.class); + step().to(InitDev.class); } protected LinkedBindingBuilder<InitStep> step() {
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..b140ab1 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
@@ -21,7 +21,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.mail.SmtpEmailSender.Encryption; +import com.google.gerrit.server.mail.Encryption; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -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..7487e2b 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. @@ -29,7 +29,7 @@ import com.google.gerrit.pgm.init.api.Section; import com.google.gerrit.pgm.init.api.Section.Factory; import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.mail.OutgoingEmail; +import com.google.gerrit.server.mail.EmailModule; import com.google.inject.Binding; import com.google.inject.Inject; import com.google.inject.Injector; @@ -99,21 +99,36 @@ 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("CommentHtml.soy"); + extractMailExample("CommentFooter.soy"); + extractMailExample("CommentFooterHtml.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"); + extractMailExample("SetAssignee.soy"); + extractMailExample("SetAssigneeHtml.soy"); if (!ui.isBatch()) { System.err.println(); @@ -143,7 +158,7 @@ private void extractMailExample(String orig) throws Exception { Path ex = site.mail_dir.resolve(orig + ".example"); - extract(ex, OutgoingEmail.class, orig); + extract(ex, EmailModule.class, orig); chmod(0444, ex); }
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/VersionedAuthorizedKeysOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java index 6739ce0..e47f23a 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -16,7 +16,6 @@ import static com.google.common.base.Preconditions.checkState; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.gerrit.pgm.init.api.InitFlags; import com.google.gerrit.pgm.init.api.VersionedMetaDataOnInit; @@ -34,6 +33,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; public class VersionedAuthorizedKeysOnInit extends VersionedMetaDataOnInit { public interface Factory {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java index a7ebd33..81ee0a2 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -14,6 +14,7 @@ package com.google.gerrit.pgm.init.api; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GroupList; @@ -66,7 +67,9 @@ } private GroupList readGroupList() throws IOException { - return GroupList.parse(readUTF8(GroupList.FILE_NAME), + return GroupList.parse( + new Project.NameKey(project), + readUTF8(GroupList.FILE_NAME), GroupList.createLoggerSink(GroupList.FILE_NAME, log)); }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java index e210d5b..78ea5a0 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -87,6 +87,12 @@ /** Prompt the user for a password, returning the string; null if blank. */ public abstract String password(String fmt, Object... args); + /** Display an error message on the system stderr. */ + public void error(String format, Object... args) { + System.err.println(String.format(format, args)); + System.err.flush(); + } + /** Prompt the user to make a choice from an enumeration's values. */ public abstract <T extends Enum<?>> T readEnum(T def, String fmt, Object... args);
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/init/api/VersionedMetaDataOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java index b953a0b..43fd991 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -37,9 +37,9 @@ public abstract class VersionedMetaDataOnInit extends VersionedMetaData { + protected final String project; private final InitFlags flags; private final SitePaths site; - private final String project; private final String ref; protected VersionedMetaDataOnInit(InitFlags flags, SitePaths site,
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/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java index f076e54..689b606 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -58,7 +58,7 @@ import com.google.gerrit.server.git.SearchingChangeCacheImpl; import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.group.GroupModule; -import com.google.gerrit.server.mail.ReplacePatchSetSender; +import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.NoteDbModule; import com.google.gerrit.server.patch.DiffExecutorModule; import com.google.gerrit.server.patch.PatchListCacheImpl;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java index 262997b..2670407 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -24,8 +24,6 @@ import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; -import org.joda.time.DateTime; -import org.joda.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +33,8 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.zip.GZIPOutputStream; /** Compresses the old error logs. */ @@ -64,12 +64,12 @@ public void start() { //compress log once and then schedule compression every day at 11:00pm queue.getDefaultQueue().execute(compressor); - DateTime now = DateTime.now(); - long milliSecondsUntil11am = - new Duration(now, now.withTimeAtStartOfDay().plusHours(23)) - .getMillis(); + ZoneId zone = ZoneId.systemDefault(); + LocalDate now = LocalDate.now(zone); + long milliSecondsUntil11pm = now.atStartOfDay(zone) + .plusHours(23).toInstant().toEpochMilli(); queue.getDefaultQueue().scheduleAtFixedRate(compressor, - milliSecondsUntil11am, HOURS.toMillis(24), MILLISECONDS); + milliSecondsUntil11pm, HOURS.toMillis(24), MILLISECONDS); } @Override
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..f0cc5c5 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
@@ -24,6 +24,7 @@ import com.google.gerrit.metrics.DisabledMetricMaker; import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker; +import com.google.gerrit.server.LibModuleLoader; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfigModule; import com.google.gerrit.server.config.SitePath; @@ -78,6 +79,10 @@ protected SiteProgram() { } + protected SiteProgram(Path sitePath) { + this.sitePath = sitePath; + } + protected SiteProgram(Path sitePath, final Provider<DataSource> dsProvider) { this.sitePath = sitePath; this.dsProvider = dsProvider; @@ -176,6 +181,7 @@ modules.add(new SchemaModule()); modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class)); modules.add(new ConfigNotesMigration.Module()); + modules.addAll(LibModuleLoader.loadModules(cfgInjector)); try { return Guice.createInjector(PRODUCTION, modules);
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config index 4d9d0f0..3bc6ae4 100644 --- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config +++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -15,24 +15,24 @@ # Version should match lib/bouncycastle/BUCK [library "bouncyCastleProvider"] - name = Bouncy Castle Crypto Provider v152 - url = https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.52/bcprov-jdk15on-1.52.jar - sha1 = 88a941faf9819d371e3174b5ed56a3f3f7d73269 + name = Bouncy Castle Crypto Provider v155 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.55/bcprov-jdk15on-1.55.jar + sha1 = 935f2e57a00ec2c489cbd2ad830d4a399708f979 remove = bcprov-.*[.]jar # Version should match lib/bouncycastle/BUCK [library "bouncyCastleSSL"] - name = Bouncy Castle Crypto SSL v152 - url = https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.52/bcpkix-jdk15on-1.52.jar - sha1 = b8ffac2bbc6626f86909589c8cc63637cc936504 + name = Bouncy Castle Crypto SSL v155 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.55/bcpkix-jdk15on-1.55.jar + sha1 = 6392d8cba22b722c6570d660ca0b3921ff1bae4f needs = bouncyCastleProvider remove = bcpkix-.*[.]jar # Version should match lib/bouncycastle/BUCK [library "bouncyCastlePGP"] - name = Bouncy Castle Crypto OpenPGP v152 - url = https://repo1.maven.org/maven2/org/bouncycastle/bcpg-jdk15on/1.52/bcpg-jdk15on-1.52.jar - sha1 = ff4665a4b5633ff6894209d5dd10b7e612291858 + name = Bouncy Castle Crypto OpenPGP v155 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcpg-jdk15on/1.55/bcpg-jdk15on-1.55.jar + sha1 = 54ce841795ecdf10f24e50c48d4fdec59c691699 needs = bouncyCastleProvider remove = bcpg-.*[.]jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java index 48754f1..115b0fd 100644 --- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -30,7 +30,7 @@ public class LibrariesTest { @Test - public void testCreate() throws Exception { + public void create() throws Exception { final SitePaths site = new SitePaths(Paths.get(".")); final ConsoleUI ui = createStrictMock(ConsoleUI.class);
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java index 89f61cc..76a3185 100644 --- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -50,7 +50,7 @@ public class UpgradeFrom2_0_xTest extends InitTestCase { @Test - public void testUpgrade() throws IOException, ConfigInvalidException { + public void upgrade() throws IOException, ConfigInvalidException { final Path p = newSitePath(); final SitePaths site = new SitePaths(p); assertTrue(site.isNew);
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK index 8cbf1a1..fc17d0d 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,33 @@ '//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/httpcomponents:httpclient", + "//lib/httpcomponents:httpcore", '//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', '//lib/prolog:runtime', ], @@ -64,7 +87,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', @@ -73,5 +96,4 @@ '//lib/bouncycastle:bcpkix', ], visibility = ['PUBLIC'], - do_it_wrong = True, )
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD index 2c18ca6..a9e303c 100644 --- a/gerrit-plugin-api/BUILD +++ b/gerrit-plugin-api/BUILD
@@ -1,51 +1,109 @@ SRCS = [ - 'gerrit-server/src/main/java/', - 'gerrit-httpd/src/main/java/', - 'gerrit-sshd/src/main/java/', + "gerrit-server/src/main/java/", + "gerrit-httpd/src/main/java/", + "gerrit-sshd/src/main/java/", ] PLUGIN_API = [ - '//gerrit-httpd:httpd', - '//gerrit-pgm:init-api', - '//gerrit-server:server', - '//gerrit-sshd:sshd', + "//gerrit-httpd:httpd", + "//gerrit-pgm:init-api", + "//gerrit-server:server", + "//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/httpcomponents:httpclient", + "//lib/httpcomponents:httpcore", + "//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-without-neverlink", + "//lib:soy", + "//lib:velocity", ] java_binary( - name = 'plugin-api', - main_class = 'Dummy', - runtime_deps = [':lib'], - visibility = ['//visibility:public'], + name = "plugin-api", + main_class = "Dummy", + visibility = ["//visibility:public"], + runtime_deps = [":lib"], ) java_library( - name = 'lib', - exports = PLUGIN_API + [ - '//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: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'], + name = "lib", + visibility = ["//visibility:public"], + exports = PLUGIN_API + EXPORTS, +) + +java_library( + name = "lib-neverlink", + neverlink = 1, + visibility = ["//visibility:public"], + exports = PLUGIN_API + EXPORTS, +) + +java_binary( + name = "plugin-api-sources", + main_class = "Dummy", + visibility = ["//visibility:public"], + 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", + ], +) + +load("//tools/bzl:javadoc.bzl", "java_doc") + +java_doc( + name = "plugin-api-javadoc", + libs = PLUGIN_API + [ + "//gerrit-antlr:query_exception", + "//gerrit-antlr:query_parser", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-gwtexpui:server", + "//gerrit-reviewdb:server", + ], + pkgs = ["com.google.gerrit"], + title = "Gerrit Review Plugin API Documentation", + visibility = ["//visibility:public"], )
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml index 078a280..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.4</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/.gitignore b/gerrit-plugin-archetype/.gitignore deleted file mode 100644 index 7075a2f..0000000 --- a/gerrit-plugin-archetype/.gitignore +++ /dev/null
@@ -1,4 +0,0 @@ -/target -/.classpath -/.project -/.settings
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml deleted file mode 100644 index 3b12a3d..0000000 --- a/gerrit-plugin-archetype/pom.xml +++ /dev/null
@@ -1,108 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-plugin-archetype</artifactId> - <version>2.13.4</version> - <name>Gerrit Code Review - Plugin Archetype</name> - <description>Maven Archetype for Gerrit Plugins</description> - <url>https://www.gerritcodereview.com/</url> - - <properties> - <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion> - </properties> - - <build> - <resources> - <resource> - <directory>src/main/resources</directory> - <filtering>true</filtering> - <includes> - <include>META-INF/maven/archetype-metadata.xml</include> - </includes> - </resource> - <resource> - <directory>src/main/resources</directory> - <filtering>false</filtering> - <excludes> - <exclude>META-INF/maven/archetype-metadata.xml</exclude> - </excludes> - </resource> - </resources> - </build> - - <licenses> - <license> - <name>The Apache Software License, Version 2.0</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> - <distribution>repo</distribution> - </license> - </licenses> - - <scm> - <url>https://gerrit.googlesource.com/gerrit</url> - <connection>https://gerrit.googlesource.com/gerrit</connection> - </scm> - - <developers> - <developer> - <name>Andrew Bonventre</name> - </developer> - <developer> - <name>Dave Borowitz</name> - </developer> - <developer> - <name>David Ostrovsky</name> - </developer> - <developer> - <name>David Pursehouse</name> - </developer> - <developer> - <name>Edwin Kempin</name> - </developer> - <developer> - <name>Hugo Arès</name> - </developer> - <developer> - <name>Martin Fick</name> - </developer> - <developer> - <name>Saša Živkov</name> - </developer> - <developer> - <name>Shawn Pearce</name> - </developer> - </developers> - - <mailingLists> - <mailingList> - <name>Repo and Gerrit Discussion</name> - <post>repo-discuss@googlegroups.com</post> - <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe> - <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe> - <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive> - </mailingList> - </mailingLists> - - <issueManagement> - <url>https://bugs.chromium.org/p/gerrit/issues/list</url> - <system>Gerrit Issue Tracker</system> - </issueManagement> -</project>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml deleted file mode 100644 index e32a0d6..0000000 --- a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ /dev/null
@@ -1,78 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<archetype-descriptor name="Gerrit Plugin"> - <requiredProperties> - <requiredProperty key="pluginName"/> - - <requiredProperty key="Gerrit-Module"> - <defaultValue>Y</defaultValue> - </requiredProperty> - <requiredProperty key="Gerrit-SshModule"> - <defaultValue>Y</defaultValue> - </requiredProperty> - <requiredProperty key="Gerrit-HttpModule"> - <defaultValue>Y</defaultValue> - </requiredProperty> - - <requiredProperty key="Implementation-Vendor"> - <defaultValue>Gerrit Code Review</defaultValue> - </requiredProperty> - - <requiredProperty key="gerritApiType"> - <defaultValue>plugin</defaultValue> - </requiredProperty> - <requiredProperty key="gerritApiVersion"> - <defaultValue>${defaultGerritApiVersion}</defaultValue> - </requiredProperty> - </requiredProperties> - - <fileSets> - <fileSet filtered="true" packaged="true"> - <directory>src/main/java</directory> - <includes> - <include>**/*.java</include> - </includes> - </fileSet> - - <fileSet filtered="true"> - <directory>src/main/resources/Documentation</directory> - <includes> - <include>**/*.md</include> - </includes> - </fileSet> - - <fileSet filtered="true"> - <directory></directory> - <include>.buckconfig</include> - <include>BUCK</include> - <include>VERSION</include> - <include>lib/gerrit/BUCK</include> - <excludes> - <exclude>**/*.java</exclude> - </excludes> - </fileSet> - - <fileSet> - <directory></directory> - <includes> - <include>.gitignore</include> - <include>.settings/*</include> - <include>LICENSE</include> - </includes> - </fileSet> - </fileSets> -</archetype-descriptor>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig deleted file mode 100644 index 1044c12..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.buckconfig +++ /dev/null
@@ -1,14 +0,0 @@ -[alias] - ${pluginName} = //:${pluginName} - plugin = //:${pluginName} - -[java] - src_roots = java, resources - -[project] - ignore = .git - -[cache] - mode = dir - dir = buck-out/cache -
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore deleted file mode 100644 index 43838b0..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore +++ /dev/null
@@ -1,9 +0,0 @@ -/.buckversion -/.buckd -/buck-out -/bucklets -/target -/.classpath -/.project -/.settings/org.maven.ide.eclipse.prefs -/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 29abf99..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs +++ /dev/null
@@ -1,6 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 -encoding//src/test/resources=UTF-8 -encoding/<project>=UTF-8
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs +++ /dev/null
@@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n
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 deleted file mode 100644 index 2a585e4..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs +++ /dev/null
@@ -1,346 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.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.problem.annotationSuperInterface=ignore -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -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.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 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=16 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=0 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=true -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=true -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false -org.eclipse.jdt.core.formatter.comment.indent_root_tags=true -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=80 -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=80 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false -org.eclipse.jdt.core.formatter.tabulation.char=space -org.eclipse.jdt.core.formatter.tabulation.size=2 -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 7397758..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null
@@ -1,60 +0,0 @@ -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Google Format -formatter_settings_version=11 -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax; -org.eclipse.jdt.ui.ondemandthreshold=99 -org.eclipse.jdt.ui.staticondemandthreshold=99 -org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/> -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=false -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=false -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=true -sp_cleanup.make_parameters_final=true -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=false -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=false -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK deleted file mode 100644 index 55a2a4a..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/BUCK +++ /dev/null
@@ -1,22 +0,0 @@ -include_defs('//bucklets/gerrit_plugin.bucklet') - -gerrit_plugin( - name = '${pluginName}', - srcs = glob(['src/main/java/**/*.java']), - resources = glob(['src/main/resources/**/*']), - manifest_entries = [ - 'Gerrit-PluginName: ${pluginName}', - 'Gerrit-ApiType: ${gerritApiType}', - 'Gerrit-ApiVersion: ${gerritApiVersion}', - 'Gerrit-Module: ${package}.Module', - 'Gerrit-SshModule: ${package}.SshModule', - 'Gerrit-HttpModule: ${package}.HttpModule', - ], -) - -# this is required for bucklets/tools/eclipse/project.py to work -java_library( - name = 'classpath', - deps = [':${pluginName}__plugin'], -) -
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE deleted file mode 100644 index 11069ed..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE +++ /dev/null
@@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION b/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION deleted file mode 100644 index 8bbb460..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/VERSION +++ /dev/null
@@ -1,5 +0,0 @@ -# Used by BUCK to include "Implementation-Version" in plugin Manifest. -# If this file doesn't exist the output of 'git describe' is used -# instead. -PLUGIN_VERSION = '${version}' -
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK b/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK deleted file mode 100644 index b1648d3..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK +++ /dev/null
@@ -1,12 +0,0 @@ -include_defs('//bucklets/maven_jar.bucklet') - -VER = '${gerritApiVersion}' -REPO = MAVEN_LOCAL - -maven_jar( - name = '${gerritApiType}-api', - id = 'com.google.gerrit:gerrit-${gerritApiType}-api:' + VER, - attach_source = False, - repository = REPO, - license = 'Apache2.0', -)
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml deleted file mode 100644 index 026e21d..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml +++ /dev/null
@@ -1,94 +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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>${groupId}</groupId> - <artifactId>${artifactId}</artifactId> - <packaging>jar</packaging> - <version>${version}</version> - <name>${pluginName}</name> - - <properties> - <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType> - <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion> - </properties> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <version>2.4</version> - <configuration> - <archive> - <manifestEntries> - <Gerrit-PluginName>${pluginName}</Gerrit-PluginName> -#if ($Gerrit-Module.equalsIgnoreCase("Y")) - <Gerrit-Module>${package}.Module</Gerrit-Module> -#end -#if ($Gerrit-SshModule.equalsIgnoreCase("Y")) - <Gerrit-SshModule>${package}.SshModule</Gerrit-SshModule> -#end -#if ($Gerrit-HttpModule.equalsIgnoreCase("Y")) - <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule> -#end - - <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor> - - <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title> - <Implementation-Version>${project.version}</Implementation-Version> - - <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType> - <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion> - </manifestEntries> - </archive> - </configuration> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>2.3.2</version> - <configuration> - <source>1.7</source> - <target>1.7</target> - <encoding>UTF-8</encoding> - </configuration> - </plugin> - </plugins> - </build> - - <dependencies> - <dependency> - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId> - <version>${Gerrit-ApiVersion}</version> - <scope>provided</scope> - </dependency> - </dependencies> -#if ($gerritApiVersion.endsWith("SNAPSHOT")) - - <repositories> - <repository> - <id>snapshot-repository</id> - <url>https://oss.sonatype.org/content/repositories/snapshots/</url> - </repository> - </repositories> -#end -</project>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java deleted file mode 100644 index 39ce59b..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java +++ /dev/null
@@ -1,24 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}; - -import com.google.inject.AbstractModule; - -class Module extends AbstractModule { - @Override - protected void configure() { - // TODO - } -}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java deleted file mode 100644 index 1ef7cc8..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java +++ /dev/null
@@ -1,24 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}; - -import com.google.gerrit.sshd.PluginCommandModule; - -class SshModule extends PluginCommandModule { - @Override - protected void configureCommands() { - // command("my-command").to(MyCommand.class); - } -}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md deleted file mode 100644 index e4e944a..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md +++ /dev/null
@@ -1,84 +0,0 @@ -Build -===== - -This plugin can be built with Buck or Maven. - -Buck ----- - -Two build modes are supported: Standalone and in Gerrit tree. -The standalone build mode is recommended, as this mode doesn't require -the Gerrit tree to exist locally. - - -### Build standalone - -Clone bucklets library: - -``` - git clone https://gerrit.googlesource.com/bucklets - -``` -and link it to @PLUGIN@ plugin directory: - -``` - cd @PLUGIN@ && ln -s ../bucklets . -``` - -Add link to the .buckversion file: - -``` - cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion -``` - -Add link to the .watchmanconfig file: -``` - cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig -``` - -To build the plugin, issue the following command: - - -``` - buck build plugin -``` - -The output is created in - -``` - buck-out/gen/@PLUGIN@.jar -``` - -### Build in Gerrit tree - -Clone or link this plugin to the plugins directory of Gerrit's source -tree, and issue the command: - -``` - buck build plugins/@PLUGIN@ -``` - -The output is created in - -``` - buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar -``` - -This project can be imported into the Eclipse IDE: - -``` - ./tools/eclipse/project.py -``` - -Maven ------ - -Note that the Maven build is provided for compatibility reasons, but -it is considered to be deprecated and will be removed in a future -version of this plugin. - -To build with Maven, run - -``` -mvn clean package -```
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md deleted file mode 100644 index beecb90..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md +++ /dev/null
@@ -1 +0,0 @@ -TODO: command documentation
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md deleted file mode 100644 index bde3084..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md +++ /dev/null
@@ -1 +0,0 @@ -TODO: config documentation
diff --git a/gerrit-plugin-gwt-archetype/.gitignore b/gerrit-plugin-gwt-archetype/.gitignore deleted file mode 100644 index 7075a2f..0000000 --- a/gerrit-plugin-gwt-archetype/.gitignore +++ /dev/null
@@ -1,4 +0,0 @@ -/target -/.classpath -/.project -/.settings
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml deleted file mode 100644 index a5e9e3c..0000000 --- a/gerrit-plugin-gwt-archetype/pom.xml +++ /dev/null
@@ -1,108 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-plugin-gwt-archetype</artifactId> - <version>2.13.4</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> - - <properties> - <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion> - </properties> - - <build> - <resources> - <resource> - <directory>src/main/resources</directory> - <filtering>true</filtering> - <includes> - <include>META-INF/maven/archetype-metadata.xml</include> - </includes> - </resource> - <resource> - <directory>src/main/resources</directory> - <filtering>false</filtering> - <excludes> - <exclude>META-INF/maven/archetype-metadata.xml</exclude> - </excludes> - </resource> - </resources> - </build> - - <licenses> - <license> - <name>The Apache Software License, Version 2.0</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> - <distribution>repo</distribution> - </license> - </licenses> - - <scm> - <url>https://gerrit.googlesource.com/gerrit</url> - <connection>https://gerrit.googlesource.com/gerrit</connection> - </scm> - - <developers> - <developer> - <name>Andrew Bonventre</name> - </developer> - <developer> - <name>Dave Borowitz</name> - </developer> - <developer> - <name>David Ostrovsky</name> - </developer> - <developer> - <name>David Pursehouse</name> - </developer> - <developer> - <name>Edwin Kempin</name> - </developer> - <developer> - <name>Hugo Arès</name> - </developer> - <developer> - <name>Martin Fick</name> - </developer> - <developer> - <name>Saša Živkov</name> - </developer> - <developer> - <name>Shawn Pearce</name> - </developer> - </developers> - - <mailingLists> - <mailingList> - <name>Repo and Gerrit Discussion</name> - <post>repo-discuss@googlegroups.com</post> - <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe> - <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe> - <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive> - </mailingList> - </mailingLists> - - <issueManagement> - <url>https://bugs.chromium.org/p/gerrit/issues/list</url> - <system>Gerrit Issue Tracker</system> - </issueManagement> -</project>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml deleted file mode 100644 index 32a603b..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ /dev/null
@@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<archetype-descriptor name="Gerrit Plugin"> - <requiredProperties> - <requiredProperty key="pluginName"/> - - <requiredProperty key="Implementation-Vendor"> - <defaultValue>Gerrit Code Review</defaultValue> - </requiredProperty> - <requiredProperty key="Gwt-Version"> - <defaultValue>2.7.0</defaultValue> - </requiredProperty> - - <requiredProperty key="gerritApiVersion"> - <defaultValue>${defaultGerritApiVersion}</defaultValue> - </requiredProperty> - </requiredProperties> - - <fileSets> - <fileSet filtered="true" packaged="true"> - <directory>src/main/java</directory> - <includes> - <include>**/*.css</include> - <include>**/*.png</include> - <include>**/*.java</include> - <include>**/*.gwt.xml</include> - </includes> - </fileSet> - - <fileSet filtered="true"> - <directory>src/main/resources/Documentation</directory> - <includes> - <include>**/*.md</include> - </includes> - </fileSet> - - <fileSet filtered="true"> - <directory></directory> - <include>.buckconfig</include> - <include>BUCK</include> - <include>VERSION</include> - <include>lib/gerrit/BUCK</include> - <include>lib/gwt/BUCK</include> - <excludes> - <exclude>**/client/</exclude> - <exclude>**/public/</exclude> - <exclude>**/*.css</exclude> - <exclude>**/*.png</exclude> - <exclude>**/*.java</exclude> - <exclude>**/*.gwt.xml</exclude> - </excludes> - </fileSet> - - <fileSet> - <directory></directory> - <includes> - <include>.gitignore</include> - <include>.settings/*</include> - <include>LICENSE</include> - </includes> - </fileSet> - </fileSets> -</archetype-descriptor>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig deleted file mode 100644 index 1044c12..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig +++ /dev/null
@@ -1,14 +0,0 @@ -[alias] - ${pluginName} = //:${pluginName} - plugin = //:${pluginName} - -[java] - src_roots = java, resources - -[project] - ignore = .git - -[cache] - mode = dir - dir = buck-out/cache -
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore deleted file mode 100644 index 43838b0..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore +++ /dev/null
@@ -1,9 +0,0 @@ -/.buckversion -/.buckd -/buck-out -/bucklets -/target -/.classpath -/.project -/.settings/org.maven.ide.eclipse.prefs -/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 29abf99..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs +++ /dev/null
@@ -1,6 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 -encoding//src/test/resources=UTF-8 -encoding/<project>=UTF-8
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs +++ /dev/null
@@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n
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 deleted file mode 100644 index 2a585e4..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs +++ /dev/null
@@ -1,346 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.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.problem.annotationSuperInterface=ignore -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -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.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 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=16 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=0 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=true -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=true -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false -org.eclipse.jdt.core.formatter.comment.indent_root_tags=true -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=80 -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=80 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false -org.eclipse.jdt.core.formatter.tabulation.char=space -org.eclipse.jdt.core.formatter.tabulation.size=2 -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 7397758..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null
@@ -1,60 +0,0 @@ -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Google Format -formatter_settings_version=11 -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax; -org.eclipse.jdt.ui.ondemandthreshold=99 -org.eclipse.jdt.ui.staticondemandthreshold=99 -org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/> -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=false -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=false -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=true -sp_cleanup.make_parameters_final=true -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=false -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=false -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK deleted file mode 100644 index f33929d..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK +++ /dev/null
@@ -1,21 +0,0 @@ -include_defs('//bucklets/gerrit_plugin.bucklet') - -gerrit_plugin( - name = '${pluginName}', - srcs = glob(['src/main/java/**/*.java']), - resources = glob(['src/main/**/*']), - gwt_module = '${package}.HelloPlugin', - manifest_entries = [ - 'Gerrit-PluginName: ${pluginName}', - 'Gerrit-ApiType: plugin', - 'Gerrit-ApiVersion: ${gerritApiVersion}', - 'Gerrit-Module: ${package}.Module', - ], -) - -# this is required for bucklets/tools/eclipse/project.py to work -java_library( - name = 'classpath', - deps = [':${pluginName}__plugin'], -) -
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE deleted file mode 100644 index 11069ed..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE +++ /dev/null
@@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION deleted file mode 100644 index 8bbb460..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION +++ /dev/null
@@ -1,5 +0,0 @@ -# Used by BUCK to include "Implementation-Version" in plugin Manifest. -# If this file doesn't exist the output of 'git describe' is used -# instead. -PLUGIN_VERSION = '${version}' -
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK deleted file mode 100644 index 0a0d8b9..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK +++ /dev/null
@@ -1,20 +0,0 @@ -include_defs('//bucklets/maven_jar.bucklet') - -VER = '${gerritApiVersion}' -REPO = MAVEN_LOCAL - -maven_jar( - name = 'plugin-api', - id = 'com.google.gerrit:gerrit-plugin-api:' + VER, - attach_source = False, - repository = REPO, - license = 'Apache2.0', -) - -maven_jar( - name = 'gwtui-api', - id = 'com.google.gerrit:gerrit-plugin-gwtui:' + VER, - attach_source = False, - repository = REPO, - license = 'Apache2.0', -)
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 deleted file mode 100644 index 511a8ec..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK +++ /dev/null
@@ -1,32 +0,0 @@ -include_defs('//bucklets/maven_jar.bucklet') - -VERSION = '${Gwt-Version}' - -maven_jar( - name = 'user', - id = 'com.google.gwt:gwt-user:' + VERSION, - license = 'Apache2.0', - attach_source = False, -) - -maven_jar( - 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/lib/ow2/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/ow2/BUCK deleted file mode 100644 index db6c76c..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/ow2/BUCK +++ /dev/null
@@ -1,32 +0,0 @@ -include_defs('//bucklets/maven_jar.bucklet') - -VERSION = '5.0.3' - -maven_jar( - name = 'ow2-asm', - id = 'org.ow2.asm:asm:' + VERSION, - sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa', - license = 'ow2', -) - -maven_jar( - name = 'ow2-asm-analysis', - id = 'org.ow2.asm:asm-analysis:' + VERSION, - sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3', - license = 'ow2', -) - -maven_jar( - name = 'ow2-asm-tree', - id = 'org.ow2.asm:asm-tree:' + VERSION, - sha1 = '287749b48ba7162fb67c93a026d690b29f410bed', - license = 'ow2', -) - -maven_jar( - name = 'ow2-asm-util', - id = 'org.ow2.asm:asm-util:' + VERSION, - sha1 = '1512e5571325854b05fb1efce1db75fcced54389', - license = 'ow2', -) -
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 deleted file mode 100644 index 2c7fe88..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml +++ /dev/null
@@ -1,122 +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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>${groupId}</groupId> - <artifactId>${artifactId}</artifactId> - <packaging>jar</packaging> - <version>${version}</version> - <name>${pluginName}</name> - - <properties> - <Gerrit-ApiType>plugin</Gerrit-ApiType> - <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion> - </properties> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <version>2.4</version> - <configuration> - <includes> - <include>**/*.*</include> - </includes> - <archive> - <manifestEntries> - <Gerrit-PluginName>${pluginName}</Gerrit-PluginName> - <Gerrit-Module>${package}.Module</Gerrit-Module> - <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule> - <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor> - - <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title> - <Implementation-Version>${project.version}</Implementation-Version> - - <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType> - <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion> - </manifestEntries> - </archive> - </configuration> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>2.3.2</version> - <configuration> - <source>1.7</source> - <target>1.7</target> - <encoding>UTF-8</encoding> - </configuration> - </plugin> - - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>gwt-maven-plugin</artifactId> - <version>${Gwt-Version}</version> - <configuration> - <module>${package}.HelloPlugin</module> - <disableClassMetadata>true</disableClassMetadata> - <disableCastChecking>true</disableCastChecking> - <webappDirectory>${project.build.directory}/classes/static</webappDirectory> - </configuration> - <executions> - <execution> - <goals> - <goal>compile</goal> - </goals> - </execution> - </executions> - </plugin> - - </plugins> - </build> - - <dependencies> - <dependency> - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId> - <version>${Gerrit-ApiVersion}</version> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-plugin-gwtui</artifactId> - <version>${Gerrit-ApiVersion}</version> - </dependency> - - <dependency> - <groupId>com.google.gwt</groupId> - <artifactId>gwt-user</artifactId> - <version>${Gwt-Version}</version> - <scope>provided</scope> - </dependency> - </dependencies> -#if ($gerritApiVersion.endsWith("SNAPSHOT")) - - <repositories> - <repository> - <id>snapshot-repository</id> - <url>https://oss.sonatype.org/content/repositories/snapshots/</url> - </repository> - </repositories> -#end -</project>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java deleted file mode 100644 index d2d9d80..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloMenu.java +++ /dev/null
@@ -1,39 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}; - -import com.google.gerrit.extensions.annotations.PluginName; -import com.google.gerrit.extensions.webui.TopMenu; -import com.google.inject.Inject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class HelloMenu implements TopMenu { - private final List<MenuEntry> menuEntries; - - @Inject - public HelloMenu(@PluginName String pluginName) { - menuEntries = new ArrayList<>(); - menuEntries.add(new MenuEntry("Hello", Collections - .singletonList(new MenuItem("Hello Screen", "#/x/" + pluginName, "")))); - } - - @Override - public List<MenuEntry> getEntries() { - return menuEntries; - } -}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml deleted file mode 100644 index 1f6f81e..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugin.gwt.xml +++ /dev/null
@@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - 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. ---> -<module rename-to="hello_gwt_plugin"> - <!-- Inherit the core Web Toolkit stuff. --> - <inherits name="com.google.gwt.user.User"/> - <!-- Other module inherits --> - <inherits name="com.google.gerrit.Plugin"/> - <inherits name="com.google.gwt.http.HTTP"/> - <!-- Using GWT built-in themes adds a number of static --> - <!-- resources to the plugin. No theme inherits lines were --> - <!-- added in order to make this plugin as simple as possible --> - <!-- Specify the app entry point class. --> - <entry-point class="${package}.client.HelloPlugin"/> - <stylesheet src="hello.css"/> -</module>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java deleted file mode 100644 index 73e5695..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/Module.java +++ /dev/null
@@ -1,31 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}; - -import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.extensions.webui.GwtPlugin; -import com.google.gerrit.extensions.webui.TopMenu; -import com.google.gerrit.extensions.webui.WebUiPlugin; -import com.google.inject.AbstractModule; - -public class Module extends AbstractModule { - - @Override - protected void configure() { - DynamicSet.bind(binder(), TopMenu.class).to(HelloMenu.class); - DynamicSet.bind(binder(), WebUiPlugin.class) - .toInstance(new GwtPlugin("hello_gwt_plugin")); - } -}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java deleted file mode 100644 index 4a7e149..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugin.java +++ /dev/null
@@ -1,31 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}.client; - -import com.google.gerrit.plugin.client.Plugin; -import com.google.gerrit.plugin.client.PluginEntryPoint; - -import ${package}.client.HelloScreen; - -/** - * HelloWorld Plugin. - */ -public class HelloPlugin extends PluginEntryPoint { - - @Override - public void onPluginLoad() { - Plugin.get().screen("", new HelloScreen.Factory()); - } -}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java deleted file mode 100644 index 09b8b92..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloScreen.java +++ /dev/null
@@ -1,35 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}.client; - -import com.google.gerrit.plugin.client.screen.Screen; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.VerticalPanel; - -public class HelloScreen extends VerticalPanel { - - static class Factory implements Screen.EntryPoint { - @Override - public void onLoad(Screen screen) { - screen.setPageTitle("Hello"); - screen.show(new HelloScreen()); - } - } - - HelloScreen() { - setStyleName("hello-panel"); - add(new Label("Hello World Screen")); - } -}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css deleted file mode 100644 index 72cf023..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css +++ /dev/null
@@ -1,3 +0,0 @@ -.hello-panel { - border-spacing: 0px 5px; -}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md deleted file mode 100644 index e225bab..0000000 --- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md +++ /dev/null
@@ -1,81 +0,0 @@ -Build -===== - -This plugin can be built with Buck or Maven. - -Buck ----- - -Two build modes are supported: Standalone and in Gerrit tree. -The standalone build mode is recommended, as this mode doesn't require -the Gerrit tree to exist locally. - - - -Clone bucklets library: - -``` - git clone https://gerrit.googlesource.com/bucklets - -``` -and link it to @PLUGIN@ plugin directory: - -``` - cd @PLUGIN@ && ln -s ../bucklets . -``` - -Add link to the .buckversion file: - -``` - cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion -``` - -Add link to the .watchmanconfig file: -``` - cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig -``` - -To build the plugin, issue the following command: - -``` - buck build plugin -``` - -The output is created in - -``` - buck-out/gen/@PLUGIN@.jar -``` - - -Clone or link this plugin to the plugins directory of Gerrit's source -tree, and issue the command: - -``` - buck build plugins/@PLUGIN@ -``` - -The output is created in - -``` - buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar -``` - -This project can be imported into the Eclipse IDE: - -``` - ./tools/eclipse/project.py -``` - -Maven ------ - -Note that the Maven build is provided for compatibility reasons, but -it is considered to be deprecated and will be removed in a future -version of this plugin. - -To build with Maven, run - -``` -mvn clean package -```
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/BUILD b/gerrit-plugin-gwtui/BUILD new file mode 100644 index 0000000..f896050 --- /dev/null +++ b/gerrit-plugin-gwtui/BUILD
@@ -0,0 +1,80 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools/bzl:java.bzl", "java_library2") + +SRCS = glob(["src/main/java/com/google/gerrit/**/*.java"]) + +DEPS = ["//lib/gwt:user-neverlink"] + +java_binary( + name = "gwtui-api", + main_class = "Dummy", + runtime_deps = [ + ":gwtui-api-lib", + "//gerrit-gwtui-common:client-lib", + ], +) + +java_library2( + name = "gwtui-api-lib", + srcs = SRCS, + exported_deps = ["//gerrit-gwtui-common:client-lib"], + resources = glob(["src/main/**/*"]), + deps = DEPS + [ + "//gerrit-common:libclient-src.jar", + "//gerrit-extension-api:libclient-src.jar", + "//gerrit-gwtexpui:libClippy-src.jar", + "//gerrit-gwtexpui:libGlobalKey-src.jar", + "//gerrit-gwtexpui:libProgress-src.jar", + "//gerrit-gwtexpui:libSafeHtml-src.jar", + "//gerrit-gwtexpui:libUserAgent-src.jar", + "//gerrit-gwtui-common:libclient-src.jar", + "//gerrit-patch-jgit:libclient-src.jar", + "//gerrit-patch-jgit:libEdit-src.jar", + "//gerrit-prettify:libclient-src.jar", + "//gerrit-reviewdb:libclient-src.jar", + "//lib/gwt:dev-neverlink", + ], +) + +java_library2( + name = "gwtui-api-lib-neverlink", + srcs = SRCS, + exported_deps = ["//gerrit-gwtui-common:client-lib"], + neverlink = 1, # we want this to be exported deps + resources = glob(["src/main/**/*"]), + deps = DEPS + ["//lib/gwt:dev"], +) + +java_binary( + name = "gwtui-api-source", + main_class = "Dummy", + runtime_deps = [ + ":libgwtui-api-lib-src.jar", + "//gerrit-gwtexpui:client-src-lib", + "//gerrit-gwtui-common:libclient-lib-src.jar", + ], +) + +load("//tools/bzl:javadoc.bzl", "java_doc") + +java_doc( + name = "gwtui-api-javadoc", + libs = DEPS + [ + ":gwtui-api-lib", + "//lib:gwtjsonrpc", + "//lib:gwtorm_client", + "//lib/gwt:dev", + "//gerrit-gwtui-common:client-lib", + "//gerrit-common:client", + "//gerrit-reviewdb:client", + ], + pkgs = [ + "com.google.gerrit.plugin", + "com.google.gwtexpui.clippy", + "com.google.gwtexpui.globalkey", + "com.google.gwtexpui.safehtml", + "com.google.gwtexpui.user", + ], + title = "Gerrit Review GWT Extension API Documentation", +)
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml index 1211841..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.4</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/.gitignore b/gerrit-plugin-js-archetype/.gitignore deleted file mode 100644 index 7075a2f..0000000 --- a/gerrit-plugin-js-archetype/.gitignore +++ /dev/null
@@ -1,4 +0,0 @@ -/target -/.classpath -/.project -/.settings
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml deleted file mode 100644 index 353f695..0000000 --- a/gerrit-plugin-js-archetype/pom.xml +++ /dev/null
@@ -1,108 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-plugin-js-archetype</artifactId> - <version>2.13.4</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> - - <properties> - <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion> - </properties> - - <build> - <resources> - <resource> - <directory>src/main/resources</directory> - <filtering>true</filtering> - <includes> - <include>META-INF/maven/archetype-metadata.xml</include> - </includes> - </resource> - <resource> - <directory>src/main/resources</directory> - <filtering>false</filtering> - <excludes> - <exclude>META-INF/maven/archetype-metadata.xml</exclude> - </excludes> - </resource> - </resources> - </build> - - <licenses> - <license> - <name>The Apache Software License, Version 2.0</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> - <distribution>repo</distribution> - </license> - </licenses> - - <scm> - <url>https://gerrit.googlesource.com/gerrit</url> - <connection>https://gerrit.googlesource.com/gerrit</connection> - </scm> - - <developers> - <developer> - <name>Andrew Bonventre</name> - </developer> - <developer> - <name>Dave Borowitz</name> - </developer> - <developer> - <name>David Ostrovsky</name> - </developer> - <developer> - <name>David Pursehouse</name> - </developer> - <developer> - <name>Edwin Kempin</name> - </developer> - <developer> - <name>Hugo Arès</name> - </developer> - <developer> - <name>Martin Fick</name> - </developer> - <developer> - <name>Saša Živkov</name> - </developer> - <developer> - <name>Shawn Pearce</name> - </developer> - </developers> - - <mailingLists> - <mailingList> - <name>Repo and Gerrit Discussion</name> - <post>repo-discuss@googlegroups.com</post> - <subscribe>https://groups.google.com/forum/#!forum/repo-discuss</subscribe> - <unsubscribe>https://groups.google.com/forum/#!forum/repo-discuss</unsubscribe> - <archive>https://groups.google.com/forum/#!forum/repo-discuss</archive> - </mailingList> - </mailingLists> - - <issueManagement> - <url>https://bugs.chromium.org/p/gerrit/issues/list</url> - <system>Gerrit Issue Tracker</system> - </issueManagement> -</project>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml deleted file mode 100644 index ef0e96c..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ /dev/null
@@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -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. ---> -<archetype-descriptor name="Gerrit Plugin"> - <requiredProperties> - <requiredProperty key="pluginName"/> - - <requiredProperty key="Implementation-Vendor"> - <defaultValue>Gerrit Code Review</defaultValue> - </requiredProperty> - - <requiredProperty key="gerritApiType"> - <defaultValue>js</defaultValue> - </requiredProperty> - <requiredProperty key="gerritApiVersion"> - <defaultValue>${defaultGerritApiVersion}</defaultValue> - </requiredProperty> - </requiredProperties> - - <fileSets> - <fileSet filtered="true" packaged="true"> - <directory>src/main/java</directory> - <includes> - <include>**/*.java</include> - </includes> - </fileSet> - - <fileSet> - <directory>src/main/js</directory> - <includes> - <include>**/*.js</include> - </includes> - </fileSet> - - <fileSet filtered="true"> - <directory>src/main/resources/Documentation</directory> - <includes> - <include>**/*.md</include> - </includes> - </fileSet> - - <fileSet> - <directory></directory> - <includes> - <include>.gitignore</include> - <include>.settings/*</include> - <include>LICENSE</include> - </includes> - </fileSet> - </fileSets> -</archetype-descriptor>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore deleted file mode 100644 index 80d6257..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore +++ /dev/null
@@ -1,5 +0,0 @@ -/target -/.classpath -/.project -/.settings/org.maven.ide.eclipse.prefs -/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 29abf99..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.resources.prefs +++ /dev/null
@@ -1,6 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/main/java=UTF-8 -encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 -encoding//src/test/resources=UTF-8 -encoding/<project>=UTF-8
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs deleted file mode 100644 index 5a0ad22..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.core.runtime.prefs +++ /dev/null
@@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -line.separator=\n
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 deleted file mode 100644 index 2a585e4..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.core.prefs +++ /dev/null
@@ -1,346 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.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.problem.annotationSuperInterface=ignore -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -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.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 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_assignment=16 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=0 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false -org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false -org.eclipse.jdt.core.formatter.comment.format_block_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=true -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true -org.eclipse.jdt.core.formatter.comment.format_line_comments=true -org.eclipse.jdt.core.formatter.comment.format_source_code=true -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false -org.eclipse.jdt.core.formatter.comment.indent_root_tags=true -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=80 -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.join_lines_in_comments=true -org.eclipse.jdt.core.formatter.join_wrapped_lines=true -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=80 -org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false -org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false -org.eclipse.jdt.core.formatter.tabulation.char=space -org.eclipse.jdt.core.formatter.tabulation.size=2 -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false -org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index 7397758..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null
@@ -1,60 +0,0 @@ -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Google Format -formatter_settings_version=11 -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=\#;com.google;com;dk;eu;junit;net;org;java;javax; -org.eclipse.jdt.ui.ondemandthreshold=99 -org.eclipse.jdt.ui.staticondemandthreshold=99 -org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/> -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=false -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=false -sp_cleanup.always_use_this_for_non_static_field_access=false -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=false -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=true -sp_cleanup.make_parameters_final=true -sp_cleanup.make_private_fields_final=true -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=true -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=false -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=false -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=false -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=false -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=false -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=false -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE deleted file mode 100644 index 11069ed..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE +++ /dev/null
@@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.
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 deleted file mode 100644 index 8f4aadd..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml +++ /dev/null
@@ -1,119 +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. ---> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <groupId>${groupId}</groupId> - <artifactId>${artifactId}</artifactId> - <packaging>jar</packaging> - <version>${version}</version> - <name>${pluginName}</name> - - <properties> - <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType> - <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion> - </properties> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <version>2.4</version> - <configuration> - <includes> - <include>**/*.js</include> - <include>**/*.class</include> - </includes> - <archive> - <manifestEntries> - <Gerrit-PluginName>${pluginName}</Gerrit-PluginName> - <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor> - - <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title> - <Implementation-Version>${project.version}</Implementation-Version> - - <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType> - <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion> - </manifestEntries> - </archive> - </configuration> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>2.3.2</version> - <configuration> - <source>1.7</source> - <target>1.7</target> - <encoding>UTF-8</encoding> - </configuration> - </plugin> - - <plugin> - <artifactId>maven-resources-plugin</artifactId> - <version>2.6</version> - <executions> - <execution> - <id>copy-resources</id> - <phase>process-resources</phase> - <goals> - <goal>copy-resources</goal> - </goals> - <configuration> - <outputDirectory>${basedir}/target/classes/static</outputDirectory> - <resources> - <resource> - <directory>src/main/js</directory> - <filtering>true</filtering> - </resource> - </resources> - </configuration> - </execution> - </executions> - </plugin> - - </plugins> - </build> - - <dependencies> - <dependency> - <groupId>com.google.gerrit</groupId> - <artifactId>gerrit-extension-api</artifactId> - <version>${Gerrit-ApiVersion}</version> - <scope>provided</scope> - </dependency> - - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.8.1</version> - <scope>test</scope> - </dependency> - </dependencies> -#if ($gerritApiVersion.endsWith("SNAPSHOT")) - - <repositories> - <repository> - <id>snapshot-repository</id> - <url>https://oss.sonatype.org/content/repositories/snapshots/</url> - </repository> - </repositories> -#end -</project>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java deleted file mode 100644 index 39d06e3..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java +++ /dev/null
@@ -1,25 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ${package}; - -import com.google.gerrit.extensions.annotations.Listen; -import com.google.gerrit.extensions.webui.JavaScriptPlugin; - -@Listen -public class MyJsExtension extends JavaScriptPlugin { - public MyJsExtension() { - super("hello-js-plugins.js"); - } -}
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js deleted file mode 100644 index fd51a42..0000000 --- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js +++ /dev/null
@@ -1 +0,0 @@ -alert("Greeting from JavaScript Gerrit plugin!"); \ No newline at end of file
diff --git a/gerrit-prettify/BUILD b/gerrit-prettify/BUILD index 063feee..18180b3 100644 --- a/gerrit-prettify/BUILD +++ b/gerrit-prettify/BUILD
@@ -1,35 +1,40 @@ -load('//tools/bzl:gwt.bzl', 'gwt_module') +load("//tools/bzl:gwt.bzl", "gwt_module") -SRC = 'src/main/java/com/google/gerrit/prettify/' +SRC = "src/main/java/com/google/gerrit/prettify/" gwt_module( - name = 'client', - srcs = glob([ - SRC + 'common/**/*.java', - ]), - gwt_xml = SRC + 'PrettyFormatter.gwt.xml', - deps = ['//lib/gwt:user'], - exported_deps = [ - '//gerrit-extension-api:client', - '//gerrit-gwtexpui:SafeHtml', - '//gerrit-patch-jgit:client', - '//gerrit-patch-jgit:Edit', - '//gerrit-reviewdb:client', - '//lib:gwtjsonrpc', - '//lib:gwtjsonrpc_src', - ], - visibility = ['//visibility:public'], + name = "client", + srcs = glob([ + SRC + "common/**/*.java", + ]), + exported_deps = [ + "//gerrit-extension-api:client", + "//gerrit-gwtexpui:SafeHtml", + "//gerrit-patch-jgit:Edit", + "//gerrit-patch-jgit:client", + "//gerrit-reviewdb:client", + "//lib:gwtjsonrpc", + "//lib:gwtjsonrpc_src", + ], + gwt_xml = SRC + "PrettyFormatter.gwt.xml", + visibility = ["//visibility:public"], + deps = ["//lib/gwt:user-neverlink"], ) java_library( - name = 'server', - srcs = glob([SRC + 'common/**/*.java']), - deps = [ - '//gerrit-patch-jgit:server', - '//gerrit-reviewdb:server', - '//lib:guava', - '//lib:gwtjsonrpc', - '//lib/jgit/org.eclipse.jgit:jgit', - ], - visibility = ['//visibility:public'], + name = "server", + srcs = glob([SRC + "common/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-patch-jgit:server", + "//gerrit-reviewdb:server", + "//lib:guava", + "//lib:gwtjsonrpc", + "//lib/jgit/org.eclipse.jgit:jgit", + ], ) + +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..532ce801 100644 --- a/gerrit-reviewdb/BUCK +++ b/gerrit-reviewdb/BUCK
@@ -30,9 +30,9 @@ srcs = glob([TESTS + 'client/**/*.java']), deps = [ ':client', + '//gerrit-server:testutil', '//lib:gwtorm', '//lib:truth', ], - source_under_test = [':client'], visibility = ['//tools/eclipse:classpath'], )
diff --git a/gerrit-reviewdb/BUILD b/gerrit-reviewdb/BUILD index a4144ec..98af668 100644 --- a/gerrit-reviewdb/BUILD +++ b/gerrit-reviewdb/BUILD
@@ -1,39 +1,45 @@ -load('//tools/bzl:gwt.bzl', 'gwt_module') -load('//tools/bzl:junit.bzl', 'junit_tests') +package( + default_visibility = ["//visibility:public"], +) -SRC = 'src/main/java/com/google/gerrit/reviewdb/' -TESTS = 'src/test/java/com/google/gerrit/reviewdb/' +load("//tools/bzl:gwt.bzl", "gwt_module") +load("//tools/bzl:junit.bzl", "junit_tests") + +SRC = "src/main/java/com/google/gerrit/reviewdb/" + +TESTS = "src/test/java/com/google/gerrit/reviewdb/" gwt_module( - name = 'client', - srcs = glob([SRC + 'client/**/*.java']), - gwt_xml = SRC + 'ReviewDB.gwt.xml', - deps = [ - '//gerrit-extension-api:client', - '//lib:gwtorm_client', - '//lib:gwtorm_client_src' - ], - visibility = ['//visibility:public'], + name = "client", + srcs = glob([SRC + "client/**/*.java"]), + gwt_xml = SRC + "ReviewDB.gwt.xml", + visibility = ["//visibility:public"], + deps = [ + "//gerrit-extension-api:client", + "//lib:gwtorm_client", + "//lib:gwtorm_client_src", + ], ) java_library( - name = 'server', - srcs = glob([SRC + '**/*.java']), - resources = glob(['src/main/resources/**/*']), - deps = [ - '//gerrit-extension-api:api', - '//lib:guava', - '//lib:gwtorm', - ], - visibility = ['//visibility:public'], + name = "server", + srcs = glob([SRC + "**/*.java"]), + resources = glob(["src/main/resources/**/*"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-extension-api:api", + "//lib:guava", + "//lib:gwtorm", + ], ) junit_tests( - name = 'client_tests', - srcs = glob([TESTS + 'client/**/*.java']), - deps = [ - ':client', - '//lib:gwtorm', - '//lib:truth', - ], + name = "client_tests", + srcs = glob([TESTS + "client/**/*.java"]), + deps = [ + ":client", + "//gerrit-server:testutil", + "//lib:gwtorm", + "//lib:truth", + ], )
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..fbaabc6 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
@@ -143,18 +143,58 @@ } public static Id fromRef(String ref) { + if (RefNames.isRefsEdit(ref)) { + return fromEditRefPart(ref); + } int cs = startIndex(ref); if (cs < 0) { return null; } 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))); } return null; } + public static Id fromAllUsersRef(String ref) { + if (ref == null) { + return null; + } + String prefix; + if (ref.startsWith(RefNames.REFS_STARRED_CHANGES)) { + prefix = RefNames.REFS_STARRED_CHANGES; + } else if (ref.startsWith(RefNames.REFS_DRAFT_COMMENTS)) { + prefix = RefNames.REFS_DRAFT_COMMENTS; + } else { + return null; + } + int cs = startIndex(ref, prefix); + if (cs < 0) { + return null; + } + int ce = nextNonDigit(ref, cs); + if (ce < ref.length() && ref.charAt(ce) == '/' + && isNumeric(ref, ce + 1)) { + return new Change.Id(Integer.parseInt(ref.substring(cs, ce))); + } + return null; + } + + private static boolean isNumeric(String s, int off) { + if (off >= s.length()) { + return false; + } + for (int i = off; i < s.length(); i++) { + if (!Character.isDigit(s.charAt(i))) { + return false; + } + } + return true; + } + public static Id fromEditRefPart(String ref) { int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) + RefNames.EDIT_PREFIX.length(); @@ -172,12 +212,16 @@ } static int startIndex(String ref) { - if (ref == null || !ref.startsWith(REFS_CHANGES)) { + return startIndex(ref, REFS_CHANGES); + } + + static int startIndex(String ref, String expectedPrefix) { + if (ref == null || !ref.startsWith(expectedPrefix)) { return -1; } // Last 2 digits. - int ls = REFS_CHANGES.length(); + int ls = expectedPrefix.length(); int le = nextNonDigit(ref, ls); if (le - ls != 2 || le >= ref.length() || ref.charAt(le) != '/') { return -1; @@ -481,6 +525,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 +551,7 @@ } public Change(Change other) { + assignee = other.assignee; changeId = other.changeId; changeKey = other.changeKey; rowVersion = other.rowVersion; @@ -535,6 +587,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..15ec625 --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -0,0 +1,283 @@ +// 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 boolean unresolved; + + public Comment(Comment c) { + this(new Key(c.key), c.author.getId(), new Timestamp(c.writtenOn.getTime()), + c.side, c.message, c.serverId, c.unresolved); + 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; + this.unresolved = c.unresolved; + } + + public Comment(Key key, Account.Id author, Timestamp writtenOn, + short side, String message, String serverId, boolean unresolved) { + 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; + this.unresolved = unresolved; + } + + 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(',') + .append("tag=").append(Objects.toString(tag, "")).append(',') + .append("unresolved=").append(unresolved) + .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/FixReplacement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/FixReplacement.java new file mode 100644 index 0000000..19d1b51 --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/FixReplacement.java
@@ -0,0 +1,36 @@ +// 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; + +public class FixReplacement { + public String path; + public Comment.Range range; + public String replacement; + + public FixReplacement(String path, Comment.Range range, String replacement) { + this.path = path; + this.range = range; + this.replacement = replacement; + } + + @Override + public String toString() { + return "FixReplacement{" + + "path='" + path + '\'' + + ", range=" + range + + ", replacement='" + replacement + '\'' + + '}'; + } +}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/FixSuggestion.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/FixSuggestion.java new file mode 100644 index 0000000..7af647a --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/FixSuggestion.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.reviewdb.client; + +import java.util.List; + +public class FixSuggestion { + public String fixId; + public String description; + public List<FixReplacement> replacements; + + public FixSuggestion(String fixId, String description, + List<FixReplacement> replacements) { + this.fixId = fixId; + this.description = description; + this.replacements = replacements; + } + + @Override + public String toString() { + return "FixSuggestion{" + + "fixId='" + fixId + '\'' + + ", description='" + description + '\'' + + ", replacements=" + replacements + + '}'; + } +}
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..accb749 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,17 @@ 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; + + /** True if this comment requires further action. */ + @Column(id = 12) + protected boolean unresolved; + + /** * 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 +204,7 @@ key = o.key; lineNbr = o.lineNbr; author = o.author; + realAuthor = o.realAuthor; writtenOn = o.writtenOn; status = o.status; side = o.side; @@ -186,6 +240,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 +323,18 @@ return tag; } + public Comment asComment(String serverId) { + Comment c = new Comment(key.asCommentKey(), author, writtenOn, side, + message, serverId, unresolved); + 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 +366,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(','); @@ -300,8 +377,10 @@ .append(','); builder.append("range=").append(Objects.toString(range, "")) .append(','); - builder.append("revId=").append(revId != null ? revId.get() : ""); - builder.append("tag=").append(Objects.toString(tag, "")); + builder.append("revId=").append(revId != null ? revId.get() : "") + .append(','); + builder.append("tag=").append(Objects.toString(tag, "")).append(','); + builder.append("unresolved=").append(unresolved); builder.append('}'); return builder.toString(); }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java index a8bf07b..cf5c5ad 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -194,6 +194,16 @@ @Column(id = 8, notNull = false, length = Integer.MAX_VALUE) protected String pushCertificate; + /** + * Optional user-supplied description for this patch set. + * <p> + * When this field is null, the description was never set on the patch set. + * When this field is an empty string, the description was set and later + * cleared. + */ + @Column(id = 9, notNull = false, length = Integer.MAX_VALUE) + protected String description; + protected PatchSet() { } @@ -201,6 +211,17 @@ id = k; } + public PatchSet(PatchSet src) { + this.id = src.id; + this.revision = src.revision; + this.uploader = src.uploader; + this.createdOn = src.createdOn; + this.draft = src.draft; + this.groups = src.groups; + this.pushCertificate = src.pushCertificate; + this.description = src.description; + } + public PatchSet.Id getId() { return id; } @@ -267,6 +288,14 @@ pushCertificate = cert; } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + @Override public String toString() { return "[PatchSet " + getId().toString() + "]";
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..30f2e1d 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,16 @@ @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; + + @Column(id = 8) + protected boolean postSubmit; + // DELETED: id = 4 (changeOpen) // DELETED: id = 5 (changeSortKey) @@ -110,7 +120,13 @@ new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId()); value = src.getValue(); granted = src.granted; + realAccountId = src.realAccountId; tag = src.tag; + postSubmit = src.postSubmit; + } + + public PatchSetApproval(PatchSetApproval src) { + this(src.getPatchSetId(), src); } public PatchSetApproval.Key getKey() { @@ -125,6 +141,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; } @@ -165,10 +190,24 @@ return tag; } + public void setPostSubmit(boolean postSubmit) { + this.postSubmit = postSubmit; + } + + public boolean isPostSubmit() { + return postSubmit; + } + @Override public String toString() { - return new StringBuilder().append('[').append(key).append(": ") - .append(value).append(",tag:").append(tag).append(']').toString(); + StringBuilder sb = new StringBuilder("[") + .append(key).append(": ").append(value) + .append(",tag:").append(tag) + .append(",realAccountId:").append(realAccountId); + if (postSubmit) { + sb.append(",postSubmit"); + } + return sb.append(']').toString(); } @Override @@ -178,7 +217,9 @@ 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) + && postSubmit == p.postSubmit; } return false; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java index 1a00fae..40cb9fc 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
@@ -54,6 +54,9 @@ /** SHA-1 of commit */ protected String revId; + /** Optional user-supplied description for the patch set. */ + protected String description; + protected PatchSetInfo() { } @@ -116,4 +119,12 @@ public String getRevId() { return revId; } + + public void setDescription(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } }
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 95f4f8e..c7c870e 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
@@ -70,6 +70,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) { @@ -94,6 +97,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); @@ -131,6 +142,13 @@ return r; } + public static String refsCacheAutomerge(String hash) { + return REFS_CACHE_AUTOMERGE + + hash.substring(0, 2) + + '/' + + hash.substring(2); + } + public static String shard(int id) { if (id < 0) { return null; @@ -177,7 +195,8 @@ } public static boolean isRefsEdit(String ref) { - return ref.startsWith(REFS_USERS) && ref.contains(EDIT_PREFIX); + return ref != null && ref.startsWith(REFS_USERS) + && ref.contains(EDIT_PREFIX); } public static boolean isRefsUsers(String ref) {
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..f08a30f --- /dev/null +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
@@ -0,0 +1,65 @@ +// 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.List; +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 List<FixSuggestion> fixSuggestions; + + 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, false); + 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("unresolved=").append(unresolved).append(',') + .append("url=").append(url).append(',') + .append("properties=").append(properties != null ? properties : "") + .append("fixSuggestions=") + .append(fixSuggestions != null ? fixSuggestions : "") + .append('}') + .toString(); + } +}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java index 0f8d005..8b7a661 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java
@@ -55,7 +55,7 @@ /** * Local filesystem location of header/footer/CSS configuration files */ - @Column(id = 3, notNull = false) + @Column(id = 3, notNull = false, length = Integer.MAX_VALUE) public transient String sitePath;
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-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java index 07c00b9..727ba8e 100644 --- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountSshKeyTest.java
@@ -34,7 +34,7 @@ private final Account.Id accountId = new Account.Id(1); @Test - public void testValidity() throws Exception { + public void validity() throws Exception { AccountSshKey key = new AccountSshKey( new AccountSshKey.Id(accountId, -1), KEY); assertThat(key.isValid()).isFalse(); @@ -45,7 +45,7 @@ } @Test - public void testGetters() throws Exception { + public void getters() throws Exception { AccountSshKey key = new AccountSshKey( new AccountSshKey.Id(accountId, 1), KEY); assertThat(key.getSshPublicKey()).isEqualTo(KEY); @@ -55,7 +55,7 @@ } @Test - public void testKeyWithNewLines() throws Exception { + public void keyWithNewLines() throws Exception { AccountSshKey key = new AccountSshKey( new AccountSshKey.Id(accountId, 1), KEY_WITH_NEWLINES); assertThat(key.getSshPublicKey()).isEqualTo(KEY);
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java index cf2d289..2aa863e 100644 --- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -64,6 +64,15 @@ } @Test + public void parseEditRefNames() { + assertRef(5, "refs/users/34/1234/edit-5/1"); + assertRef(5, "refs/users/34/1234/edit-5"); + assertNotRef("refs/changes/34/1234/edit-5/1"); + assertNotRef("refs/users/34/1234/EDIT-5/1"); + assertNotRef("refs/users/34/1234"); + } + + @Test public void parseChangeMetaRefNames() { assertRef(1, "refs/changes/01/1/meta"); assertRef(1234, "refs/changes/34/1234/meta"); @@ -74,6 +83,44 @@ } @Test + public void parseRobotCommentRefNames() { + assertRef(1, "refs/changes/01/1/robot-comments"); + assertRef(1234, "refs/changes/34/1234/robot-comments"); + + assertNotRef("refs/changes/01/1/robot-comment"); + assertNotRef("refs/changes/01/1/ROBOT-COMMENTS"); + assertNotRef("refs/changes/01/1/1/robot-comments"); + } + + @Test + public void parseStarredChangesRefNames() { + assertAllUsersRef(1, "refs/starred-changes/01/1/1001"); + assertAllUsersRef(1234, "refs/starred-changes/34/1234/1001"); + + assertNotRef("refs/starred-changes/01/1/1001"); + assertNotAllUsersRef(null); + assertNotAllUsersRef("refs/starred-changes/01/1/1xx1"); + assertNotAllUsersRef("refs/starred-changes/01/1/"); + assertNotAllUsersRef("refs/starred-changes/01/1"); + assertNotAllUsersRef("refs/starred-changes/35/1234/1001"); + assertNotAllUsersRef("refs/starred-changeS/01/1/1001"); + } + + @Test + public void parseDraftRefNames() { + assertAllUsersRef(1, "refs/draft-comments/01/1/1001"); + assertAllUsersRef(1234, "refs/draft-comments/34/1234/1001"); + + assertNotRef("refs/draft-comments/01/1/1001"); + assertNotAllUsersRef(null); + assertNotAllUsersRef("refs/draft-comments/01/1/1xx1"); + assertNotAllUsersRef("refs/draft-comments/01/1/"); + assertNotAllUsersRef("refs/draft-comments/01/1"); + assertNotAllUsersRef("refs/draft-comments/35/1234/1001"); + assertNotAllUsersRef("refs/draft-commentS/01/1/1001"); + } + + @Test public void toRefPrefix() { assertThat(new Change.Id(1).toRefPrefix()) .isEqualTo("refs/changes/01/1/"); @@ -110,6 +157,15 @@ assertThat(Change.Id.fromRef(refName)).isNull(); } + private static void assertAllUsersRef(int changeId, String refName) { + assertThat(Change.Id.fromAllUsersRef(refName)) + .isEqualTo(new Change.Id(changeId)); + } + + private static void assertNotAllUsersRef(String refName) { + assertThat(Change.Id.fromAllUsersRef(refName)).isNull(); + } + private static void assertRefPart(int changeId, String refName) { assertEquals(new Change.Id(changeId), Change.Id.fromRefPart(refName)); }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java index eba08c8..008c77f 100644 --- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
@@ -16,19 +16,14 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; +import com.google.gerrit.testutil.GerritBaseTests; import org.junit.Test; import java.util.HashMap; import java.util.Map; -public class PatchSetApprovalTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - +public class PatchSetApprovalTest extends GerritBaseTests { @Test public void keyEquality() { PatchSetApproval.Key k1 = new PatchSetApproval.Key(
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java index 0f8aba6..7a4531b 100644 --- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -82,7 +82,7 @@ } @Test - public void testToRefName() { + public void toRefName() { assertThat(new PatchSet.Id(new Change.Id(1), 23).toRefName()) .isEqualTo("refs/changes/01/1/23"); assertThat(new PatchSet.Id(new Change.Id(1234), 5).toRefName())
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java index 57cedd5..9bbfb33 100644 --- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/RefNamesTest.java
@@ -92,7 +92,7 @@ } @Test - public void testParseShardedRefsPart() throws Exception { + public void testparseShardedRefsPart() throws Exception { assertThat(parseShardedRefPart("01/1")).isEqualTo(1); assertThat(parseShardedRefPart("01/1-drafts")).isEqualTo(1); assertThat(parseShardedRefPart("01/1-drafts/2")).isEqualTo(1);
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK index 4fc578c..6fc7c4b 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', @@ -63,12 +64,15 @@ '//lib/jgit/org.eclipse.jgit:jgit', '//lib/jgit/org.eclipse.jgit.archive:jgit-archive', '//lib/joda:joda-time', + '//lib/jsoup:jsoup', '//lib/log:api', '//lib/log:jsonevent-layout', '//lib/log:log4j', '//lib/lucene:lucene-analyzers-common', '//lib/lucene:lucene-core-and-backward-codecs', '//lib/lucene:lucene-queryparser', + '//lib/mime4j:core', + '//lib/mime4j:dom', '//lib/ow2:ow2-asm', '//lib/ow2:ow2-asm-tree', '//lib/ow2:ow2-asm-util', @@ -90,6 +94,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 +185,7 @@ '//gerrit-server/src/main/prolog:common', '//lib/antlr:java_runtime', ], - source_under_test = [':server'], + visibility = ['PUBLIC'], ) java_test( @@ -203,11 +208,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..6ca423f 100644 --- a/gerrit-server/BUILD +++ b/gerrit-server/BUILD
@@ -1,208 +1,243 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") CONSTANTS_SRC = [ - 'src/main/java/com/google/gerrit/server/documentation/Constants.java', + "src/main/java/com/google/gerrit/server/documentation/Constants.java", ] SRCS = glob( - ['src/main/java/**/*.java'], - exclude = CONSTANTS_SRC, + ["src/main/java/**/*.java"], + exclude = CONSTANTS_SRC, ) -RESOURCES = glob(['src/main/resources/**/*']) + +RESOURCES = glob(["src/main/resources/**/*"]) java_library( - name = 'constants', - srcs = CONSTANTS_SRC, - visibility = ['//visibility:public'], + name = "constants", + srcs = CONSTANTS_SRC, + visibility = ["//visibility:public"], ) java_library( - name = 'server', - srcs = SRCS, - resources = RESOURCES, - deps = [ - ':constants', - '//gerrit-antlr:query_exception', - '//gerrit-antlr:query_parser', - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//gerrit-patch-commonsnet:commons-net', - '//gerrit-patch-jgit:server', - '//gerrit-prettify:server', - '//gerrit-reviewdb:server', - '//gerrit-util-cli:cli', - '//gerrit-util-ssl:ssl', - '//lib:args4j', - '//lib:automaton', - '//lib:blame-cache', - '//lib:grappa', - '//lib:gson', - '//lib:guava', - '//lib:guava-retrying', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:jsch', - '//lib:juniversalchardet', - '//lib:mime-util', - '//lib:pegdown', - '//lib:protobuf', - '//lib:servlet-api-3_1', - '//lib:tukaani-xz', - '//lib:velocity', - '//lib/antlr:java_runtime', - '//lib/auto:auto-value', - '//lib/commons:codec', - '//lib/commons:compress', - '//lib/commons:dbcp', - '//lib/commons:lang', - '//lib/commons:net', - '//lib/commons:validator', - '//lib/dropwizard:dropwizard-core', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.archive:jgit-archive', - '//lib/joda:joda-time', - '//lib/log:api', - '//lib/log:jsonevent-layout', - '//lib/log:log4j', - '//lib/lucene:lucene-analyzers-common', - '//lib/lucene:lucene-core-and-backward-codecs', - '//lib/lucene:lucene-queryparser', - '//lib/ow2:ow2-asm', - '//lib/ow2:ow2-asm-tree', - '//lib/ow2:ow2-asm-util', - '//lib/prolog:runtime', - ], - visibility = ['//visibility:public'], + name = "server", + srcs = SRCS, + resources = RESOURCES, + visibility = ["//visibility:public"], + deps = [ + ":constants", + "//gerrit-antlr:query_exception", + "//gerrit-antlr:query_parser", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-patch-commonsnet:commons-net", + "//gerrit-patch-jgit:server", + "//gerrit-prettify:server", + "//gerrit-reviewdb:server", + "//gerrit-util-cli:cli", + "//gerrit-util-ssl:ssl", + "//lib:args4j", + "//lib:automaton", + "//lib:blame-cache", + "//lib:grappa", + "//lib:gson", + "//lib:guava", + "//lib:guava-retrying", + "//lib:gwtjsonrpc", + "//lib:gwtorm", + "//lib:jsch", + "//lib:juniversalchardet", + "//lib:mime-util", + "//lib:pegdown", + "//lib:protobuf", + "//lib:servlet-api-3_1", + "//lib:soy", + "//lib:tukaani-xz", + "//lib:velocity", + "//lib/antlr:java_runtime", + "//lib/auto:auto-value", + "//lib/commons:codec", + "//lib/commons:compress", + "//lib/commons:dbcp", + "//lib/commons:lang", + "//lib/commons:net", + "//lib/commons:validator", + "//lib/dropwizard:dropwizard-core", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit.archive:jgit-archive", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/joda:joda-time", + "//lib/jsoup:jsoup", + "//lib/log:api", + "//lib/log:jsonevent-layout", + "//lib/log:log4j", + "//lib/lucene:lucene-analyzers-common", + "//lib/lucene:lucene-core-and-backward-codecs", + "//lib/lucene:lucene-queryparser", + "//lib/mime4j:core", + "//lib/mime4j:dom", + "//lib/ow2:ow2-asm", + "//lib/ow2:ow2-asm-tree", + "//lib/ow2:ow2-asm-util", + "//lib/prolog:runtime", + ], ) TESTUTIL_DEPS = [ - ':server', - '//gerrit-common:server', - '//gerrit-cache-h2:cache-h2', - '//gerrit-extension-api:api', - '//gerrit-gpg:gpg', - '//gerrit-lucene:lucene', - '//gerrit-reviewdb:server', - '//lib:gwtorm', - '//lib:h2', - '//lib:truth', - '//lib/guice:guice', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.junit:junit', - '//lib/joda:joda-time', - '//lib/log:api', - '//lib/log:impl_log4j', - '//lib/log:log4j', + ":server", + "//gerrit-common:server", + "//gerrit-cache-h2:cache-h2", + "//gerrit-extension-api:api", + "//gerrit-gpg:gpg", + "//gerrit-lucene:lucene", + "//gerrit-reviewdb:server", + "//lib:gwtorm", + "//lib:h2", + "//lib:truth", + "//lib/guice:guice", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/jgit/org.eclipse.jgit.junit:junit", + "//lib/joda:joda-time", + "//lib/log:api", + "//lib/log:impl_log4j", + "//lib/log:log4j", ] TESTUTIL = glob([ - 'src/test/java/com/google/gerrit/testutil/**/*.java', - 'src/test/java/com/google/gerrit/server/project/Util.java', + "src/test/java/com/google/gerrit/testutil/**/*.java", + "src/test/java/com/google/gerrit/server/project/Util.java", ]) java_library( - name = 'testutil', - srcs = TESTUTIL, - deps = TESTUTIL_DEPS + [ - '//lib/auto:auto-value', - '//lib/easymock:easymock', - '//lib/powermock:powermock-api-easymock', - '//lib/powermock:powermock-api-support', - '//lib/powermock:powermock-core', - '//lib/powermock:powermock-module-junit4', - '//lib/powermock:powermock-module-junit4-common', - ], - exports = [ - '//lib/easymock:easymock', - '//lib/powermock:powermock-api-easymock', - '//lib/powermock:powermock-api-support', - '//lib/powermock:powermock-core', - '//lib/powermock:powermock-module-junit4', - '//lib/powermock:powermock-module-junit4-common', - ], - visibility = ['//visibility:public'], + name = "testutil", + testonly = 1, + srcs = TESTUTIL, + visibility = ["//visibility:public"], + exports = [ + "//lib/easymock", + "//lib/powermock:powermock-api-easymock", + "//lib/powermock:powermock-api-support", + "//lib/powermock:powermock-core", + "//lib/powermock:powermock-module-junit4", + "//lib/powermock:powermock-module-junit4-common", + ], + deps = TESTUTIL_DEPS + [ + "//lib/auto:auto-value", + "//lib/easymock:easymock", + "//lib/powermock:powermock-api-easymock", + "//lib/powermock:powermock-api-support", + "//lib/powermock:powermock-core", + "//lib/powermock:powermock-module-junit4", + "//lib/powermock:powermock-module-junit4-common", + ], ) PROLOG_TEST_CASE = [ - 'src/test/java/com/google/gerrit/rules/PrologTestCase.java', + "src/test/java/com/google/gerrit/rules/PrologTestCase.java", ] + PROLOG_TESTS = glob( - ['src/test/java/com/google/gerrit/rules/**/*.java'], - exclude = PROLOG_TEST_CASE, + ["src/test/java/com/google/gerrit/rules/**/*.java"], + exclude = PROLOG_TEST_CASE, ) java_library( - name = 'prolog_test_case', - srcs = PROLOG_TEST_CASE, - deps = [ - ':server', - ':testutil', - '//gerrit-common:server', - '//gerrit-extension-api:api', - '//lib:guava', - '//lib:junit', - '//lib:truth', - '//lib/guice:guice', - '//lib/prolog:runtime', - ], + name = "prolog_test_case", + testonly = 1, + srcs = PROLOG_TEST_CASE, + deps = [ + ":server", + ":testutil", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//lib:guava", + "//lib:junit", + "//lib:truth", + "//lib/guice", + "//lib/prolog:runtime", + ], ) junit_tests( - name = 'prolog_tests', - srcs = PROLOG_TESTS, - resources = glob(['src/test/resources/com/google/gerrit/rules/**/*']), - deps = TESTUTIL_DEPS + [ - ':prolog_test_case', - ':testutil', - '//gerrit-server/src/main/prolog:common', - '//lib/prolog:runtime', - ], + name = "prolog_tests", + srcs = PROLOG_TESTS, + resources = glob(["src/test/resources/com/google/gerrit/rules/**/*"]), + deps = TESTUTIL_DEPS + [ + ":prolog_test_case", + ":testutil", + "//gerrit-server/src/main/prolog:common", + "//lib/prolog:runtime", + ], ) QUERY_TESTS = glob( - ['src/test/java/com/google/gerrit/server/query/**/*.java'], + ["src/test/java/com/google/gerrit/server/query/**/*.java"], +) + +java_library( + name = "query_tests_code", + testonly = 1, + srcs = QUERY_TESTS, + visibility = ["//visibility:public"], + 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", + ], ) junit_tests( - name = 'query_tests', - 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'], + name = "query_tests", + size = "medium", + srcs = QUERY_TESTS, + visibility = ["//visibility:public"], + 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", + ], ) junit_tests( - name = 'server_tests', - srcs = glob( - ['src/test/java/**/*.java'], - exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS - ), - deps = TESTUTIL_DEPS + [ - ':testutil', - '//gerrit-antlr:query_exception', - '//gerrit-common:annotations', - '//gerrit-patch-jgit:server', - '//gerrit-server/src/main/prolog:common', - '//lib:args4j', - '//lib:grappa', - '//lib:gson', - '//lib:guava', - '//lib:guava-retrying', - '//lib:protobuf', - '//lib/dropwizard:dropwizard-core', - '//lib/guice:guice-assistedinject', - '//lib/prolog:runtime', - ], - visibility = ['//visibility:public'], + name = "server_tests", + size = "medium", + srcs = glob( + ["src/test/java/**/*.java"], + exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS, + ), + resources = glob(["src/test/resources/com/google/gerrit/server/mail/*"]), + visibility = ["//visibility:public"], + deps = TESTUTIL_DEPS + [ + ":testutil", + "//gerrit-antlr:query_exception", + "//gerrit-common:annotations", + "//gerrit-patch-jgit:server", + "//gerrit-server/src/main/prolog:common", + "//lib:args4j", + "//lib:grappa", + "//lib:gson", + "//lib:guava", + "//lib:guava-retrying", + "//lib:protobuf", + "//lib/dropwizard:dropwizard-core", + "//lib/guice:guice-assistedinject", + "//lib/prolog:runtime", + ], +) + +load("//tools/bzl:javadoc.bzl", "java_doc") + +java_doc( + name = "doc", + libs = [":server"], + pkgs = ["com.google.gerrit"], + title = "Gerrit Review Server Documentation", )
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java index 14f12b8..5ca04c7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -17,15 +17,16 @@ import com.google.auto.value.AutoValue; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; -import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.server.CurrentUser; public class AuditEvent { public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000"; - protected static final Multimap<String, ?> EMPTY_PARAMS = HashMultimap.create(); + protected static final Multimap<String, ?> EMPTY_PARAMS = + MultimapBuilder.hashKeys().hashSetValues().build(); public final String sessionId; public final CurrentUser who;
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/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java index bc6f732..2cd4df6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server; -import com.google.common.base.Optional; import com.google.common.primitives.Ints; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.change.ChangeTriplet; @@ -30,6 +29,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; @Singleton public class ChangeFinder {
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..55c7e21 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,16 @@ 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.common.Nullable; +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 +31,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 +44,56 @@ */ @Singleton public class ChangeMessagesUtil { + public static final String TAG_ABANDON = + "autogenerated:gerrit:abandon"; + public static final String TAG_CHERRY_PICK_CHANGE = + "autogenerated:gerrit:cherryPickChange"; + public static final String TAG_DELETE_ASSIGNEE = + "autogenerated:gerrit:deleteAssignee"; + public static final String TAG_DELETE_REVIEWER = + "autogenerated:gerrit:deleteReviewer"; + public static final String TAG_DELETE_VOTE = + "autogenerated:gerrit:deleteVote"; + public static final String TAG_MERGED = + "autogenerated:gerrit:merged"; + public static final String TAG_MOVE = + "autogenerated:gerrit:move"; + public static final String TAG_RESTORE = + "autogenerated:gerrit:restore"; + public static final String TAG_REVERT = + "autogenerated:gerrit:revert"; + public static final String TAG_SET_ASSIGNEE = + "autogenerated:gerrit:setAssignee"; + public static final String TAG_SET_DESCRIPTION = + "autogenerated:gerrit:setPsDescription"; + public static final String TAG_SET_HASHTAGS = + "autogenerated:gerrit:setHashtag"; + public static final String TAG_SET_TOPIC = + "autogenerated:gerrit:setTopic"; + public static final String TAG_UPLOADED_PATCH_SET = + "autogenerated:gerrit:newPatchSet"; + + public static ChangeMessage newMessage( BatchUpdate.ChangeContext ctx, + String body, @Nullable String tag) throws OrmException { + return newMessage( + ctx.getDb(), ctx.getChange().currentPatchSetId(), + ctx.getUser(), ctx.getWhen(), body, tag); + } + + public static ChangeMessage newMessage( + ReviewDb db, PatchSet.Id psId, CurrentUser user, Timestamp when, + String body, @Nullable String tag) 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); + m.setTag(tag); + 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..5d160d9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -0,0 +1,479 @@ +// 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 com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; +import static java.util.stream.Collectors.toList; + +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.common.Nullable; +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.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.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.Optional; +import java.util.function.Predicate; +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.inReplyTo, b.inReplyTo, NULLS_FIRST) + .compare(a.message, b.message) + .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); + } + + public static String extractMessageId(@Nullable String tag) { + if (tag == null || !tag.startsWith("mailMessageId=")) { + return null; + } + return tag.substring("mailMessageId=".length()); + } + + 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, + false); + 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()) { + return Optional.ofNullable( + db.patchComments() + .get(PatchLineComment.Key.from(notes.getChangeId(), key))) + .map(plc -> plc.asComment(serverId)); + } + Predicate<Comment> p = c -> key.equals(c.key); + Optional<Comment> c = + publishedByChange(db, notes).stream().filter(p).findFirst(); + if (c.isPresent()) { + return c; + } + return draftByChange(db, notes).stream().filter(p).findFirst(); + } + + 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); + } + if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to delete non-existent entities. + 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/LibModuleLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/LibModuleLoader.java new file mode 100644 index 0000000..abea78f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/LibModuleLoader.java
@@ -0,0 +1,64 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import static java.util.stream.Collectors.toList; + +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.ProvisionException; + +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +/** Loads configured Guice modules from {@code gerrit.installModule}. */ +public class LibModuleLoader { + private static final Logger log = + LoggerFactory.getLogger(LibModuleLoader.class); + + public static List<Module> loadModules(Injector parent) { + Config cfg = getConfig(parent); + return Arrays.stream(cfg.getStringList("gerrit", null, "installModule")) + .map(m -> createModule(parent, m)) + .collect(toList()); + } + + private static Config getConfig(Injector i) { + return i.getInstance(Key.get(Config.class, GerritServerConfig.class)); + } + + private static Module createModule(Injector injector, String className) { + Module m = injector.getInstance(loadModule(className)); + log.info("Installed module {}", className); + return m; + } + + @SuppressWarnings("unchecked") + private static Class<Module> loadModule(String className) { + try { + return (Class<Module>) Class.forName(className); + } catch (ClassNotFoundException | LinkageError e) { + String msg = "Cannot load LibModule " + className; + log.error(msg, e); + throw new ProvisionException(msg, e); + } + } +}
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/PatchSetUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java index 0dcf3bf..ee40cd4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; import static com.google.gerrit.server.notedb.PatchSetState.DRAFT; import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED; @@ -27,6 +28,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.notedb.PatchSetState; import com.google.gwtorm.server.OrmException; @@ -89,7 +91,7 @@ public PatchSet insert(ReviewDb db, RevWalk rw, ChangeUpdate update, PatchSet.Id psId, ObjectId commit, boolean draft, - List<String> groups, String pushCertificate) + List<String> groups, String pushCertificate, String description) throws OrmException, IOException { checkNotNull(groups, "groups may not be null"); ensurePatchSetMatches(psId, update); @@ -101,9 +103,11 @@ ps.setDraft(draft); ps.setGroups(groups); ps.setPushCertificate(pushCertificate); + ps.setDescription(description); db.patchSets().insert(Collections.singleton(ps)); update.setCommit(rw, commit, pushCertificate); + update.setPsDescription(description); update.setGroups(groups); if (draft) { update.setPatchSetState(DRAFT); @@ -126,7 +130,10 @@ checkArgument(ps.isDraft(), "cannot delete non-draft patch set %s", ps.getId()); update.setPatchSetState(PatchSetState.DELETED); - db.patchSets().delete(Collections.singleton(ps)); + if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to delete non-existent entities. + db.patchSets().delete(Collections.singleton(ps)); + } } private void ensurePatchSetMatches(PatchSet.Id psId, ChangeUpdate update) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java new file mode 100644 index 0000000..91b568c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -0,0 +1,284 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.AccountDirectory.FillOptions; +import com.google.gerrit.server.account.AccountLoader; +import com.google.gerrit.server.change.ReviewerSuggestion; +import com.google.gerrit.server.change.SuggestReviewers; +import com.google.gerrit.server.change.SuggestedReviewer; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.project.ProjectControl; +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.ChangeQueryBuilder; +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 org.apache.commons.lang.mutable.MutableDouble; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ReviewerRecommender { + private static final Logger log = + LoggerFactory.getLogger(ReviewersUtil.class); + private static final double BASE_REVIEWER_WEIGHT = 10; + private static final double BASE_OWNER_WEIGHT = 1; + private static final double BASE_COMMENT_WEIGHT = 0.5; + private static final double[] WEIGHTS = new double[] { + BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,}; + private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms + + private final ChangeQueryBuilder changeQueryBuilder; + private final Config config; + private final DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap; + private final InternalChangeQuery internalChangeQuery; + private final WorkQueue workQueue; + private final Provider<ReviewDb> dbProvider; + private final ApprovalsUtil approvalsUtil; + + @Inject + ReviewerRecommender(ChangeQueryBuilder changeQueryBuilder, + DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap, + InternalChangeQuery internalChangeQuery, + WorkQueue workQueue, + Provider<ReviewDb> dbProvider, + ApprovalsUtil approvalsUtil, + @GerritServerConfig Config config) { + Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS); + fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); + this.changeQueryBuilder = changeQueryBuilder; + this.config = config; + this.internalChangeQuery = internalChangeQuery; + this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap; + this.workQueue = workQueue; + this.dbProvider = dbProvider; + this.approvalsUtil = approvalsUtil; + } + + public List<Account.Id> suggestReviewers( + ChangeNotes changeNotes, + SuggestReviewers suggestReviewers, ProjectControl projectControl, + List<Account.Id> candidateList) + throws OrmException { + String query = suggestReviewers.getQuery(); + double baseWeight = config.getInt("addReviewer", "baseWeight", 1); + + Map<Account.Id, MutableDouble> reviewerScores; + if (Strings.isNullOrEmpty(query)) { + reviewerScores = baseRankingForEmptyQuery(baseWeight); + } else { + reviewerScores = baseRankingForCandidateList( + candidateList, projectControl, baseWeight); + } + + // Send the query along with a candidate list to all plugins and merge the + // results. Plugins don't necessarily need to use the candidates list, they + // can also return non-candidate account ids. + List<Callable<Set<SuggestedReviewer>>> tasks = + new ArrayList<>(reviewerSuggestionPluginMap.plugins().size()); + List<Double> weights = + new ArrayList<>(reviewerSuggestionPluginMap.plugins().size()); + + for (DynamicMap.Entry<ReviewerSuggestion> plugin : + reviewerSuggestionPluginMap) { + tasks.add(() -> plugin.getProvider().get() + .suggestReviewers(projectControl.getProject().getNameKey(), + changeNotes.getChangeId(), query, reviewerScores.keySet())); + String pluginWeight = config.getString("addReviewer", + plugin.getPluginName() + "-" + plugin.getExportName(), "weight"); + if (Strings.isNullOrEmpty(pluginWeight)) { + pluginWeight = "1"; + } + try { + weights.add(Double.parseDouble(pluginWeight)); + } catch (NumberFormatException e) { + log.error("Exception while parsing weight for " + + plugin.getPluginName() + "-" + plugin.getExportName(), e); + weights.add(1d); + } + } + + try { + List<Future<Set<SuggestedReviewer>>> futures = workQueue + .getDefaultQueue() + .invokeAll(tasks, PLUGIN_QUERY_TIMEOUT, TimeUnit.MILLISECONDS); + Iterator<Double> weightIterator = weights.iterator(); + for (Future<Set<SuggestedReviewer>> f : futures) { + double weight = weightIterator.next(); + for (SuggestedReviewer s : f.get()) { + if (reviewerScores.containsKey(s.account)) { + reviewerScores.get(s.account).add(s.score * weight); + } else { + reviewerScores.put(s.account, new MutableDouble(s.score * weight)); + } + } + } + } catch (ExecutionException | InterruptedException e) { + log.error("Exception while suggesting reviewers", e); + return ImmutableList.of(); + } + + if (changeNotes != null) { + // Remove change owner + reviewerScores.remove(changeNotes.getChange().getOwner()); + + // Remove existing reviewers + reviewerScores.keySet().removeAll( + approvalsUtil.getReviewers(dbProvider.get(), changeNotes) + .byState(REVIEWER)); + } + + // Sort results + Stream<Entry<Account.Id, MutableDouble>> sorted = + reviewerScores.entrySet().stream() + .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())); + List<Account.Id> sortedSuggestions = sorted + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + return sortedSuggestions; + } + + private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery( + double baseWeight) throws OrmException{ + // Get the user's last 25 changes, check approvals + try { + List<ChangeData> result = internalChangeQuery + .setLimit(25) + .setRequestedFields(ImmutableSet.of(ChangeField.REVIEWER.getName())) + .query(changeQueryBuilder.owner("self")); + Map<Account.Id, MutableDouble> suggestions = new HashMap<>(); + for (ChangeData cd : result) { + for (PatchSetApproval approval : cd.currentApprovals()) { + Account.Id id = approval.getAccountId(); + if (suggestions.containsKey(id)) { + suggestions.get(id).add(baseWeight); + } else { + suggestions.put(id, new MutableDouble(baseWeight)); + } + } + } + return suggestions; + } catch (QueryParseException e) { + // Unhandled, because owner:self will never provoke a QueryParseException + log.error("Exception while suggesting reviewers", e); + return ImmutableMap.of(); + } + } + + private Map<Account.Id, MutableDouble> baseRankingForCandidateList( + List<Account.Id> candidates, + ProjectControl projectControl, + double baseWeight) throws OrmException { + // Get each reviewer's activity based on number of applied labels + // (weighted 10d), number of comments (weighted 0.5d) and number of owned + // changes (weighted 1d). + Map<Account.Id, MutableDouble> reviewers = new LinkedHashMap<>(); + if (candidates.size() == 0) { + return reviewers; + } + List<Predicate<ChangeData>> predicates = new ArrayList<>(); + for (Account.Id id : candidates) { + try { + Predicate<ChangeData> projectQuery = + changeQueryBuilder.project(projectControl.getProject().getName()); + + // Get all labels for this project and create a compound OR query to + // fetch all changes where users have applied one of these labels + List<LabelType> labelTypes = + projectControl.getLabelTypes().getLabelTypes(); + List<Predicate<ChangeData>> labelPredicates = + new ArrayList<>(labelTypes.size()); + for (LabelType type : labelTypes) { + labelPredicates + .add(changeQueryBuilder.label(type.getName() + ",user=" + id)); + } + Predicate<ChangeData> reviewerQuery = + Predicate.and(projectQuery, Predicate.or(labelPredicates)); + + Predicate<ChangeData> ownerQuery = Predicate.and(projectQuery, + changeQueryBuilder.owner(id.toString())); + Predicate<ChangeData> commentedByQuery = Predicate.and(projectQuery, + changeQueryBuilder.commentby(id.toString())); + + predicates.add(reviewerQuery); + predicates.add(ownerQuery); + predicates.add(commentedByQuery); + reviewers.put(id, new MutableDouble()); + } catch (QueryParseException e) { + // Unhandled: If an exception is thrown, we won't increase the + // candidates's score + log.error("Exception while suggesting reviewers", e); + } + } + + List<List<ChangeData>> result = internalChangeQuery + .setLimit(25) + .setRequestedFields(ImmutableSet.of()) + .query(predicates); + + Iterator<List<ChangeData>> queryResultIterator = result.iterator(); + Iterator<Account.Id> reviewersIterator = reviewers.keySet().iterator(); + + int i = 0; + Account.Id currentId = null; + while (queryResultIterator.hasNext()) { + List<ChangeData> currentResult = queryResultIterator.next(); + if (i % WEIGHTS.length == 0) { + currentId = reviewersIterator.next(); + } + + reviewers.get(currentId).add(WEIGHTS[i % WEIGHTS.length] * + baseWeight * currentResult.size()); + i++; + } + return reviewers; + } +}
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 f246f3e..03b5d2b 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,21 +14,21 @@ package com.google.gerrit.server; -import com.google.common.base.Function; -import com.google.common.base.MoreObjects; +import static java.util.stream.Collectors.toList; + import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.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; import com.google.gerrit.extensions.common.GroupBaseInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; -import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Url; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.Description.Units; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer0; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.Project; @@ -44,6 +44,7 @@ import com.google.gerrit.server.change.SuggestReviewers; import com.google.gerrit.server.index.account.AccountIndex; import com.google.gerrit.server.index.account.AccountIndexCollection; +import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.query.QueryParseException; @@ -53,170 +54,182 @@ import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Set; public class ReviewersUtil { + @Singleton + private static class Metrics { + final Timer0 queryAccountsLatency; + final Timer0 recommendAccountsLatency; + final Timer0 loadAccountsLatency; + final Timer0 queryGroupsLatency; + + @Inject + Metrics(MetricMaker metricMaker) { + queryAccountsLatency = metricMaker.newTimer( + "reviewer_suggestion/query_accounts", + new Description( + "Latency for querying accounts for reviewer suggestion") + .setCumulative() + .setUnit(Units.MILLISECONDS)); + recommendAccountsLatency = metricMaker.newTimer( + "reviewer_suggestion/recommend_accounts", + new Description( + "Latency for recommending accounts for reviewer suggestion") + .setCumulative() + .setUnit(Units.MILLISECONDS)); + loadAccountsLatency = metricMaker.newTimer( + "reviewer_suggestion/load_accounts", + new Description( + "Latency for loading accounts for reviewer suggestion") + .setCumulative() + .setUnit(Units.MILLISECONDS)); + queryGroupsLatency = metricMaker.newTimer( + "reviewer_suggestion/query_groups", + new Description( + "Latency for querying groups for reviewer suggestion") + .setCumulative() + .setUnit(Units.MILLISECONDS)); + } + } + 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); - } - }); - private final AccountLoader accountLoader; + // Generate a candidate list at 3x the size of what the user wants to see to + // give the ranking algorithm a good set of candidates it can work with + private static final int CANDIDATE_LIST_MULTIPLIER = 3; + private final AccountCache accountCache; - private final AccountIndexCollection indexes; - private final AccountQueryBuilder queryBuilder; - private final AccountQueryProcessor queryProcessor; private final AccountControl accountControl; - private final Provider<ReviewDb> dbProvider; + private final AccountIndexCollection accountIndexes; + private final AccountLoader accountLoader; + private final AccountQueryBuilder accountQueryBuilder; + private final AccountQueryProcessor accountQueryProcessor; private final GroupBackend groupBackend; private final GroupMembers.Factory groupMembersFactory; private final Provider<CurrentUser> currentUser; + private final Provider<ReviewDb> dbProvider; + private final ReviewerRecommender reviewerRecommender; + private final Metrics metrics; @Inject - ReviewersUtil(AccountLoader.Factory accountLoaderFactory, - AccountCache accountCache, - AccountIndexCollection indexes, - AccountQueryBuilder queryBuilder, - AccountQueryProcessor queryProcessor, + ReviewersUtil(AccountCache accountCache, AccountControl.Factory accountControlFactory, - Provider<ReviewDb> dbProvider, + AccountIndexCollection accountIndexes, + AccountLoader.Factory accountLoaderFactory, + AccountQueryBuilder accountQueryBuilder, + AccountQueryProcessor accountQueryProcessor, GroupBackend groupBackend, GroupMembers.Factory groupMembersFactory, - Provider<CurrentUser> currentUser) { + Provider<CurrentUser> currentUser, + Provider<ReviewDb> dbProvider, + ReviewerRecommender reviewerRecommender, + Metrics metrics) { Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS); fillOptions.addAll(AccountLoader.DETAILED_OPTIONS); - this.accountLoader = accountLoaderFactory.create(fillOptions); this.accountCache = accountCache; - this.indexes = indexes; - this.queryBuilder = queryBuilder; - this.queryProcessor = queryProcessor; this.accountControl = accountControlFactory.get(); + this.accountIndexes = accountIndexes; + this.accountLoader = accountLoaderFactory.create(fillOptions); + this.accountQueryBuilder = accountQueryBuilder; + this.accountQueryProcessor = accountQueryProcessor; + this.currentUser = currentUser; this.dbProvider = dbProvider; this.groupBackend = groupBackend; this.groupMembersFactory = groupMembersFactory; - this.currentUser = currentUser; + this.reviewerRecommender = reviewerRecommender; + this.metrics = metrics; } public interface VisibilityControl { boolean isVisibleTo(Account.Id account) throws OrmException; } - public List<SuggestedReviewerInfo> suggestReviewers( + public List<SuggestedReviewerInfo> suggestReviewers(ChangeNotes changeNotes, SuggestReviewers suggestReviewers, ProjectControl projectControl, - VisibilityControl visibilityControl) - throws IOException, OrmException, BadRequestException { + VisibilityControl visibilityControl, boolean excludeGroups) + throws IOException, OrmException { String query = suggestReviewers.getQuery(); - boolean suggestAccounts = suggestReviewers.getSuggestAccounts(); - int suggestFrom = suggestReviewers.getSuggestFrom(); int limit = suggestReviewers.getLimit(); - if (Strings.isNullOrEmpty(query)) { - throw new BadRequestException("missing query field"); - } - - if (!suggestAccounts || query.length() < suggestFrom) { + if (!suggestReviewers.getSuggestAccounts()) { return Collections.emptyList(); } - Collection<AccountInfo> suggestedAccounts = - suggestAccounts(suggestReviewers, visibilityControl); - - List<SuggestedReviewerInfo> reviewer = new ArrayList<>(); - for (AccountInfo a : suggestedAccounts) { - SuggestedReviewerInfo info = new SuggestedReviewerInfo(); - info.account = a; - info.count = 1; - reviewer.add(info); + List<Account.Id> candidateList = new ArrayList<>(); + if (!Strings.isNullOrEmpty(query)) { + candidateList = suggestAccounts(suggestReviewers, visibilityControl); } - 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); - } + List<Account.Id> sortedRecommendations = recommendAccounts(changeNotes, + suggestReviewers, projectControl, candidateList); + List<SuggestedReviewerInfo> suggestedReviewer = + loadAccounts(sortedRecommendations); + + if (!excludeGroups && suggestedReviewer.size() < limit + && !Strings.isNullOrEmpty(query)) { + // Add groups at the end as individual accounts are usually more + // important. + suggestedReviewer.addAll(suggestAccountGroups(suggestReviewers, + projectControl, visibilityControl, limit - suggestedReviewer.size())); } - reviewer = ORDERING.immutableSortedCopy(reviewer); - if (reviewer.size() <= limit) { - return reviewer; + if (suggestedReviewer.size() <= limit) { + return suggestedReviewer; } - return reviewer.subList(0, limit); + return suggestedReviewer.subList(0, limit); } - private Collection<AccountInfo> suggestAccounts(SuggestReviewers suggestReviewers, + private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) throws OrmException { - AccountIndex searchIndex = indexes.getSearchIndex(); - if (searchIndex != null) { - return suggestAccountsFromIndex(suggestReviewers, visibilityControl); + try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) { + AccountIndex searchIndex = accountIndexes.getSearchIndex(); + if (searchIndex != null) { + return suggestAccountsFromIndex(suggestReviewers, visibilityControl); + } + return suggestAccountsFromDb(suggestReviewers, visibilityControl); } - return suggestAccountsFromDb(suggestReviewers, visibilityControl); } - private Collection<AccountInfo> suggestAccountsFromIndex( + private List<Account.Id> suggestAccountsFromIndex( SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) - throws OrmException { + throws OrmException { try { - Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>(); - QueryResult<AccountState> result = queryProcessor - .setLimit(suggestReviewers.getLimit()) - .query(queryBuilder.defaultQuery(suggestReviewers.getQuery())); + Set<Account.Id> matches = new HashSet<>(); + QueryResult<AccountState> result = accountQueryProcessor + .setLimit(suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER) + .query(accountQueryBuilder.defaultQuery(suggestReviewers.getQuery())); for (AccountState accountState : result.entities()) { Account.Id id = accountState.getAccount().getId(); if (visibilityControl.isVisibleTo(id)) { - matches.put(id, accountLoader.get(id)); + matches.add(id); } } - - accountLoader.fill(); - - return matches.values(); + return new ArrayList<>(matches); } catch (QueryParseException e) { return ImmutableList.of(); } } - private Collection<AccountInfo> suggestAccountsFromDb( + private List<Account.Id> suggestAccountsFromDb( SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) throws OrmException { String query = suggestReviewers.getQuery(); - int limit = suggestReviewers.getLimit(); + int limit = suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER; String a = query; String b = a + MAX_SUFFIX; - Map<Account.Id, AccountInfo> r = new LinkedHashMap<>(); - Map<Account.Id, String> queryEmail = new HashMap<>(); + Set<Account.Id> r = new HashSet<>(); for (Account p : dbProvider.get().accounts() .suggestByFullName(a, b, limit)) { @@ -237,42 +250,89 @@ if (r.size() < limit) { for (AccountExternalId e : dbProvider.get().accountExternalIds() .suggestByEmailAddress(a, b, limit - r.size())) { - if (!r.containsKey(e.getAccountId())) { + if (!r.contains(e.getAccountId())) { Account p = accountCache.get(e.getAccountId()).getAccount(); if (p.isActive()) { - if (addSuggestion(r, p.getId(), visibilityControl)) { - queryEmail.put(e.getAccountId(), e.getEmailAddress()); - } + addSuggestion(r, p.getId(), visibilityControl); } } } } - - accountLoader.fill(); - for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) { - AccountInfo info = r.get(p.getKey()); - if (info != null) { - info.email = p.getValue(); - } - } - return new ArrayList<>(r.values()); + return new ArrayList<>(r); } - private boolean addSuggestion(Map<Account.Id, AccountInfo> map, + private boolean addSuggestion(Set<Account.Id> map, Account.Id account, VisibilityControl visibilityControl) throws OrmException { - if (!map.containsKey(account) + if (!map.contains(account) // Can the suggestion see the change? && visibilityControl.isVisibleTo(account) // Can the current user see the account? && accountControl.canSee(account)) { - map.put(account, accountLoader.get(account)); + map.add(account); return true; } return false; } - private List<GroupReference> suggestAccountGroup( + private List<Account.Id> recommendAccounts(ChangeNotes changeNotes, + SuggestReviewers suggestReviewers, ProjectControl projectControl, + List<Account.Id> candidateList) throws OrmException { + try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) { + return reviewerRecommender.suggestReviewers(changeNotes, suggestReviewers, + projectControl, candidateList); + } + } + + private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds) + throws OrmException { + try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) { + List<SuggestedReviewerInfo> reviewer = accountIds.stream() + .map(accountLoader::get) + .filter(Objects::nonNull) + .map(a -> { + SuggestedReviewerInfo info = new SuggestedReviewerInfo(); + info.account = a; + info.count = 1; + return info; + }).collect(toList()); + accountLoader.fill(); + return reviewer; + } + } + + private List<SuggestedReviewerInfo> suggestAccountGroups( + SuggestReviewers suggestReviewers, ProjectControl projectControl, + VisibilityControl visibilityControl, int limit) + throws OrmException, IOException { + try (Timer0.Context ctx = metrics.queryGroupsLatency.start()) { + List<SuggestedReviewerInfo> groups = new ArrayList<>(); + for (GroupReference g : suggestAccountGroups(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; + } + groups.add(suggestedReviewerInfo); + if (groups.size() >= limit) { + break; + } + } + } + return groups; + } + } + + private List<GroupReference> suggestAccountGroups( SuggestReviewers suggestReviewers, ProjectControl ctl) { return Lists.newArrayList( Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl), @@ -285,7 +345,8 @@ int size; } - private GroupAsReviewer suggestGroupAsReviewer(SuggestReviewers suggestReviewers, + private GroupAsReviewer suggestGroupAsReviewer( + SuggestReviewers suggestReviewers, Project project, GroupReference group, VisibilityControl visibilityControl) throws OrmException, IOException { GroupAsReviewer result = new GroupAsReviewer();
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..cc6f5db 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
@@ -14,20 +14,21 @@ package com.google.gerrit.server; +import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; -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.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; +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.Project; @@ -63,6 +64,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; @@ -101,6 +104,25 @@ } } + @AutoValue + public abstract static class StarRef { + private static final StarRef MISSING = + new AutoValue_StarredChangesUtil_StarRef(null, ImmutableSortedSet.of()); + + private static StarRef create(Ref ref, Iterable<String> labels) { + return new AutoValue_StarredChangesUtil_StarRef( + checkNotNull(ref), + ImmutableSortedSet.copyOf(labels)); + } + + @Nullable public abstract Ref ref(); + public abstract ImmutableSortedSet<String> labels(); + + public ObjectId objectId() { + return ref() != null ? ref().getObjectId() : ObjectId.zeroId(); + } + } + public static class IllegalLabelException extends IllegalArgumentException { private static final long serialVersionUID = 1L; @@ -155,8 +177,8 @@ public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId) throws OrmException { try (Repository repo = repoManager.openRepository(allUsers)) { - return ImmutableSortedSet.copyOf( - readLabels(repo, RefNames.refsStarredChanges(changeId, accountId))); + return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)) + .labels(); } catch (IOException e) { throw new OrmException( String.format("Reading stars from change %d for account %d failed", @@ -169,9 +191,9 @@ Set<String> labelsToRemove) throws OrmException { try (Repository repo = repoManager.openRepository(allUsers)) { String refName = RefNames.refsStarredChanges(changeId, accountId); - ObjectId oldObjectId = getObjectId(repo, refName); + StarRef old = readLabels(repo, refName); - SortedSet<String> labels = readLabels(repo, oldObjectId); + Set<String> labels = new HashSet<>(old.labels()); if (labelsToAdd != null) { labels.addAll(labelsToAdd); } @@ -180,10 +202,10 @@ } if (labels.isEmpty()) { - deleteRef(repo, refName, oldObjectId); + deleteRef(repo, refName, old.objectId()); } else { checkMutuallyExclusiveLabels(labels); - updateLabels(repo, refName, oldObjectId, labels); + updateLabels(repo, refName, old.objectId(), labels); } indexer.index(dbProvider.get(), project, changeId); @@ -224,11 +246,11 @@ } } - public ImmutableMultimap<Account.Id, String> byChange(Change.Id changeId) + public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) throws OrmException { try (Repository repo = repoManager.openRepository(allUsers)) { - ImmutableMultimap.Builder<Account.Id, String> builder = - new ImmutableMultimap.Builder<>(); + ImmutableMap.Builder<Account.Id, StarRef> builder = + ImmutableMap.builder(); for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) { Integer id = Ints.tryParse(refPart); @@ -236,7 +258,7 @@ continue; } Account.Id accountId = new Account.Id(id); - builder.putAll(accountId, + builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId))); } return builder.build(); @@ -249,29 +271,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 +287,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 +300,20 @@ } } + private boolean hasStar(Repository repo, Change.Id changeId, + Account.Id accountId, String label) { + try { + return readLabels(repo, + RefNames.refsStarredChanges(changeId, accountId)).labels() + .contains(label); + } catch (IOException e) { + log.error(String.format( + "Cannot query stars by account %d on change %d", + accountId.get(), changeId.get()), e); + return false; + } + } + public ImmutableMultimap<Account.Id, String> byChangeFromIndex( Change.Id changeId) throws OrmException, NoSuchChangeException { Set<String> fields = ImmutableSet.of( @@ -341,8 +335,8 @@ public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) { try (Repository repo = repoManager.openRepository(allUsers)) { - return getObjectId(repo, - RefNames.refsStarredChanges(changeId, accountId)); + Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId)); + return ref != null ? ref.getObjectId() : ObjectId.zeroId(); } catch (IOException e) { log.error(String.format( "Getting star object ID for account %d on change %d failed", @@ -351,39 +345,33 @@ } } - private static ObjectId getObjectId(Repository repo, String refName) + private static StarRef readLabels(Repository repo, String refName) throws IOException { Ref ref = repo.exactRef(refName); - return ref != null ? ref.getObjectId() : ObjectId.zeroId(); - } - - private static SortedSet<String> readLabels(Repository repo, String refName) - throws IOException { - return readLabels(repo, getObjectId(repo, refName)); - } - - private static TreeSet<String> readLabels(Repository repo, ObjectId id) - throws IOException { - if (ObjectId.zeroId().equals(id)) { - return new TreeSet<>(); + if (ref == null) { + return StarRef.MISSING; } try (ObjectReader reader = repo.newObjectReader()) { - ObjectLoader obj = reader.open(id, Constants.OBJ_BLOB); - TreeSet<String> labels = new TreeSet<>(); - Iterables.addAll(labels, + ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB); + return StarRef.create( + ref, Splitter.on(CharMatcher.whitespace()).omitEmptyStrings() .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8))); - return labels; } } - public static ObjectId writeLabels(Repository repo, SortedSet<String> labels) + public static ObjectId writeLabels(Repository repo, Collection<String> labels) throws IOException { validateLabels(labels); try (ObjectInserter oi = repo.newObjectInserter()) { - ObjectId id = oi.insert(Constants.OBJ_BLOB, - Joiner.on("\n").join(labels).getBytes(UTF_8)); + ObjectId id = oi.insert( + Constants.OBJ_BLOB, + labels.stream() + .sorted() + .distinct() + .collect(joining("\n")) + .getBytes(UTF_8)); oi.flush(); return id; } @@ -396,7 +384,7 @@ } } - private static void validateLabels(Set<String> labels) { + private static void validateLabels(Collection<String> labels) { if (labels == null) { return; } @@ -413,7 +401,7 @@ } private void updateLabels(Repository repo, String refName, - ObjectId oldObjectId, SortedSet<String> labels) + ObjectId oldObjectId, Collection<String> labels) throws IOException, OrmException { try (RevWalk rw = new RevWalk(repo)) { RefUpdate u = repo.updateRef(refName);
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 a8345e5..789d9a7 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
@@ -37,40 +37,36 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + @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; @@ -88,8 +84,7 @@ DynamicSet<FileHistoryWebLink> fileLogLinks, DynamicSet<DiffWebLink> diffLinks, DynamicSet<ProjectWebLink> projectLinks, - DynamicSet<BranchWebLink> branchLinks - ) { + DynamicSet<BranchWebLink> branchLinks) { this.patchSetLinks = patchSetLinks; this.parentLinks = parentLinks; this.fileLinks = fileLinks; @@ -105,31 +100,24 @@ * @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 List<WebLinkInfo> getPatchSetLinks(Project.NameKey project, + String commit) { + return filterLinks( + patchSetLinks, + webLink -> webLink.getPatchSetWebLink(project.get(), commit)); } /** + * * @param project Project name. * @param revision SHA1 of the parent revision. * @return Links for patch sets. */ - public FluentIterable<WebLinkInfo> getParentLinks(final Project.NameKey project, - final String revision) { - return filterLinks(parentLinks, new Function<WebLink, WebLinkInfo>() { - - @Override - public WebLinkInfo apply(WebLink webLink) { - return ((ParentWebLink)webLink).getParentWebLink(project.get(), revision); - } - }); + public List<WebLinkInfo> getParentLinks(Project.NameKey project, + String revision) { + return filterLinks( + parentLinks, + webLink -> webLink.getParentWebLink(project.get(), revision)); } /** @@ -139,15 +127,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 List<WebLinkInfo> getFileLinks(String project, + String revision, String file) { + return filterLinks( + fileLinks, + webLink -> webLink.getFileWebLink(project, revision, file)); } /** @@ -157,40 +141,26 @@ * @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<WebLinkInfoCommon> getFileHistoryLinksCommon( - final String project, final String revision, final String file) { + public List<WebLinkInfoCommon> getFileHistoryLinks( + 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; - } - }) - .filter(INVALID_WEBLINK_COMMON); + .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) + .toList(); } /** @@ -205,20 +175,17 @@ * @param fileB File name of side B. * @return Links for file diffs. */ - public FluentIterable<DiffWebLinkInfo> getDiffLinks(final String project, final int changeId, + public List<DiffWebLinkInfo> getDiffLinks(final String project, final int changeId, final Integer patchSetIdA, final String revisionA, final String fileA, 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); - } - }) - .filter(INVALID_WEBLINK); + patchSetIdB, revisionB, fileB)) + .filter(INVALID_WEBLINK) + .toList(); } /** @@ -226,14 +193,10 @@ * @param project Project name. * @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); - } - }); + public List<WebLinkInfo> getProjectLinks(final String project) { + return filterLinks( + projectLinks, + webLink -> webLink.getProjectWeblink(project)); } /** @@ -242,21 +205,18 @@ * @param branch Branch name * @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); - } - }); + public List<WebLinkInfo> getBranchLinks(final String project, final String 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> List<WebLinkInfo> filterLinks(DynamicSet<T> links, + Function<T, WebLinkInfo> transformer) { return FluentIterable .from(links) .transform(transformer) - .filter(INVALID_WEBLINK); + .filter(INVALID_WEBLINK) + .toList(); } }
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..8b4453f 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,11 +16,10 @@ 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; +import com.google.gerrit.server.mail.send.EmailSender; import com.google.inject.Inject; import java.util.Collection; @@ -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/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java index 149931d..846b44b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.account; -import com.google.common.base.Optional; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableSet; @@ -52,6 +51,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -252,17 +252,13 @@ if (accountIndexes.getSearchIndex() != null) { AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get()); - return accountState != null - ? Optional.of(accountState.getAccount().getId()) - : Optional.<Account.Id>absent(); + return Optional.ofNullable(accountState) + .map(s -> s.getAccount().getId()); } try (ReviewDb db = schema.open()) { - AccountExternalId id = db.accountExternalIds().get(key); - if (id != null) { - return Optional.of(id.getAccountId()); - } - return Optional.absent(); + return Optional.ofNullable(db.accountExternalIds().get(key)) + .map(AccountExternalId::getAccountId); } } }
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/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java index 63d2fc6..9ac69ba 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -52,10 +52,6 @@ @SuppressWarnings("serial") public static class DirectoryException extends Exception { - public DirectoryException(String message) { - super(message); - } - public DirectoryException(String message, Throwable why) { super(message, why); }
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/AccountLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java index f84d399..89e9419 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
@@ -94,7 +94,7 @@ directory.fillAccountInfo( Iterables.concat(created.values(), provided), options); } catch (DirectoryException e) { - Throwables.propagateIfPossible(e.getCause(), OrmException.class); + Throwables.throwIfInstanceOf(e.getCause(), OrmException.class); throw new OrmException(e); } }
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 7e403ad..3e5a046 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", @@ -352,7 +353,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 @@ -406,8 +407,8 @@ if (who.getEmailAddress() != null) { byEmailCache.evict(who.getEmailAddress()); - byIdCache.evict(to); } + byIdCache.evict(to); } return new AuthResult(to, key, false);
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/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java index 216672c..8cc392a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -29,7 +29,7 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AddSshKey.Input; -import com.google.gerrit.server.mail.AddKeySender; +import com.google.gerrit.server.mail.send.AddKeySender; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java index 0e8c051..45dbe60 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -15,13 +15,13 @@ package com.google.gerrit.server.account; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountSshKey; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; public class AuthorizedKeys { public static final String FILE_NAME = "authorized_keys"; @@ -47,7 +47,7 @@ key.setInvalid(); keys.add(Optional.of(key)); } else if (line.startsWith(DELETED_KEY_COMMENT)) { - keys.add(Optional.<AccountSshKey> absent()); + keys.add(Optional.empty()); seq++; } else if (line.startsWith("#")) { continue;
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/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java index 8d121c2..905d2f0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -38,7 +38,7 @@ import com.google.gerrit.server.api.accounts.AccountExternalIdCreator; import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.index.account.AccountIndexer; -import com.google.gerrit.server.mail.OutgoingEmailValidator; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtorm.server.OrmDuplicateKeyException; import com.google.gwtorm.server.OrmException;
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..ecee4b8 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,14 +26,12 @@ 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; import com.google.gerrit.server.config.AuthConfig; -import com.google.gerrit.server.mail.OutgoingEmailValidator; -import com.google.gerrit.server.mail.RegisterNewEmailSender; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.gerrit.server.mail.send.RegisterNewEmailSender; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -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/GeneralPreferencesLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java index 8339baf..24a0dae 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneralPreferencesLoader.java
@@ -16,6 +16,8 @@ import static com.google.gerrit.server.config.ConfigUtil.loadSection; import static com.google.gerrit.server.config.ConfigUtil.skipField; +import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE; +import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN; import static com.google.gerrit.server.git.UserConfigSections.KEY_ID; import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH; import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET; @@ -24,6 +26,7 @@ import static com.google.gerrit.server.git.UserConfigSections.URL_ALIAS; import com.google.common.base.Strings; +import com.google.common.collect.Lists; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.MenuItem; import com.google.gerrit.reviewdb.client.Account; @@ -91,7 +94,7 @@ loadSection(p.getConfig(), UserConfigSections.GENERAL, null, new GeneralPreferencesInfo(), updateDefaults(allUserPrefs), in); - + loadChangeTableColumns(r, p, dp); return loadMyMenusAndUrlAliases(r, p, dp); } } @@ -161,6 +164,21 @@ return !Strings.isNullOrEmpty(val) ? val : defaultValue; } + public GeneralPreferencesInfo loadChangeTableColumns(GeneralPreferencesInfo r, + VersionedAccountPreferences v, VersionedAccountPreferences d) { + r.changeTable = changeTable(v); + + if (r.changeTable.isEmpty() && !v.isDefaults()) { + r.changeTable = changeTable(d); + } + return r; + } + + private static List<String> changeTable(VersionedAccountPreferences v) { + return Lists.newArrayList(v.getConfig().getStringList( + CHANGE_TABLE, null, CHANGE_TABLE_COLUMN)); + } + private static Map<String, String> urlAliases(VersionedAccountPreferences v) { HashMap<String, String> urlAliases = new HashMap<>(); Config cfg = v.getConfig();
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/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java index 81c860e..e47ceb3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
@@ -47,7 +47,7 @@ directory.fillAccountInfo(Collections.singleton(info), EnumSet.allOf(FillOptions.class)); } catch (DirectoryException e) { - Throwables.propagateIfPossible(e.getCause(), OrmException.class); + Throwables.throwIfInstanceOf(e.getCause(), OrmException.class); throw new OrmException(e); } return info;
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/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java index c7a2241..3d966d2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.account; +import com.google.common.collect.ImmutableList; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.AccountGroup; @@ -31,8 +32,8 @@ @Nullable AccountGroup get(AccountGroup.UUID uuid); - /** @return sorted iteration of groups. */ - Iterable<AccountGroup> all(); + /** @return sorted list of groups. */ + ImmutableList<AccountGroup> all(); /** Notify the cache that a new group was constructed. */ void onCreateGroup(AccountGroup.NameKey newGroupName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java index e5e2f99..4f5cc2b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,9 +14,9 @@ package com.google.gerrit.server.account; -import com.google.common.base.Optional; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupName; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -33,8 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; /** Tracks group objects in memory for efficient access. */ @@ -130,7 +130,7 @@ return null; } try { - return byName.get(name.get()).orNull(); + return byName.get(name.get()).orElse(null); } catch (ExecutionException e) { log.warn(String.format("Cannot lookup group %s by name", name.get()), e); return null; @@ -143,7 +143,7 @@ return null; } try { - return byUUID.get(uuid.get()).orNull(); + return byUUID.get(uuid.get()).orElse(null); } catch (ExecutionException e) { log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e); return null; @@ -151,12 +151,12 @@ } @Override - public Iterable<AccountGroup> all() { + public ImmutableList<AccountGroup> all() { try (ReviewDb db = schema.open()) { - return Collections.unmodifiableList(db.accountGroups().all().toList()); + return ImmutableList.copyOf(db.accountGroups().all()); } catch (OrmException e) { log.warn("Cannot list internal groups", e); - return Collections.emptyList(); + return ImmutableList.of(); } } @@ -183,7 +183,7 @@ public Optional<AccountGroup> load(final AccountGroup.Id key) throws Exception { try (ReviewDb db = schema.open()) { - return Optional.fromNullable(db.accountGroups().get(key)); + return Optional.ofNullable(db.accountGroups().get(key)); } } } @@ -203,9 +203,9 @@ AccountGroup.NameKey key = new AccountGroup.NameKey(name); AccountGroupName r = db.accountGroupNames().get(key); if (r != null) { - return Optional.fromNullable(db.accountGroups().get(r.getId())); + return Optional.ofNullable(db.accountGroups().get(r.getId())); } - return Optional.absent(); + return Optional.empty(); } } } @@ -228,7 +228,7 @@ if (r.size() == 1) { return Optional.of(r.get(0)); } else if (r.size() == 0) { - return Optional.absent(); + return Optional.empty(); } else { throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java index 9971301..0d1fd20 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -16,18 +16,18 @@ import com.google.gerrit.reviewdb.client.AccountGroup; -import java.util.Set; +import java.util.Collection; /** Tracks group inclusions in memory for efficient access. */ public interface GroupIncludeCache { /** @return groups directly a member of the passed group. */ - Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group); + Collection<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group); /** @return any groups the passed group belongs to. */ - Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId); + Collection<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId); /** @return set of any UUIDs that are not internal groups. */ - Set<AccountGroup.UUID> allExternalMembers(); + Collection<AccountGroup.UUID> allExternalMembers(); void evictSubgroupsOf(AccountGroup.UUID groupId); void evictParentGroupsOf(AccountGroup.UUID groupId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java index 9bd6b30..02889bf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -16,11 +16,12 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableList; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupById; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.cache.CacheModule; +import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Module; @@ -31,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -52,17 +54,17 @@ protected void configure() { cache(PARENT_GROUPS_NAME, AccountGroup.UUID.class, - new TypeLiteral<Set<AccountGroup.UUID>>() {}) + new TypeLiteral<ImmutableList<AccountGroup.UUID>>() {}) .loader(ParentGroupsLoader.class); cache(SUBGROUPS_NAME, AccountGroup.UUID.class, - new TypeLiteral<Set<AccountGroup.UUID>>() {}) + new TypeLiteral<ImmutableList<AccountGroup.UUID>>() {}) .loader(SubgroupsLoader.class); cache(EXTERNAL_NAME, String.class, - new TypeLiteral<Set<AccountGroup.UUID>>() {}) + new TypeLiteral<ImmutableList<AccountGroup.UUID>>() {}) .loader(AllExternalLoader.class); bind(GroupIncludeCacheImpl.class); @@ -71,22 +73,31 @@ }; } - private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups; - private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups; - private final LoadingCache<String, Set<AccountGroup.UUID>> external; + private final + LoadingCache<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> + subgroups; + private final + LoadingCache<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> + parentGroups; + private final LoadingCache<String, ImmutableList<AccountGroup.UUID>> external; @Inject GroupIncludeCacheImpl( - @Named(SUBGROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups, - @Named(PARENT_GROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups, - @Named(EXTERNAL_NAME) LoadingCache<String, Set<AccountGroup.UUID>> external) { + @Named(SUBGROUPS_NAME) + LoadingCache<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> + subgroups, + @Named(PARENT_GROUPS_NAME) + LoadingCache<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> + parentGroups, + @Named(EXTERNAL_NAME) + LoadingCache<String, ImmutableList<AccountGroup.UUID>> external) { this.subgroups = subgroups; this.parentGroups = parentGroups; this.external = external; } @Override - public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID groupId) { + public Collection<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID groupId) { try { return subgroups.get(groupId); } catch (ExecutionException e) { @@ -96,7 +107,8 @@ } @Override - public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId) { + public Collection<AccountGroup.UUID> parentGroupsOf( + AccountGroup.UUID groupId) { try { return parentGroups.get(groupId); } catch (ExecutionException e) { @@ -124,17 +136,17 @@ } @Override - public Set<AccountGroup.UUID> allExternalMembers() { + public Collection<AccountGroup.UUID> allExternalMembers() { try { return external.get(EXTERNAL_NAME); } catch (ExecutionException e) { log.warn("Cannot load set of non-internal groups", e); - return Collections.emptySet(); + return ImmutableList.of(); } } static class SubgroupsLoader extends - CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> { + CacheLoader<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> { private final SchemaFactory<ReviewDb> schema; @Inject @@ -143,11 +155,12 @@ } @Override - public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception { + public ImmutableList<AccountGroup.UUID> load( + AccountGroup.UUID key) throws OrmException { try (ReviewDb db = schema.open()) { List<AccountGroup> group = db.accountGroups().byUUID(key).toList(); if (group.size() != 1) { - return Collections.emptySet(); + return ImmutableList.of(); } Set<AccountGroup.UUID> ids = new HashSet<>(); @@ -155,13 +168,13 @@ .byGroup(group.get(0).getId())) { ids.add(agi.getIncludeUUID()); } - return ImmutableSet.copyOf(ids); + return ImmutableList.copyOf(ids); } } } static class ParentGroupsLoader extends - CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> { + CacheLoader<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> { private final SchemaFactory<ReviewDb> schema; @Inject @@ -170,7 +183,8 @@ } @Override - public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception { + public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) + throws OrmException { try (ReviewDb db = schema.open()) { Set<AccountGroup.Id> ids = new HashSet<>(); for (AccountGroupById agi : db.accountGroupById() @@ -182,13 +196,13 @@ for (AccountGroup g : db.accountGroups().get(ids)) { groupArray.add(g.getGroupUUID()); } - return ImmutableSet.copyOf(groupArray); + return ImmutableList.copyOf(groupArray); } } } static class AllExternalLoader extends - CacheLoader<String, Set<AccountGroup.UUID>> { + CacheLoader<String, ImmutableList<AccountGroup.UUID>> { private final SchemaFactory<ReviewDb> schema; @Inject @@ -197,7 +211,7 @@ } @Override - public Set<AccountGroup.UUID> load(String key) throws Exception { + public ImmutableList<AccountGroup.UUID> load(String key) throws Exception { try (ReviewDb db = schema.open()) { Set<AccountGroup.UUID> ids = new HashSet<>(); for (AccountGroupById agi : db.accountGroupById().all()) { @@ -205,7 +219,7 @@ ids.add(agi.getIncludeUUID()); } } - return ImmutableSet.copyOf(ids); + return ImmutableList.copyOf(ids); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java index 3eaeebe..f38d071 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -112,7 +112,7 @@ return r; } - private boolean search(Set<AccountGroup.UUID> ids) { + private boolean search(Iterable<AccountGroup.UUID> ids) { return user.getEffectiveGroups().containsAnyOf(ids); }
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..2028654 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; @@ -34,14 +32,6 @@ /** 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 +67,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 groupCache.all().stream() + .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/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java index b70cabd..3714cee 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.account; import static com.google.gerrit.server.config.ConfigUtil.storeSection; +import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN; import static com.google.gerrit.server.git.UserConfigSections.KEY_ID; import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH; import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET; @@ -87,6 +88,7 @@ Account.Id id = rsrc.getUser().getAccountId(); GeneralPreferencesInfo n = loader.merge(id, i); + n.changeTable = i.changeTable; n.my = i.my; n.urlAliases = i.urlAliases; @@ -105,6 +107,7 @@ storeSection(prefs.getConfig(), UserConfigSections.GENERAL, null, i, GeneralPreferencesInfo.defaults()); + storeMyChangeTableColumns(prefs, i.changeTable); storeMyMenus(prefs, i.my); storeUrlAliases(prefs, i.urlAliases); prefs.commit(md); @@ -125,6 +128,16 @@ } } + public static void storeMyChangeTableColumns(VersionedAccountPreferences + prefs, List<String> changeTable) { + Config cfg = prefs.getConfig(); + if (changeTable != null) { + unsetSection(cfg, UserConfigSections.CHANGE_TABLE); + cfg.setStringList(UserConfigSections.CHANGE_TABLE, null, + CHANGE_TABLE_COLUMN, changeTable); + } + } + private static void set(Config cfg, String section, String key, String val) { if (Strings.isNullOrEmpty(val)) { cfg.unset(UserConfigSections.MY, section, key);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java index 3fccacce..77cee7a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -16,10 +16,10 @@ import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GroupDescription; @@ -139,7 +139,7 @@ @Override public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) { Multimap<GroupMembership, AccountGroup.UUID> lookups = - ArrayListMultimap.create(); + MultimapBuilder.hashKeys().arrayListValues().build(); for (AccountGroup.UUID uuid : uuids) { if (uuid == null) { continue; @@ -169,7 +169,7 @@ @Override public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> uuids) { Multimap<GroupMembership, AccountGroup.UUID> lookups = - ArrayListMultimap.create(); + MultimapBuilder.hashKeys().arrayListValues().build(); for (AccountGroup.UUID uuid : uuids) { if (uuid == null) { continue;
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..41ae498 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,11 +15,10 @@ package com.google.gerrit.server.account; import static com.google.common.base.Preconditions.checkState; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; -import com.google.common.base.Function; -import com.google.common.base.Optional; import com.google.common.base.Strings; -import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.gerrit.common.errors.InvalidSshKeyException; import com.google.gerrit.reviewdb.client.Account; @@ -46,6 +45,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; /** * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users @@ -192,7 +192,8 @@ /** Returns all SSH keys. */ private List<AccountSshKey> getKeys() { checkLoaded(); - return Lists.newArrayList(Optional.presentInstances(keys)); + return keys.stream().filter(Optional::isPresent).map(Optional::get) + .collect(toList()); } /** @@ -205,8 +206,7 @@ */ private AccountSshKey getKey(int seq) { checkLoaded(); - Optional<AccountSshKey> key = keys.get(seq - 1); - return key.orNull(); + return keys.get(seq - 1).orElse(null); } /** @@ -246,7 +246,7 @@ private boolean deleteKey(int seq) { checkLoaded(); if (seq <= keys.size() && keys.get(seq - 1).isPresent()) { - keys.set(seq - 1, Optional.<AccountSshKey> absent()); + keys.set(seq - 1, Optional.empty()); return true; } return false; @@ -278,15 +278,10 @@ * @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(); - } - }); - keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(), - Optional.<AccountSshKey> absent())); + Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get())); + keys = new ArrayList<>( + Collections.nCopies(o.max(newKeys).getKey().get(), + Optional.empty())); for (AccountSshKey key : newKeys) { keys.set(key.getKey().get() - 1, Optional.of(key)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java index a3cd0c9..c43e4da 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/WatchConfig.java
@@ -21,13 +21,12 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Enums; import com.google.common.base.Joiner; -import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.base.Strings; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; @@ -239,6 +238,11 @@ return projectWatches; } + public void setProjectWatches( + Map<ProjectWatchKey, Set<NotifyType>> projectWatches) { + this.projectWatches = projectWatches; + } + @Override protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { @@ -251,10 +255,11 @@ Config cfg = readConfig(WATCH_CONFIG); for (String projectName : cfg.getSubsections(PROJECT)) { - cfg.unset(PROJECT, projectName, KEY_NOTIFY); + cfg.unsetSection(PROJECT, projectName); } - Multimap<String, String> notifyValuesByProject = ArrayListMultimap.create(); + Multimap<String, String> notifyValuesByProject = + MultimapBuilder.hashKeys().arrayListValues().build(); for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches .entrySet()) { NotifyValue notifyValue = @@ -319,9 +324,9 @@ if (i + 1 < notifyValue.length() - 2) { for (String nt : Splitter.on(',').trimResults().splitToList( notifyValue.substring(i + 1, notifyValue.length() - 1))) { - Optional<NotifyType> notifyType = - Enums.getIfPresent(NotifyType.class, nt); - if (!notifyType.isPresent()) { + NotifyType notifyType = + Enums.getIfPresent(NotifyType.class, nt).orNull(); + if (notifyType == null) { validationErrorSink.error(new ValidationError(WATCH_CONFIG, String.format( "Invalid notify type %s in project watch " @@ -329,7 +334,7 @@ nt, accountId.get(), project, notifyValue))); continue; } - notifyTypes.add(notifyType.get()); + notifyTypes.add(notifyType); } } return create(filter, notifyTypes);
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 1bca929..5e010ae 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; @@ -49,6 +54,7 @@ import com.google.gerrit.server.account.GetWatchedProjects; import com.google.gerrit.server.account.Index; 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; @@ -100,6 +106,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; private final Index index; @Inject @@ -128,6 +137,9 @@ SshKeys sshKeys, GetAgreements getAgreements, PutAgreement putAgreement, + GetActive getActive, + PutActive putActive, + DeleteActive deleteActive, Index index, @Assisted AccountResource account) { this.account = account; @@ -156,6 +168,9 @@ this.gpgApiAdapter = gpgApiAdapter; this.getAgreements = getAgreements; this.putAgreement = putAgreement; + this.getActive = getActive; + this.putActive = putActive; + this.deleteActive = deleteActive; this.index = index; } @@ -173,6 +188,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..a265160 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,9 +29,11 @@ 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; +import com.google.gerrit.extensions.common.MergePatchSetInput; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.Response; @@ -40,8 +43,12 @@ 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.DeleteDraftChange; +import com.google.gerrit.server.change.CreateMergePatchSet; +import com.google.gerrit.server.change.DeleteAssignee; +import com.google.gerrit.server.change.DeleteChange; +import com.google.gerrit.server.change.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 +57,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; @@ -58,9 +66,11 @@ import com.google.gerrit.server.change.SubmittedTogether; import com.google.gerrit.server.change.SuggestChangeReviewers; import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.project.InvalidChangeOperationException; 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,16 +94,21 @@ private final Abandon abandon; private final Revert revert; private final Restore restore; - private final SubmittedTogether submittedTogether; + private final CreateMergePatchSet updateByMerge; + private final Provider<SubmittedTogether> submittedTogether; private final PublishDraftPatchSet.CurrentRevision publishDraftChange; - private final DeleteDraftChange deleteDraftChange; + private final DeleteChange deleteChange; private final GetTopic getTopic; private final PutTopic putTopic; private final PostReviewers postReviewers; 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,15 +126,20 @@ Abandon abandon, Revert revert, Restore restore, - SubmittedTogether submittedTogether, + CreateMergePatchSet updateByMerge, + Provider<SubmittedTogether> submittedTogether, PublishDraftPatchSet.CurrentRevision publishDraftChange, - DeleteDraftChange deleteDraftChange, + DeleteChange deleteChange, GetTopic getTopic, PutTopic putTopic, PostReviewers postReviewers, ChangeJson.Factory changeJson, PostHashtags postHashtags, GetHashtags getHashtags, + PutAssignee putAssignee, + GetAssignee getAssignee, + GetPastAssignees getPastAssignees, + DeleteAssignee deleteAssignee, ListChangeComments listComments, ListChangeDrafts listDrafts, Check check, @@ -136,15 +156,20 @@ this.suggestReviewers = suggestReviewers; this.abandon = abandon; this.restore = restore; + this.updateByMerge = updateByMerge; this.submittedTogether = submittedTogether; this.publishDraftChange = publishDraftChange; - this.deleteDraftChange = deleteDraftChange; + this.deleteChange = deleteChange; this.getTopic = getTopic; this.putTopic = putTopic; this.postReviewers = postReviewers; 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 +273,40 @@ } } - @SuppressWarnings("unchecked") + @Override + public ChangeInfo createMergePatchSet(MergePatchSetInput in) + throws RestApiException { + try { + return updateByMerge.apply(change, in).value(); + } catch (IOException | UpdateException | InvalidChangeOperationException + | NoSuchChangeException | OrmException e) { + throw new RestApiException("Cannot update change by merge", e); + } + } + @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); } @@ -280,7 +324,7 @@ @Override public void delete() throws RestApiException { try { - deleteDraftChange.apply(change, null); + deleteChange.apply(change, null); } catch (UpdateException e) { throw new RestApiException("Cannot delete change", e); } @@ -394,6 +438,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..a77d80b 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,32 @@ 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.GetDescription; +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.PutDescription; 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 +95,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 +107,21 @@ 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; + private final PutDescription putDescription; + private final GetDescription getDescription; @Inject RevisionApiImpl(GitRepositoryManager repoManager, @@ -113,6 +131,7 @@ Rebase rebase, RebaseUtil rebaseUtil, Submit submit, + PreviewSubmit submitPreview, PublishDraftPatchSet publish, Reviewed.PutReviewed putReviewed, Reviewed.DeleteReviewed deleteReviewed, @@ -123,15 +142,21 @@ 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, + PutDescription putDescription, + GetDescription getDescription, @Assisted RevisionResource r) { this.repoManager = repoManager; this.changes = changes; @@ -141,6 +166,7 @@ this.rebaseUtil = rebaseUtil; this.review = review; this.submit = submit; + this.submitPreview = submitPreview; this.publish = publish; this.files = files; this.putReviewed = putReviewed; @@ -150,15 +176,21 @@ 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.putDescription = putDescription; + this.getDescription = getDescription; this.revision = r; } @@ -187,6 +219,17 @@ } @Override + public BinaryResult submitPreview() throws RestApiException { + return submitPreview("zip"); + } + + @Override + public BinaryResult submitPreview(String format) throws RestApiException { + submitPreview.setFormat(format); + return submitPreview.apply(revision); + } + + @Override public void publish() throws RestApiException { try { publish.apply(revision, new PublishDraftPatchSet.Input()); @@ -264,7 +307,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 +336,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 +347,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 +358,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 +379,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 +406,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 +457,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); @@ -405,8 +476,21 @@ } @Override + public BinaryResult patch(String path) throws RestApiException { + try { + return getPatch.setPath(path).apply(revision); + } catch (IOException e) { + throw new RestApiException("Cannot get patch", e); + } + } + + @Override public Map<String, ActionInfo> actions() throws RestApiException { - return revisionActions.apply(revision).value(); + try { + return revisionActions.apply(revision).value(); + } catch (OrmException e) { + throw new RestApiException("Cannot get actions", e); + } } @Override @@ -427,4 +511,42 @@ 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); + } + } + }; + } + + @Override + public void description(String description) throws RestApiException { + PutDescription.Input in = new PutDescription.Input(); + in.description = description; + try { + putDescription.apply(revision, in); + } catch (UpdateException e) { + throw new RestApiException("Cannot set description", e); + } + } + + @Override + public String description() throws RestApiException { + return getDescription.apply(revision); + } + + @Override + public String etag() throws RestApiException { + return revisionActions.getETag(revision); + } }
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/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index b28258c..8f7f1b2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; +import com.google.gerrit.extensions.api.projects.DeleteTagsInput; import com.google.gerrit.extensions.api.projects.DescriptionInput; import com.google.gerrit.extensions.api.projects.ProjectApi; import com.google.gerrit.extensions.api.projects.ProjectInput; @@ -40,6 +41,7 @@ import com.google.gerrit.server.project.ChildProjectsCollection; import com.google.gerrit.server.project.CreateProject; import com.google.gerrit.server.project.DeleteBranches; +import com.google.gerrit.server.project.DeleteTags; import com.google.gerrit.server.project.GetAccess; import com.google.gerrit.server.project.GetConfig; import com.google.gerrit.server.project.GetDescription; @@ -87,6 +89,7 @@ private final ListBranches listBranches; private final ListTags listTags; private final DeleteBranches deleteBranches; + private final DeleteTags deleteTags; @AssistedInject ProjectApiImpl(CurrentUser user, @@ -107,11 +110,12 @@ ListBranches listBranches, ListTags listTags, DeleteBranches deleteBranches, + DeleteTags deleteTags, @Assisted ProjectResource project) { this(user, createProjectFactory, projectApi, projects, getDescription, putDescription, childApi, children, projectJson, branchApiFactory, tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches, - listTags, deleteBranches, project, null); + listTags, deleteBranches, deleteTags, project, null); } @AssistedInject @@ -133,11 +137,12 @@ ListBranches listBranches, ListTags listTags, DeleteBranches deleteBranches, + DeleteTags deleteTags, @Assisted String name) { this(user, createProjectFactory, projectApi, projects, getDescription, putDescription, childApi, children, projectJson, branchApiFactory, tagApiFactory, getAccess, setAccess, getConfig, putConfig, listBranches, - listTags, deleteBranches, null, name); + listTags, deleteBranches, deleteTags, null, name); } private ProjectApiImpl(CurrentUser user, @@ -158,6 +163,7 @@ ListBranches listBranches, ListTags listTags, DeleteBranches deleteBranches, + DeleteTags deleteTags, ProjectResource project, String name) { this.user = user; @@ -180,6 +186,7 @@ this.listBranches = listBranches; this.listTags = listTags; this.deleteBranches = deleteBranches; + this.deleteTags = deleteTags; } @Override @@ -345,6 +352,15 @@ } } + @Override + public void deleteTags(DeleteTagsInput in) throws RestApiException { + try { + deleteTags.apply(checkExists(), in); + } catch (OrmException | IOException e) { + throw new RestApiException("Cannot delete tags", e); + } + } + private ProjectResource checkExists() throws ResourceNotFoundException { if (project == null) { throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java index 3adfd00..aa2c402 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -20,8 +20,12 @@ import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.project.CreateTag; +import com.google.gerrit.server.project.DeleteTag; import com.google.gerrit.server.project.ListTags; import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.TagResource; +import com.google.gerrit.server.project.TagsCollection; +import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -34,16 +38,22 @@ private final ListTags listTags; private final CreateTag.Factory createTagFactory; + private final DeleteTag deleteTag; + private final TagsCollection tags; private final String ref; private final ProjectResource project; @Inject TagApiImpl(ListTags listTags, CreateTag.Factory createTagFactory, + DeleteTag deleteTag, + TagsCollection tags, @Assisted ProjectResource project, @Assisted String ref) { this.listTags = listTags; this.createTagFactory = createTagFactory; + this.deleteTag = deleteTag; + this.tags = tags; this.project = project; this.ref = ref; } @@ -66,4 +76,17 @@ throw new RestApiException(e.getMessage()); } } + + @Override + public void delete() throws RestApiException { + try { + deleteTag.apply(resource(), new DeleteTag.Input()); + } catch (OrmException | IOException e) { + throw new RestApiException(e.getMessage()); + } + } + + private TagResource resource() throws RestApiException, IOException { + return tags.parse(project, IdString.fromDecoded(ref)); + } }
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/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java index 3567811..354dc62 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -154,8 +154,8 @@ } }); } catch (PrivilegedActionException e) { - Throwables.propagateIfPossible(e.getException(), NamingException.class); - Throwables.propagateIfPossible(e.getException(), RuntimeException.class); + Throwables.throwIfInstanceOf(e.getException(), NamingException.class); + Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class); LdapRealm.log.warn("Internal error", e.getException()); return null; } finally {
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/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java index eaaafd6..217df2f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,7 +16,6 @@ import static java.util.concurrent.TimeUnit.HOURS; -import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.reviewdb.client.Account; @@ -27,6 +26,7 @@ import com.google.inject.Scopes; import com.google.inject.TypeLiteral; +import java.util.Optional; import java.util.Set; public class LdapModule extends CacheModule {
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..4af066f 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
@@ -16,20 +16,22 @@ import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.gerrit.common.data.GroupReference; 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; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.EmailExpander; +import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.auth.AuthenticationUnavailableException; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.GerritServerConfig; @@ -48,6 +50,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -67,8 +70,11 @@ 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 String mandatoryGroup; + private final LdapGroupBackend groupBackend; + private final Config config; private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache; @@ -78,12 +84,14 @@ Helper helper, AuthConfig authConfig, EmailExpander emailExpander, + LdapGroupBackend groupBackend, @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache, @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache, @GerritServerConfig final Config config) { this.helper = helper; this.authConfig = authConfig; this.emailExpander = emailExpander; + this.groupBackend = groupBackend; this.usernameCache = usernameCache; this.membershipCache = membershipCache; this.config = config; @@ -91,16 +99,17 @@ 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); + mandatoryGroup = optional(config, "mandatoryGroup"); } static SearchScope scope(final Config c, final String setting) { @@ -196,7 +205,7 @@ } @Override - public boolean allowsEdit(final Account.FieldName field) { + public boolean allowsEdit(final AccountFieldName field) { return !readOnlyAccountFields.contains(field); } @@ -262,8 +271,23 @@ // in the middle of authenticating the user, its likely we will // need to know what access rights they have soon. // - if (fetchMemberOfEagerly) { - membershipCache.put(username, helper.queryForGroups(ctx, username, m)); + if (fetchMemberOfEagerly || mandatoryGroup != null) { + Set<AccountGroup.UUID> groups = helper.queryForGroups(ctx, username, m); + if (mandatoryGroup != null) { + GroupReference mandatoryGroupRef = + GroupBackends.findExactSuggestion(groupBackend, mandatoryGroup); + if (mandatoryGroupRef == null) { + throw new AccountException("Could not identify mandatory group: " + + mandatoryGroup); + } + if (!groups.contains(mandatoryGroupRef.getUUID())) { + throw new AccountException("Not member of mandatory LDAP group: " + + mandatoryGroupRef.getName()); + } + } + // Regardless if we enabled fetchMemberOfEagerly, we already have the + // groups and it would be a waste not to cache them. + membershipCache.put(username, groups); } return who; } finally { @@ -294,7 +318,7 @@ } try { Optional<Account.Id> id = usernameCache.get(accountName); - return id != null ? id.orNull() : null; + return id != null ? id.orElse(null) : null; } catch (ExecutionException e) { log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e); return null; @@ -312,13 +336,10 @@ @Override public Optional<Account.Id> load(String username) throws Exception { try (ReviewDb db = schema.open()) { - final AccountExternalId extId = - db.accountExternalIds().get( - new AccountExternalId.Key(SCHEME_GERRIT, username)); - if (extId != null) { - return Optional.of(extId.getAccountId()); - } - return Optional.absent(); + return Optional.ofNullable( + db.accountExternalIds().get( + new AccountExternalId.Key(SCHEME_GERRIT, username))) + .map(AccountExternalId::getAccountId); } } }
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 c07b4c8..6b92289 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, @@ -46,17 +46,17 @@ this.editableAccountFields = new HashSet<>(); // User name should be always editable, because not all OAuth providers // expose them - editableAccountFields.add(FieldName.USER_NAME); + editableAccountFields.add(AccountFieldName.USER_NAME); 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); } @@ -106,12 +106,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..1641a03 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
@@ -14,10 +14,12 @@ package com.google.gerrit.server.change; -import com.google.common.base.Strings; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimap; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.AbandonInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -26,21 +28,12 @@ import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.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; +import com.google.gerrit.server.git.AbandonOp; 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.mail.AbandonedSender; -import com.google.gerrit.server.mail.ReplyToChangeSender; -import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -50,34 +43,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; + @Singleton public class Abandon implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> { private static final Logger log = LoggerFactory.getLogger(Abandon.class); - private final AbandonedSender.Factory abandonedSenderFactory; private final Provider<ReviewDb> dbProvider; private final ChangeJson.Factory json; - private final ChangeMessagesUtil cmUtil; - private final PatchSetUtil psUtil; private final BatchUpdate.Factory batchUpdateFactory; - private final ChangeAbandoned changeAbandoned; + private final AbandonOp.Factory abandonOpFactory; + private final NotifyUtil notifyUtil; @Inject - Abandon(AbandonedSender.Factory abandonedSenderFactory, + Abandon( Provider<ReviewDb> dbProvider, ChangeJson.Factory json, - ChangeMessagesUtil cmUtil, - PatchSetUtil psUtil, BatchUpdate.Factory batchUpdateFactory, - ChangeAbandoned changeAbandoned) { - this.abandonedSenderFactory = abandonedSenderFactory; + AbandonOp.Factory abandonOpFactory, + NotifyUtil notifyUtil) { this.dbProvider = dbProvider; this.json = json; - this.cmUtil = cmUtil; - this.psUtil = psUtil; this.batchUpdateFactory = batchUpdateFactory; - this.changeAbandoned = changeAbandoned; + this.abandonOpFactory = abandonOpFactory; + this.notifyUtil = notifyUtil; } @Override @@ -87,104 +77,95 @@ if (!control.canAbandon(dbProvider.get())) { throw new AuthException("abandon not permitted"); } - Change change = abandon(control, input.message, input.notify); + Change change = abandon(control, input.message, input.notify, + notifyUtil.resolveAccounts(input.notifyDetails)); return json.create(ChangeJson.NO_OPTIONS).format(change); } + public Change abandon(ChangeControl control) + throws RestApiException, UpdateException { + return abandon(control, "", NotifyHandling.ALL, ImmutableListMultimap.of()); + } + public Change abandon(ChangeControl control, String msgTxt) throws RestApiException, UpdateException { - return abandon(control, msgTxt, NotifyHandling.ALL); + return abandon(control, msgTxt, NotifyHandling.ALL, + ImmutableListMultimap.of()); } public Change abandon(ChangeControl control, String msgTxt, - NotifyHandling notifyHandling) throws RestApiException, UpdateException { + NotifyHandling notifyHandling, + Multimap<RecipientType, Account.Id> accountsToNotify) + 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())) { + AbandonOp op = abandonOpFactory.create(account, msgTxt, notifyHandling, + accountsToNotify); + try (BatchUpdate u = + batchUpdateFactory.create( + dbProvider.get(), + control.getProject().getNameKey(), + control.getUser(), + TimeUtil.nowTs())) { u.addOp(control.getId(), op).execute(); } - return op.change; + return op.getChange(); } - private class Op extends BatchUpdate.Op { - private final Account account; - private final String msgTxt; - - private Change change; - private PatchSet patchSet; - private ChangeMessage message; - private NotifyHandling notifyHandling; - - private Op(String msgTxt, Account account, NotifyHandling notifyHandling) { - this.account = account; - this.msgTxt = msgTxt; - this.notifyHandling = notifyHandling; + /** + * 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, + Multimap<RecipientType, Account.Id> accountsToNotify) + throws RestApiException, UpdateException { + if (controls.isEmpty()) { + return; } - - @Override - public boolean updateChange(ChangeContext ctx) throws OrmException, - ResourceConflictException { - change = ctx.getChange(); - PatchSet.Id psId = change.currentPatchSetId(); - ChangeUpdate update = ctx.getUpdate(psId); - if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + status(change)); - } else if (change.getStatus() == Change.Status.DRAFT) { - throw new ResourceConflictException( - "draft changes cannot be abandoned"); - } - patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); - change.setStatus(Change.Status.ABANDONED); - change.setLastUpdatedOn(ctx.getWhen()); - - update.setStatus(change.getStatus()); - message = newMessage(ctx); - cmUtil.addChangeMessage(ctx.getDb(), update, message); - return true; - } - - private ChangeMessage newMessage(ChangeContext ctx) throws OrmException { - StringBuilder msg = new StringBuilder(); - msg.append("Abandoned"); - if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) { - msg.append("\n\n"); - 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; - } - - @Override - public void postUpdate(Context ctx) throws OrmException { - try { - ReplyToChangeSender cm = - abandonedSenderFactory.create(ctx.getProject(), change.getId()); - if (account != null) { - cm.setFrom(account.getId()); + Account account = user.isIdentifiedUser() + ? user.asIdentifiedUser().getAccount() + : null; + 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())); } - cm.setChangeMessage(message.getMessage(), ctx.getWhen()); - cm.setNotify(notifyHandling); - cm.send(); - } catch (Exception e) { - log.error("Cannot email update for change " + change.getId(), e); + u.addOp( + control.getId(), + abandonOpFactory.create(account, msgTxt, notifyHandling, + accountsToNotify)); } - changeAbandoned.fire(change, patchSet, account, msgTxt, ctx.getWhen(), - 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, + ImmutableListMultimap.of()); + } + + public void batchAbandon(Project.NameKey project, CurrentUser user, + Collection<ChangeControl> controls) + throws RestApiException, UpdateException { + batchAbandon(project, user, controls, "", NotifyHandling.ALL, + ImmutableListMultimap.of()); + } + @Override public UiAction.Description getDescription(ChangeResource resource) { boolean canAbandon = false; @@ -200,8 +181,4 @@ && resource.getChange().getStatus() != Change.Status.DRAFT && canAbandon); } - - private static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/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/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java index 4992c8e..4c405d9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -14,69 +14,164 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.ActionVisitor; +import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ActionInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.project.ChangeControl; +import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.util.Providers; +import java.util.ArrayList; +import java.util.EnumSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; @Singleton public class ActionJson { private final Revisions revisions; + private final ChangeJson.Factory changeJsonFactory; private final ChangeResource.Factory changeResourceFactory; private final DynamicMap<RestView<ChangeResource>> changeViews; + private final DynamicSet<ActionVisitor> visitorSet; @Inject ActionJson( Revisions revisions, + ChangeJson.Factory changeJsonFactory, ChangeResource.Factory changeResourceFactory, - DynamicMap<RestView<ChangeResource>> changeViews) { + DynamicMap<RestView<ChangeResource>> changeViews, + DynamicSet<ActionVisitor> visitorSet) { this.revisions = revisions; + this.changeJsonFactory = changeJsonFactory; this.changeResourceFactory = changeResourceFactory; this.changeViews = changeViews; + this.visitorSet = visitorSet; } - public Map<String, ActionInfo> format(RevisionResource rsrc) { - return toActionMap(rsrc); + public Map<String, ActionInfo> format(RevisionResource rsrc) + throws OrmException { + ChangeInfo changeInfo = null; + RevisionInfo revisionInfo = null; + List<ActionVisitor> visitors = visitors(); + if (!visitors.isEmpty()) { + changeInfo = changeJson().format(rsrc); + revisionInfo = + checkNotNull(Iterables.getOnlyElement(changeInfo.revisions.values())); + changeInfo.revisions = null; + } + return toActionMap(rsrc, visitors, changeInfo, revisionInfo); + } + + private ChangeJson changeJson() { + return changeJsonFactory.create(EnumSet.noneOf(ListChangesOption.class)); + } + + private ArrayList<ActionVisitor> visitors() { + return Lists.newArrayList(visitorSet); } public ChangeInfo addChangeActions(ChangeInfo to, ChangeControl ctl) { - to.actions = toActionMap(ctl); + List<ActionVisitor> visitors = visitors(); + to.actions = toActionMap(ctl, visitors, copy(visitors, to)); return to; } - public RevisionInfo addRevisionActions(RevisionInfo to, - RevisionResource rsrc) { - to.actions = toActionMap(rsrc); + public RevisionInfo addRevisionActions(@Nullable ChangeInfo changeInfo, + RevisionInfo to, RevisionResource rsrc) throws OrmException { + List<ActionVisitor> visitors = visitors(); + if (!visitors.isEmpty()) { + if (changeInfo != null) { + changeInfo = copy(visitors, changeInfo); + } else { + changeInfo = changeJson().format(rsrc); + } + } + to.actions = toActionMap(rsrc, visitors, changeInfo, copy(visitors, to)); return to; } - private Map<String, ActionInfo> toActionMap(ChangeControl ctl) { + private ChangeInfo copy(List<ActionVisitor> visitors, ChangeInfo changeInfo) { + if (visitors.isEmpty()) { + return null; + } + // Include all fields from ChangeJson#toChangeInfo that are not protected by + // any ListChangesOptions. + ChangeInfo copy = new ChangeInfo(); + copy.project = changeInfo.project; + copy.branch = changeInfo.branch; + copy.topic = changeInfo.topic; + copy.assignee = changeInfo.assignee; + copy.hashtags = changeInfo.hashtags; + copy.changeId = changeInfo.changeId; + copy.submitType = changeInfo.submitType; + copy.mergeable = changeInfo.mergeable; + copy.insertions = changeInfo.insertions; + copy.deletions = changeInfo.deletions; + copy.subject = changeInfo.subject; + copy.status = changeInfo.status; + copy.owner = changeInfo.owner; + copy.created = changeInfo.created; + copy.updated = changeInfo.updated; + copy._number = changeInfo._number; + copy.starred = changeInfo.starred; + copy.stars = changeInfo.stars; + copy.submitted = changeInfo.submitted; + copy.id = changeInfo.id; + return copy; + } + + private RevisionInfo copy(List<ActionVisitor> visitors, + RevisionInfo revisionInfo) { + if (visitors.isEmpty()) { + return null; + } + // Include all fields from ChangeJson#toRevisionInfo that are not protected + // by any ListChangesOptions. + RevisionInfo copy = new RevisionInfo(); + copy.isCurrent = revisionInfo.isCurrent; + copy._number = revisionInfo._number; + copy.ref = revisionInfo.ref; + copy.created = revisionInfo.created; + copy.uploader = revisionInfo.uploader; + copy.draft = revisionInfo.draft; + copy.fetch = revisionInfo.fetch; + copy.kind = revisionInfo.kind; + copy.description = revisionInfo.description; + return copy; + } + + private Map<String, ActionInfo> toActionMap( + ChangeControl ctl, List<ActionVisitor> visitors, ChangeInfo changeInfo) { Map<String, ActionInfo> out = new LinkedHashMap<>(); if (!ctl.getUser().isIdentifiedUser()) { return out; } Provider<CurrentUser> userProvider = Providers.of(ctl.getUser()); - for (UiAction.Description d : UiActions.from( + FluentIterable<UiAction.Description> descs = UiActions.from( changeViews, changeResourceFactory.create(ctl), - userProvider)) { - out.put(d.getId(), new ActionInfo(d)); - } - + userProvider); // The followup action is a client-side only operation that does not // have a server side handler. It must be manually registered into the // resulting action map. @@ -86,20 +181,39 @@ PrivateInternals_UiActionDescription.setMethod(descr, "POST"); descr.setTitle("Create follow-up change"); descr.setLabel("Follow-Up"); - out.put(descr.getId(), new ActionInfo(descr)); + descs = descs.append(descr); + } + + ACTION: for (UiAction.Description d : descs) { + ActionInfo actionInfo = new ActionInfo(d); + for (ActionVisitor visitor : visitors) { + if (!visitor.visit(d.getId(), actionInfo, changeInfo)) { + continue ACTION; + } + } + out.put(d.getId(), actionInfo); } return out; } - private Map<String, ActionInfo> toActionMap(RevisionResource rsrc) { + private Map<String, ActionInfo> toActionMap(RevisionResource rsrc, + List<ActionVisitor> visitors, ChangeInfo changeInfo, + RevisionInfo revisionInfo) { + if (!rsrc.getControl().getUser().isIdentifiedUser()) { + return ImmutableMap.of(); + } Map<String, ActionInfo> out = new LinkedHashMap<>(); - if (rsrc.getControl().getUser().isIdentifiedUser()) { - Provider<CurrentUser> userProvider = Providers.of( - rsrc.getControl().getUser()); - for (UiAction.Description d : UiActions.from( - revisions, rsrc, userProvider)) { - out.put(d.getId(), new ActionInfo(d)); + Provider<CurrentUser> userProvider = Providers.of( + rsrc.getControl().getUser()); + ACTION: for (UiAction.Description d : UiActions.from( + revisions, rsrc, userProvider)) { + ActionInfo actionInfo = new ActionInfo(d); + for (ActionVisitor visitor : visitors) { + if (!visitor.visit(d.getId(), actionInfo, changeInfo, revisionInfo)) { + continue ACTION; + } } + out.put(d.getId(), actionInfo); } return out; }
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..9c517f0 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,12 +14,20 @@ package com.google.gerrit.server.change; +import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.eclipse.jgit.api.ArchiveCommand; +import org.eclipse.jgit.api.ArchiveCommand.Format; import org.eclipse.jgit.archive.TarFormat; import org.eclipse.jgit.archive.Tbz2Format; import org.eclipse.jgit.archive.TgzFormat; import org.eclipse.jgit.archive.TxzFormat; import org.eclipse.jgit.archive.ZipFormat; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectLoader; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; public enum ArchiveFormat { TGZ("application/x-gzip", new TgzFormat()), @@ -52,4 +60,17 @@ Iterable<String> getSuffixes() { return format.suffixes(); } -} + + public ArchiveOutputStream createArchiveOutputStream(OutputStream o) + throws IOException { + return (ArchiveOutputStream)this.format.createArchiveOutputStream(o); + } + + public <T extends Closeable> void putEntry(T out, String path, byte[] data) + throws IOException { + @SuppressWarnings("unchecked") + ArchiveCommand.Format<T> fmt = (Format<T>) format; + fmt.putEntry(out, path, FileMode.REGULAR_FILE, + new ObjectLoader.SmallObject(FileMode.TYPE_FILE, data)); + } +} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java index 878cc81..330ff7b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -14,9 +14,7 @@ package com.google.gerrit.server.change; -import com.google.common.base.Optional; import com.google.common.base.Strings; -import com.google.common.collect.FluentIterable; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.common.DiffWebLinkInfo; import com.google.gerrit.extensions.common.EditInfo; @@ -65,6 +63,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; @Singleton public class ChangeEdits implements @@ -490,7 +489,7 @@ FileInfo r = new FileInfo(); ChangeEdit edit = rsrc.getChangeEdit(); Change change = edit.getChange(); - FluentIterable<DiffWebLinkInfo> links = + List<DiffWebLinkInfo> links = webLinks.getDiffLinks(change.getProject().get(), change.getChangeId(), edit.getBasePatchSet().getPatchSetId(), @@ -499,7 +498,7 @@ 0, edit.getRefName(), rsrc.getPath()); - r.webLinks = links.isEmpty() ? null : links.toList(); + r.webLinks = links.isEmpty() ? null : links; return r; }
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..28d28ed 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,12 +17,16 @@ 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.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimap; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; @@ -33,12 +37,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; @@ -47,7 +50,8 @@ import com.google.gerrit.server.git.SendEmailExecutor; 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.mail.send.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 +66,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 +89,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; @@ -106,10 +110,13 @@ private Change.Status status; private String topic; private String message; + private String patchSetDescription; private List<String> groups = Collections.emptyList(); private CommitValidators.Policy validatePolicy = CommitValidators.Policy.GERRIT; private NotifyHandling notify = NotifyHandling.ALL; + private Multimap<RecipientType, Account.Id> accountsToNotify = + ImmutableListMultimap.of(); private Set<Account.Id> reviewers; private Set<Account.Id> extraCC; private Map<String, Short> approvals; @@ -128,6 +135,7 @@ @Inject ChangeInserter(ProjectControl.GenericFactory projectControlFactory, + IdentifiedUser.GenericFactory userFactory, ChangeControl.GenericFactory changeControlFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, @@ -142,6 +150,7 @@ @Assisted RevCommit commit, @Assisted String refName) { this.projectControlFactory = projectControlFactory; + this.userFactory = userFactory; this.changeControlFactory = changeControlFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; @@ -216,6 +225,11 @@ return this; } + public ChangeInserter setPatchSetDescription(String patchSetDescription) { + this.patchSetDescription = patchSetDescription; + return this; + } + public ChangeInserter setValidatePolicy(CommitValidators.Policy validate) { this.validatePolicy = checkNotNull(validate); return this; @@ -226,6 +240,12 @@ return this; } + public ChangeInserter setAccountsToNotify( + Multimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); + return this; + } + public ChangeInserter setReviewers(Set<Account.Id> reviewers) { this.reviewers = reviewers; return this; @@ -333,6 +353,7 @@ update.setSubjectForCommit("Create change"); update.setBranch(change.getDest().get()); update.setTopic(change.getTopic()); + update.setPsDescription(patchSetDescription); boolean draft = status == Change.Status.DRAFT; List<String> newGroups = groups; @@ -340,7 +361,7 @@ newGroups = GroupCollector.getDefaultGroups(commit); } patchSet = psUtil.insert(ctx.getDb(), ctx.getRevWalk(), update, psId, - commit, draft, newGroups, pushCert); + commit, draft, newGroups, pushCert, patchSetDescription); /* TODO: fixStatus is used here because the tests * (byStatusClosed() in AbstractQueryChangesTest) @@ -353,24 +374,46 @@ 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, ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); 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) { + if (sendMail + && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) { Runnable sender = new Runnable() { @Override public void run() { @@ -380,6 +423,7 @@ cm.setFrom(change.getOwner()); cm.setPatchSet(patchSet, patchSetInfo); cm.setNotify(notify); + cm.setAccountsToNotify(accountsToNotify); cm.addReviewers(reviewers); cm.addExtraCC(extraCC); cm.send(); @@ -440,9 +484,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 +494,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 6d81319..4ad9f15 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
@@ -14,6 +14,7 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS; import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES; import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS; @@ -32,26 +33,29 @@ 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; import com.google.common.base.Throwables; import com.google.common.collect.FluentIterable; 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.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.collect.Table; +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.LabelTypes; @@ -62,6 +66,7 @@ import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.client.ListChangesOption; +import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ApprovalInfo; import com.google.gerrit.extensions.common.ChangeInfo; @@ -73,6 +78,7 @@ import com.google.gerrit.extensions.common.PushCertificateInfo; import com.google.gerrit.extensions.common.ReviewerUpdateInfo; import com.google.gerrit.extensions.common.RevisionInfo; +import com.google.gerrit.extensions.common.VotingRangeInfo; import com.google.gerrit.extensions.common.WebLinkInfo; import com.google.gerrit.extensions.config.DownloadCommand; import com.google.gerrit.extensions.config.DownloadScheme; @@ -87,6 +93,7 @@ import com.google.gerrit.reviewdb.client.Project; 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.CurrentUser; import com.google.gerrit.server.GpgException; @@ -100,13 +107,15 @@ 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; 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.SubmitRuleEvaluator; +import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gerrit.server.query.QueryResult; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData.ChangedLines; @@ -116,6 +125,7 @@ import com.google.inject.assistedinject.AssistedInject; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -133,14 +143,34 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; public class ChangeJson { private static final Logger log = LoggerFactory.getLogger(ChangeJson.class); + + // Submit rule options in this class should always use fastEvalLabels for + // efficiency reasons. Callers that care about submittability after taking + // vote squashing into account should be looking at the submit action. + public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT = + ChangeField.SUBMIT_RULE_OPTIONS_LENIENT + .toBuilder() + .fastEvalLabels(true) + .build(); + + public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT = + ChangeField.SUBMIT_RULE_OPTIONS_STRICT + .toBuilder() + .fastEvalLabels(true) + .build(); + 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,9 +197,11 @@ private final ChangeNotes.Factory notesFactory; private final ChangeResource.Factory changeResourceFactory; private final ChangeKindCache changeKindCache; + private final ChangeIndexCollection indexes; + private final ApprovalsUtil approvalsUtil; + private boolean lazyLoad = true; private AccountLoader accountLoader; - private Map<Change.Id, List<SubmitRecord>> submitRecords; private FixInput fix; @AssistedInject @@ -195,6 +227,8 @@ ChangeNotes.Factory notesFactory, ChangeResource.Factory changeResourceFactory, ChangeKindCache changeKindCache, + ChangeIndexCollection indexes, + ApprovalsUtil approvalsUtil, @Assisted Set<ListChangesOption> options) { this.db = db; this.labelNormalizer = ln; @@ -217,11 +251,18 @@ this.notesFactory = notesFactory; this.changeResourceFactory = changeResourceFactory; this.changeKindCache = changeKindCache; + this.indexes = indexes; + this.approvalsUtil = approvalsUtil; 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; @@ -250,7 +291,7 @@ } public ChangeInfo format(ChangeData cd) throws OrmException { - return format(cd, Optional.<PatchSet.Id> absent(), true); + return format(cd, Optional.empty(), true); } private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId, @@ -267,7 +308,7 @@ } catch (PatchListNotAvailableException | GpgException | OrmException | IOException | RuntimeException e) { if (!has(CHECK)) { - Throwables.propagateIfPossible(e, OrmException.class); + Throwables.throwIfInstanceOf(e, OrmException.class); throw new OrmException(e); } return checkOnly(cd); @@ -282,13 +323,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 +352,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) { @@ -339,7 +381,7 @@ ChangeInfo i = out.get(cd.getId()); if (i == null) { try { - i = toChangeInfo(cd, Optional.<PatchSet.Id> absent()); + i = toChangeInfo(cd, Optional.empty()); } catch (PatchListNotAvailableException | GpgException | OrmException | IOException | RuntimeException e) { if (has(CHECK)) { @@ -419,16 +461,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; @@ -464,9 +513,11 @@ // list permitted labels, since users can't vote on those patch sets. if (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId())) { - out.permittedLabels = permittedLabels(ctl, cd); + out.permittedLabels = + cd.change().getStatus() != Change.Status.ABANDONED + ? permittedLabels(ctl, cd) + : ImmutableMap.of(); } - out.removableReviewers = removableReviewers(ctl, out.labels.values()); out.reviewers = new HashMap<>(); for (Map.Entry<ReviewerStateInternal, Map<Account.Id, Timestamp>> e @@ -474,6 +525,8 @@ out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet())); } + + out.removableReviewers = removableReviewers(ctl, out); } if (has(REVIEWER_UPDATES)) { @@ -495,8 +548,10 @@ } finish(out); + // This block must come after the ChangeInfo is mostly populated, since + // it will be passed to ActionVisitors as-is. if (needRevisions) { - out.revisions = revisions(ctl, cd, src); + out.revisions = revisions(ctl, cd, src, out); if (out.revisions != null) { for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) { if (entry.getValue().isCurrent) { @@ -529,23 +584,14 @@ return result; } + private boolean submittable(ChangeData cd) throws OrmException { + return SubmitRecord.findOkRecord( + cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) + .isPresent(); + } + 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 - // fastEvalLabels/allowDraft/etc. - // TODO(dborowitz): Handle this better at the ChangeData level. - if (submitRecords == null) { - submitRecords = new HashMap<>(); - } - List<SubmitRecord> records = submitRecords.get(cd.getId()); - if (records == null) { - records = new SubmitRuleEvaluator(cd) - .setFastEvalLabels(true) - .setAllowDraft(true) - .evaluate(); - submitRecords.put(cd.getId(), records); - } - return records; + return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT); } private Map<String, LabelInfo> labelsFor(ChangeControl ctl, @@ -561,9 +607,9 @@ LabelTypes labelTypes = ctl.getLabelTypes(); Map<String, LabelWithStatus> withStatus = cd.change().getStatus().isOpen() ? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed) - : labelsForClosedChange(cd, labelTypes, standard, detailed); + : labelsForClosedChange(ctl, 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, @@ -659,11 +705,15 @@ private void setAllApprovals(ChangeControl baseCtrl, ChangeData cd, Map<String, LabelWithStatus> labels) throws OrmException { + Change.Status status = cd.change().getStatus(); + checkState(status.isOpen(), + "should not call setAllApprovals on %s change", status); + // Include a user in the output for this label if either: // - 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()); } @@ -677,6 +727,8 @@ for (Account.Id accountId : allUsers) { IdentifiedUser user = userFactory.create(accountId); ChangeControl ctl = baseCtrl.forUser(user); + Map<String, VotingRangeInfo> pvr = + getPermittedVotingRanges(permittedLabels(ctl, cd)); for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) { LabelType lt = ctl.getLabelTypes().byLabel(e.getKey()); if (lt == null) { @@ -685,6 +737,8 @@ continue; } Integer value; + VotingRangeInfo permittedVotingRange = + pvr.getOrDefault(lt.getName(), null); String tag = null; Timestamp date = null; PatchSetApproval psa = current.get(accountId, lt.getName()); @@ -698,6 +752,9 @@ } tag = psa.getTag(); date = psa.getGranted(); + if (psa.isPostSubmit()) { + log.warn("unexpected post-submit approval on open change: {}", psa); + } } else { // Either the user cannot vote on this label, or they were added as a // reviewer but have not responded yet. Explicitly check whether the @@ -705,19 +762,53 @@ value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null; } addApproval(e.getValue().label(), - approvalInfo(accountId, value, tag, date)); + approvalInfo(accountId, value, permittedVotingRange, tag, date)); } } } + private Map<String, VotingRangeInfo> getPermittedVotingRanges( + Map<String, Collection<String>> permittedLabels) { + Map<String, VotingRangeInfo> permittedVotingRanges = + Maps.newHashMapWithExpectedSize(permittedLabels.size()); + for (String label : permittedLabels.keySet()) { + List<Integer> permittedVotingRange = permittedLabels.get(label) + .stream() + .map(this::parseRangeValue) + .filter(java.util.Objects::nonNull) + .sorted() + .collect(toList()); + + if (permittedVotingRange.isEmpty()) { + permittedVotingRanges.put(label, null); + } else { + int minPermittedValue = permittedVotingRange.get(0); + int maxPermittedValue = Iterables.getLast(permittedVotingRange); + permittedVotingRanges.put(label, + new VotingRangeInfo(minPermittedValue, maxPermittedValue)); + } + } + return permittedVotingRanges; + } + + private Integer parseRangeValue(String value) { + if (value.startsWith("+")) { + value = value.substring(1); + } else if (value.startsWith(" ")) { + value = value.trim(); + } + return Ints.tryParse(value); + } + private Timestamp getSubmittedOn(ChangeData cd) throws OrmException { Optional<PatchSetApproval> s = cd.getSubmitApproval(); return s.isPresent() ? s.get().getGranted() : null; } - private Map<String, LabelWithStatus> labelsForClosedChange(ChangeData cd, - LabelTypes labelTypes, boolean standard, boolean detailed) + private Map<String, LabelWithStatus> labelsForClosedChange( + ChangeControl baseCtrl, ChangeData cd, LabelTypes labelTypes, + boolean standard, boolean detailed) throws OrmException { Set<Account.Id> allUsers = new HashSet<>(); if (detailed) { @@ -730,10 +821,9 @@ } } - // We can only approximately reconstruct what the submit rule evaluator - // would have done. These should really come from a stored submit record. Set<String> labelNames = new HashSet<>(); - Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create(); + Multimap<Account.Id, PatchSetApproval> current = + MultimapBuilder.hashKeys().hashSetValues().build(); for (PatchSetApproval a : cd.currentApprovals()) { allUsers.add(a.getAccountId()); LabelType type = labelTypes.byLabel(a.getLabelId()); @@ -745,25 +835,52 @@ } } - // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167. - Map<String, LabelWithStatus> labels = - new TreeMap<>(labelTypes.nameComparator()); - for (String name : labelNames) { - LabelType type = labelTypes.byLabel(name); - LabelWithStatus l = LabelWithStatus.create(new LabelInfo(), null); - if (detailed) { - setLabelValues(type, l); + Map<String, LabelWithStatus> labels; + if (cd.change().getStatus() == Change.Status.MERGED) { + // Since voting on merged changes is allowed all labels which apply to + // the change must be returned. All applying labels can be retrieved from + // the submit records, which is what initLabels does. + // It's not possible to only compute the labels based on the approvals + // since merged changes may not have approvals for all labels (e.g. if not + // all labels are required for submit or if the change was auto-closed due + // to direct push or if new labels were defined after the change was + // merged). + labels = initLabels(cd, labelTypes, standard); + + // Also include all labels for which approvals exists. E.g. there can be + // approvals for labels that are ignored by a Prolog submit rule and hence + // it wouldn't be included in the submit records. + for (String name : labelNames) { + if (!labels.containsKey(name)) { + labels.put(name, LabelWithStatus.create(new LabelInfo(), null)); + } } - labels.put(type.getName(), l); + } else { + // For abandoned changes return only labels for which approvals exist. + // Other labels are not needed since voting on abandoned changes is not + // allowed. + labels = new TreeMap<>(labelTypes.nameComparator()); + for (String name : labelNames) { + labels.put(name, LabelWithStatus.create(new LabelInfo(), null)); + } + } + + if (detailed) { + labels.entrySet().stream() + .filter(e -> labelTypes.byLabel(e.getKey()) != null) + .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()), + e.getValue())); } for (Account.Id accountId : allUsers) { Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size()); - + Map<String, VotingRangeInfo> pvr = Collections.emptyMap(); if (detailed) { + ChangeControl ctl = baseCtrl.forUser(userFactory.create(accountId)); + pvr = getPermittedVotingRanges(permittedLabels(ctl, cd)); for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) { - ApprovalInfo ai = approvalInfo(accountId, 0, null, null); + ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null); byLabel.put(entry.getKey(), ai); addApproval(entry.getValue().label(), ai); } @@ -778,8 +895,12 @@ ApprovalInfo info = byLabel.get(type.getName()); if (info != null) { info.value = Integer.valueOf(val); + info.permittedVotingRange = pvr.getOrDefault(type.getName(), null); info.date = psa.getGranted(); info.tag = psa.getTag(); + if (psa.isPostSubmit()) { + info.postSubmit = true; + } } if (!standard) { continue; @@ -791,17 +912,18 @@ return labels; } - private ApprovalInfo approvalInfo(Account.Id id, Integer value, String tag, - Timestamp date) { - ApprovalInfo ai = getApprovalInfo(id, value, tag, date); + private ApprovalInfo approvalInfo(Account.Id id, Integer value, + VotingRangeInfo permittedVotingRange, String tag, Timestamp date) { + ApprovalInfo ai = getApprovalInfo(id, value, permittedVotingRange, tag, date); accountLoader.put(ai); return ai; } - public static ApprovalInfo getApprovalInfo( - Account.Id id, Integer value, String tag, Timestamp date) { + public static ApprovalInfo getApprovalInfo(Account.Id id, Integer value, + VotingRangeInfo permittedVotingRange, String tag, Timestamp date) { ApprovalInfo ai = new ApprovalInfo(id.get()); ai.value = value; + ai.permittedVotingRange = permittedVotingRange; ai.date = date; ai.tag = tag; return ai; @@ -824,10 +946,12 @@ private Map<String, Collection<String>> permittedLabels(ChangeControl ctl, ChangeData cd) throws OrmException { - if (ctl == null) { + if (ctl == null || !ctl.getUser().isIdentifiedUser()) { return null; } + Map<String, Short> labels = null; + boolean isMerged = ctl.getChange().getStatus() == Change.Status.MERGED; LabelTypes labelTypes = ctl.getLabelTypes(); SetMultimap<String, String> permitted = LinkedHashMultimap.create(); for (SubmitRecord rec : submitRecords(cd)) { @@ -836,12 +960,20 @@ } for (SubmitRecord.Label r : rec.labels) { LabelType type = labelTypes.byLabel(r.label); - if (type == null) { + if (type == null || (isMerged && !type.allowPostSubmit())) { continue; } PermissionRange range = ctl.getRange(Permission.forLabel(r.label)); for (LabelValue v : type.getValues()) { - if (range.contains(v.getValue())) { + boolean ok = range.contains(v.getValue()); + if (isMerged) { + if (labels == null) { + labels = currentLabels(ctl); + } + short prev = labels.getOrDefault(type.getName(), (short) 0); + ok &= v.getValue() >= prev; + } + if (ok) { permitted.put(r.label, v.formatValue()); } } @@ -861,6 +993,17 @@ return permitted.asMap(); } + private Map<String, Short> currentLabels(ChangeControl ctl) + throws OrmException { + Map<String, Short> result = new HashMap<>(); + for (PatchSetApproval psa : approvalsUtil.byPatchSetUser( + db.get(), ctl, ctl.getChange().currentPatchSetId(), + ctl.getUser().getAccountId())) { + result.put(psa.getLabel(), psa.getValue()); + } + return result; + } + private Collection<ChangeMessageInfo> messages(ChangeControl ctl, ChangeData cd, Map<PatchSet.Id, PatchSet> map) throws OrmException { @@ -889,7 +1032,18 @@ } private Collection<AccountInfo> removableReviewers(ChangeControl ctl, - Collection<LabelInfo> labels) { + ChangeInfo out) { + // Although this is called removableReviewers, this method also determines + // which CCs are removable. + // + // For reviewers, we need to look at each approval, because the reviewer + // should only be considered removable if *all* of their approvals can be + // removed. First, add all reviewers with *any* removable approval to the + // "removable" set. Along the way, if we encounter a non-removable approval, + // add the reviewer to the "fixed" set. Before we return, remove all members + // of "fixed" from "removable", because not all of their approvals can be + // removed. + Collection<LabelInfo> labels = out.labels.values(); Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size()); Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size()); for (LabelInfo label : labels) { @@ -905,6 +1059,24 @@ } } } + + // CCs are simpler than reviewers. They are removable if the ChangeControl + // would permit a non-negative approval by that account to be removed, in + // which case add them to removable. We don't need to add unremovable CCs to + // "fixed" because we only visit each CC once here. + Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC); + if (ccs != null) { + for (AccountInfo ai : ccs) { + Account.Id id = new Account.Id(ai._accountId); + if (ctl.canRemoveReviewer(id, 0)) { + removable.add(id); + } + } + } + + // Subtract any reviewers with non-removable approvals from the "removable" + // set. This also subtracts any CCs that for some reason also hold + // unremovable approvals. removable.removeAll(fixed); List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size()); @@ -916,27 +1088,32 @@ 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<PatchSet.Id, PatchSet> map, ChangeInfo changeInfo) + 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())) && ctl.isPatchVisible(in, db.get())) { - res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, false)); + res.put(in.getRevision().get(), + toRevisionInfo(ctl, cd, in, repo, false, changeInfo)); } } return res; @@ -975,19 +1152,18 @@ 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); + ctl, changeDataFactory.create(db.get(), ctl), in, repo, true, null); accountLoader.fill(); return rev; } } private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd, - PatchSet in, Repository repo, boolean fillCommit) - throws PatchListNotAvailableException, GpgException, OrmException, - IOException { + PatchSet in, @Nullable Repository repo, boolean fillCommit, + @Nullable ChangeInfo changeInfo) throws PatchListNotAvailableException, + GpgException, OrmException, IOException { Change c = ctl.getChange(); RevisionInfo out = new RevisionInfo(); out.isCurrent = in.getId().equals(c.currentPatchSetId()); @@ -998,6 +1174,7 @@ out.draft = in.isDraft() ? true : null; out.fetch = makeFetchMap(ctl, in); out.kind = changeKindCache.getChangeKind(repo, cd, in); + out.description = in.getDescription(); boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT)); @@ -1012,9 +1189,15 @@ out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit); } if (addFooters) { + Ref ref = repo.exactRef(ctl.getChange().getDest().get()); + RevCommit mergeTip = null; + if (ref != null){ + mergeTip = rw.parseCommit(ref.getObjectId()); + rw.parseBody(mergeTip); + } out.commitWithFooters = mergeUtilFactory .create(projectCache.get(project)) - .createCherryPickCommitMessage(commit, ctl, in.getId()); + .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId()); } } } @@ -1022,13 +1205,14 @@ 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)) && has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) { - actionJson.addRevisionActions(out, + actionJson.addRevisionActions(changeInfo, out, new RevisionResource(changeResourceFactory.create(ctl), in)); } @@ -1059,9 +1243,9 @@ info.message = commit.getFullMessage(); if (addLinks) { - FluentIterable<WebLinkInfo> links = + List<WebLinkInfo> links = webLinks.getPatchSetLinks(project, commit.name()); - info.webLinks = links.isEmpty() ? null : links.toList(); + info.webLinks = links.isEmpty() ? null : links; } for (RevCommit parent : commit.getParents()) { @@ -1070,9 +1254,9 @@ i.commit = parent.name(); i.subject = parent.getShortMessage(); if (addLinks) { - FluentIterable<WebLinkInfo> parentLinks = + List<WebLinkInfo> parentLinks = webLinks.getParentLinks(project, parent.name()); - i.webLinks = parentLinks.isEmpty() ? null : parentLinks.toList(); + i.webLinks = parentLinks.isEmpty() ? null : parentLinks; } info.parents.add(i); } @@ -1148,14 +1332,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..b3207e9 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); @@ -222,7 +230,8 @@ } if ((prior.getParentCount() != 1 || next.getParentCount() != 1) - && !onlyFirstParentChanged(prior, next)) { + && (!onlyFirstParentChanged(prior, next) + || prior.getParentCount() == 0)) { // Trivial rebases done by machine only work well on 1 parent. return ChangeKind.REWORK; } @@ -246,6 +255,10 @@ // it was a rework. } return ChangeKind.REWORK; + } finally { + if (close) { + repo.close(); + } } } @@ -303,7 +316,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 +323,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 +346,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 +382,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 +401,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 +409,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/ChangeMessages.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java index 8236d3d..92b4150 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -23,7 +23,8 @@ } public String revertChangeDefaultMessage; - public String reviewerNotFound; + public String reviewerNotFoundUser; + public String reviewerNotFoundUserOrGroup; public String groupIsNotAllowed; public String groupHasTooManyMembers;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java index 7069e6d..fc3e70a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -15,12 +15,13 @@ package com.google.gerrit.server.change; import com.google.auto.value.AutoValue; -import com.google.common.base.Optional; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; +import java.util.Optional; + @AutoValue public abstract class ChangeTriplet { public static String format(Change change) { @@ -44,7 +45,7 @@ int t2 = triplet.lastIndexOf('~'); int t1 = triplet.lastIndexOf('~', t2 - 1); if (t1 < 0 || t2 < 0) { - return Optional.absent(); + return Optional.empty(); } String project = Url.decode(triplet.substring(0, t1));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java index 1a063f4..b5eb193 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -60,10 +60,12 @@ public ChangeInfo apply(RevisionResource revision, CherryPickInput input) throws OrmException, IOException, UpdateException, RestApiException { final ChangeControl control = revision.getControl(); + int parent = input.parent == null ? 1 : input.parent; if (input.message == null || input.message.trim().isEmpty()) { throw new BadRequestException("message must be non-empty"); - } else if (input.destination == null || input.destination.trim().isEmpty()) { + } else if (input.destination == null + || input.destination.trim().isEmpty()) { throw new BadRequestException("destination must be non-empty"); } @@ -91,7 +93,7 @@ Change.Id cherryPickedChangeId = cherryPickChange.cherryPick(revision.getChange(), revision.getPatchSet(), input.message, refName, - refControl); + refControl, parent); return json.create(ChangeJson.NO_OPTIONS).format(revision.getProject(), cherryPickedChangeId); } catch (InvalidChangeOperationException e) {
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..21ba93b 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; @@ -113,8 +114,8 @@ } public Change.Id cherryPick(Change change, PatchSet patch, - final String message, final String ref, - final RefControl refControl) throws NoSuchChangeException, + final String message, final String ref, final RefControl refControl, + int parent) throws NoSuchChangeException, OrmException, MissingObjectException, IncorrectObjectTypeException, IOException, InvalidChangeOperationException, IntegrationException, UpdateException, @@ -146,6 +147,13 @@ CodeReviewCommit commitToCherryPick = revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get())); + if (parent <= 0 || parent > commitToCherryPick.getParentCount()) { + throw new InvalidChangeOperationException(String.format( + "Cherry Pick: Parent %s does not exist. Please specify a parent in" + + " range [1, %s].", + parent, commitToCherryPick.getParentCount())); + } + Timestamp now = TimeUtil.nowTs(); PersonIdent committerIdent = identifiedUser.newCommitterIdent(now, serverTimeZone); @@ -159,10 +167,12 @@ CodeReviewCommit cherryPickCommit; try { - ProjectState projectState = refControl.getProjectControl().getProjectState(); - cherryPickCommit = - mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip, - commitToCherryPick, committerIdent, commitMessage, revWalk); + ProjectState projectState = refControl.getProjectControl() + .getProjectState(); + cherryPickCommit = mergeUtilFactory.create(projectState) + .createCherryPickFromCommit(git, oi, mergeTip, + commitToCherryPick, committerIdent, commitMessage, revWalk, + parent - 1); Change.Key changeKey; final List<String> idList = cherryPickCommit.getFooterLines( @@ -236,7 +246,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 +281,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 +289,9 @@ .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(), + ChangeMessagesUtil.TAG_CHERRY_PICK_CHANGE); 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..eb6d151 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,22 @@ 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.common.Nullable; 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.FixReplacementInfo; +import com.google.gerrit.extensions.common.FixSuggestionInfo; +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.FixReplacement; +import com.google.gerrit.reviewdb.client.FixSuggestion; +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; @@ -34,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; class CommentJson { @@ -57,104 +63,167 @@ 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; + r.unresolved = c.unresolved; + if (loader != null) { + r.author = loader.get(c.author.getId()); } } - if (c.getLine() > 0) { - r.line = c.getLine(); + + protected 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; + rci.fixSuggestions = toFixSuggestionInfos(c.fixSuggestions); + fillCommentInfo(c, rci, loader); + return rci; + } + + private List<FixSuggestionInfo> toFixSuggestionInfos( + @Nullable List<FixSuggestion> fixSuggestions) { + if (fixSuggestions == null || fixSuggestions.isEmpty()) { + return null; + } + + return fixSuggestions.stream() + .map(this::toFixSuggestionInfo) + .collect(Collectors.toList()); + } + + private FixSuggestionInfo toFixSuggestionInfo(FixSuggestion fixSuggestion) { + FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo(); + fixSuggestionInfo.fixId = fixSuggestion.fixId; + fixSuggestionInfo.description = fixSuggestion.description; + fixSuggestionInfo.replacements = fixSuggestion.replacements.stream() + .map(this::toFixReplacementInfo) + .collect(Collectors.toList()); + return fixSuggestionInfo; + } + + private FixReplacementInfo toFixReplacementInfo( + FixReplacement fixReplacement) { + FixReplacementInfo fixReplacementInfo = new FixReplacementInfo(); + fixReplacementInfo.path = fixReplacement.path; + fixReplacementInfo.range = toRange(fixReplacement.range); + fixReplacementInfo.replacement = fixReplacement.replacement; + return fixReplacementInfo; + } + + 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..20e5b9d 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")); @@ -663,7 +658,7 @@ public boolean updateChange(ChangeContext ctx) throws OrmException, PatchSetInfoNotAvailableException { // Delete dangling key references. - ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb()); + ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb()); accountPatchReviewStore.get().clearReviewed(psId); db.changeMessages().delete( db.changeMessages().byChange(psId.getParentKey()));
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/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java new file mode 100644 index 0000000..6eb144a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -0,0 +1,212 @@ +// 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.MoreObjects; +import com.google.common.base.Strings; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.client.ListChangesOption; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.MergeInput; +import com.google.gerrit.extensions.common.MergePatchSetInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.MergeConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MergeIdenticalTreeException; +import com.google.gerrit.server.git.MergeUtil; +import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.ChangeIdUtil; + +import java.io.IOException; +import java.sql.Timestamp; +import java.util.EnumSet; +import java.util.TimeZone; + +@Singleton +public class CreateMergePatchSet implements + RestModifyView<ChangeResource, MergePatchSetInput> { + + private final Provider<ReviewDb> db; + private final GitRepositoryManager gitManager; + private final TimeZone serverTimeZone; + private final Provider<CurrentUser> user; + private final ChangeJson.Factory jsonFactory; + private final PatchSetUtil psUtil; + private final MergeUtil.Factory mergeUtilFactory; + private final BatchUpdate.Factory batchUpdateFactory; + private final PatchSetInserter.Factory patchSetInserterFactory; + + @Inject + CreateMergePatchSet(Provider<ReviewDb> db, + GitRepositoryManager gitManager, + @GerritPersonIdent PersonIdent myIdent, + Provider<CurrentUser> user, + ChangeJson.Factory json, + PatchSetUtil psUtil, + MergeUtil.Factory mergeUtilFactory, + BatchUpdate.Factory batchUpdateFactory, + PatchSetInserter.Factory patchSetInserterFactory) { + this.db = db; + this.gitManager = gitManager; + this.serverTimeZone = myIdent.getTimeZone(); + this.user = user; + this.jsonFactory = json; + this.psUtil = psUtil; + this.mergeUtilFactory = mergeUtilFactory; + this.batchUpdateFactory = batchUpdateFactory; + this.patchSetInserterFactory = patchSetInserterFactory; + } + + @Override + public Response<ChangeInfo> apply(ChangeResource req, MergePatchSetInput in) + throws NoSuchChangeException, OrmException, IOException, + InvalidChangeOperationException, RestApiException, UpdateException { + if (in.merge == null) { + throw new BadRequestException("merge field is required"); + } + + MergeInput merge = in.merge; + if (Strings.isNullOrEmpty(merge.source)) { + throw new BadRequestException("merge.source must be non-empty"); + } + + ChangeControl ctl = req.getControl(); + if (!ctl.isVisible(db.get())) { + throw new InvalidChangeOperationException( + "Base change not found: " + req.getId()); + } + PatchSet ps = psUtil.current(db.get(), ctl.getNotes()); + if (!ctl.canAddPatchSet(db.get())) { + throw new AuthException("cannot add patch set"); + } + + ProjectControl projectControl = ctl.getProjectControl(); + Change change = ctl.getChange(); + Project.NameKey project = change.getProject(); + Branch.NameKey dest = change.getDest(); + try (Repository git = gitManager.openRepository(project); + ObjectInserter oi = git.newObjectInserter(); + RevWalk rw = new RevWalk(oi.newReader())) { + + RevCommit sourceCommit = + MergeUtil.resolveCommit(git, rw, merge.source); + if (!projectControl.canReadCommit(db.get(), git, sourceCommit)) { + throw new ResourceNotFoundException( + "cannot find source commit: " + merge.source + " to merge."); + } + + RevCommit currentPsCommit = + rw.parseCommit(ObjectId.fromString(ps.getRevision().get())); + + Timestamp now = TimeUtil.nowTs(); + IdentifiedUser me = user.get().asIdentifiedUser(); + PersonIdent author = me.newCommitterIdent(now, serverTimeZone); + + RevCommit newCommit = + createMergeCommit(in, projectControl, dest, git, oi, rw, + currentPsCommit, sourceCommit, author, + ObjectId.fromString(change.getKey().get().substring(1))); + + PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId()); + PatchSetInserter psInserter = + patchSetInserterFactory.create(ctl, nextPsId, newCommit); + try (BatchUpdate bu = batchUpdateFactory + .create(db.get(), project, me, now)) { + bu.setRepository(git, rw, oi); + bu.addOp(ctl.getId(), psInserter + .setMessage("Uploaded patch set " + nextPsId.get() + ".") + .setDraft(ps.isDraft()) + .setNotify(NotifyHandling.NONE)); + bu.execute(); + } + + ChangeJson json = + jsonFactory.create(EnumSet.of(ListChangesOption.CURRENT_REVISION)); + return Response.ok(json.format(psInserter.getChange())); + } + } + + private RevCommit createMergeCommit(MergePatchSetInput in, + ProjectControl projectControl, Branch.NameKey dest, Repository git, + ObjectInserter oi, RevWalk rw, RevCommit currentPsCommit, + RevCommit sourceCommit, PersonIdent author, ObjectId changeId) + throws ResourceNotFoundException, MergeIdenticalTreeException, + MergeConflictException, IOException { + + ObjectId parentCommit; + if (in.inheritParent) { + // inherit first parent from previous patch set + parentCommit = currentPsCommit.getParent(0); + } else { + // get the current branch tip of destination branch + Ref destRef = git.getRefDatabase().exactRef(dest.get()); + if (destRef != null) { + parentCommit = destRef.getObjectId(); + } else { + throw new ResourceNotFoundException("cannot find destination branch"); + } + } + RevCommit mergeTip = rw.parseCommit(parentCommit); + + String commitMsg; + if (Strings.emptyToNull(in.subject) != null) { + commitMsg = ChangeIdUtil.insertId(in.subject, changeId); + } else { + // reuse previous patch set commit message + commitMsg = currentPsCommit.getFullMessage(); + } + + String mergeStrategy = MoreObjects.firstNonNull( + Strings.emptyToNull(in.merge.strategy), + mergeUtilFactory.create(projectControl.getProjectState()) + .mergeStrategyName()); + + return MergeUtil.createMergeCommit(git, oi, mergeTip, sourceCommit, + mergeStrategy, author, commitMsg, rw); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java new file mode 100644 index 0000000..c5343e6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -0,0 +1,128 @@ +// 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(); + Account deletedAssignee = op.getDeletedAssignee(); + return deletedAssignee == null + ? Response.none() + : Response.ok(AccountJson.toAccountInfo(deletedAssignee)); + } + } + + 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), + ChangeMessagesUtil.TAG_DELETE_ASSIGNEE); + 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/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java similarity index 75% rename from gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java rename to gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java index a125272..18d7074 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -22,10 +22,11 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.change.DeleteDraftChange.Input; +import com.google.gerrit.server.change.DeleteChange.Input; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.project.ChangeControl; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -34,25 +35,25 @@ import org.eclipse.jgit.lib.Config; @Singleton -public class DeleteDraftChange implements +public class DeleteChange implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { public static class Input { } private final Provider<ReviewDb> db; private final BatchUpdate.Factory updateFactory; - private final Provider<DeleteDraftChangeOp> opProvider; + private final Provider<DeleteChangeOp> opProvider; private final boolean allowDrafts; @Inject - public DeleteDraftChange(Provider<ReviewDb> db, + public DeleteChange(Provider<ReviewDb> db, BatchUpdate.Factory updateFactory, - Provider<DeleteDraftChangeOp> opProvider, + Provider<DeleteChangeOp> opProvider, @GerritServerConfig Config cfg) { this.db = db; this.updateFactory = updateFactory; this.opProvider = opProvider; - this.allowDrafts = DeleteDraftChangeOp.allowDrafts(cfg); + this.allowDrafts = DeleteChangeOp.allowDrafts(cfg); } @Override @@ -71,14 +72,21 @@ @Override public UiAction.Description getDescription(ChangeResource rsrc) { try { + Change.Status status = rsrc.getChange().getStatus(); + ChangeControl changeControl = rsrc.getControl(); + boolean visible = isActionAllowed(changeControl, status) + && changeControl.canDelete(db.get(), status); return new UiAction.Description() .setLabel("Delete") - .setTitle("Delete draft change " + rsrc.getId()) - .setVisible(allowDrafts - && rsrc.getChange().getStatus() == Status.DRAFT - && rsrc.getControl().canDeleteDraft(db.get())); + .setTitle("Delete change " + rsrc.getId()) + .setVisible(visible); } catch (OrmException e) { throw new IllegalStateException(e); } } + + private boolean isActionAllowed(ChangeControl changeControl, + Status status) { + return status != Status.DRAFT || allowDrafts || changeControl.isAdmin(); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java index 7c1e959..604b615 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.change; -import com.google.common.base.Optional; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; @@ -27,6 +26,7 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; @Singleton public class DeleteChangeEdit implements RestModifyView<ChangeResource, Input> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java new file mode 100644 index 0000000..afec66a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -0,0 +1,194 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; + +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.BatchUpdate.RepoContext; +import com.google.gerrit.server.git.BatchUpdateReviewDb; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +class DeleteChangeOp extends BatchUpdate.Op { + static boolean allowDrafts(Config cfg) { + return cfg.getBoolean("change", "allowDrafts", true); + } + + static ReviewDb unwrap(ReviewDb db) { + // This is special. We want to delete exactly the rows that are present in + // the database, even when reading everything else from NoteDb, so we need + // to bypass the write-only wrapper. + if (db instanceof BatchUpdateReviewDb) { + db = ((BatchUpdateReviewDb) db).unsafeGetDelegate(); + } + return ReviewDbUtil.unwrapDb(db); + } + + + private final PatchSetUtil psUtil; + private final StarredChangesUtil starredChangesUtil; + private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore; + private final boolean allowDrafts; + + private Change.Id id; + + @Inject + DeleteChangeOp(PatchSetUtil psUtil, + StarredChangesUtil starredChangesUtil, + DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, + @GerritServerConfig Config cfg) { + this.psUtil = psUtil; + this.starredChangesUtil = starredChangesUtil; + this.accountPatchReviewStore = accountPatchReviewStore; + this.allowDrafts = allowDrafts(cfg); + } + + @Override + public boolean updateChange(ChangeContext ctx) throws RestApiException, + OrmException, IOException, NoSuchChangeException { + checkState(ctx.getOrder() == BatchUpdate.Order.DB_BEFORE_REPO, + "must use DeleteChangeOp with DB_BEFORE_REPO"); + checkState(id == null, "cannot reuse DeleteChangeOp"); + + id = ctx.getChange().getId(); + Collection<PatchSet> patchSets = psUtil.byChange(ctx.getDb(), + ctx.getNotes()); + + ensureDeletable(ctx, id, patchSets); + // Cleaning up is only possible as long as the change and its elements are + // still part of the database. + cleanUpReferences(ctx, id, patchSets); + deleteChangeElementsFromDb(ctx, id); + + ctx.deleteChange(); + return true; + } + + private void ensureDeletable(ChangeContext ctx, Change.Id id, + Collection<PatchSet> patchSets) throws ResourceConflictException, + MethodNotAllowedException, OrmException, AuthException, IOException { + Change.Status status = ctx.getChange().getStatus(); + if (status == Change.Status.MERGED) { + throw new MethodNotAllowedException("Deleting merged change " + id + + " is not allowed"); + } + for (PatchSet patchSet : patchSets) { + if (isPatchSetMerged(ctx, patchSet)) { + throw new ResourceConflictException(String.format( + "Cannot delete change %s: patch set %s is already merged", + id, patchSet.getPatchSetId())); + } + } + + if (!ctx.getControl().canDelete(ctx.getDb(), status)) { + throw new AuthException("Deleting change " + id + " is not permitted"); + } + + if (status == Change.Status.DRAFT) { + if (!allowDrafts && !ctx.getControl().isAdmin()) { + throw new MethodNotAllowedException("Draft workflow is disabled"); + } + for (PatchSet ps : patchSets) { + if (!ps.isDraft()) { + throw new ResourceConflictException("Cannot delete draft change " + id + + ": patch set " + ps.getPatchSetId() + " is not a draft"); + } + } + } + } + + private boolean isPatchSetMerged(ChangeContext ctx, PatchSet patchSet) + throws IOException { + Repository repository = ctx.getRepository(); + Ref destinationRef = repository.exactRef(ctx.getChange().getDest().get()); + if (destinationRef == null) { + return false; + } + + RevWalk revWalk = ctx.getRevWalk(); + ObjectId objectId = ObjectId.fromString(patchSet.getRevision().get()); + RevCommit revCommit = revWalk.parseCommit(objectId); + return IncludedInResolver.includedInOne(repository, revWalk, revCommit, + Collections.singletonList(destinationRef)); + } + + private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id) + throws OrmException { + if (PrimaryStorage.of(ctx.getChange()) != REVIEW_DB) { + return; + } + // Avoid OrmConcurrencyException trying to delete non-existent entities. + // Only delete from ReviewDb here; deletion from NoteDb is handled in + // BatchUpdate. + ReviewDb db = unwrap(ctx.getDb()); + db.patchComments().delete(db.patchComments().byChange(id)); + db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id)); + db.patchSets().delete(db.patchSets().byChange(id)); + db.changeMessages().delete(db.changeMessages().byChange(id)); + } + + private void cleanUpReferences(ChangeContext ctx, Change.Id id, + Collection<PatchSet> patchSets) throws OrmException, + NoSuchChangeException { + for (PatchSet ps : patchSets) { + accountPatchReviewStore.get().clearReviewed(ps.getId()); + } + + // Non-atomic operation on Accounts table; not much we can do to make it + // atomic. + starredChangesUtil.unstarAll(ctx.getChange().getProject(), id); + } + + @Override + public void updateRepo(RepoContext ctx) throws IOException { + String prefix = new PatchSet.Id(id, 1).toRefName(); + prefix = prefix.substring(0, prefix.length() - 1); + for (Ref ref + : ctx.getRepository().getRefDatabase().getRefs(prefix).values()) { + ctx.addRefUpdate( + new ReceiveCommand( + ref.getObjectId(), ObjectId.zeroId(), ref.getName())); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java deleted file mode 100644 index 3ca0e1b..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java +++ /dev/null
@@ -1,138 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.change; - -import static com.google.common.base.Preconditions.checkState; - -import com.google.common.collect.ImmutableList; -import com.google.gerrit.extensions.registration.DynamicItem; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.MethodNotAllowedException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.reviewdb.server.ReviewDbUtil; -import com.google.gerrit.server.PatchSetUtil; -import com.google.gerrit.server.StarredChangesUtil; -import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.git.BatchUpdate; -import com.google.gerrit.server.git.BatchUpdate.ChangeContext; -import com.google.gerrit.server.git.BatchUpdate.RepoContext; -import com.google.gerrit.server.git.BatchUpdateReviewDb; -import com.google.gerrit.server.project.NoSuchChangeException; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.transport.ReceiveCommand; - -import java.io.IOException; -import java.util.List; - -class DeleteDraftChangeOp extends BatchUpdate.Op { - static boolean allowDrafts(Config cfg) { - return cfg.getBoolean("change", "allowDrafts", true); - } - - static ReviewDb unwrap(ReviewDb db) { - // This is special. We want to delete exactly the rows that are present in - // the database, even when reading everything else from NoteDb, so we need - // to bypass the write-only wrapper. - if (db instanceof BatchUpdateReviewDb) { - db = ((BatchUpdateReviewDb) db).unsafeGetDelegate(); - } - return ReviewDbUtil.unwrapDb(db); - } - - - private final PatchSetUtil psUtil; - private final StarredChangesUtil starredChangesUtil; - private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore; - private final boolean allowDrafts; - - private Change.Id id; - - @Inject - DeleteDraftChangeOp(PatchSetUtil psUtil, - StarredChangesUtil starredChangesUtil, - DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, - @GerritServerConfig Config cfg) { - this.psUtil = psUtil; - this.starredChangesUtil = starredChangesUtil; - this.accountPatchReviewStore = accountPatchReviewStore; - this.allowDrafts = allowDrafts(cfg); - } - - @Override - public boolean updateChange(ChangeContext ctx) throws RestApiException, - OrmException, IOException, NoSuchChangeException { - checkState(ctx.getOrder() == BatchUpdate.Order.DB_BEFORE_REPO, - "must use DeleteDraftChangeOp with DB_BEFORE_REPO"); - checkState(id == null, "cannot reuse DeleteDraftChangeOp"); - - Change change = ctx.getChange(); - id = change.getId(); - - ReviewDb db = unwrap(ctx.getDb()); - if (change.getStatus() != Change.Status.DRAFT) { - throw new ResourceConflictException("Change is not a draft: " + id); - } - if (!allowDrafts) { - throw new MethodNotAllowedException("Draft workflow is disabled"); - } - if (!ctx.getControl().canDeleteDraft(ctx.getDb())) { - throw new AuthException("Not permitted to delete this draft change"); - } - List<PatchSet> patchSets = ImmutableList.copyOf( - psUtil.byChange(ctx.getDb(), ctx.getNotes())); - for (PatchSet ps : patchSets) { - if (!ps.isDraft()) { - throw new ResourceConflictException("Cannot delete draft change " + id - + ": patch set " + ps.getPatchSetId() + " is not a draft"); - } - accountPatchReviewStore.get().clearReviewed(ps.getId()); - } - - // Only delete from ReviewDb here; deletion from NoteDb is handled in - // BatchUpdate. - db.patchComments().delete(db.patchComments().byChange(id)); - db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id)); - db.patchSets().delete(db.patchSets().byChange(id)); - db.changeMessages().delete(db.changeMessages().byChange(id)); - - // Non-atomic operation on Accounts table; not much we can do to make it - // atomic. - starredChangesUtil.unstarAll(change.getProject(), id); - - ctx.deleteChange(); - return true; - } - - @Override - public void updateRepo(RepoContext ctx) throws IOException { - String prefix = new PatchSet.Id(id, 1).toRefName(); - prefix = prefix.substring(0, prefix.length() - 1); - for (Ref ref - : ctx.getRepository().getRefDatabase().getRefs(prefix).values()) { - ctx.addRefUpdate( - new ReceiveCommand( - ref.getObjectId(), ObjectId.zeroId(), ref.getName())); - } - } -}
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..37930dd 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,19 +14,18 @@ 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.common.CommentInfo; 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.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; @@ -39,6 +38,7 @@ import com.google.inject.Singleton; import java.util.Collections; +import java.util.Optional; @Singleton public class DeleteDraftComment @@ -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/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java index 1cd8726..79cc35b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.change; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; + import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; @@ -34,6 +36,7 @@ import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.git.BatchUpdate.RepoContext; import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.project.NoSuchChangeException; @@ -59,7 +62,7 @@ private final BatchUpdate.Factory updateFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; - private final Provider<DeleteDraftChangeOp> deleteChangeOpProvider; + private final Provider<DeleteChangeOp> deleteChangeOpProvider; private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore; private final boolean allowDrafts; @@ -68,7 +71,7 @@ BatchUpdate.Factory updateFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, - Provider<DeleteDraftChangeOp> deleteChangeOpProvider, + Provider<DeleteChangeOp> deleteChangeOpProvider, DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, @GerritServerConfig Config cfg) { this.db = db; @@ -97,7 +100,7 @@ private Collection<PatchSet> patchSetsBeforeDeletion; private PatchSet patchSet; - private DeleteDraftChangeOp deleteChangeOp; + private DeleteChangeOp deleteChangeOp; private Op(PatchSet.Id psId) { this.psId = psId; @@ -116,7 +119,7 @@ if (!allowDrafts) { throw new MethodNotAllowedException("Draft workflow is disabled"); } - if (!ctx.getControl().canDeleteDraft(ctx.getDb())) { + if (!ctx.getControl().canDelete(ctx.getDb(), Change.Status.DRAFT)) { throw new AuthException("Not permitted to delete this draft patch set"); } @@ -146,11 +149,14 @@ psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet); accountPatchReviewStore.get().clearReviewed(psId); - // Use the unwrap from DeleteDraftChangeOp to handle BatchUpdateReviewDb. - ReviewDb db = DeleteDraftChangeOp.unwrap(ctx.getDb()); - db.changeMessages().delete(db.changeMessages().byPatchSet(psId)); - db.patchComments().delete(db.patchComments().byPatchSet(psId)); - db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId)); + if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to delete non-existent entities. + // Use the unwrap from DeleteChangeOp to handle BatchUpdateReviewDb. + ReviewDb db = DeleteChangeOp.unwrap(ctx.getDb()); + db.changeMessages().delete(db.changeMessages().byPatchSet(psId)); + db.patchComments().delete(db.patchComments().byPatchSet(psId)); + db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId)); + } } private void deleteOrUpdateDraftChange(ChangeContext ctx) @@ -195,7 +201,7 @@ rsrc.getPatchSet().getPatchSetId())) .setVisible(allowDrafts && rsrc.getPatchSet().isDraft() - && rsrc.getControl().canDeleteDraft(db.get()) + && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT) && psCount > 1); } catch (OrmException e) { throw new IllegalStateException(e);
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..47f5901 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,15 @@ package com.google.gerrit.server.change; -import com.google.common.base.Predicate; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; + 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,18 +38,17 @@ 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; import com.google.gerrit.server.git.BatchUpdate.Context; import com.google.gerrit.server.git.BatchUpdateReviewDb; import com.google.gerrit.server.git.UpdateException; -import com.google.gerrit.server.mail.DeleteReviewerSender; +import com.google.gerrit.server.mail.send.DeleteReviewerSender; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -62,13 +64,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; @@ -79,6 +79,7 @@ private final Provider<IdentifiedUser> user; private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; private final NotesMigration migration; + private final NotifyUtil notifyUtil; @Inject DeleteReviewer(Provider<ReviewDb> dbProvider, @@ -90,7 +91,8 @@ ReviewerDeleted reviewerDeleted, Provider<IdentifiedUser> user, DeleteReviewerSender.Factory deleteReviewerSenderFactory, - NotesMigration migration) { + NotesMigration migration, + NotifyUtil notifyUtil) { this.dbProvider = dbProvider; this.approvalsUtil = approvalsUtil; this.psUtil = psUtil; @@ -101,15 +103,23 @@ this.user = user; this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; this.migration = migration; + this.notifyUtil = notifyUtil; } @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 +129,7 @@ private class Op extends BatchUpdate.Op { private final Account reviewer; + private final DeleteReviewerInput input; ChangeMessage changeMessage; Change currChange; PatchSet currPs; @@ -126,8 +137,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,61 +160,66 @@ } 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"); } } - ctx.getDb().patchSetApprovals().delete(del); + if (votesRemoved) { + msg.append(removedVotesMsg); + } else { + msg.append("."); + } + if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to update non-existent entities. + 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(), + ChangeMessagesUtil.TAG_DELETE_REVIEWER); + cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); return true; } @Override public void postUpdate(Context ctx) { - if (changeMessage == null) { - return; + if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) { + 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; + PrimaryStorage r = PrimaryStorage.of(ctx.getChange()); - if (migration.readChanges()) { + if (migration.readChanges() + && r == PrimaryStorage.REVIEW_DB) { // Because NoteDb and ReviewDb have different semantics for zero-value // approvals, we must fall back to ReviewDb as the source of truth here. ReviewDb db = ctx.getDb(); @@ -218,13 +235,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 +244,33 @@ } 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.setAccountsToNotify( + notifyUtil.resolveAccounts(input.notifyDetails)); + cm.send(); + } catch (Exception err) { + log.error("Cannot email update for change " + change.getId(), err); + } } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java index f1bdba5..951635e 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,10 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; + 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 +30,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; @@ -41,9 +43,11 @@ 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.mail.DeleteVoteSender; -import com.google.gerrit.server.mail.ReplyToChangeSender; +import com.google.gerrit.server.mail.send.DeleteVoteSender; +import com.google.gerrit.server.mail.send.ReplyToChangeSender; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; 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; @@ -69,6 +73,7 @@ private final IdentifiedUser.GenericFactory userFactory; private final VoteDeleted voteDeleted; private final DeleteVoteSender.Factory deleteVoteSenderFactory; + private final NotifyUtil notifyUtil; @Inject DeleteVote(Provider<ReviewDb> db, @@ -78,7 +83,8 @@ ChangeMessagesUtil cmUtil, IdentifiedUser.GenericFactory userFactory, VoteDeleted voteDeleted, - DeleteVoteSender.Factory deleteVoteSenderFactory) { + DeleteVoteSender.Factory deleteVoteSenderFactory, + NotifyUtil notifyUtil) { this.db = db; this.batchUpdateFactory = batchUpdateFactory; this.approvalsUtil = approvalsUtil; @@ -87,6 +93,7 @@ this.userFactory = userFactory; this.voteDeleted = voteDeleted; this.deleteVoteSenderFactory = deleteVoteSenderFactory; + this.notifyUtil = notifyUtil; } @Override @@ -137,64 +144,66 @@ PatchSet.Id psId = change.currentPatchSetId(); ps = psUtil.current(db.get(), ctl.getNotes()); - PatchSetApproval psa = null; - StringBuilder msg = new StringBuilder(); - - // get all of the current approvals + boolean found = false; LabelTypes labelTypes = ctx.getControl().getLabelTypes(); - Map<String, Short> currentApprovals = new HashMap<>(); - for (LabelType lt : labelTypes.getLabelTypes()) { - currentApprovals.put(lt.getName(), (short) 0); - for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (lt.getLabelId().equals(a.getLabelId())) { - currentApprovals.put(lt.getName(), a.getValue()); - } - } - } - // removing votes so we need to determine the new set of approval scores - newApprovals.putAll(currentApprovals); + for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (ctl.canRemoveReviewer(a)) { - if (a.getLabel().equals(label)) { - // set the approval to 0 if vote is being removed - newApprovals.put(a.getLabel(), (short) 0); - // set old value only if the vote changed - oldApprovals.put(a.getLabel(), a.getValue()); - msg.append("Removed ") - .append(a.getLabel()).append(formatLabelValue(a.getValue())) - .append(" by ").append(userFactory.create(a.getAccountId()) - .getNameEmail()) - .append("\n"); - psa = a; - a.setValue((short)0); - ctx.getUpdate(psId).removeApprovalFor(a.getAccountId(), label); - break; - } - } else { + ctx.getDb(), ctl, psId, accountId)) { + if (labelTypes.byLabel(a.getLabelId()) == null) { + continue; // Ignore undefined labels. + } else if (!a.getLabel().equals(label)) { + // Populate map for non-matching labels, needed by VoteDeleted. + newApprovals.put(a.getLabel(), a.getValue()); + continue; + } else if (!ctl.canRemoveReviewer(a)) { throw new AuthException("delete vote not permitted"); } + // Set the approval to 0 if vote is being removed. + newApprovals.put(a.getLabel(), (short) 0); + found = true; + + // Set old value, as required by VoteDeleted. + oldApprovals.put(a.getLabel(), a.getValue()); + break; } - if (psa == null) { + if (!found) { throw new ResourceNotFoundException(); } - ctx.getDb().patchSetApprovals().update(Collections.singleton(psa)); - if (msg.length() > 0) { - changeMessage = - new ChangeMessage(new ChangeMessage.Key(change.getId(), - ChangeUtil.messageUUID(ctx.getDb())), - ctx.getAccountId(), - ctx.getWhen(), - change.currentPatchSetId()); - changeMessage.setMessage(msg.toString()); - cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), - changeMessage); + ctx.getUpdate(psId).removeApprovalFor(accountId, label); + if (PrimaryStorage.of(ctx.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to update non-existent entities. + 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(), + ChangeMessagesUtil.TAG_DELETE_VOTE); + 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) { @@ -202,13 +211,15 @@ } IdentifiedUser user = ctx.getIdentifiedUser(); - if (input.notify.compareTo(NotifyHandling.NONE) > 0) { + if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) { try { ReplyToChangeSender cm = deleteVoteSenderFactory.create( ctx.getProject(), change.getId()); cm.setFrom(user.getAccountId()); cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen()); cm.setNotify(input.notify); + cm.setAccountsToNotify( + notifyUtil.resolveAccounts(input.notifyDetails)); cm.send(); } catch (Exception e) { log.error("Cannot email update for change " + change.getId(), e); @@ -220,11 +231,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..9994085 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,19 +14,24 @@ 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.common.collect.Multimap; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.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.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.SendEmailExecutor; -import com.google.gerrit.server.mail.CommentSender; +import com.google.gerrit.server.mail.send.CommentSender; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gwtorm.server.OrmException; @@ -48,11 +53,14 @@ interface Factory { EmailReviewComments create( NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify, ChangeNotes notes, PatchSet patchSet, IdentifiedUser user, ChangeMessage message, - List<PatchLineComment> comments); + List<Comment> comments, + String patchSetComment, + List<LabelVote> labels); } private final ExecutorService sendEmailsExecutor; @@ -62,11 +70,14 @@ private final ThreadLocalRequestContext requestContext; private final NotifyHandling notify; + private final Multimap<RecipientType, Account.Id> accountsToNotify; private final ChangeNotes notes; private final PatchSet patchSet; private final IdentifiedUser user; private final ChangeMessage message; - private List<PatchLineComment> comments; + private final List<Comment> comments; + private final String patchSetComment; + private final List<LabelVote> labels; private ReviewDb db; @Inject @@ -77,22 +88,28 @@ SchemaFactory<ReviewDb> schemaFactory, ThreadLocalRequestContext requestContext, @Assisted NotifyHandling notify, + @Assisted Multimap<RecipientType, Account.Id> accountsToNotify, @Assisted ChangeNotes notes, @Assisted PatchSet patchSet, @Assisted IdentifiedUser user, @Assisted ChangeMessage message, - @Assisted List<PatchLineComment> comments) { + @Assisted List<Comment> comments, + @Nullable @Assisted String patchSetComment, + @Assisted List<LabelVote> labels) { this.sendEmailsExecutor = executor; this.patchSetInfoFactory = patchSetInfoFactory; this.commentSenderFactory = commentSenderFactory; this.schemaFactory = schemaFactory; this.requestContext = requestContext; this.notify = notify; + this.accountsToNotify = accountsToNotify; this.notes = notes; this.patchSet = patchSet; this.user = user; this.message = message; - this.comments = PLC_ORDER.sortedCopy(comments); + this.comments = COMMENT_ORDER.sortedCopy(comments); + this.patchSetComment = patchSetComment; + this.labels = labels; } void sendAsync() { @@ -110,8 +127,11 @@ cm.setPatchSet(patchSet, patchSetInfoFactory.get(notes.getProjectName(), patchSet)); cm.setChangeMessage(message.getMessage(), message.getWrittenOn()); - cm.setPatchLineComments(comments); + cm.setComments(comments); + cm.setPatchSetComment(patchSetComment); + cm.setLabels(labels); cm.setNotify(notify); + cm.setAccountsToNotify(accountsToNotify); cm.send(); } catch (Exception e) { log.error("Cannot email comments for " + patchSet.getId(), 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..5ad259b --- /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.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; + +import java.util.Optional; + +@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.ofNullable(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/GetBlame.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java index 0f0f5a6..408a1ae 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetBlame.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.change; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.extensions.common.BlameInfo; import com.google.gerrit.extensions.common.RangeInfo; import com.google.gerrit.extensions.restapi.BadRequestException; @@ -132,7 +132,8 @@ private List<BlameInfo> blame(ObjectId id, String path, Repository repository, RevWalk revWalk) throws IOException { - ListMultimap<BlameInfo, RangeInfo> ranges = ArrayListMultimap.create(); + ListMultimap<BlameInfo, RangeInfo> ranges = + MultimapBuilder.hashKeys().arrayListValues().build(); List<BlameInfo> result = new ArrayList<>(); if (blameCache.findLastCommit(repository, id, path) == null) { return result;
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/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDescription.java new file mode 100644 index 0000000..b8a34d2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDescription.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.server.change; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.inject.Singleton; + +@Singleton +public class GetDescription implements RestReadView<RevisionResource> { + @Override + public String apply(RevisionResource rsrc) { + return Strings.nullToEmpty(rsrc.getPatchSet().getDescription()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java index 5cf5895..d7c60f0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.base.MoreObjects; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -207,7 +206,7 @@ ? resource.getRevision().getEdit().get().getRefName() : resource.getRevision().getPatchSet().getRefName(); - FluentIterable<DiffWebLinkInfo> links = + List<DiffWebLinkInfo> links = webLinks.getDiffLinks(state.getProject().getName(), resource.getPatchKey().getParentKey().getParentKey().get(), basePatchSet != null ? basePatchSet.getId().get() : null, @@ -216,7 +215,7 @@ resource.getPatchKey().getParentKey().get(), revB, ps.getNewName()); - result.webLinks = links.isEmpty() ? null : links.toList(); + result.webLinks = links.isEmpty() ? null : links; if (!webLinksOnly) { if (ps.isBinary()) { @@ -281,9 +280,9 @@ private List<WebLinkInfo> getFileWebLinks(Project project, String rev, String file) { - FluentIterable<WebLinkInfo> links = + List<WebLinkInfo> links = webLinks.getFileLinks(project.getName(), rev, file); - return links.isEmpty() ? null : links.toList(); + return links.isEmpty() ? null : links; } public GetDiff setBase(String base) {
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/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java index a13e7be..365b204 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -18,6 +18,7 @@ import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.GitRepositoryManager; @@ -30,6 +31,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; import org.kohsuke.args4j.Option; import java.io.IOException; @@ -43,20 +45,25 @@ public class GetPatch implements RestReadView<RevisionResource> { private final GitRepositoryManager repoManager; + private final String FILE_NOT_FOUND = "File not found: %s."; + @Option(name = "--zip") private boolean zip; @Option(name = "--download") private boolean download; + @Option(name = "--path") + private String path; + @Inject GetPatch(GitRepositoryManager repoManager) { this.repoManager = repoManager; } @Override - public BinaryResult apply(RevisionResource rsrc) - throws ResourceConflictException, IOException { + public BinaryResult apply(RevisionResource rsrc) throws + ResourceConflictException, IOException, ResourceNotFoundException { Project.NameKey project = rsrc.getControl().getProject().getNameKey(); final Repository repo = repoManager.openRepository(project); boolean close = true; @@ -93,9 +100,15 @@ } private void format(OutputStream out) throws IOException { - out.write(formatEmailHeader(commit).getBytes(UTF_8)); + // Only add header if no path is specified + if (path == null) { + out.write(formatEmailHeader(commit).getBytes(UTF_8)); + } try (DiffFormatter fmt = new DiffFormatter(out)) { fmt.setRepository(repo); + if (path != null) { + fmt.setPathFilter(PathFilter.create(path)); + } fmt.format(base.getTree(), commit.getTree()); fmt.flush(); } @@ -108,6 +121,11 @@ } }; + if (path != null && bin.asString().isEmpty()) { + throw new ResourceNotFoundException( + String.format(FILE_NOT_FOUND, path)); + } + if (zip) { bin.disableGzip() .setContentType("application/zip") @@ -134,6 +152,11 @@ } } + public GetPatch setPath(String path) { + this.path = path; + return this; + } + private static String formatEmailHeader(RevCommit commit) { StringBuilder b = new StringBuilder(); PersonIdent author = commit.getAuthorIdent();
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..57e5cea 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; @@ -59,7 +59,8 @@ } @Override - public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) { + public Response<Map<String, ActionInfo>> apply(RevisionResource rsrc) + throws OrmException { return Response.withMustRevalidate(delegate.format(rsrc)); } @@ -72,7 +73,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/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java index 344cb44..d0ba624 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.change; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.extensions.config.ExternalIncludedIn; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.BadRequestException; @@ -81,7 +81,8 @@ } IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev); - Multimap<String, String> external = ArrayListMultimap.create(); + Multimap<String, String> external = + MultimapBuilder.hashKeys().arrayListValues().build(); for (ExternalIncludedIn ext : includedIn) { Multimap<String, String> extIncludedIns = ext.getIncludedIn( project.get(), rev.name(), d.getTags(), d.getBranches());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java new file mode 100644 index 0000000..43b796a2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/LimitedByteArrayOutputStream.java
@@ -0,0 +1,70 @@ +// 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 com.google.common.base.Preconditions.checkArgument; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +class LimitedByteArrayOutputStream extends OutputStream { + + private final int maxSize; + private final ByteArrayOutputStream buffer; + + /** + * Constructs a LimitedByteArrayOutputStream, which stores output + * in memory up to a certain specified size. When the output exceeds + * the specified size a LimitExceededException is thrown. + * + * @param max the maximum size in bytes which may be stored. + * @param initial the initial size. It must be smaller than the max size. + */ + public LimitedByteArrayOutputStream(int max, int initial) { + checkArgument(initial <= max); + maxSize = max; + buffer = new ByteArrayOutputStream(initial); + } + + private void checkOversize(int additionalSize) throws IOException { + if (buffer.size() + additionalSize > maxSize) { + throw new LimitExceededException(); + } + } + + @Override + public void write(int b) throws IOException{ + checkOversize(1); + buffer.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + checkOversize(len); + buffer.write(b, off, len); + } + + /** + * @return a newly allocated byte array with contents of the buffer. + */ + public byte[] toByteArray() { + return buffer.toByteArray(); + } + + class LimitExceededException extends IOException { + private static final long serialVersionUID = 1L; + } +}
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/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java index 62d75aa..a6e0935 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -64,12 +64,15 @@ private static final String CACHE_NAME = "mergeability"; - public static final BiMap<SubmitType, Character> SUBMIT_TYPES = ImmutableBiMap.of( - SubmitType.FAST_FORWARD_ONLY, 'F', - SubmitType.MERGE_IF_NECESSARY, 'M', - SubmitType.REBASE_IF_NECESSARY, 'R', - SubmitType.MERGE_ALWAYS, 'A', - SubmitType.CHERRY_PICK, 'C'); + public static final BiMap<SubmitType, Character> SUBMIT_TYPES = + new ImmutableBiMap.Builder<SubmitType, Character>() + .put(SubmitType.FAST_FORWARD_ONLY, 'F') + .put(SubmitType.MERGE_IF_NECESSARY, 'M') + .put(SubmitType.REBASE_ALWAYS, 'P') + .put(SubmitType.REBASE_IF_NECESSARY, 'R') + .put(SubmitType.MERGE_ALWAYS, 'A') + .put(SubmitType.CHERRY_PICK, 'C') + .build(); static { checkState(SUBMIT_TYPES.size() == SubmitType.values().length,
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..4d32222 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); @@ -50,9 +53,14 @@ DynamicMap.mapOf(binder(), VOTE_KIND); get(CHANGE_KIND).to(GetChange.class); + post(CHANGE_KIND, "merge").to(CreateMergePatchSet.class); 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); @@ -60,7 +68,7 @@ post(CHANGE_KIND, "check").to(Check.class); put(CHANGE_KIND, "topic").to(PutTopic.class); delete(CHANGE_KIND, "topic").to(PutTopic.class); - delete(CHANGE_KIND).to(DeleteDraftChange.class); + delete(CHANGE_KIND).to(DeleteChange.class); post(CHANGE_KIND, "abandon").to(Abandon.class); post(CHANGE_KIND, "hashtags").to(PostHashtags.class); post(CHANGE_KIND, "publish").to(PublishDraftPatchSet.CurrentRevision.class); @@ -78,6 +86,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,13 +101,17 @@ 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); + put(REVISION_KIND, "description").to(PutDescription.class); + get(REVISION_KIND, "description").to(GetDescription.class); get(REVISION_KIND, "patch").to(GetPatch.class); get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class); 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 +122,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 +152,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..4a3f45a 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,9 @@ 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(), ChangeMessagesUtil.TAG_MOVE); cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java new file mode 100644 index 0000000..2bab427 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -0,0 +1,122 @@ +// 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.joining; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimap; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.NotifyInfo; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.AccountResolver; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +@Singleton +public class NotifyUtil { + private final Provider<ReviewDb> dbProvider; + private final AccountResolver accountResolver; + + @Inject + NotifyUtil(Provider<ReviewDb> dbProvider, + AccountResolver accountResolver) { + this.dbProvider = dbProvider; + this.accountResolver = accountResolver; + } + + public static boolean shouldNotify(NotifyHandling notify, + @Nullable Map<RecipientType, NotifyInfo> notifyDetails) { + if (!isNullOrEmpty(notifyDetails)) { + return true; + } + + return notify.compareTo(NotifyHandling.NONE) > 0; + } + + private static boolean isNullOrEmpty( + @Nullable Map<RecipientType, NotifyInfo> notifyDetails) { + if (notifyDetails == null || notifyDetails.isEmpty()) { + return true; + } + + for (NotifyInfo notifyInfo : notifyDetails.values()) { + if (!isEmpty(notifyInfo)) { + return false; + } + } + + return true; + } + + private static boolean isEmpty(NotifyInfo notifyInfo) { + return notifyInfo.accounts == null || notifyInfo.accounts.isEmpty(); + } + + public Multimap<RecipientType, Account.Id> resolveAccounts( + @Nullable Map<RecipientType, NotifyInfo> notifyDetails) + throws OrmException, BadRequestException { + if (isNullOrEmpty(notifyDetails)) { + return ImmutableListMultimap.of(); + } + + Multimap<RecipientType, Account.Id> m = null; + for (Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) { + List<String> accounts = e.getValue().accounts; + if (accounts != null) { + if (m == null) { + m = ArrayListMultimap.create(); + } + m.putAll(e.getKey(), find(dbProvider.get(), accounts)); + } + } + + return m != null ? m : ImmutableListMultimap.of(); + } + + private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails) + throws OrmException, BadRequestException { + List<String> missing = new ArrayList<>(nameOrEmails.size()); + List<Account.Id> r = new ArrayList<>(nameOrEmails.size()); + for (String nameOrEmail : nameOrEmails) { + Account a = accountResolver.find(db, nameOrEmail); + if (a != null) { + r.add(a.getId()); + } else { + missing.add(nameOrEmail); + } + } + + if (!missing.isEmpty()) { + throw new BadRequestException( + "The following accounts that should be notified could not be resolved: " + + missing.stream().distinct().sorted().collect(joining(", "))); + } + + return r; + } +}
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..995a869 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
@@ -19,9 +19,13 @@ import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimap; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; +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; @@ -30,30 +34,26 @@ 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; import com.google.gerrit.server.git.BatchUpdate.RepoContext; import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidators; -import com.google.gerrit.server.mail.ReplacePatchSetSender; +import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.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 +91,16 @@ private final ChangeControl origCtl; // Fields exposed as setters. - private SshInfo sshInfo; private String message; + private String description; 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 Multimap<RecipientType, Account.Id> accountsToNotify = + ImmutableListMultimap.of(); private boolean allowClosed; private boolean copyApprovals = true; @@ -144,8 +146,8 @@ return this; } - public PatchSetInserter setSshInfo(SshInfo sshInfo) { - this.sshInfo = sshInfo; + public PatchSetInserter setDescription(String description) { + this.description = description; return this; } @@ -170,8 +172,14 @@ return this; } - public PatchSetInserter setSendMail(boolean sendMail) { - this.sendMail = sendMail; + public PatchSetInserter setNotify(NotifyHandling notify) { + this.notify = notify; + return this; + } + + public PatchSetInserter setAccountsToNotify( + Multimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); return this; } @@ -198,7 +206,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)); @@ -228,16 +235,16 @@ } } patchSet = psUtil.insert(db, ctx.getRevWalk(), ctx.getUpdate(psId), - psId, commit, draft, newGroups, null); + psId, commit, draft, newGroups, null, description); - 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, + ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); changeMessage.setMessage(message); } @@ -257,7 +264,7 @@ @Override public void postUpdate(Context ctx) throws OrmException { - if (sendMail) { + if (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty()) { try { ReplacePatchSetSender cm = replacePatchSetFactory.create( ctx.getProject(), change.getId()); @@ -266,6 +273,8 @@ cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen()); cm.addReviewers(oldReviewers.byState(REVIEWER)); cm.addExtraCC(oldReviewers.byState(CC)); + cm.setNotify(notify); + cm.setAccountsToNotify(accountsToNotify); cm.send(); } catch (Exception err) { log.error("Cannot send email for new patch set on change " @@ -273,30 +282,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 +309,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..a0a3296 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,14 @@ 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.common.base.Preconditions.checkState; +import static com.google.gerrit.server.CommentsUtil.setCommentRevId; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import com.google.auto.value.AutoValue; @@ -28,8 +30,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; 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.hash.HashCode; import com.google.common.hash.Hashing; import com.google.gerrit.common.Nullable; @@ -41,32 +43,47 @@ 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.RecipientType; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput; import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling; +import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; import com.google.gerrit.extensions.api.changes.ReviewResult; +import com.google.gerrit.extensions.api.changes.ReviewerInfo; +import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.client.Side; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.common.FixReplacementInfo; +import com.google.gerrit.extensions.common.FixSuggestionInfo; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.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; import com.google.gerrit.extensions.restapi.Url; +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.CommentRange; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.FixReplacement; +import com.google.gerrit.reviewdb.client.FixSuggestion; +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 +92,8 @@ 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.NoteDbChangeState.PrimaryStorage; +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; @@ -97,7 +116,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; @Singleton public class PostReview implements RestModifyView<RevisionResource, ReviewInput> { @@ -109,13 +130,15 @@ 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; + private final NotifyUtil notifyUtil; @Inject PostReview(Provider<ReviewDb> db, @@ -124,18 +147,20 @@ 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, + NotifyUtil notifyUtil) { 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 +169,8 @@ this.email = email; this.commentAdded = commentAdded; this.postReviewers = postReviewers; + this.migration = migration; + this.notifyUtil = notifyUtil; } @Override @@ -162,6 +189,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,11 +198,20 @@ 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; } + Multimap<RecipientType, Account.Id> accountsToNotify = + notifyUtil.resolveAccounts(input.notifyDetails); + Map<String, AddReviewerResult> reviewerJsonResults = null; List<PostReviewers.Addition> reviewerResults = Lists.newArrayList(); boolean hasError = false; @@ -181,8 +219,11 @@ if (input.reviewers != null) { reviewerJsonResults = Maps.newHashMap(); for (AddReviewerInput reviewerInput : input.reviewers) { + // Prevent notifications because setting reviewers is batched. + reviewerInput.notify = NotifyHandling.NONE; + PostReviewers.Addition result = postReviewers.prepareApplication( - revision.getChangeResource(), reviewerInput); + revision.getChangeResource(), reviewerInput, true); reviewerJsonResults.put(reviewerInput.reviewer, result.result); if (result.result.error != null) { hasError = true; @@ -205,24 +246,79 @@ try (BatchUpdate bu = batchUpdateFactory.create(db.get(), revision.getChange().getProject(), revision.getUser(), ts)) { + Account.Id id = bu.getUser().getAccountId(); + boolean ccOrReviewer = input.labels != null; + + if (!ccOrReviewer) { + // Check if user was already CCed or reviewing prior to this review. + ReviewerSet currentReviewers = approvalsUtil.getReviewers( + db.get(), revision.getChangeResource().getNotes()); + ccOrReviewer = currentReviewers.all().contains(id); + } + // Apply reviewer changes first. Revision emails should be sent to the - // updated set of reviewers. + // updated set of reviewers. Also keep track of whether the user added + // themselves as a reviewer or to the CC list. for (PostReviewers.Addition reviewerResult : reviewerResults) { bu.addOp(revision.getChange().getId(), reviewerResult.op); + if (!ccOrReviewer && reviewerResult.result.reviewers != null) { + for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) { + if (Objects.equals(id.get(), reviewerInfo._accountId)) { + ccOrReviewer = true; + break; + } + } + } + if (!ccOrReviewer && reviewerResult.result.ccs != null) { + for (AccountInfo accountInfo : reviewerResult.result.ccs) { + if (Objects.equals(id.get(), accountInfo._accountId)) { + ccOrReviewer = true; + break; + } + } + } } - bu.addOp( - revision.getChange().getId(), - new Op(revision.getPatchSet().getId(), input)); + + if (!ccOrReviewer) { + // User posting this review isn't currently in the reviewer or CC list, + // isn't being explicitly added, and isn't voting on any label. + // Automatically CC them on this change so they receive replies. + PostReviewers.Addition selfAddition = + postReviewers.ccCurrentUser(bu.getUser(), revision); + bu.addOp(revision.getChange().getId(), selfAddition.op); + } + + bu.addOp(revision.getChange().getId(), + new Op(revision.getPatchSet().getId(), input, accountsToNotify, + reviewerResults)); bu.execute(); for (PostReviewers.Addition reviewerResult : reviewerResults) { reviewerResult.gatherResults(); } + + emailReviewers(revision.getChange(), reviewerResults, input.notify, + accountsToNotify); } return Response.ok(output); } + private void emailReviewers(Change change, + List<PostReviewers.Addition> reviewerAdditions, NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) { + List<Account.Id> to = new ArrayList<>(); + List<Account.Id> cc = new ArrayList<>(); + for (PostReviewers.Addition addition : reviewerAdditions) { + if (addition.op.state == ReviewerState.REVIEWER) { + to.addAll(addition.op.reviewers.keySet()); + } else if (addition.op.state == ReviewerState.CC) { + cc.addAll(addition.op.reviewers.keySet()); + } + } + postReviewers.emailReviewers(change, to, cc, notify, accountsToNotify); + } + private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in) throws BadRequestException, AuthException, UnprocessableEntityException, OrmException { @@ -231,6 +327,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(); @@ -245,6 +348,10 @@ continue; } + if (caller.getUser().isInternalUser()) { + continue; + } + PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName())); if (r == null || r.isEmpty() || !r.contains(ent.getValue())) { throw new AuthException(String.format( @@ -258,7 +365,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,94 +425,273 @@ } } - private void checkComments(RevisionResource revision, Map<String, List<CommentInput>> in) + private <T extends CommentInput> void checkComments(RevisionResource revision, + Map<String, List<T>> commentsPerPath) throws BadRequestException, OrmException { - Iterator<Map.Entry<String, List<CommentInput>>> 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(); - String path = ent.getKey(); - if (!filePaths.contains(path) && !Patch.COMMIT_MSG.equals(path)) { - throw new BadRequestException(String.format( - "file %s not found in revision %s", - path, revision.getChange().currentPatchSetId())); - } + cleanUpComments(commentsPerPath); + ensureCommentsAreAddable(revision, commentsPerPath); + } - List<CommentInput> list = ent.getValue(); - if (list == null) { - mapItr.remove(); + private <T extends CommentInput> void cleanUpComments( + Map<String, List<T>> commentsPerPath) { + Iterator<List<T>> mapValueIterator = commentsPerPath.values().iterator(); + while (mapValueIterator.hasNext()) { + List<T> comments = mapValueIterator.next(); + if (comments == null) { + mapValueIterator.remove(); continue; } - Iterator<CommentInput> listItr = list.iterator(); - while (listItr.hasNext()) { - CommentInput c = listItr.next(); - if (c == null) { - listItr.remove(); - continue; - } - if (c.line != null && c.line < 0) { - throw new BadRequestException(String.format( - "negative line number %d not allowed on %s", - c.line, path)); - } - c.message = Strings.nullToEmpty(c.message).trim(); - if (c.message.isEmpty()) { - listItr.remove(); - } - } - if (list.isEmpty()) { - mapItr.remove(); + cleanUpComments(comments); + + if (comments.isEmpty()) { + mapValueIterator.remove(); } } } + private <T extends CommentInput> void cleanUpComments(List<T> comments) { + Iterator<T> commentsIterator = comments.iterator(); + while (commentsIterator.hasNext()) { + T comment = commentsIterator.next(); + if (comment == null) { + commentsIterator.remove(); + continue; + } + + comment.message = Strings.nullToEmpty(comment.message).trim(); + if (comment.message.isEmpty()) { + commentsIterator.remove(); + } + } + } + + private <T extends CommentInput> void ensureCommentsAreAddable( + RevisionResource revision, Map<String, List<T>> commentsPerPath) + throws OrmException, BadRequestException { + Set<String> revisionFilePaths = getAffectedFilePaths(revision); + for (Map.Entry<String, List<T>> entry : commentsPerPath.entrySet()) { + String path = entry.getKey(); + PatchSet.Id patchSetId = revision.getChange().currentPatchSetId(); + ensurePathRefersToAvailableOrMagicFile(path, revisionFilePaths, + patchSetId); + + List<T> comments = entry.getValue(); + for (T comment : comments) { + ensureLineIsNonNegative(comment.line, path); + ensureCommentNotOnMagicFilesOfAutoMerge(path, comment); + } + } + } + + private Set<String> getAffectedFilePaths(RevisionResource revision) + throws OrmException { + ChangeData changeData = changeDataFactory.create(db.get(), + revision.getControl()); + return new HashSet<>(changeData.filePaths(revision.getPatchSet())); + } + + private void ensurePathRefersToAvailableOrMagicFile(String path, + Set<String> availableFilePaths, PatchSet.Id patchSetId) + throws BadRequestException { + if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) { + throw new BadRequestException(String.format( + "file %s not found in revision %s", path, patchSetId)); + } + } + + private void ensureLineIsNonNegative(Integer line, String path) + throws BadRequestException { + if (line != null && line < 0) { + throw new BadRequestException(String.format( + "negative line number %d not allowed on %s", line, path)); + } + } + + private <T extends CommentInput> void ensureCommentNotOnMagicFilesOfAutoMerge( + String path, T comment) throws BadRequestException { + if (Patch.isMagic(path) && comment.side == Side.PARENT + && comment.parent == null) { + throw new BadRequestException( + String.format("cannot comment on %s on auto-merge", path)); + } + } + + private void checkRobotComments(RevisionResource revision, + Map<String, List<RobotCommentInput>> in) + throws BadRequestException, OrmException { + for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) { + String commentPath = e.getKey(); + for (RobotCommentInput c : e.getValue()) { + ensureRobotIdIsSet(c.robotId, commentPath); + ensureRobotRunIdIsSet(c.robotRunId, commentPath); + ensureFixSuggestionsAreAddable(c.fixSuggestions, commentPath); + } + } + checkComments(revision, in); + } + + private void ensureRobotIdIsSet(String robotId, String commentPath) + throws BadRequestException { + if (robotId == null) { + throw new BadRequestException(String + .format("robotId is missing for robot comment on %s", commentPath)); + } + } + + private void ensureRobotRunIdIsSet(String robotRunId, String commentPath) + throws BadRequestException { + if (robotRunId == null) { + throw new BadRequestException(String + .format("robotRunId is missing for robot comment on %s", + commentPath)); + } + } + + private void ensureFixSuggestionsAreAddable( + List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) + throws BadRequestException { + if (fixSuggestionInfos == null) { + return; + } + + for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) { + ensureDescriptionIsSet(commentPath, fixSuggestionInfo.description); + ensureFixReplacementsAreAddable(commentPath, + fixSuggestionInfo.replacements); + } + } + + private void ensureDescriptionIsSet(String commentPath, String description) + throws BadRequestException { + if (description == null) { + throw new BadRequestException(String.format("A description is required " + + "for the suggested fix of the robot comment on %s", commentPath)); + } + } + + private void ensureFixReplacementsAreAddable(String commentPath, + List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException { + ensureReplacementsArePresent(commentPath, fixReplacementInfos); + + for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) { + ensureReplacementPathIsSet(commentPath, fixReplacementInfo.path); + ensureReplacementPathRefersToFileOfComment(commentPath, + fixReplacementInfo.path); + ensureRangeIsSet(commentPath, fixReplacementInfo.range); + ensureRangeIsValid(commentPath, fixReplacementInfo.range); + ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement); + } + } + + private void ensureReplacementsArePresent(String commentPath, + List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException { + if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) { + throw new BadRequestException(String.format("At least one replacement is " + + "required for the suggested fix of the robot comment on %s", + commentPath)); + } + } + + private void ensureReplacementPathIsSet(String commentPath, + String replacementPath) throws BadRequestException { + if (replacementPath == null) { + throw new BadRequestException(String.format("A file path must be given " + + "for the replacement of the robot comment on %s", commentPath)); + } + } + + private void ensureReplacementPathRefersToFileOfComment(String commentPath, + String replacementPath) throws BadRequestException { + if (!Objects.equals(commentPath, replacementPath)) { + throw new BadRequestException(String.format("Replacements may only be " + + "specified for the file %s on which the robot comment was added", + commentPath)); + } + } + + private void ensureRangeIsSet(String commentPath, + com.google.gerrit.extensions.client.Comment.Range range) + throws BadRequestException { + if (range == null) { + throw new BadRequestException(String.format("A range must be given " + + "for the replacement of the robot comment on %s", commentPath)); + } + } + + private void ensureRangeIsValid(String commentPath, + com.google.gerrit.extensions.client.Comment.Range range) + throws BadRequestException { + if (range == null) { + return; + } + if (!range.isValid()) { + throw new BadRequestException(String.format("Range (%s:%s - %s:%s) is not" + + " valid for the replacement of the robot comment on %s", + range.startLine, range.startCharacter, range.endLine, + range.endCharacter, commentPath)); + } + } + + private void ensureReplacementStringIsSet(String commentPath, + String replacement) throws BadRequestException { + if (replacement == null) { + throw new BadRequestException(String.format("A content for replacement " + + "must be indicated for the replacement of the robot comment on %s", + commentPath)); + } + } + /** - * 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 Multimap<RecipientType, Account.Id> accountsToNotify; + 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<String> labelDelta = new ArrayList<>(); + private List<Comment> comments = new ArrayList<>(); + private List<LabelVote> 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, + Multimap<RecipientType, Account.Id> accountsToNotify, + List<PostReviewers.Addition> reviewerResults) { this.psId = psId; this.in = in; + this.accountsToNotify = checkNotNull(accountsToNotify); + this.reviewerResults = reviewerResults; } @Override @@ -410,24 +702,29 @@ 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; } @Override - public void postUpdate(Context ctx) { + public void postUpdate(Context ctx) throws OrmException { if (message == null) { return; } - if (in.notify.compareTo(NotifyHandling.NONE) > 0) { + if (in.notify.compareTo(NotifyHandling.NONE) > 0 + || !accountsToNotify.isEmpty()) { email.create( in.notify, + accountsToNotify, notes, ps, user, message, - comments).sendAsync(); + comments, + in.message, + labelDelta).sendAsync(); } commentAdded.fire( notes.getChange(), ps, user.getAccount(), message.getMessage(), @@ -440,7 +737,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 +746,180 @@ } } - 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> newRobotComments = getNewRobotComments(ctx); + commentsUtil.putRobotComments(ctx.getUpdate(psId), newRobotComments); + comments.addAll(newRobotComments); + return !newRobotComments.isEmpty(); + } + + private List<RobotComment> getNewRobotComments(ChangeContext ctx) + throws OrmException { + 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 = createRobotCommentFromInput(ctx, path, c); + if (existingIds.contains(CommentSetEntry.create(e))) { + continue; + } + toAdd.add(e); + } + } + return toAdd; + } + + private RobotComment createRobotCommentFromInput(ChangeContext ctx, + String path, RobotCommentInput robotCommentInput) throws OrmException { + RobotComment robotComment = commentsUtil.newRobotComment( + ctx, path, psId, robotCommentInput.side(), robotCommentInput.message, + robotCommentInput.robotId, robotCommentInput.robotRunId); + robotComment.parentUuid = Url.decode(robotCommentInput.inReplyTo); + robotComment.url = robotCommentInput.url; + robotComment.properties = robotCommentInput.properties; + robotComment.setLineNbrAndRange(robotCommentInput.line, + robotCommentInput.range); + robotComment.tag = in.tag; + setCommentRevId(robotComment, patchListCache, ctx.getChange(), ps); + robotComment.fixSuggestions = createFixSuggestionsFromInput(ctx, + robotCommentInput.fixSuggestions); + return robotComment; + } + + private List<FixSuggestion> createFixSuggestionsFromInput(ChangeContext ctx, + List<FixSuggestionInfo> fixSuggestionInfos) throws OrmException { + if (fixSuggestionInfos == null) { + return Collections.emptyList(); + } + + List<FixSuggestion> fixSuggestions = + new ArrayList<>(fixSuggestionInfos.size()); + for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) { + fixSuggestions.add( + createFixSuggestionFromInput(ctx, fixSuggestionInfo)); + } + return fixSuggestions; + } + + private FixSuggestion createFixSuggestionFromInput(ChangeContext ctx, + FixSuggestionInfo fixSuggestionInfo) throws OrmException { + List<FixReplacement> fixReplacements = + toFixReplacements(fixSuggestionInfo.replacements); + String fixId = ChangeUtil.messageUUID(ctx.getDb()); + return new FixSuggestion(fixId, fixSuggestionInfo.description, + fixReplacements); + } + + private List<FixReplacement> toFixReplacements( + List<FixReplacementInfo> fixReplacementInfos) { + return fixReplacementInfos.stream() + .map(this::toFixReplacement) + .collect(Collectors.toList()); + } + + private FixReplacement toFixReplacement( + FixReplacementInfo fixReplacementInfo) { + Comment.Range range = new Comment.Range(fixReplacementInfo.range); + return new FixReplacement(fixReplacementInfo.path, range, + fixReplacementInfo.replacement); } private Set<CommentSetEntry> readExistingComments(ChangeContext ctx) 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 +933,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 +958,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 +995,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 +1061,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 +1072,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); @@ -685,14 +1085,88 @@ } } - if ((!del.isEmpty() || !ups.isEmpty()) - && ctx.getChange().getStatus().isClosed()) { + validatePostSubmitLabels(ctx, labelTypes, previous, ups, del); + + // 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); + if (PrimaryStorage.of(update.getChange()) == REVIEW_DB) { + // Avoid OrmConcurrencyException trying to delete non-existent entities. + ctx.getDb().patchSetApprovals().delete(del); + ctx.getDb().patchSetApprovals().upsert(ups); + } + return !del.isEmpty() || !ups.isEmpty(); + } + + private void validatePostSubmitLabels(ChangeContext ctx, + LabelTypes labelTypes, Map<String, Short> previous, + List<PatchSetApproval> ups, List<PatchSetApproval> del) + throws ResourceConflictException { + if (ctx.getChange().getStatus().isOpen()) { + return; // Not closed, nothing to validate. + } else if (del.isEmpty() && ups.isEmpty()) { + return; // No new votes. + } else if (ctx.getChange().getStatus() != Change.Status.MERGED) { throw new ResourceConflictException("change is closed"); } - forceCallerAsReviewer(ctx, current, ups, del); - ctx.getDb().patchSetApprovals().delete(del); - ctx.getDb().patchSetApprovals().upsert(ups); - return !del.isEmpty() || !ups.isEmpty(); + + // Disallow reducing votes on any labels post-submit. This assumes the + // high values were broadly necessary to submit, so reducing them would + // make it possible to take a merged change and make it no longer + // submittable. + List<PatchSetApproval> reduced = new ArrayList<>(ups.size() + del.size()); + List<String> disallowed = + new ArrayList<>(labelTypes.getLabelTypes().size()); + + for (PatchSetApproval psa : del) { + LabelType lt = checkNotNull(labelTypes.byLabel(psa.getLabel())); + String normName = lt.getName(); + if (!lt.allowPostSubmit()) { + disallowed.add(normName); + } + Short prev = previous.get(normName); + if (prev != null && prev != 0) { + reduced.add(psa); + } + } + + for (PatchSetApproval psa : ups) { + LabelType lt = checkNotNull(labelTypes.byLabel(psa.getLabel())); + String normName = lt.getName(); + if (!lt.allowPostSubmit()) { + disallowed.add(normName); + } + Short prev = previous.get(normName); + if (prev == null) { + continue; + } + checkState(prev != psa.getValue()); // Should be filtered out above. + if (prev > psa.getValue()) { + reduced.add(psa); + } else { + // Set postSubmit bit in ReviewDb; not required for NoteDb, which sets + // it automatically. + psa.setPostSubmit(true); + } + } + + if (!disallowed.isEmpty()) { + throw new ResourceConflictException( + "Voting on labels disallowed after submit: " + + disallowed.stream().distinct().sorted() + .collect(joining(", "))); + } + if (!reduced.isEmpty()) { + throw new ResourceConflictException( + "Cannot reduce vote on labels for closed change: " + + reduced.stream().map(p -> p.getLabel()).distinct().sorted() + .collect(joining(", "))); + } } private void forceCallerAsReviewer(ChangeContext ctx, @@ -703,12 +1177,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); @@ -752,8 +1224,8 @@ String msg = Strings.nullToEmpty(in.message).trim(); StringBuilder buf = new StringBuilder(); - for (String d : labelDelta) { - buf.append(" ").append(d); + for (LabelVote d : labelDelta) { + buf.append(" ").append(d.format()); } if (comments.size() == 1) { buf.append("\n\n(1 comment)"); @@ -767,23 +1239,15 @@ return false; } - message = new ChangeMessage( - new ChangeMessage.Key( - psId.getParentKey(), ChangeUtil.messageUUID(ctx.getDb())), - user.getAccountId(), - ctx.getWhen(), - psId); - message.setTag(in.tag); - message.setMessage(String.format( - "Patch Set %d:%s", - psId.get(), - buf.toString())); + message = ChangeMessagesUtil.newMessage( + ctx.getDb(), psId, user, ctx.getWhen(), + "Patch Set " + psId.get() + ":" + buf, in.tag); cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), message); return true; } private void addLabelDelta(String name, short value) { - labelDelta.add(LabelVote.create(name, value).format()); + labelDelta.add(LabelVote.create(name, value)); } } }
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..a17447f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -14,17 +14,22 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.gerrit.extensions.client.ReviewerState.CC; import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.GroupDescription; 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.RecipientType; import com.google.gerrit.extensions.api.changes.ReviewerInfo; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.restapi.BadRequestException; @@ -38,6 +43,7 @@ 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.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.account.AccountCache; @@ -52,7 +58,7 @@ import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.group.SystemGroupBackend; -import com.google.gerrit.server.mail.AddReviewerSender; +import com.google.gerrit.server.mail.send.AddReviewerSender; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; @@ -96,10 +102,11 @@ 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; + private final NotifyUtil notifyUtil; @Inject PostReviewers(AccountsCollection accounts, @@ -115,10 +122,11 @@ Provider<IdentifiedUser> user, IdentifiedUser.GenericFactory identifiedUserFactory, @GerritServerConfig Config cfg, - AccountCache accountCache, ReviewerJson json, ReviewerAdded reviewerAdded, - NotesMigration migration) { + NotesMigration migration, + AccountCache accountCache, + NotifyUtil notifyUtil) { this.accounts = accounts; this.reviewerFactory = reviewerFactory; this.approvalsUtil = approvalsUtil; @@ -132,10 +140,11 @@ this.user = user; this.identifiedUserFactory = identifiedUserFactory; this.cfg = cfg; - this.accountCache = accountCache; this.json = json; this.reviewerAdded = reviewerAdded; this.migration = migration; + this.accountCache = accountCache; + this.notifyUtil = notifyUtil; } @Override @@ -145,7 +154,7 @@ throw new BadRequestException("missing reviewer field"); } - Addition addition = prepareApplication(rsrc, input); + Addition addition = prepareApplication(rsrc, input, true); if (addition.op == null) { return addition.result; } @@ -159,32 +168,54 @@ return addition.result; } - public Addition prepareApplication(ChangeResource rsrc, AddReviewerInput input) - throws OrmException, RestApiException, IOException { + public Addition prepareApplication(ChangeResource rsrc, + AddReviewerInput input, boolean allowGroup) + throws OrmException, RestApiException, IOException { Account.Id accountId; try { accountId = accounts.parse(input.reviewer).getAccountId(); } catch (UnprocessableEntityException e) { - try { - return putGroup(rsrc, input); - } catch (UnprocessableEntityException e2) { - throw new UnprocessableEntityException(MessageFormat - .format(ChangeMessages.get().reviewerNotFound, input.reviewer)); + if (allowGroup) { + try { + return putGroup(rsrc, input); + } catch (UnprocessableEntityException e2) { + throw new UnprocessableEntityException(MessageFormat.format( + ChangeMessages.get().reviewerNotFoundUserOrGroup, + input.reviewer)); + } } + throw new UnprocessableEntityException(MessageFormat + .format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer)); } return putAccount(input.reviewer, reviewerFactory.create(rsrc, accountId), - input.state()); + input.state(), input.notify, + notifyUtil.resolveAccounts(input.notifyDetails)); + } + + Addition ccCurrentUser(CurrentUser user, RevisionResource revision) { + return new Addition( + user.getUserName(), revision.getChangeResource(), + ImmutableMap.of(user.getAccountId(), revision.getControl()), + CC, NotifyHandling.NONE, ImmutableListMultimap.of()); } private Addition putAccount(String reviewer, ReviewerResource rsrc, - ReviewerState state) throws UnprocessableEntityException { + ReviewerState state, NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) + 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, + accountsToNotify); } - 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 +265,8 @@ } } - return new Addition(input.reviewer, rsrc, reviewers, input.state()); + return new Addition(input.reviewer, rsrc, reviewers, input.state(), + input.notify, notifyUtil.resolveAccounts(input.notifyDetails)); } private boolean isValidReviewer(Account member, ChangeControl control) { @@ -258,18 +290,20 @@ 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, ImmutableListMultimap.of()); } protected Addition(String reviewer, ChangeResource rsrc, - Map<Account.Id, ChangeControl> reviewers, ReviewerState state) { + Map<Account.Id, ChangeControl> reviewers, ReviewerState state, + NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) { result = new AddReviewerResult(reviewer); if (reviewers == null) { this.reviewers = ImmutableMap.of(); @@ -277,7 +311,7 @@ return; } this.reviewers = reviewers; - op = new Op(rsrc, reviewers, state); + op = new Op(rsrc, reviewers, state, notify, accountsToNotify); } void gatherResults() throws OrmException { @@ -304,9 +338,11 @@ } } - class Op extends BatchUpdate.Op { + public class Op extends BatchUpdate.Op { final Map<Account.Id, ChangeControl> reviewers; final ReviewerState state; + final NotifyHandling notify; + final Multimap<RecipientType, Account.Id> accountsToNotify; List<PatchSetApproval> addedReviewers; Collection<Account.Id> addedCCs; @@ -314,10 +350,13 @@ private PatchSet patchSet; Op(ChangeResource rsrc, Map<Account.Id, ChangeControl> reviewers, - ReviewerState state) { + ReviewerState state, NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) { this.rsrc = rsrc; this.reviewers = reviewers; this.state = state; + this.notify = notify; + this.accountsToNotify = checkNotNull(accountsToNotify); } @Override @@ -353,20 +392,22 @@ if (addedCCs == null) { addedCCs = new ArrayList<>(); } - emailReviewers(rsrc.getChange(), addedReviewers, addedCCs); + emailReviewers(rsrc.getChange(), + Lists.transform(addedReviewers, r -> r.getAccountId()), addedCCs, + notify, accountsToNotify); 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) { + public void emailReviewers(Change change, Collection<Account.Id> added, + Collection<Account.Id> copied, NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) { if (added.isEmpty() && copied.isEmpty()) { return; } @@ -376,9 +417,9 @@ // The user knows they added themselves, don't bother emailing them. List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size()); Account.Id userId = user.get().getAccountId(); - for (PatchSetApproval psa : added) { - if (!psa.getAccountId().equals(userId)) { - toMail.add(psa.getAccountId()); + for (Account.Id id : added) { + if (!id.equals(userId)) { + toMail.add(id); } } List<Account.Id> toCopy = Lists.newArrayListWithCapacity(copied.size()); @@ -394,6 +435,10 @@ try { AddReviewerSender cm = addReviewerSenderFactory .create(change.getProject(), change.getId()); + if (notify != null) { + cm.setNotify(notify); + } + cm.setAccountsToNotify(accountsToNotify); cm.setFrom(userId); cm.addReviewers(toMail); cm.addExtraCC(toCopy);
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..b783447 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.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.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.NotImplementedException; +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.change.LimitedByteArrayOutputStream.LimitExceededException; +import com.google.gerrit.server.config.GerritServerConfig; +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.Config; +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 static int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024; + + private final Provider<ReviewDb> dbProvider; + private final Provider<MergeOp> mergeOpProvider; + private final AllowedFormats allowedFormats; + private int maxBundleSize; + private String format; + + @Option(name = "--format") + public void setFormat(String f) { + this.format = f; + } + + @Inject + PreviewSubmit(Provider<ReviewDb> dbProvider, + Provider<MergeOp> mergeOpProvider, + AllowedFormats allowedFormats, + @GerritServerConfig Config cfg) { + this.dbProvider = dbProvider; + this.mergeOpProvider = mergeOpProvider; + this.allowedFormats = allowedFormats; + this.maxBundleSize = cfg.getInt("download", "maxBundleSize", + MAX_DEFAULT_BUNDLE_SIZE); + } + + @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 { + try (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()); + ObjectId oldId = r.getOldId(); + if (!oldId.equals(ObjectId.zeroId())) { + bw.assume(or.getCodeReviewRevWalk().parseCommit(oldId)); + } + } + // This naming scheme cannot produce directory/file conflicts + // as no projects contains ".git/": + String path = p.get() + ".git"; + + LimitedByteArrayOutputStream bos = + new LimitedByteArrayOutputStream(maxBundleSize, 1024); + bw.writeBundle(NullProgressMonitor.INSTANCE, bos); + f.putEntry(aos, path, bos.toByteArray()); + } + } catch (LimitExceededException e) { + throw new NotImplementedException("The bundle is too big to " + + "generate at the server"); + } + } + }; + } + 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..87f81fe 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.change; -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; @@ -36,6 +36,7 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; @Singleton public class PublishChangeEdit implements @@ -71,19 +72,21 @@ } @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; + private final NotifyUtil notifyUtil; @Inject - Publish(ChangeEditUtil editUtil) { + Publish(ChangeEditUtil editUtil, + NotifyUtil notifyUtil) { this.editUtil = editUtil; + this.notifyUtil = notifyUtil; } @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 +101,11 @@ "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, + notifyUtil.resolveAccounts(in.notifyDetails)); return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java index d17d69b..ba27005 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; @@ -44,9 +44,9 @@ 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.mail.CreateChangeSender; import com.google.gerrit.server.mail.MailUtil.MailRecipients; -import com.google.gerrit.server.mail.ReplacePatchSetSender; +import com.google.gerrit.server.mail.send.CreateChangeSender; +import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gwtorm.server.OrmException; @@ -252,15 +252,13 @@ 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() + ".", + ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); 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..271fd33 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.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.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.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.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 { + if (!rsrc.getControl().canEditAssignee()) { + throw new AuthException("Changing Assignee not permitted"); + } + if (input.assignee == null || input.assignee.trim().isEmpty()) { + throw new BadRequestException("missing assignee field"); + } + + try (BatchUpdate bu = batchUpdateFactory.create(db.get(), + rsrc.getChange().getProject(), rsrc.getControl().getUser(), + TimeUtil.nowTs())) { + SetAssigneeOp op = assigneeFactory.create(input.assignee); + bu.addOp(rsrc.getId(), op); + + PostReviewers.Addition reviewersAddition = + addAssigneeAsCC(rsrc, input.assignee); + bu.addOp(rsrc.getId(), reviewersAddition.op); + + bu.execute(); + 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, false); + } + + @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/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java new file mode 100644 index 0000000..2a32652 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
@@ -0,0 +1,132 @@ +// 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.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.DefaultInput; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.notedb.ChangeUpdate; +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 java.util.Collections; + +@Singleton +public class PutDescription implements RestModifyView<RevisionResource, + PutDescription.Input>, UiAction<RevisionResource> { + private final Provider<ReviewDb> dbProvider; + private final ChangeMessagesUtil cmUtil; + private final BatchUpdate.Factory batchUpdateFactory; + private final PatchSetUtil psUtil; + + public static class Input { + @DefaultInput + public String description; + } + + @Inject + PutDescription(Provider<ReviewDb> dbProvider, + ChangeMessagesUtil cmUtil, + BatchUpdate.Factory batchUpdateFactory, + PatchSetUtil psUtil) { + this.dbProvider = dbProvider; + this.cmUtil = cmUtil; + this.batchUpdateFactory = batchUpdateFactory; + this.psUtil = psUtil; + } + + @Override + public Response<String> apply(RevisionResource rsrc, Input input) + throws UpdateException, RestApiException { + ChangeControl ctl = rsrc.getControl(); + if (!ctl.canEditDescription()) { + throw new AuthException("changing description not permitted"); + } + Op op = + new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId()); + try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(), + rsrc.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) { + u.addOp(rsrc.getChange().getId(), op); + u.execute(); + } + return Strings.isNullOrEmpty(op.newDescription) ? Response.none() + : Response.ok(op.newDescription); + } + + private class Op extends BatchUpdate.Op { + private final Input input; + private final PatchSet.Id psId; + + private String oldDescription; + private String newDescription; + + Op(Input input, PatchSet.Id psId) { + this.input = input; + this.psId = psId; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); + ChangeUpdate update = ctx.getUpdate(psId); + newDescription = Strings.nullToEmpty(input.description); + oldDescription = Strings.nullToEmpty(ps.getDescription()); + if (oldDescription.equals(newDescription)) { + return false; + } + String summary; + if (oldDescription.isEmpty()) { + summary = "Description set to \"" + newDescription + "\""; + } else if (newDescription.isEmpty()) { + summary = "Description \"" + oldDescription + "\" removed"; + } else { + summary = "Description changed to \"" + newDescription + "\""; + } + + ps.setDescription(newDescription); + update.setPsDescription(newDescription); + + ctx.getDb().patchSets().update(Collections.singleton(ps)); + + ChangeMessage cmsg = + ChangeMessagesUtil.newMessage(ctx.getDb(), psId, ctx.getUser(), + ctx.getWhen(), summary, ChangeMessagesUtil.TAG_SET_DESCRIPTION); + cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); + return true; + } + } + + @Override + public UiAction.Description getDescription(RevisionResource rsrc) { + return new UiAction.Description().setLabel("Edit Description") + .setVisible(rsrc.getControl().canEditDescription()); + } +}
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..0742c6d 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,10 @@ 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 +25,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; @@ -46,13 +43,14 @@ import java.sql.Timestamp; import java.util.Collections; +import java.util.Optional; @Singleton public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> { 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..62ef261 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,8 @@ 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, + ChangeMessagesUtil.TAG_SET_TOPIC); cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); return true; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java index baa0990..ac1b770 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.change; -import com.google.common.base.Optional; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AcceptsPost; import com.google.gerrit.extensions.restapi.AuthException; @@ -39,6 +38,7 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; @Singleton public class RebaseChangeEdit implements
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..7c2be7f 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; @@ -67,6 +68,8 @@ private CommitValidators.Policy validate; private boolean forceContentMerge; private boolean copyApprovals = true; + private boolean detailedCommitMessage; + private boolean postMessage = true; private RevCommit rebasedCommit; private PatchSet.Id rebasedPatchSetId; @@ -116,6 +119,17 @@ return this; } + public RebaseChangeOp setDetailedCommitMessage( + boolean detailedCommitMessage) { + this.detailedCommitMessage = detailedCommitMessage; + return this; + } + + public RebaseChangeOp setPostMessage(boolean postMessage) { + this.postMessage = postMessage; + return this; + } + @Override public void updateRepo(RepoContext ctx) throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException, @@ -127,6 +141,7 @@ RevWalk rw = ctx.getRevWalk(); RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get())); rw.parseBody(original); + RevCommit baseCommit; if (baseCommitish != null) { baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish)); @@ -136,7 +151,16 @@ ctx.getRepository(), ctx.getRevWalk())); } - rebasedCommit = rebaseCommit(ctx, original, baseCommit); + String newCommitMessage; + if (detailedCommitMessage) { + rw.parseBody(baseCommit); + newCommitMessage = newMergeUtil().createCommitMessageOnSubmit(original, + baseCommit, ctl, originalPatchSet.getId()); + } else { + newCommitMessage = original.getFullMessage(); + } + + rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage); RevId baseRevId = new RevId((baseCommitish != null) ? baseCommitish : ObjectId.toString(baseCommit.getId())); @@ -149,13 +173,15 @@ ctx.getRepository(), ctl.getChange().currentPatchSetId()); patchSetInserter = patchSetInserterFactory .create(ctl, rebasedPatchSetId, rebasedCommit) + .setDescription("Rebase") .setDraft(originalPatchSet.isDraft()) - .setSendMail(false) + .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(fireRevisionCreated) - .setCopyApprovals(copyApprovals) - .setMessage( - "Patch Set " + rebasedPatchSetId.get() + .setCopyApprovals(copyApprovals); + if (postMessage) { + patchSetInserter.setMessage("Patch Set " + rebasedPatchSetId.get() + ": Patch Set " + originalPatchSet.getId().get() + " was rebased"); + } if (base != null) { patchSetInserter.setGroups(base.patchSet().getGroups()); @@ -214,9 +240,9 @@ * @throws MergeConflictException the rebase failed due to a merge conflict. * @throws IOException the merge failed for another reason. */ - private RevCommit rebaseCommit(RepoContext ctx, RevCommit original, - ObjectId base) throws ResourceConflictException, MergeConflictException, - IOException { + private RevCommit rebaseCommit( + RepoContext ctx, RevCommit original, ObjectId base, String commitMessage) + throws ResourceConflictException, IOException { RevCommit parentCommit = original.getParent(0); if (base.equals(parentCommit)) { @@ -237,7 +263,7 @@ cb.setTreeId(merger.getResultTreeId()); cb.setParentId(base); cb.setAuthor(original.getAuthorIdent()); - cb.setMessage(original.getFullMessage()); + cb.setMessage(commitMessage); if (committerIdent != null) { cb.setCommitter(committerIdent); } else {
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..b9f4483 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
@@ -14,14 +14,21 @@ package com.google.gerrit.server.change; +import static java.util.stream.Collectors.joining; + +import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; -import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; 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.change.Rebuild.Input; -import com.google.gerrit.server.notedb.ChangeRebuilder; +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.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; @@ -31,6 +38,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import java.io.IOException; +import java.util.List; @Singleton public class Rebuild implements RestModifyView<ChangeResource, Input> { @@ -40,29 +48,63 @@ private final Provider<ReviewDb> db; private final NotesMigration migration; private final ChangeRebuilder rebuilder; + private final ChangeBundleReader bundleReader; + private final CommentsUtil commentsUtil; + private final ChangeNotes.Factory notesFactory; @Inject Rebuild(Provider<ReviewDb> db, NotesMigration migration, - ChangeRebuilder rebuilder) { + ChangeRebuilder rebuilder, + ChangeBundleReader bundleReader, + CommentsUtil commentsUtil, + ChangeNotes.Factory notesFactory) { this.db = db; this.migration = migration; this.rebuilder = rebuilder; + this.bundleReader = bundleReader; + this.commentsUtil = commentsUtil; + this.notesFactory = notesFactory; } @Override - public Response<?> apply(ChangeResource rsrc, Input input) + public BinaryResult apply(ChangeResource rsrc, Input input) throws ResourceNotFoundException, IOException, OrmException, ConfigInvalidException { if (!migration.commitChangeWrites()) { throw new ResourceNotFoundException(); + } if (!migration.readChanges()) { + // ChangeBundle#fromNotes currently doesn't work if reading isn't enabled, + // so don't attempt a diff. + rebuild(rsrc); + return BinaryResult.create("Rebuilt change successfully"); } + + // Not the same transaction as the rebuild, so may result in spurious diffs + // in the case of races. This should be easy enough to detect by rerunning. + ChangeBundle reviewDbBundle = bundleReader.fromReviewDb( + ReviewDbUtil.unwrapDb(db.get()), rsrc.getId()); + rebuild(rsrc); + ChangeNotes notes = notesFactory.create( + db.get(), rsrc.getChange().getProject(), rsrc.getId()); + ChangeBundle noteDbBundle = ChangeBundle.fromNotes(commentsUtil, notes); + List<String> diffs = reviewDbBundle.differencesFrom(noteDbBundle); + if (diffs.isEmpty()) { + return BinaryResult.create("No differences between ReviewDb and NoteDb"); + } + return BinaryResult.create( + diffs.stream() + .collect(joining( + "\n", "Differences between ReviewDb and NoteDb:\n", "\n"))); + } + + private void rebuild(ChangeResource rsrc) throws ResourceNotFoundException, + OrmException, IOException { try { rebuilder.rebuild(db.get(), rsrc.getId()); } catch (NoSuchChangeException e) { throw new ResourceNotFoundException( IdString.fromDecoded(rsrc.getId().toString())); } - return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java index 74d7552..e7a346f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -19,11 +19,11 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; @@ -72,10 +72,10 @@ // Map of patch set -> immediate parent. ListMultimap<PatchSetData, PatchSetData> parents = - ArrayListMultimap.create(in.size(), 3); + MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build(); // Map of patch set -> immediate children. ListMultimap<PatchSetData, PatchSetData> children = - ArrayListMultimap.create(in.size(), 3); + MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build(); // All other patch sets of the same change as startPs. List<PatchSetData> otherPatchSetsOfStart = new ArrayList<>();
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..a1972f9 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,15 +29,14 @@ 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; 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.mail.ReplyToChangeSender; -import com.google.gerrit.server.mail.RestoredSender; +import com.google.gerrit.server.mail.send.ReplyToChangeSender; +import com.google.gerrit.server.mail.send.RestoredSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gwtorm.server.OrmException; @@ -131,16 +130,8 @@ 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(), + ChangeMessagesUtil.TAG_RESTORE); } @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..ab318c1 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; @@ -46,7 +45,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.git.validators.CommitValidators; -import com.google.gerrit.server.mail.RevertedSender; +import com.google.gerrit.server.mail.send.RevertedSender; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.ProjectControl; @@ -274,14 +273,9 @@ 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(), + ChangeMessagesUtil.TAG_REVERT); cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(patchSetId), changeMessage); return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java new file mode 100644 index 0000000..6affd9f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
@@ -0,0 +1,45 @@ +// 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.Nullable; +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; + +import java.util.Set; + +/** + * Listener to provide reviewer suggestions. + * <p> + * Invoked by Gerrit a user who is searching for a reviewer to add to a change. + */ +@ExtensionPoint +public interface ReviewerSuggestion { + /** + * Reviewer suggestion. + * + * @param project The name key of the project the suggestion is for. + * @param changeId The changeId that the suggestion is for. Can be an {@code null}. + * @param query The query as typed by the user. Can be an {@code null}. + * @param candidates A set of candidates for the ranking. Can be empty. + * @return Set of suggested reviewers as a tuple of account id and score. + * The account ids listed here don't have to be a part of candidates. + */ + Set<SuggestedReviewer> suggestReviewers(Project.NameKey project, + @Nullable Change.Id changeId, @Nullable String query, + Set<Account.Id> candidates); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java index a8fd013..152563b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.change; -import com.google.common.base.Optional; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestResource.HasETag; import com.google.gerrit.extensions.restapi.RestView; @@ -28,6 +27,8 @@ import com.google.gerrit.server.project.ChangeControl; import com.google.inject.TypeLiteral; +import java.util.Optional; + public class RevisionResource implements RestResource, HasETag { public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND = new TypeLiteral<RestView<RevisionResource>>() {}; @@ -38,7 +39,7 @@ private boolean cacheable = true; public RevisionResource(ChangeResource change, PatchSet ps) { - this(change, ps, Optional.<ChangeEdit> absent()); + this(change, ps, Optional.empty()); } public RevisionResource(ChangeResource change, PatchSet ps,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java index 30a09cf..4572994 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -15,7 +15,6 @@ package com.google.gerrit.server.change; import com.google.common.base.Joiner; -import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AuthException; @@ -38,6 +37,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; @Singleton public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
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..47323c2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -0,0 +1,170 @@ +// 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 com.google.common.base.Preconditions.checkNotNull; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.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.mail.send.SetAssigneeSender; +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.Provider; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public class SetAssigneeOp extends BatchUpdate.Op { + private static final Logger log = + LoggerFactory.getLogger(SetAssigneeOp.class); + + public interface Factory { + SetAssigneeOp create(String assignee); + } + + private final AccountsCollection accounts; + private final ChangeMessagesUtil cmUtil; + private final AccountInfoCacheFactory.Factory accountInfosFactory; + private final DynamicSet<AssigneeValidationListener> validationListeners; + private final String assignee; + private final String anonymousCowardName; + private final AssigneeChanged assigneeChanged; + private final SetAssigneeSender.Factory setAssigneeSenderFactory; + private final Provider<IdentifiedUser> user; + + private Change change; + private Account newAssignee; + private Account oldAssignee; + + @AssistedInject + SetAssigneeOp(AccountsCollection accounts, + ChangeMessagesUtil cmUtil, + AccountInfoCacheFactory.Factory accountInfosFactory, + DynamicSet<AssigneeValidationListener> validationListeners, + @AnonymousCowardName String anonymousCowardName, + AssigneeChanged assigneeChanged, + SetAssigneeSender.Factory setAssigneeSenderFactory, + Provider<IdentifiedUser> user, + @Assisted String assignee) { + this.accounts = accounts; + this.cmUtil = cmUtil; + this.accountInfosFactory = accountInfosFactory; + this.validationListeners = validationListeners; + this.assigneeChanged = assigneeChanged; + this.anonymousCowardName = anonymousCowardName; + this.assignee = checkNotNull(assignee); + this.setAssigneeSenderFactory = setAssigneeSenderFactory; + this.user = user; + } + + @Override + public boolean updateChange(BatchUpdate.ChangeContext ctx) + throws OrmException, RestApiException { + change = ctx.getChange(); + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); + Optional<Account.Id> oldAssigneeId = + Optional.ofNullable(change.getAssignee()); + oldAssignee = null; + if (oldAssigneeId.isPresent()) { + oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get()); + } + IdentifiedUser newAssigneeUser = accounts.parse(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", assignee)); + } + if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) { + throw new AuthException(String.format( + "Change %s is not visible to %s.", + change.getChangeId(), + assignee)); + } + try { + for (AssigneeValidationListener validator : validationListeners) { + validator.validateAssignee(change, newAssigneeUser.getAccount()); + } + } catch (ValidationException e) { + throw new ResourceConflictException(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(), + ChangeMessagesUtil.TAG_SET_ASSIGNEE); + cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); + } + + @Override + public void postUpdate(Context ctx) throws OrmException { + try { + SetAssigneeSender cm = setAssigneeSenderFactory + .create(change.getProject(), change.getId(), newAssignee.getId()); + cm.setFrom(user.get().getAccountId()); + cm.send(); + } catch (Exception err) { + log.error("Cannot send email to new assignee of change " + change.getId(), + err); + } + assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen()); + } + + public Account 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..3b2117d 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
@@ -25,10 +25,10 @@ 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.MethodNotAllowedException; 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; @@ -86,9 +86,11 @@ @Override public boolean updateChange(ChangeContext ctx) - throws AuthException, BadRequestException, OrmException, IOException { + throws AuthException, BadRequestException, MethodNotAllowedException, + OrmException, IOException { if (!notesMigration.readChanges()) { - throw new BadRequestException("Cannot add hashtags; NoteDb is disabled"); + throw new MethodNotAllowedException( + "Cannot add hashtags; NoteDb is disabled"); } if (input == null || (input.add == null && input.remove == null)) { @@ -129,18 +131,13 @@ 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(), + ChangeMessagesUtil.TAG_SET_HASHTAGS); 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..131513b 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, @@ -48,8 +54,8 @@ @Override public List<SuggestedReviewerInfo> apply(ChangeResource rsrc) throws BadRequestException, OrmException, IOException { - return reviewersUtil.suggestReviewers(this, - rsrc.getControl().getProjectControl(), getVisibility(rsrc)); + return reviewersUtil.suggestReviewers(rsrc.getNotes(), this, + 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/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java index f159c69..2af1f6b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -33,7 +33,6 @@ protected final ReviewersUtil reviewersUtil; private final boolean suggestAccounts; - private final int suggestFrom; private final int maxAllowed; private final int maxAllowedWithoutConfirmation; protected int limit; @@ -62,10 +61,6 @@ return suggestAccounts; } - public int getSuggestFrom() { - return suggestFrom; - } - public int getLimit() { return limit; } @@ -98,7 +93,6 @@ this.suggestAccounts = (av != AccountVisibility.NONE); } - this.suggestFrom = cfg.getInt("suggest", null, "from", 0); this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed", PostReviewers.DEFAULT_MAX_REVIEWERS); this.maxAllowedWithoutConfirmation = cfg.getInt(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java similarity index 70% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java index ea0def0..353bf3b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.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. @@ -11,9 +11,12 @@ // WITHOUT WARRANTIES OR CONDITIONS OF 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; -package com.google.gerrit.server.mail; +import com.google.gerrit.reviewdb.client.Account; -public enum RecipientType { - TO, CC, BCC +public class SuggestedReviewer { + + public Account.Id account; + public double score; }
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..47c32d7 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,11 +18,10 @@ 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; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Ordering; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; @@ -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; @@ -111,7 +106,7 @@ public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws OrmException, IOException { Multimap<Project.NameKey, ChangeData> byProject = - ArrayListMultimap.create(); + MultimapBuilder.hashKeys().arrayListValues().build(); for (ChangeData cd : in) { byProject.put(cd.change().getProject(), cd); } @@ -157,7 +152,8 @@ Set<RevCommit> commits = byCommit.keySet(); Multimap<RevCommit, RevCommit> children = collectChildren(commits); - Multimap<RevCommit, RevCommit> pending = ArrayListMultimap.create(); + Multimap<RevCommit, RevCommit> pending = + MultimapBuilder.hashKeys().arrayListValues().build(); Deque<RevCommit> todo = new ArrayDeque<>(); RevFlag done = rw.newFlag("done"); @@ -201,7 +197,8 @@ private static Multimap<RevCommit, RevCommit> collectChildren( Set<RevCommit> commits) { - Multimap<RevCommit, RevCommit> children = ArrayListMultimap.create(); + Multimap<RevCommit, RevCommit> children = + MultimapBuilder.hashKeys().arrayListValues().build(); for (RevCommit c : commits) { for (RevCommit p : c.getParents()) { if (commits.contains(p)) { @@ -229,7 +226,7 @@ private Multimap<RevCommit, PatchSetData> byCommit(RevWalk rw, Collection<ChangeData> in) throws OrmException, IOException { Multimap<RevCommit, PatchSetData> byCommit = - ArrayListMultimap.create(in.size(), 1); + MultimapBuilder.hashKeys(in.size()).arrayListValues(1).build(); for (ChangeData cd : in) { PatchSet maxPs = null; for (PatchSet ps : cd.patchSets()) {
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 9db4d3d..a984bf9 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. @@ -20,6 +20,7 @@ import com.google.gerrit.audit.AuditModule; import com.google.gerrit.common.EventListener; import com.google.gerrit.common.UserScopedEventListener; +import com.google.gerrit.extensions.api.changes.ActionVisitor; import com.google.gerrit.extensions.api.projects.CommentLinkInfo; import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter; @@ -30,6 +31,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; @@ -100,10 +102,13 @@ import com.google.gerrit.server.change.ChangeJson; import com.google.gerrit.server.change.ChangeKindCacheImpl; import com.google.gerrit.server.change.MergeabilityCacheImpl; +import com.google.gerrit.server.change.ReviewerSuggestion; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.EventsMetrics; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.AbandonOp; import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.EmailMerge; import com.google.gerrit.server.git.GitModule; import com.google.gerrit.server.git.GitModules; @@ -117,7 +122,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; @@ -128,17 +132,20 @@ import com.google.gerrit.server.group.GroupInfoCache; import com.google.gerrit.server.group.GroupModule; import com.google.gerrit.server.index.change.ReindexAfterUpdate; -import com.google.gerrit.server.mail.AddKeySender; -import com.google.gerrit.server.mail.AddReviewerSender; -import com.google.gerrit.server.mail.CreateChangeSender; -import com.google.gerrit.server.mail.DeleteReviewerSender; 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.MergedSender; -import com.google.gerrit.server.mail.RegisterNewEmailSender; -import com.google.gerrit.server.mail.ReplacePatchSetSender; -import com.google.gerrit.server.mail.VelocityRuntimeProvider; +import com.google.gerrit.server.mail.send.AddKeySender; +import com.google.gerrit.server.mail.send.AddReviewerSender; +import com.google.gerrit.server.mail.send.CreateChangeSender; +import com.google.gerrit.server.mail.send.DeleteReviewerSender; +import com.google.gerrit.server.mail.send.FromAddressGenerator; +import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider; +import com.google.gerrit.server.mail.send.MailSoyTofuProvider; +import com.google.gerrit.server.mail.send.MailTemplates; +import com.google.gerrit.server.mail.send.MergedSender; +import com.google.gerrit.server.mail.send.RegisterNewEmailSender; +import com.google.gerrit.server.mail.send.ReplacePatchSetSender; +import com.google.gerrit.server.mail.send.SetAssigneeSender; +import com.google.gerrit.server.mail.send.VelocityRuntimeProvider; import com.google.gerrit.server.mime.FileTypeRegistry; import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry; import com.google.gerrit.server.notedb.NoteDbModule; @@ -162,6 +169,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; @@ -171,6 +179,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; @@ -252,6 +261,7 @@ factory(ProjectState.Factory.class); factory(RegisterNewEmailSender.Factory.class); factory(ReplacePatchSetSender.Factory.class); + factory(SetAssigneeSender.Factory.class); bind(PermissionCollection.Factory.class); bind(AccountVisibility.class) .toProvider(AccountVisibilityProvider.class) @@ -276,6 +286,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) @@ -301,6 +314,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); @@ -332,6 +346,7 @@ DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class); DynamicSet.setOf(binder(), UserScopedEventListener.class); DynamicSet.setOf(binder(), CommitValidationListener.class); + DynamicSet.setOf(binder(), ChangeMessageModifier.class); DynamicSet.setOf(binder(), RefOperationValidationListener.class); DynamicSet.setOf(binder(), MergeValidationListener.class); DynamicSet.setOf(binder(), ProjectCreationValidationListener.class); @@ -345,6 +360,7 @@ DynamicMap.mapOf(binder(), DownloadScheme.class); DynamicMap.mapOf(binder(), DownloadCommand.class); DynamicMap.mapOf(binder(), CloneCommand.class); + DynamicMap.mapOf(binder(), ReviewerSuggestion.class); DynamicSet.setOf(binder(), ExternalIncludedIn.class); DynamicMap.mapOf(binder(), ProjectConfigEntry.class); DynamicSet.setOf(binder(), PatchSetWebLink.class); @@ -359,16 +375,19 @@ DynamicSet.setOf(binder(), AccountExternalIdCreator.class); DynamicSet.setOf(binder(), WebUiPlugin.class); DynamicItem.itemOf(binder(), AccountPatchReviewStore.class); + DynamicSet.setOf(binder(), AssigneeValidationListener.class); + DynamicSet.setOf(binder(), ActionVisitor.class); factory(UploadValidators.Factory.class); DynamicSet.setOf(binder(), UploadValidationListener.class); DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class); + DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class); install(new GitwebConfig.LegacyModule(cfg)); bind(AnonymousUser.class); - factory(CommitValidators.Factory.class); + factory(AbandonOp.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..ab4b463 --- /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", true); + 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..54fc3fa 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,24 @@ 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,25 +40,28 @@ 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.Optional; import java.util.concurrent.TimeUnit; public class GetServerInfo implements RestReadView<ConfigResource> { @@ -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,13 +209,17 @@ 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")) - .or("Reply and score") + " (Shortcut: a)"; + Optional.ofNullable(cfg.getString("change", null, "replyTooltip")) + .orElse("Reply and score") + " (Shortcut: a)"; info.replyLabel = - Optional.fromNullable(cfg.getString("change", null, "replyLabel")) - .or("Reply") + "\u2026"; + Optional.ofNullable(cfg.getString("change", null, "replyLabel")) + .orElse("Reply") + "\u2026"; info.updateDelay = (int) ConfigUtil.getTimeUnit( cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS); info.submitWholeTopic = Submit.wholeTopicEnabled(cfg); @@ -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..8ab2135 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
@@ -16,12 +16,15 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.base.Optional; +import com.google.common.collect.Multimap; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.client.PatchSet; @@ -34,13 +37,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; @@ -56,6 +58,7 @@ import org.eclipse.jgit.revwalk.RevWalk; import java.io.IOException; +import java.util.Optional; /** * Utility functions to manipulate change edits. @@ -69,7 +72,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 +83,6 @@ PatchSetInserter.Factory patchSetInserterFactory, ChangeControl.GenericFactory changeControlFactory, ChangeIndexer indexer, - ProjectCache projectCache, Provider<ReviewDb> db, Provider<CurrentUser> user, ChangeKindCache changeKindCache, @@ -91,7 +92,6 @@ this.patchSetInserterFactory = patchSetInserterFactory; this.changeControlFactory = changeControlFactory; this.indexer = indexer; - this.projectCache = projectCache; this.db = db; this.user = user; this.changeKindCache = changeKindCache; @@ -147,7 +147,7 @@ } Ref ref = repo.getRefDatabase().firstExactRef(refNames); if (ref == null) { - return Optional.absent(); + return Optional.empty(); } try (RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit(ref.getObjectId()); @@ -158,34 +158,84 @@ } /** - * Promote change edit to patch set, by squashing the edit into - * its parent. + * Promote change edit to patch set, by squashing the edit into its parent. * * @param edit change edit to publish + * @param notify Notify handling that defines to whom email notifications + * should be sent after the change edit is published. + * @param accountsToNotify Accounts that should be notified after the change + * edit is published. * @throws NoSuchChangeException * @throws IOException * @throws OrmException * @throws UpdateException * @throws RestApiException */ - public void publish(ChangeEdit edit) throws NoSuchChangeException, - IOException, OrmException, RestApiException, UpdateException { + public void publish(final ChangeEdit edit, NotifyHandling notify, + Multimap<RecipientType, Account.Id> accountsToNotify) + 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) + .setAccountsToNotify(accountsToNotify); + + 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 +280,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..53d837f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -0,0 +1,84 @@ +// 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(Change change, Account account, Account oldAssignee, + Timestamp when) { + if (!listeners.iterator().hasNext()) { + return; + } + try { + Event event = new Event( + util.changeInfo(change), + util.accountInfo(account), + util.accountInfo(oldAssignee), + when); + for (AssigneeChangedListener l : listeners) { + try { + l.onAssigneeChanged(event); + } catch (Exception e) { + util.logEventListenerError(event, l, e); + } + } + } 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 e303d8b..5a7aec2 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 00d276b..8b4a6a0 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 5dda4d1..1d2682a 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 0c75e2e..f1bb50a 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 9e3e5a2..4f6d298 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 e519410..162a6b1 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; @@ -83,20 +84,16 @@ 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)); + ChangeJson.getApprovalInfo(a.getId(), value, null, null, ts)); } return result; } @@ -104,12 +101,31 @@ public void logEventListenerError(Object event, Object listener, Exception error) { if (log.isDebugEnabled()) { - log.debug(String.format( - "Error in event listener %s for event %s", - listener.getClass().getName(), event.getClass().getName()), error); + log.debug( + String.format( + "Error in event listener %s for event %s", + listener.getClass().getName(), + event.getClass().getName()), + error); } else { - log.warn("Error in listener {} for event {}: {}", - listener.getClass().getName(), event.getClass().getName(), + log.warn( + "Error in listener {} for event {}: {}", + listener.getClass().getName(), + event.getClass().getName(), + error.getMessage()); + } + } + + public static void logEventListenerError(Object listener, Exception error) { + if (log.isDebugEnabled()) { + log.debug( + String.format( + "Error in event listener %s", listener.getClass().getName()), + error); + } else { + log.warn( + "Error in listener {}: {}", + listener.getClass().getName(), error.getMessage()); } }
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 27770fd..233a89e 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 e9c44a5..8860a42 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(this, l, 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 42aa9a3..4bc4764 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 27f3be5..7f03c63 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 bf1b2ba..2e583a8 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..fe261e9 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,81 +30,74 @@ 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( - RestCollection<?, R> collection, - R resource, - Provider<CurrentUser> userProvider) { + public static <R extends RestResource> FluentIterable<UiAction.Description> + from( + RestCollection<?, R> collection, + R resource, + Provider<CurrentUser> userProvider) { return from(collection.views(), resource, userProvider); } - public static <R extends RestResource> Iterable<UiAction.Description> from( - 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; - } + public static <R extends RestResource> FluentIterable<UiAction.Description> + from( + DynamicMap<RestView<R>> views, + R resource, + Provider<CurrentUser> userProvider) { + 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/AbandonOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java new file mode 100644 index 0000000..84ac33b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java
@@ -0,0 +1,148 @@ +// 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.git; + +import com.google.common.base.Strings; +import com.google.common.collect.Multimap; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +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.ChangeMessagesUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.extensions.events.ChangeAbandoned; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.BatchUpdate.Context; +import com.google.gerrit.server.mail.send.AbandonedSender; +import com.google.gerrit.server.mail.send.ReplyToChangeSender; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gwtorm.server.OrmException; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AbandonOp extends BatchUpdate.Op { + private static final Logger log = LoggerFactory.getLogger(AbandonOp.class); + + private final AbandonedSender.Factory abandonedSenderFactory; + private final ChangeMessagesUtil cmUtil; + private final PatchSetUtil psUtil; + private final ChangeAbandoned changeAbandoned; + + private final String msgTxt; + private final NotifyHandling notifyHandling; + private final Multimap<RecipientType, Account.Id> accountsToNotify; + private final Account account; + + private Change change; + private PatchSet patchSet; + private ChangeMessage message; + + public interface Factory { + AbandonOp create( + @Assisted @Nullable Account account, + @Assisted @Nullable String msgTxt, + @Assisted NotifyHandling notifyHandling, + @Assisted Multimap<RecipientType, Account.Id> accountsToNotify); + } + + @AssistedInject + AbandonOp( + AbandonedSender.Factory abandonedSenderFactory, + ChangeMessagesUtil cmUtil, + PatchSetUtil psUtil, + ChangeAbandoned changeAbandoned, + @Assisted @Nullable Account account, + @Assisted @Nullable String msgTxt, + @Assisted NotifyHandling notifyHandling, + @Assisted Multimap<RecipientType, Account.Id> accountsToNotify) { + this.abandonedSenderFactory = abandonedSenderFactory; + this.cmUtil = cmUtil; + this.psUtil = psUtil; + this.changeAbandoned = changeAbandoned; + + this.account = account; + this.msgTxt = Strings.nullToEmpty(msgTxt); + this.notifyHandling = notifyHandling; + this.accountsToNotify = accountsToNotify; + } + + @Nullable + public Change getChange() { + return change; + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws OrmException, ResourceConflictException { + change = ctx.getChange(); + PatchSet.Id psId = change.currentPatchSetId(); + ChangeUpdate update = ctx.getUpdate(psId); + if (!change.getStatus().isOpen()) { + throw new ResourceConflictException("change is " + status(change)); + } else if (change.getStatus() == Change.Status.DRAFT) { + throw new ResourceConflictException("draft changes cannot be abandoned"); + } + patchSet = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); + change.setStatus(Change.Status.ABANDONED); + change.setLastUpdatedOn(ctx.getWhen()); + + update.setStatus(change.getStatus()); + message = newMessage(ctx); + cmUtil.addChangeMessage(ctx.getDb(), update, message); + return true; + } + + private ChangeMessage newMessage(ChangeContext ctx) throws OrmException { + StringBuilder msg = new StringBuilder(); + msg.append("Abandoned"); + if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) { + msg.append("\n\n"); + msg.append(msgTxt.trim()); + } + + return ChangeMessagesUtil.newMessage( + ctx, msg.toString(), ChangeMessagesUtil.TAG_ABANDON); + } + + @Override + public void postUpdate(Context ctx) throws OrmException { + try { + ReplyToChangeSender cm = + abandonedSenderFactory.create(ctx.getProject(), change.getId()); + if (account != null) { + cm.setFrom(account.getId()); + } + cm.setChangeMessage(message.getMessage(), ctx.getWhen()); + cm.setNotify(notifyHandling); + cm.setAccountsToNotify(accountsToNotify); + cm.send(); + } catch (Exception e) { + log.error("Cannot email update for change " + change.getId(), e); + } + changeAbandoned.fire( + change, patchSet, account, msgTxt, ctx.getWhen(), notifyHandling); + } + + private static String status(Change change) { + return change != null ? change.getStatus().name().toLowerCase() : "deleted"; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java index c686403..bc2b4df 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,10 +17,10 @@ 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.concurrent.TimeUnit.MILLISECONDS; +import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; +import com.google.common.base.Stopwatch; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ListMultimap; @@ -34,24 +34,28 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.Description.Units; +import com.google.gerrit.metrics.Field; +import com.google.gerrit.metrics.MetricMaker; +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.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.reviewdb.server.ReviewDbUtil; import com.google.gerrit.reviewdb.server.ReviewDbWrapper; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.config.ConfigUtil; -import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.NoteDbUpdateManager; +import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; @@ -59,14 +63,14 @@ import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.NoSuchRefException; import com.google.gerrit.server.util.RequestId; -import com.google.gwtorm.server.OrmConcurrencyException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Singleton; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import org.eclipse.jgit.lib.BatchRefUpdate; -import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; @@ -226,7 +230,7 @@ this.dbWrapper = dbWrapper; this.threadLocalRepo = repo; this.threadLocalRevWalk = rw; - updates = new TreeMap<>(ReviewDbUtil.intKeyOrdering()); + updates = new TreeMap<>(comparing(PatchSet.Id::get)); } @Override @@ -346,6 +350,22 @@ } } + @Singleton + private static class Metrics { + final Timer1<Boolean> executeChangeOpsLatency; + + @Inject + Metrics(MetricMaker metricMaker) { + executeChangeOpsLatency = metricMaker.newTimer( + "batch_update/execute_change_ops", + new Description( + "BatchUpdate change update latency, excluding reindexing") + .setCumulative() + .setUnit(Units.MILLISECONDS), + Field.ofBoolean("success")); + } + } + private static Order getOrder(Collection<BatchUpdate> updates) { Order o = null; for (BatchUpdate u : updates) { @@ -379,7 +399,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 +422,19 @@ } listener.afterUpdateRepos(); for (BatchUpdate u : updates) { - u.executeRefUpdates(); + u.executeRefUpdates(dryrun); } listener.afterRefUpdates(); for (BatchUpdate u : updates) { - u.executeChangeOps(updateChangesInParallel); + u.reindexChanges( + u.executeChangeOps(updateChangesInParallel, dryrun)); } listener.afterUpdateChanges(); break; case DB_BEFORE_REPO: for (BatchUpdate u : updates) { - u.executeChangeOps(updateChangesInParallel); + u.reindexChanges( + u.executeChangeOps(updateChangesInParallel, dryrun)); } listener.afterUpdateChanges(); for (BatchUpdate u : updates) { @@ -419,7 +442,7 @@ } listener.afterUpdateRepos(); for (BatchUpdate u : updates) { - u.executeRefUpdates(); + u.executeRefUpdates(dryrun); } listener.afterRefUpdates(); break; @@ -447,9 +470,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 @@ -466,7 +490,7 @@ throw new ResourceNotFoundException(e.getMessage(), e); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new UpdateException(e); } } @@ -479,12 +503,12 @@ private final GitReferenceUpdated gitRefUpdated; private final GitRepositoryManager repoManager; private final ListeningExecutorService changeUpdateExector; + private final Metrics metrics; private final NoteDbUpdateManager.Factory updateManagerFactory; private final NotesMigration notesMigration; private final ReviewDb db; private final SchemaFactory<ReviewDb> schemaFactory; - private final long logThresholdNanos; private final Project.NameKey project; private final CurrentUser user; private final Timestamp when; @@ -509,7 +533,6 @@ @AssistedInject BatchUpdate( - @GerritServerConfig Config cfg, AllUsersName allUsers, ChangeControl.GenericFactory changeControlFactory, ChangeIndexer indexer, @@ -519,6 +542,7 @@ @GerritPersonIdent PersonIdent serverIdent, GitReferenceUpdated gitRefUpdated, GitRepositoryManager repoManager, + Metrics metrics, NoteDbUpdateManager.Factory updateManagerFactory, NotesMigration notesMigration, SchemaFactory<ReviewDb> schemaFactory, @@ -533,15 +557,11 @@ this.changeUpdateFactory = changeUpdateFactory; this.gitRefUpdated = gitRefUpdated; this.indexer = indexer; + this.metrics = metrics; this.notesMigration = notesMigration; this.repoManager = repoManager; this.schemaFactory = schemaFactory; this.updateManagerFactory = updateManagerFactory; - - this.logThresholdNanos = MILLISECONDS.toNanos( - ConfigUtil.getTimeUnit( - cfg, "change", null, "updateDebugLogThreshold", - SECONDS.toMillis(2), MILLISECONDS)); this.db = db; this.project = project; this.user = user; @@ -640,13 +660,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 { @@ -657,11 +681,9 @@ op.updateRepo(ctx); } - if (!repoOnlyOps.isEmpty()) { - logDebug("Executing updateRepo on {} RepoOnlyOps", ops.size()); - for (RepoOnlyOp op : repoOnlyOps) { - op.updateRepo(ctx); - } + logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size()); + for (RepoOnlyOp op : repoOnlyOps) { + op.updateRepo(ctx); } if (inserter != null) { @@ -671,12 +693,13 @@ logDebug("No objects to flush"); } } catch (Exception e) { - Throwables.propagateIfPossible(e, RestApiException.class); + Throwables.throwIfInstanceOf(e, RestApiException.class); throw new UpdateException(e); } } - 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 +710,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,63 +723,75 @@ } } if (!ok) { - throw new UpdateException("BatchRefUpdate failed: " + batchRefUpdate); + throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate); } } - private void executeChangeOps(boolean parallel) - throws UpdateException, RestApiException { - logDebug("Executing change ops (parallel? {})", parallel); - ListeningExecutorService executor = parallel - ? changeUpdateExector - : MoreExecutors.newDirectExecutorService(); - - List<ChangeTask> tasks = new ArrayList<>(ops.keySet().size()); + private List<ChangeTask> executeChangeOps(boolean parallel, + boolean dryrun) throws UpdateException, + RestApiException { + List<ChangeTask> tasks; + boolean success = false; + Stopwatch sw = Stopwatch.createStarted(); try { - if (notesMigration.commitChangeWrites() && repo != null) { - // A NoteDb change may have been rebuilt since the repo was originally - // opened, so make sure we see that. - logDebug("Preemptively scanning for repo changes"); - repo.scanForRepoChanges(); - } - if (!ops.isEmpty() && notesMigration.failChangeWrites()) { - // Fail fast before attempting any writes if changes are read-only, as - // this is a programmer error. - logDebug("Failing early due to read-only Changes table"); - throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY); - } - 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()); - tasks.add(task); - if (!parallel) { - logDebug("Direct execution of task for ops: {}", ops); + logDebug("Executing change ops (parallel? {})", parallel); + ListeningExecutorService executor = parallel + ? changeUpdateExector + : MoreExecutors.newDirectExecutorService(); + + tasks = new ArrayList<>(ops.keySet().size()); + try { + if (notesMigration.commitChangeWrites() && repo != null) { + // A NoteDb change may have been rebuilt since the repo was originally + // opened, so make sure we see that. + logDebug("Preemptively scanning for repo changes"); + repo.scanForRepoChanges(); } - futures.add(executor.submit(task)); - } - if (parallel) { - logDebug("Waiting on futures for {} ops spanning {} changes", - ops.size(), ops.keySet().size()); - } - // TODO(dborowitz): Timing is wrong for non-parallel updates. - long startNanos = System.nanoTime(); - Futures.allAsList(futures).get(); - maybeLogSlowUpdate(startNanos, "change"); + if (!ops.isEmpty() && notesMigration.failChangeWrites()) { + // Fail fast before attempting any writes if changes are read-only, as + // this is a programmer error. + logDebug("Failing early due to read-only Changes table"); + throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY); + } + 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(), + dryrun); + tasks.add(task); + if (!parallel) { + logDebug("Direct execution of task for ops: {}", ops); + } + futures.add(executor.submit(task)); + } + if (parallel) { + logDebug("Waiting on futures for {} ops spanning {} changes", + ops.size(), ops.keySet().size()); + } + Futures.allAsList(futures).get(); - if (notesMigration.commitChangeWrites()) { - startNanos = System.nanoTime(); - executeNoteDbUpdates(tasks); - maybeLogSlowUpdate(startNanos, "NoteDb"); + if (notesMigration.commitChangeWrites()) { + if (!dryrun) { + executeNoteDbUpdates(tasks); + } + } + success = true; + } catch (ExecutionException | InterruptedException e) { + Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class); + Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class); + throw new UpdateException(e); + } catch (OrmException | IOException e) { + throw new UpdateException(e); } - } catch (ExecutionException | InterruptedException e) { - Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class); - Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class); - throw new UpdateException(e); - } catch (OrmException | IOException e) { - throw new UpdateException(e); + } finally { + metrics.executeChangeOpsLatency.record( + success, sw.elapsed(NANOSECONDS), NANOSECONDS); } + return tasks; + } + private void reindexChanges(List<ChangeTask> tasks) { // Reindex changes. for (ChangeTask task : tasks) { if (task.deleted) { @@ -763,26 +802,6 @@ } } - private static class SlowUpdateException extends Exception { - private static final long serialVersionUID = 1L; - - private SlowUpdateException(String fmt, Object... args) { - super(String.format(fmt, args)); - } - } - - private void maybeLogSlowUpdate(long startNanos, String desc) { - long elapsedNanos = System.nanoTime() - startNanos; - if (!log.isDebugEnabled() || elapsedNanos <= logThresholdNanos) { - return; - } - // Always log even without RequestId. - log.debug("Slow " + desc + " update", - new SlowUpdateException( - "Slow %s update (%d ms) to %s for %s", - desc, NANOSECONDS.toMillis(elapsedNanos), project, ops.keySet())); - } - private void executeNoteDbUpdates(List<ChangeTask> tasks) { // Aggregate together all NoteDb ref updates from the ops we executed, // possibly in parallel. Each task had its own NoteDbUpdateManager instance @@ -874,6 +893,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 +901,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 @@ -917,10 +938,17 @@ @SuppressWarnings("resource") // Not always opened. NoteDbUpdateManager updateManager = null; try { - ChangeContext ctx; + PrimaryStorage storage; db.changes().beginTransaction(id); try { - ctx = newChangeContext(db, repo, rw, id); + ChangeContext ctx = newChangeContext(db, repo, rw, id); + storage = PrimaryStorage.of(ctx.getChange()); + if (storage == PrimaryStorage.NOTE_DB + && !notesMigration.readChanges()) { + throw new OrmException( + "must have NoteDb enabled to update change " + id); + } + // Call updateChange on each op. logDebug("Calling updateChange on {} ops", changeOps.size()); for (Op op : changeOps) { @@ -940,32 +968,45 @@ updateManager = stageNoteDbUpdate(ctx, deleted); } - // Bump lastUpdatedOn or rowVersion and commit. - Iterable<Change> cs = changesToUpdate(ctx); - if (newChanges.containsKey(id)) { - // Insert rather than upsert in case of a race on change IDs. - logDebug("Inserting change"); - db.changes().insert(cs); - } else if (deleted) { - logDebug("Deleting change"); - db.changes().delete(cs); + if (storage == PrimaryStorage.REVIEW_DB) { + // If primary storage of this change is in ReviewDb, bump + // lastUpdatedOn or rowVersion and commit. Otherwise, don't waste + // time updating ReviewDb at all. + Iterable<Change> cs = changesToUpdate(ctx); + if (isNewChange(id)) { + // Insert rather than upsert in case of a race on change IDs. + logDebug("Inserting change"); + db.changes().insert(cs); + } else if (deleted) { + logDebug("Deleting change"); + db.changes().delete(cs); + } else { + logDebug("Updating change"); + db.changes().update(cs); + } + if (!dryrun) { + db.commit(); + } } else { - logDebug("Updating change"); - db.changes().update(cs); + logDebug( + "Skipping ReviewDb write since primary storage is {}", storage); } - db.commit(); } finally { db.rollback(); } - if (notesMigration.commitChangeWrites()) { + // Do not execute the NoteDbUpdateManager, as we don't want too much + // contention on the underlying repo, and we would rather use a single + // ObjectInserter/BatchRefUpdate later. + // + // TODO(dborowitz): May or may not be worth trying to batch together + // flushed inserters as well. + if (storage == PrimaryStorage.NOTE_DB) { + // Should have failed above if NoteDb is disabled. + checkState(notesMigration.commitChangeWrites()); + noteDbResult = updateManager.stage().get(id); + } else if (notesMigration.commitChangeWrites()) { try { - // Do not execute the NoteDbUpdateManager, as we don't want too much - // contention on the underlying repo, and we would rather use a - // single ObjectInserter/BatchRefUpdate later. - // - // TODO(dborowitz): May or may not be worth trying to batch - // together flushed inserters as well. noteDbResult = updateManager.stage().get(id); } catch (IOException ex) { // Ignore all errors trying to update NoteDb at this point. We've @@ -991,7 +1032,7 @@ RevWalk rw, Change.Id id) throws OrmException, NoSuchChangeException { Change c = newChanges.get(id); if (c == null) { - c = ReviewDbUtil.unwrapDb(db).changes().get(id); + c = ChangeNotes.readOneReviewDbChange(db, id); if (c == null) { logDebug("Failed to get change {} from unwrapped db", id); throw new NoSuchChangeException(id); @@ -1015,20 +1056,29 @@ for (ChangeUpdate u : ctx.updates.values()) { updateManager.add(u); } + + Change c = ctx.getChange(); if (deleted) { - updateManager.deleteChange(ctx.getChange().getId()); + updateManager.deleteChange(c.getId()); } try { - updateManager.stageAndApplyDelta(ctx.getChange()); - } catch (OrmConcurrencyException ex) { - // Refused to apply update because NoteDb was out of sync. Go ahead with - // this ReviewDb update; it's still out of sync, but this is no worse - // than before, and it will eventually get rebuilt. - logDebug("Ignoring OrmConcurrencyException while staging"); + updateManager.stageAndApplyDelta(c); + } catch (MismatchedStateException ex) { + // Refused to apply update because NoteDb was out of sync, which can + // only happen if ReviewDb is the primary storage for this change. + // + // Go ahead with this ReviewDb update; it's still out of sync, but this + // is no worse than before, and it will eventually get rebuilt. + logDebug("Ignoring MismatchedStateException while staging"); } + return updateManager; } + private boolean isNewChange(Change.Id id) { + return newChanges.containsKey(id); + } + private void logDebug(String msg, Throwable t) { if (log.isDebugEnabled()) { BatchUpdate.this.logDebug("[" + taskId + "]" + msg, t);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java index cfbaa41..a3b30d1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChainedReceiveCommands.java
@@ -17,8 +17,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.base.Optional; - import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; @@ -28,6 +26,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; /** * Collection of {@link ReceiveCommand}s that supports multiple updates per ref. @@ -96,7 +95,7 @@ if (cmd != null) { return !cmd.getNewId().equals(ObjectId.zeroId()) ? Optional.of(cmd.getNewId()) - : Optional.<ObjectId>absent(); + : Optional.empty(); } return refCache.get(refName); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.java new file mode 100644 index 0000000..75911f3f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMessageModifier.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.git; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.reviewdb.client.Branch; + +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Allows to modify the commit message for new commits generated by Rebase + * Always submit strategy. + * + * Invoked by Gerrit when all information about new commit is already known such + * as parent(s), tree hash, etc, but commit's message can still be modified. + */ +@ExtensionPoint +public interface ChangeMessageModifier { + + /** + * Implementation must return non-Null commit message. + * + * mergeTip and original commit are guaranteed to have their body parsed, + * meaning that their commit messages and footers can be accessed. + * + * @param newCommitMessage the new commit message that was result of either + * <ul> + * <li>{@link MergeUtil#createDetailedCommitMessage} called before</li> + * <li>other extensions or plugins implementing the same point and + * called before.</li> + * </ul> + * @param original the commit of the change being submitted. <b>Note that its + * commit message may be different than newCommitMessage argument.</b> + * @param mergeTip the current HEAD of the destination branch, which will be a + * parent of a new commit being generated + * @param destination the branch onto which the change is being submitted + * @return a new not null commit message. + */ + String onSubmit(String newCommitMessage, RevCommit original, + RevCommit mergeTip, Branch.NameKey destination); +}
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..8e2028f 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
@@ -14,18 +14,15 @@ 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.common.collect.MultimapBuilder; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; @@ -85,20 +82,10 @@ 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 = - ArrayListMultimap.create(); + MultimapBuilder.hashKeys().arrayListValues().build(); for (ChangeData cd : changeData.values()) { ret.put(cd.change().getDest(), cd); }
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/DestinationList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java index 7c02e5b..c4d4d61 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DestinationList.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.git; -import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Branch; @@ -27,7 +27,8 @@ public class DestinationList extends TabFile { public static final String DIR_NAME = "destinations"; - private SetMultimap<String, Branch.NameKey> destinations = HashMultimap.create(); + private SetMultimap<String, Branch.NameKey> destinations = + MultimapBuilder.hashKeys().hashSetValues().build(); public Set<Branch.NameKey> getDestinations(String label) { return destinations.get(label);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java index 66e0704..6273702 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/EmailMerge.java
@@ -14,15 +14,17 @@ package com.google.gerrit.server.git; +import com.google.common.collect.Multimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.mail.MergedSender; +import com.google.gerrit.server.mail.send.MergedSender; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gwtorm.server.OrmException; @@ -43,7 +45,8 @@ public interface Factory { EmailMerge create(Project.NameKey project, Change.Id changeId, - Account.Id submitter, NotifyHandling notifyHandling); + Account.Id submitter, NotifyHandling notifyHandling, + Multimap<RecipientType, Account.Id> accountsToNotify); } private final ExecutorService sendEmailsExecutor; @@ -56,6 +59,8 @@ private final Change.Id changeId; private final Account.Id submitter; private final NotifyHandling notifyHandling; + private final Multimap<RecipientType, Account.Id> accountsToNotify; + private ReviewDb db; @Inject @@ -67,7 +72,8 @@ @Assisted Project.NameKey project, @Assisted Change.Id changeId, @Assisted @Nullable Account.Id submitter, - @Assisted NotifyHandling notifyHandling) { + @Assisted NotifyHandling notifyHandling, + @Assisted Multimap<RecipientType, Account.Id> accountsToNotify) { this.sendEmailsExecutor = executor; this.mergedSenderFactory = mergedSenderFactory; this.schemaFactory = schemaFactory; @@ -77,6 +83,7 @@ this.changeId = changeId; this.submitter = submitter; this.notifyHandling = notifyHandling; + this.accountsToNotify = accountsToNotify; } public void sendAsync() { @@ -92,6 +99,7 @@ cm.setFrom(submitter); } cm.setNotify(notifyHandling); + cm.setAccountsToNotify(accountsToNotify); cm.send(); } catch (Exception e) { log.error("Cannot email merged notification for " + changeId, e);
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/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java index 29e14ec..1724808 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.git; import com.google.gerrit.reviewdb.client.Project; +import com.google.inject.ImplementedBy; import com.google.inject.Singleton; import org.eclipse.jgit.errors.RepositoryNotFoundException; @@ -30,6 +31,7 @@ * registered in Guice so they are globally available within the server * environment. */ +@ImplementedBy(value = LocalDiskRepositoryManager.class) public interface GitRepositoryManager { /** * Get (or open) a repository by name. @@ -61,31 +63,4 @@ /** @return set of all known projects, sorted by natural NameKey order. */ SortedSet<Project.NameKey> list(); - - /** - * Read the {@code GIT_DIR/description} file for gitweb. - * <p> - * NB: This code should really be in JGit, as a member of the Repository - * object. Until it moves there, its here. - * - * @param name the repository name, relative to the base directory. - * @return description text; null if no description has been configured. - * @throws RepositoryNotFoundException the named repository does not exist. - * @throws IOException the description file exists, but is not readable by - * this process. - */ - String getProjectDescription(Project.NameKey name) - throws RepositoryNotFoundException, IOException; - - /** - * Set the {@code GIT_DIR/description} file for gitweb. - * <p> - * NB: This code should really be in JGit, as a member of the Repository - * object. Until it moves there, its here. - * - * @param name the repository name, relative to the base directory. - * @param description new description text for the repository. - */ - void setProjectDescription(Project.NameKey name, - final String description); }
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..2f4f43c 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,9 +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; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -151,20 +148,14 @@ Lookup groupLookup) { this.patchSetsBySha = patchSetsBySha; this.groupLookup = groupLookup; - groups = ArrayListMultimap.create(); - groupAliases = HashMultimap.create(); + groups = MultimapBuilder.hashKeys().arrayListValues().build(); + groupAliases = MultimapBuilder.hashKeys().hashSetValues().build(); } 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/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java index bd76ad4..880fc0b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -16,6 +16,10 @@ import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; @@ -26,6 +30,8 @@ import java.util.Set; public class GroupList extends TabFile { + private static final Logger log = LoggerFactory.getLogger(GroupList.class); + public static final String FILE_NAME = "groups"; private final Map<AccountGroup.UUID, GroupReference> byUUID; @@ -34,12 +40,16 @@ this.byUUID = byUUID; } - public static GroupList parse(String text, ValidationError.Sink errors) - throws IOException { + public static GroupList parse(Project.NameKey project, String text, + ValidationError.Sink errors) throws IOException { List<Row> rows = parse(text, FILE_NAME, TRIM, TRIM, errors); Map<AccountGroup.UUID, GroupReference> groupsByUUID = new HashMap<>(rows.size()); for (Row row : rows) { + if (row.left == null) { + log.warn("null field in group list for {}:\n{}", project, text); + continue; + } AccountGroup.UUID uuid = new AccountGroup.UUID(row.left); String name = row.right; GroupReference ref = new GroupReference(uuid, name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java index ad5cf20..dc15a8b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -24,7 +24,6 @@ import com.google.inject.Singleton; import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; @@ -35,13 +34,10 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.IO; -import org.eclipse.jgit.util.RawParseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; @@ -58,19 +54,13 @@ /** Manages Git repositories stored on the local filesystem. */ @Singleton -public class LocalDiskRepositoryManager implements GitRepositoryManager, - LifecycleListener { +public class LocalDiskRepositoryManager implements GitRepositoryManager { private static final Logger log = LoggerFactory.getLogger(LocalDiskRepositoryManager.class); - private static final String UNNAMED = - "Unnamed repository; edit this file to name it for gitweb."; - public static class Module extends LifecycleModule { @Override protected void configure() { - bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class); - listener().to(LocalDiskRepositoryManager.class); listener().to(LocalDiskRepositoryManager.Lifecycle.class); } } @@ -139,15 +129,6 @@ namesUpdateLock = new ReentrantLock(true /* fair */); } - @Override - public void start() { - names = list(); - } - - @Override - public void stop() { - } - /** * Return the basePath under which the specified project is stored. * @@ -207,7 +188,8 @@ @Override public Repository createRepository(Project.NameKey name) - throws RepositoryNotFoundException, RepositoryCaseMismatchException { + throws RepositoryNotFoundException, RepositoryCaseMismatchException, + IOException { Path path = getBasePath(name); if (isUnreasonableName(name)) { throw new RepositoryNotFoundException("Invalid name: " + name); @@ -218,6 +200,10 @@ if (dir != null) { // Already exists on disk, use the repository we found. // + Project.NameKey onDiskName = getProjectName( + path, dir.getCanonicalFile().toPath()); + onCreateProject(onDiskName); + loc = FileKey.exact(dir, FS.DETECTED); if (!names.contains(name)) { @@ -274,66 +260,6 @@ } } - @Override - public String getProjectDescription(final Project.NameKey name) - throws RepositoryNotFoundException, IOException { - try (Repository e = openRepository(name)) { - return getProjectDescription(e); - } - } - - private String getProjectDescription(final Repository e) throws IOException { - final File d = new File(e.getDirectory(), "description"); - - String description; - try { - description = RawParseUtils.decode(IO.readFully(d)); - } catch (FileNotFoundException err) { - return null; - } - - if (description != null) { - description = description.trim(); - if (description.isEmpty()) { - description = null; - } - if (UNNAMED.equals(description)) { - description = null; - } - } - return description; - } - - @Override - public void setProjectDescription(Project.NameKey name, String description) { - // Update git's description file, in case gitweb is being used - // - try (Repository e = openRepository(name)) { - String old = getProjectDescription(e); - if ((old == null && description == null) - || (old != null && old.equals(description))) { - return; - } - - LockFile f = new LockFile(new File(e.getDirectory(), "description")); - if (f.lock()) { - String d = description; - if (d != null) { - d = d.trim(); - if (d.length() > 0) { - d += "\n"; - } - } else { - d = ""; - } - f.write(Constants.encode(d)); - f.commit(); - } - } catch (IOException e) { - log.error("Cannot update description for " + name, e); - } - } - private boolean isUnreasonableName(final Project.NameKey nameKey) { final String name = nameKey.get(); @@ -362,15 +288,19 @@ public SortedSet<Project.NameKey> list() { // The results of this method are cached by ProjectCacheImpl. Control only // enters here if the cache was flushed by the administrator to force - // scanning the filesystem. Don't rely on the cached names collection. + // scanning the filesystem. + // Don't rely on the cached names collection but update it to contain + // the set of found project names + ProjectVisitor visitor = new ProjectVisitor(basePath); + scanProjects(visitor); + namesUpdateLock.lock(); try { - ProjectVisitor visitor = new ProjectVisitor(basePath); - scanProjects(visitor); - return Collections.unmodifiableSortedSet(visitor.found); + names = Collections.unmodifiableSortedSet(visitor.found); } finally { namesUpdateLock.unlock(); } + return names; } protected void scanProjects(ProjectVisitor visitor) { @@ -383,6 +313,18 @@ } } + private static Project.NameKey getProjectName(Path startFolder, Path p) { + String projectName = startFolder.relativize(p).toString(); + if (File.separatorChar != '/') { + projectName = projectName.replace(File.separatorChar, '/'); + } + if (projectName.endsWith(Constants.DOT_GIT_EXT)) { + int newLen = projectName.length() - Constants.DOT_GIT_EXT.length(); + projectName = projectName.substring(0, newLen); + } + return new Project.NameKey(projectName); + } + protected class ProjectVisitor extends SimpleFileVisitor<Path> { private final SortedSet<Project.NameKey> found = new TreeSet<>(); private Path startFolder; @@ -413,7 +355,7 @@ } private void addProject(Path p) { - Project.NameKey nameKey = getProjectName(p); + Project.NameKey nameKey = getProjectName(startFolder, p); if (getBasePath(nameKey).equals(startFolder)) { if (isUnreasonableName(nameKey)) { log.warn( @@ -423,17 +365,5 @@ } } } - - private Project.NameKey getProjectName(Path p) { - String projectName = startFolder.relativize(p).toString(); - if (File.separatorChar != '/') { - projectName = projectName.replace(File.separatorChar, '/'); - } - if (projectName.endsWith(Constants.DOT_GIT_EXT)) { - int newLen = projectName.length() - Constants.DOT_GIT_EXT.length(); - projectName = projectName.substring(0, newLen); - } - return new Project.NameKey(projectName); - } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LockFailureException.java similarity index 60% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/git/LockFailureException.java index ea0def0..7380b0a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LockFailureException.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. @@ -12,8 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.git; -public enum RecipientType { - TO, CC, BCC +import java.io.IOException; + +/** Thrown when updating a ref in Git fails with LOCK_FAILURE. */ +public class LockFailureException extends IOException { + private static final long serialVersionUID = 1L; + + public LockFailureException(String message) { + super(message); + } }
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..1ed671a 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,31 +17,28 @@ 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; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; -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; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.common.data.SubmitTypeRecord; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -49,9 +46,9 @@ 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.change.NotifyUtil; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.git.MergeOpRepoManager.OpenBranch; import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo; @@ -62,7 +59,7 @@ import com.google.gerrit.server.git.validators.MergeValidators; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.SubmitRuleEvaluator; +import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.util.RequestId; @@ -105,6 +102,9 @@ public class MergeOp implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(MergeOp.class); + private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = + SubmitRuleOptions.defaults().build(); + public static class CommitStatus { private final ImmutableMap<Change.Id, ChangeData> changes; private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch; @@ -122,13 +122,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() { @@ -180,7 +177,7 @@ // However, do NOT expose that ChangeData directly, as it is way out of // date by this point. ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id); - return checkNotNull(cd.getSubmitRecords(), + return checkNotNull(cd.getSubmitRecords(SUBMIT_RULE_OPTIONS), "getSubmitRecord only valid after submit rules are evalutated"); } @@ -222,6 +219,7 @@ private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; private final MergeOpRepoManager orm; + private final NotifyUtil notifyUtil; private Timestamp ts; private RequestId submissionId; @@ -230,6 +228,9 @@ private CommitStatus commits; private ReviewDb db; private SubmitInput submitInput; + private Multimap<RecipientType, Account.Id> accountsToNotify; + private Set<Project.NameKey> allProjects; + private boolean dryrun; @Inject MergeOp(ChangeMessagesUtil cmUtil, @@ -240,7 +241,8 @@ InternalChangeQuery internalChangeQuery, SubmitStrategyFactory submitStrategyFactory, SubmoduleOp.Factory subOpFactory, - MergeOpRepoManager orm) { + MergeOpRepoManager orm, + NotifyUtil notifyUtil) { this.cmUtil = cmUtil; this.batchUpdateFactory = batchUpdateFactory; this.internalUserFactory = internalUserFactory; @@ -250,6 +252,7 @@ this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; this.orm = orm; + this.notifyUtil = notifyUtil; } @Override @@ -257,19 +260,6 @@ orm.close(); } - private static Optional<SubmitRecord> findOkRecord( - Collection<SubmitRecord> in) { - 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; - } - }); - } - public static void checkSubmitRule(ChangeData cd) throws ResourceConflictException, OrmException { PatchSet patchSet = cd.currentPatchSet(); @@ -278,7 +268,7 @@ "missing current patch set for change " + cd.getId()); } List<SubmitRecord> results = getSubmitRecords(cd); - if (findOkRecord(results).isPresent()) { + if (SubmitRecord.findOkRecord(results).isPresent()) { // Rules supplied a valid solution. return; } else if (results.isEmpty()) { @@ -318,12 +308,7 @@ private static List<SubmitRecord> getSubmitRecords(ChangeData cd) throws OrmException { - List<SubmitRecord> results = cd.getSubmitRecords(); - if (results == null) { - results = new SubmitRuleEvaluator(cd).evaluate(); - cd.setSubmitRecords(results); - } - return results; + return cd.submitRecords(SUBMIT_RULE_OPTIONS); } private static String describeLabels(ChangeData cd, @@ -396,14 +381,32 @@ SubmitRecord forced = new SubmitRecord(); forced.status = SubmitRecord.Status.FORCED; records.add(forced); - cd.setSubmitRecords(records); + cd.setSubmitRecords(SUBMIT_RULE_OPTIONS, records); } } + /** + * 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.accountsToNotify = + notifyUtil.resolveAccounts(submitInput.notifyDetails); + this.dryrun = dryrun; this.caller = caller; this.ts = TimeUtil.nowTs(); submissionId = RequestId.forChange(change); @@ -412,7 +415,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,39 +451,33 @@ "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); - } catch (UpdateException | SubmoduleException e) { + submissionId, dryrun); + } catch (SubmoduleException e) { + throw new IntegrationException(e); + } catch (UpdateException e) { // BatchUpdate may have inadvertently wrapped an IntegrationException // thrown by some legacy SubmitStrategyOp code that intended the error // message to be user-visible. Copy the message from the wrapped @@ -491,23 +489,25 @@ if (e.getCause() instanceof IntegrationException) { msg = e.getCause().getMessage(); } else { - msg = "Error submitting change" + (cs.size() != 1 ? "s" : "") + ": \n" - + e.getMessage(); + msg = "Error submitting change" + (cs.size() != 1 ? "s" : ""); } throw new IntegrationException(msg, e); } } + 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 +519,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 +549,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, accountsToNotify, + submoduleOp, dryrun); } private Set<RevCommit> getAlreadyAccepted(OpenRepo or, @@ -701,7 +707,7 @@ } } Multimap<ObjectId, PatchSet.Id> revisions = - HashMultimap.create(cds.size(), 1); + MultimapBuilder.hashKeys(cds.size()).hashSetValues(1).build(); for (Map.Entry<String, Ref> e : or.repo.getRefDatabase().exactRef( refNames.toArray(new String[refNames.size()])).entrySet()) { revisions.put( @@ -723,19 +729,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 +760,11 @@ 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(), + ChangeMessagesUtil.TAG_MERGED, + "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..751933c 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,15 @@ 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.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 +33,34 @@ 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.Optional; 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.of(or.rw.parseCommit(ref.getObjectId())) + : Optional.empty(); + 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 ae11630..b97dbc6 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
@@ -15,16 +15,18 @@ package com.google.gerrit.server.git; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; -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; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; @@ -33,6 +35,7 @@ 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.PatchSet.Id; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; @@ -44,6 +47,7 @@ import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectState; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -98,6 +102,32 @@ */ public class MergeUtil { private static final Logger log = LoggerFactory.getLogger(MergeUtil.class); + + static class PluggableCommitMessageGenerator { + private final DynamicSet<ChangeMessageModifier> changeMessageModifiers; + + @Inject + PluggableCommitMessageGenerator( + DynamicSet<ChangeMessageModifier> changeMessageModifiers) { + this.changeMessageModifiers = changeMessageModifiers; + } + + public String generate(RevCommit original, RevCommit mergeTip, + ChangeControl ctl, String current) { + checkNotNull(original.getRawBuffer()); + if (mergeTip != null) { + checkNotNull(mergeTip.getRawBuffer()); + } + for (ChangeMessageModifier changeMessageModifier : changeMessageModifiers) { + current = changeMessageModifier.onSubmit(current, original, + mergeTip, ctl.getChange().getDest()); + checkNotNull(current, changeMessageModifier.getClass().getName() + + ".OnSubmit returned null instead of new commit message"); + } + return current; + } + } + private static final String R_HEADS_MASTER = Constants.R_HEADS + Constants.MASTER; @@ -123,25 +153,28 @@ private final ProjectState project; private final boolean useContentMerge; private final boolean useRecursiveMerge; + private final PluggableCommitMessageGenerator commitMessageGenerator; @AssistedInject MergeUtil(@GerritServerConfig Config serverConfig, - final Provider<ReviewDb> db, - final IdentifiedUser.GenericFactory identifiedUserFactory, - @CanonicalWebUrl @Nullable final Provider<String> urlProvider, - final ApprovalsUtil approvalsUtil, - @Assisted final ProjectState project) { + Provider<ReviewDb> db, + IdentifiedUser.GenericFactory identifiedUserFactory, + @CanonicalWebUrl @Nullable Provider<String> urlProvider, + ApprovalsUtil approvalsUtil, + PluggableCommitMessageGenerator commitMessageGenerator, + @Assisted ProjectState project) { this(serverConfig, db, identifiedUserFactory, urlProvider, approvalsUtil, - project, project.isUseContentMerge()); + project, commitMessageGenerator, project.isUseContentMerge()); } @AssistedInject MergeUtil(@GerritServerConfig Config serverConfig, - final Provider<ReviewDb> db, - final IdentifiedUser.GenericFactory identifiedUserFactory, - @CanonicalWebUrl @Nullable final Provider<String> urlProvider, - final ApprovalsUtil approvalsUtil, - @Assisted final ProjectState project, + Provider<ReviewDb> db, + IdentifiedUser.GenericFactory identifiedUserFactory, + @CanonicalWebUrl @Nullable Provider<String> urlProvider, + ApprovalsUtil approvalsUtil, + @Assisted ProjectState project, + PluggableCommitMessageGenerator commitMessageGenerator, @Assisted boolean useContentMerge) { this.db = db; this.identifiedUserFactory = identifiedUserFactory; @@ -150,6 +183,7 @@ this.project = project; this.useContentMerge = useContentMerge; this.useRecursiveMerge = useRecursiveMerge(serverConfig); + this.commitMessageGenerator = commitMessageGenerator; } public CodeReviewCommit getFirstFastForward( @@ -185,13 +219,13 @@ public CodeReviewCommit createCherryPickFromCommit(Repository repo, ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit, PersonIdent cherryPickCommitterIdent, String commitMsg, - CodeReviewRevWalk rw) + CodeReviewRevWalk rw, int parentIndex) throws MissingObjectException, IncorrectObjectTypeException, IOException, MergeIdenticalTreeException, MergeConflictException { final ThreeWayMerger m = newThreeWayMerger(repo, inserter); - m.setBase(originalCommit.getParent(0)); + m.setBase(originalCommit.getParent(parentIndex)); if (m.merge(mergeTip, originalCommit)) { ObjectId tree = m.getResultTreeId(); if (tree.equals(mergeTip.getTree())) { @@ -214,7 +248,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"); } @@ -246,7 +281,24 @@ return sb.toString(); } - public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl, + /** + * Adds footers to existing commit message based on the state of the change. + * + * This adds the following footers if they are missing: + * + * <ul> + * <li> Reviewed-on: <i>url</i></li> + * <li> Reviewed-by | Tested-by | <i>Other-Label-Name</i>: <i>reviewer</i> + * </li> + * <li> Change-Id </li> + * </ul> + * + * @param n + * @param ctl + * @param psId + * @return new message + */ + private String createDetailedCommitMessage(RevCommit n, ChangeControl ctl, PatchSet.Id psId) { Change c = ctl.getChange(); final List<FooterLine> footers = n.getFooterLines(); @@ -350,12 +402,32 @@ msgbuf.append('\n'); } } - return msgbuf.toString(); } - public String createCherryPickCommitMessage(final CodeReviewCommit n) { - return createCherryPickCommitMessage(n, n.getControl(), n.getPatchsetId()); + public String createCommitMessageOnSubmit(CodeReviewCommit n, + RevCommit mergeTip) { + return createCommitMessageOnSubmit(n, mergeTip, n.getControl(), + n.getPatchsetId()); + } + + /** + * Creates a commit message for a change, which can be customized by plugins. + * + * By default, adds footers to existing commit message based on the state of + * the change. Plugins implementing {@link ChangeMessageModifier} can modify + * the resulting commit message arbitrarily. + * + * @param n + * @param mergeTip + * @param ctl + * @param id + * @return new message + */ + public String createCommitMessageOnSubmit(RevCommit n, RevCommit mergeTip, + ChangeControl ctl, Id id) { + return commitMessageGenerator.generate(n, mergeTip, ctl, + createDetailedCommitMessage(n, ctl, id)); } private static boolean isCodeReview(LabelId id) { @@ -599,14 +671,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..d7c1cda 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,13 +22,13 @@ 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; import com.google.gerrit.server.git.BatchUpdate.Context; -import com.google.gerrit.server.mail.MergedSender; +import com.google.gerrit.server.mail.send.MergedSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.util.RequestScopePropagator; @@ -135,6 +135,7 @@ // we cannot reconstruct the submit records for when this change was // submitted, this is why we must fix the status update.fixStatus(Change.Status.MERGED); + update.setCurrentPatchSet(); } StringBuilder msgBuf = new StringBuilder(); @@ -149,19 +150,14 @@ } } 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(), + ChangeMessagesUtil.TAG_MERGED); 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/MultiBaseLocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java index dffcf30..db739b1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManager.java
@@ -35,9 +35,6 @@ protected void configure() { bind(GitRepositoryManager.class).to( MultiBaseLocalDiskRepositoryManager.class); - bind(LocalDiskRepositoryManager.class).to( - MultiBaseLocalDiskRepositoryManager.class); - listener().to(MultiBaseLocalDiskRepositoryManager.class); listener().to(MultiBaseLocalDiskRepositoryManager.Lifecycle.class); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java index d081fe6..9810fec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -81,7 +81,7 @@ @Override public void update(final int completed) { boolean w = false; - synchronized (this) { + synchronized (MultiProgressMonitor.this) { count += completed; if (total != UNKNOWN) { int percent = count * 100 / total; @@ -124,8 +124,10 @@ return false; } - public synchronized int getCount() { - return count; + public int getCount() { + synchronized(MultiProgressMonitor.this) { + return count; + } } } @@ -319,7 +321,7 @@ if (!tasks.isEmpty()) { boolean first = true; for (Task t : tasks) { - int count = t.count; + int count = t.getCount(); if (count == 0) { continue; }
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 3b1fa09..f3ed9f2 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"; @@ -144,6 +144,7 @@ private static final String KEY_FUNCTION = "function"; private static final String KEY_DEFAULT_VALUE = "defaultValue"; private static final String KEY_COPY_MIN_SCORE = "copyMinScore"; + private static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit"; private static final String KEY_COPY_MAX_SCORE = "copyMaxScore"; private static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE = "copyAllScoresOnMergeFirstParentUpdate"; private static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase"; @@ -151,15 +152,18 @@ 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 = + private static final SubmitType DEFAULT_SUBMIT_ACTION = SubmitType.MERGE_IF_NECESSARY; - private static final ProjectState defaultStateValue = + private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE; private Project.NameKey projectName; @@ -180,6 +184,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 { @@ -267,6 +272,10 @@ return branchOrderSection; } + public Map<Project.NameKey, SubscribeSection> getSubscribeSections() { + return subscribeSections; + } + public Collection<SubscribeSection> getSubscribeSections( Branch.NameKey branch) { Collection<SubscribeSection> ret = new ArrayList<>(); @@ -481,6 +490,13 @@ if (p.getDescription() == null) { p.setDescription(""); } + + if (rc.getStringList(ACCESS, null, KEY_INHERIT_FROM).length > 1) { + // The config must not contain more than one parent to inherit from + // as there is no guarantee which of the parents would be used then. + error(new ValidationError(PROJECT_CONFIG, + "Cannot inherit from multiple projects")); + } p.setParentName(rc.getString(ACCESS, null, KEY_INHERIT_FROM)); p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, InheritableBoolean.INHERIT)); @@ -495,9 +511,10 @@ p.setRejectImplicitMerges(getEnum(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, InheritableBoolean.INHERIT)); - p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction)); + p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, + DEFAULT_SUBMIT_ACTION)); p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, InheritableBoolean.INHERIT)); - p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue)); + p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE)); p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT)); p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT)); @@ -627,6 +644,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 +652,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()); } @@ -784,6 +803,9 @@ KEY_DEFAULT_VALUE, dv, name))); } } + label.setAllowPostSubmit( + rc.getBoolean(LABEL, name, KEY_ALLOW_POST_SUBMIT, + LabelType.DEF_ALLOW_POST_SUBMIT)); label.setCopyMinScore( rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, LabelType.DEF_COPY_MIN_SCORE)); @@ -805,7 +827,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); } } @@ -904,7 +926,8 @@ } private void readGroupList() throws IOException { - groupList = GroupList.parse(readUTF8(GroupList.FILE_NAME), this); + groupList = GroupList.parse( + projectName, readUTF8(GroupList.FILE_NAME), this); } private Map<String, GroupReference> mapGroupReferences() { @@ -946,10 +969,10 @@ set(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, p.getRejectImplicitMerges(), InheritableBoolean.INHERIT); - set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction); + set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_ACTION); set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT); - set(rc, PROJECT, null, KEY_STATE, p.getState(), defaultStateValue); + set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE); set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard()); set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard()); @@ -1147,7 +1170,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); } } @@ -1177,6 +1201,8 @@ rc.setString(LABEL, name, KEY_FUNCTION, label.getFunctionName()); rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue()); + setBooleanConfigKey(rc, name, KEY_ALLOW_POST_SUBMIT, label.allowPostSubmit(), + LabelType.DEF_ALLOW_POST_SUBMIT); setBooleanConfigKey(rc, name, KEY_COPY_MIN_SCORE, label.isCopyMinScore(), LabelType.DEF_COPY_MIN_SCORE); setBooleanConfigKey(rc, name, KEY_COPY_MAX_SCORE, label.isCopyMaxScore(), @@ -1248,14 +1274,19 @@ private void saveSubscribeSections(Config rc) { for (Project.NameKey p : subscribeSections.keySet()) { SubscribeSection s = subscribeSections.get(p); + List<String> matchings = new ArrayList<>(); for (RefSpec r : s.getMatchingRefSpecs()) { - rc.setString(SUBSCRIBE_SECTION, p.get(), - SUBSCRIBE_MATCH_REFS, r.toString()); + matchings.add(r.toString()); } + rc.setStringList(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_MATCH_REFS, + matchings); + + List<String> multimatchs = new ArrayList<>(); for (RefSpec r : s.getMultiMatchRefSpecs()) { - rc.setString(SUBSCRIBE_SECTION, p.get(), - SUBSCRIBE_MULTI_MATCH_REFS, r.toString()); + multimatchs.add(r.toString()); } + rc.setStringList(SUBSCRIBE_SECTION, p.get(), + SUBSCRIBE_MULTI_MATCH_REFS, multimatchs); } } @@ -1282,4 +1313,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/ReadOnlyRepository.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java index 32faeac..2a16148 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
@@ -101,6 +101,11 @@ return delegate.getReflogReader(refName); } + @Override + public String getGitwebDescription() throws IOException { + return delegate.getGitwebDescription(); + } + private static class RefDb extends RefDatabase { private final RefDatabase delegate;
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..cdd8b0c 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,19 @@ 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.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.collect.SortedSetMultimap; @@ -60,6 +58,7 @@ import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.extensions.api.changes.HashtagsInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicMap; @@ -103,6 +102,7 @@ import com.google.gerrit.server.git.validators.RefOperationValidationException; import com.google.gerrit.server.git.validators.RefOperationValidators; import com.google.gerrit.server.git.validators.ValidationMessage; +import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.mail.MailUtil.MailRecipients; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; @@ -169,6 +169,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -288,7 +289,6 @@ private final GitReferenceUpdated gitRefUpdated; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; - private final GitRepositoryManager repoManager; private final ProjectCache projectCache; private final String canonicalWebUrl; private final CommitValidators.Factory commitValidatorsFactory; @@ -315,6 +315,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 = @@ -332,6 +334,7 @@ private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final NotesMigration notesMigration; private final ChangeEditUtil editUtil; + private final ChangeIndexer indexer; private final List<ValidationMessage> messages = new ArrayList<>(); private ListMultimap<Error, String> errors = LinkedListMultimap.create(); @@ -353,7 +356,6 @@ PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, ProjectCache projectCache, - GitRepositoryManager repoManager, TagCache tagCache, AccountCache accountCache, @Nullable SearchingChangeCacheImpl changeCache, @@ -376,6 +378,7 @@ DynamicMap<ProjectConfigEntry> pluginConfigEntries, NotesMigration notesMigration, ChangeEditUtil editUtil, + ChangeIndexer indexer, BatchUpdate.Factory batchUpdateFactory, SetHashtagsOp.Factory hashtagsFactory, ReplaceOp.Factory replaceOpFactory, @@ -391,7 +394,6 @@ this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.projectCache = projectCache; - this.repoManager = repoManager; this.canonicalWebUrl = canonicalWebUrl; this.tagCache = tagCache; this.accountCache = accountCache; @@ -423,6 +425,7 @@ this.notesMigration = notesMigration; this.editUtil = editUtil; + this.indexer = indexer; this.messageSender = new ReceivePackMessageSender(); @@ -490,6 +493,7 @@ advHooks.add(new HackPushNegotiateHook()); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setPostReceiveHook(lazyPostReceive.get()); + rp.setAllowPushOptions(true); } public void init() { @@ -643,8 +647,11 @@ logDebug("Reloading project in cache"); projectCache.evict(project); ProjectState ps = projectCache.get(project.getNameKey()); - repoManager.setProjectDescription(project.getNameKey(), // - ps.getProject().getDescription()); + try { + repo.setGitwebDescription(ps.getProject().getDescription()); + } catch (IOException e) { + log.warn("cannot update description of " + project.getName(), e); + } } if (!MagicBranch.isMagicBranch(refName) @@ -677,14 +684,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 +697,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 +818,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 +906,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 +1043,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,12 +1242,27 @@ @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, " + "OWNER_REVIEWERS, ALL. If not set, the default is ALL.") NotifyHandling notify = NotifyHandling.ALL; + @Option(name = "--notify-to", metaVar = "USER", + usage = "user that should be notified") + List<Account.Id> tos = new ArrayList<>(); + + @Option(name = "--notify-cc", metaVar = "USER", + usage = "user that should be CC'd") + List<Account.Id> ccs = new ArrayList<>(); + + @Option(name = "--notify-bcc", metaVar = "USER", + usage = "user that should be BCC'd") + List<Account.Id> bccs = new ArrayList<>(); + @Option(name = "--reviewer", aliases = {"-r"}, metaVar = "EMAIL", usage = "add reviewer to changes") void reviewer(Account.Id id) { @@ -1305,14 +1324,23 @@ return new MailRecipients(reviewer, cc); } - String parse(CmdLineParser clp, Repository repo, Set<String> refs) - throws CmdLineException { + Multimap<RecipientType, Account.Id> getAccountsToNotify() { + Multimap<RecipientType, Account.Id> accountsToNotify = + MultimapBuilder.hashKeys().arrayListValues().build(); + accountsToNotify.putAll(RecipientType.TO, tos); + accountsToNotify.putAll(RecipientType.CC, ccs); + accountsToNotify.putAll(RecipientType.BCC, bccs); + return accountsToNotify; + } + + 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 +1349,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 +1378,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 +1406,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 +1454,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 +1475,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 +1491,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 +1602,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 +1676,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 +1689,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 +1712,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 +1805,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 @@ -1801,6 +1852,18 @@ return; } + // In case the change look up from the index failed, + // double check against the existing refs + if (foundInExistingRef(existing.get(p.commit))) { + if (pending.size() == 1) { + reject(magicBranch.cmd, + "commit(s) already exists (as current patchset)"); + newChanges = Collections.emptyList(); + return; + } + itr.remove(); + continue; + } newChangeIds.add(p.changeKey); } newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get())); @@ -1854,8 +1917,62 @@ } } + private boolean foundInExistingRef(Collection<Ref> existingRefs) + throws OrmException { + for (Ref ref : existingRefs) { + ChangeNotes notes = notesFactory.create(db, project.getNameKey(), + Change.Id.fromRef(ref.getName())); + Change change = notes.getChange(); + if (change.getDest().equals(magicBranch.dest)) { + logDebug("Found change {} from existing refs.", change.getKey()); + // reindex the change asynchronously + indexer.indexAsync(project.getNameKey(), change.getId()); + return true; + } + } + return false; + } + + 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 +2055,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()); } @@ -1981,9 +2102,11 @@ .setApprovals(approvals) .setMessage(msg.toString()) .setNotify(magicBranch.notify) + .setAccountsToNotify(magicBranch.getAccountsToNotify()) .setRequestScopePropagator(requestScopePropagator) .setSendMail(true) - .setUpdateRef(true)); + .setUpdateRef(false) + .setPatchSetDescription(magicBranch.message)); if (!magicBranch.hashtags.isEmpty()) { bu.addOp( changeId, @@ -2035,7 +2158,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 +2222,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; } @@ -2339,10 +2456,12 @@ rw.parseBody(newCommit); RevCommit priorCommit = revisions.inverse().get(priorPatchSet); - replaceOp = replaceOpFactory.create(requestScopePropagator, - projectControl, notes.getChange().getDest(), checkMergedInto, - priorPatchSet, priorCommit, psId, newCommit, info, groups, - magicBranch, rp.getPushCertificate()); + replaceOp = replaceOpFactory + .create(projectControl, notes.getChange().getDest(), checkMergedInto, + priorPatchSet, priorCommit, psId, newCommit, info, groups, + magicBranch, rp.getPushCertificate()) + .setRequestScopePropagator(requestScopePropagator) + .setUpdateRef(false); bu.addOp(notes.getChangeId(), replaceOp); if (progress != null) { bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress)); @@ -2407,10 +2526,11 @@ private void initChangeRefMaps() { if (refsByChange == null) { int estRefsPerChange = 4; - refsById = HashMultimap.create(); - refsByChange = ArrayListMultimap.create( - allRefs.size() / estRefsPerChange, - estRefsPerChange); + refsById = MultimapBuilder.hashKeys().hashSetValues().build(); + refsByChange = + MultimapBuilder.hashKeys(allRefs.size() / estRefsPerChange) + .arrayListValues(estRefsPerChange) + .build(); for (Ref ref : allRefs.values()) { ObjectId obj = ref.getObjectId(); if (obj != null) { @@ -2556,12 +2676,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/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java index 51c2a80..5871299 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,11 +16,15 @@ import static org.eclipse.jgit.lib.RefDatabase.ALL; +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.util.MagicBranch; @@ -46,6 +50,13 @@ private static final Logger log = LoggerFactory .getLogger(ReceiveCommitsAdvertiseRefsHook.class); + @VisibleForTesting + @AutoValue + public abstract static class Result { + public abstract Map<String, Ref> allRefs(); + public abstract Set<ObjectId> additionalHaves(); + } + private final Provider<InternalChangeQuery> queryProvider; private final Project.NameKey projectName; @@ -77,28 +88,53 @@ throw ex; } } + Result r = advertiseRefs(oldRefs); + rp.setAdvertisedRefs(r.allRefs(), r.additionalHaves()); + } + + @VisibleForTesting + public Result advertiseRefs(Map<String, Ref> oldRefs) { Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size()); + Set<ObjectId> allPatchSets = Sets.newHashSetWithExpectedSize(oldRefs.size()); for (Map.Entry<String, Ref> e : oldRefs.entrySet()) { String name = e.getKey(); if (!skip(name)) { r.put(name, e.getValue()); } + if (name.startsWith(RefNames.REFS_CHANGES)) { + allPatchSets.add(e.getValue().getObjectId()); + } } - rp.setAdvertisedRefs(r, advertiseOpenChanges()); + return new AutoValue_ReceiveCommitsAdvertiseRefsHook_Result( + r, advertiseOpenChanges(allPatchSets)); } - private Set<ObjectId> advertiseOpenChanges() { + private static final ImmutableSet<String> OPEN_CHANGES_FIELDS = + ImmutableSet.of( + // Required for ChangeIsVisibleToPrdicate. + ChangeField.CHANGE.getName(), + // Required during advertiseOpenChanges. + ChangeField.PATCH_SET.getName()); + + private Set<ObjectId> advertiseOpenChanges(Set<ObjectId> allPatchSets) { // Advertise some recent open changes, in case a commit is based on one. int limit = 32; try { Set<ObjectId> r = Sets.newHashSetWithExpectedSize(limit); for (ChangeData cd : queryProvider.get() + .setRequestedFields(OPEN_CHANGES_FIELDS) .enforceVisibility(true) .setLimit(limit) .byProjectOpen(projectName)) { PatchSet ps = cd.currentPatchSet(); if (ps != null) { - r.add(ObjectId.fromString(ps.getRevision().get())); + ObjectId id = ObjectId.fromString(ps.getRevision().get()); + // Ensure we actually observed a patch set ref pointing to this + // object, in case the database is out of sync with the repo and the + // object doesn't actually exist. + if (allPatchSets.contains(id)) { + r.add(id); + } } } return r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java index 562db08..96593ac 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RefCache.java
@@ -14,11 +14,10 @@ package com.google.gerrit.server.git; -import com.google.common.base.Optional; - import org.eclipse.jgit.lib.ObjectId; import java.io.IOException; +import java.util.Optional; /** * Simple short-lived cache of individual refs read from a repo.
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..32ec941 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; @@ -46,7 +45,7 @@ import com.google.gerrit.server.git.BatchUpdate.RepoContext; import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput; import com.google.gerrit.server.mail.MailUtil.MailRecipients; -import com.google.gerrit.server.mail.ReplacePatchSetSender; +import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; @@ -65,11 +64,11 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.PushCertificate; +import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -79,7 +78,6 @@ public class ReplaceOp extends BatchUpdate.Op { public interface Factory { ReplaceOp create( - RequestScopePropagator requestScopePropagator, ProjectControl projectControl, Branch.NameKey dest, boolean checkMergedInto, @@ -113,7 +111,6 @@ private final PatchSetUtil psUtil; private final ReplacePatchSetSender.Factory replacePatchSetFactory; - private final RequestScopePropagator requestScopePropagator; private final ProjectControl projectControl; private final Branch.NameKey dest; private final boolean checkMergedInto; @@ -134,6 +131,8 @@ private ChangeMessage msg; private String rejectMessage; private MergedByPushOp mergedByPushOp; + private RequestScopePropagator requestScopePropagator; + private boolean updateRef; @AssistedInject ReplaceOp(AccountResolver accountResolver, @@ -150,7 +149,6 @@ PatchSetUtil psUtil, ReplacePatchSetSender.Factory replacePatchSetFactory, @SendEmailExecutor ExecutorService sendEmailExecutor, - @Assisted RequestScopePropagator requestScopePropagator, @Assisted ProjectControl projectControl, @Assisted Branch.NameKey dest, @Assisted boolean checkMergedInto, @@ -177,7 +175,6 @@ this.replacePatchSetFactory = replacePatchSetFactory; this.sendEmailExecutor = sendEmailExecutor; - this.requestScopePropagator = requestScopePropagator; this.projectControl = projectControl; this.dest = dest; this.checkMergedInto = checkMergedInto; @@ -189,11 +186,13 @@ this.groups = groups; this.magicBranch = magicBranch; this.pushCertificate = pushCertificate; + this.updateRef = true; } @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) { @@ -203,6 +202,12 @@ requestScopePropagator, patchSetId, mergedInto.getName()); } } + + if (updateRef) { + ctx.addRefUpdate( + new ReceiveCommand(ObjectId.zeroId(), commit, + patchSetId.toRefName())); + } } @Override @@ -224,9 +229,11 @@ update.setSubjectForCommit("Create patch set " + patchSetId.get()); String reviewMessage = null; + String psDescription = null; if (magicBranch != null) { recipients.add(magicBranch.getMailRecipients()); reviewMessage = magicBranch.message; + psDescription = magicBranch.message; approvals.putAll(magicBranch.labels); Set<String> hashtags = magicBranch.hashtags; if (hashtags != null && !hashtags.isEmpty()) { @@ -247,21 +254,24 @@ ctx.getDb(), ctx.getRevWalk(), update, patchSetId, commit, draft, groups, pushCertificate != null ? pushCertificate.toTextWithSignature() - : null); + : null, psDescription); + update.setPsDescription(psDescription); recipients.add(getRecipientsFromFooters( ctx.getDb(), accountResolver, draft, commit.getFooterLines())); recipients.remove(ctx.getAccountId()); 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,15 +286,12 @@ 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(), ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); cmUtil.addChangeMessage(ctx.getDb(), update, msg); if (mergedByPushOp == null) { - resetChange(ctx, msg); + resetChange(ctx); } else { mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)) .updateChange(ctx); @@ -328,16 +335,8 @@ return current; } - private void resetChange(ChangeContext ctx, ChangeMessage msg) - throws OrmException { + private void resetChange(ChangeContext ctx) { Change change = ctx.getChange(); - if (change.getStatus().isClosed()) { - ctx.getDb().patchSets().delete(Collections.singleton(newPatchSet)); - ctx.getDb().changeMessages().delete(Collections.singleton(msg)); - rejectMessage = CHANGE_IS_CLOSED; - return; - } - if (!change.currentPatchSetId().equals(priorPatchSetId)) { return; } @@ -367,8 +366,10 @@ // BatchUpdate's perspective there is no ref update. Thus we have to fire it // manually. final Account account = ctx.getAccount(); - gitRefUpdated.fire(ctx.getProject(), newPatchSet.getRefName(), - ObjectId.zeroId(), commit, account); + if (!updateRef) { + gitRefUpdated.fire(ctx.getProject(), newPatchSet.getRefName(), + ObjectId.zeroId(), commit, account); + } if (changeKind != ChangeKind.TRIVIAL_REBASE) { Runnable sender = new Runnable() { @@ -380,8 +381,9 @@ cm.setFrom(account.getId()); cm.setPatchSet(newPatchSet, info); cm.setChangeMessage(msg.getMessage(), ctx.getWhen()); - if (magicBranch != null && magicBranch.notify != null) { + if (magicBranch != null) { cm.setNotify(magicBranch.notify); + cm.setAccountsToNotify(magicBranch.getAccountsToNotify()); } cm.addReviewers(recipients.getReviewers()); cm.addExtraCC(recipients.getCcOnly()); @@ -455,10 +457,25 @@ return newPatchSet; } + public Change getChange() { + return change; + } + public String getRejectMessage() { return rejectMessage; } + public ReplaceOp setUpdateRef(boolean updateRef) { + this.updateRef = updateRef; + return this; + } + + public ReplaceOp setRequestScopePropagator( + RequestScopePropagator requestScopePropagator) { + this.requestScopePropagator = requestScopePropagator; + return this; + } + private Ref findMergedInto(Context ctx, String first, RevCommit commit) { try { RefDatabase refDatabase = ctx.getRepository().getRefDatabase();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java index 1dfa51e..77f697a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -14,8 +14,6 @@ package com.google.gerrit.server.git; -import com.google.common.base.Optional; - import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; @@ -24,6 +22,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Optional; /** {@link RefCache} backed directly by a repository. */ public class RepoRefCache implements RefCache { @@ -42,9 +41,7 @@ return id; } Ref ref = refdb.exactRef(refName); - id = ref != null - ? Optional.of(ref.getObjectId()) - : Optional.<ObjectId>absent(); + id = Optional.ofNullable(ref).map(Ref::getObjectId); ids.put(refName, id); return id; }
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..5fbd80e 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
@@ -14,9 +14,9 @@ package com.google.gerrit.server.git; -import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.gerrit.common.data.SubscribeSection; import com.google.gerrit.extensions.restapi.RestApiException; @@ -103,12 +103,21 @@ private final ProjectState.Factory projectStateFactory; private final VerboseSuperprojectUpdate verboseSuperProject; private final boolean enableSuperProjectSubscriptions; - private final Multimap<Branch.NameKey, SubmoduleSubscription> targets; - private final Set<Branch.NameKey> updatedBranches; private final MergeOpRepoManager orm; - private final Map<Branch.NameKey, CodeReviewCommit> branchTips; private final Map<Branch.NameKey, GitModules> branchGitModules; + + // always update-to-current branch tips during submit process + private final Map<Branch.NameKey, CodeReviewCommit> branchTips; + // branches for all the submitting changes + private final Set<Branch.NameKey> updatedBranches; + // branches which in either a submodule or a superproject + private final Set<Branch.NameKey> affectedBranches; + // sorted version of affectedBranches private final ImmutableSet<Branch.NameKey> sortedBranches; + // map of superproject branch and its submodule subscriptions + private final Multimap<Branch.NameKey, SubmoduleSubscription> targets; + // map of superproject and its branches which has submodule subscriptions + private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject; @AssistedInject public SubmoduleOp( @@ -130,9 +139,11 @@ "enableSuperProjectSubscriptions", true); this.orm = orm; this.updatedBranches = updatedBranches; - this.targets = HashMultimap.create(); + this.targets = MultimapBuilder.hashKeys().hashSetValues().build(); + this.affectedBranches = new HashSet<>(); this.branchTips = new HashMap<>(); this.branchGitModules = new HashMap<>(); + this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build(); this.sortedBranches = calculateSubscriptionMap(); } @@ -154,8 +165,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); } @@ -181,9 +195,12 @@ Collection<SubmoduleSubscription> subscriptions = superProjectSubscriptionsForSubmoduleBranch(current); for (SubmoduleSubscription sub : subscriptions) { - Branch.NameKey superProject = sub.getSuperProject(); - searchForSuperprojects(superProject, currentVisited, allVisited); - targets.put(superProject, sub); + Branch.NameKey superBranch = sub.getSuperProject(); + searchForSuperprojects(superBranch, currentVisited, allVisited); + targets.put(superBranch, sub); + branchesByProject.put(superBranch.getParentKey(), superBranch); + affectedBranches.add(superBranch); + affectedBranches.add(sub.getSubmodule()); } } catch (IOException e) { throw new SubmoduleException("Cannot find superprojects for " + current, @@ -251,7 +268,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 +307,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."); @@ -319,22 +336,21 @@ return; } - SetMultimap<Project.NameKey, Branch.NameKey> dst = branchesByProject(); LinkedHashSet<Project.NameKey> superProjects = new LinkedHashSet<>(); try { for (Project.NameKey project : projects) { // only need superprojects - if (dst.containsKey(project)) { + if (branchesByProject.containsKey(project)) { superProjects.add(project); // get a new BatchUpdate for the super project - OpenRepo or = orm.openRepo(project, false); - for (Branch.NameKey branch : dst.get(project)) { + OpenRepo or = orm.openRepo(project); + for (Branch.NameKey branch : branchesByProject.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 +364,23 @@ 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()); + addBranchTip(subscriber, currentCommit); } - currentCommit = or.rw.parseCommit(r.getObjectId()); StringBuilder msgbuf = new StringBuilder(""); PersonIdent author = null; @@ -404,7 +425,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 +457,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 +467,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); } @@ -461,7 +484,7 @@ oldCommit = subOr.rw.parseCommit(dce.getObjectId()); } - final RevCommit newCommit; + final CodeReviewCommit newCommit; if (branchTips.containsKey(s.getSubmodule())) { newCommit = branchTips.get(s.getSubmodule()); } else { @@ -471,6 +494,7 @@ return null; } newCommit = subOr.rw.parseCommit(ref.getObjectId()); + addBranchTip(s.getSubmodule(), newCommit); } if (Objects.equals(newCommit, oldCommit)) { @@ -532,41 +556,57 @@ return dc; } - public SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject() { - SetMultimap<Project.NameKey, Branch.NameKey> ret = HashMultimap.create(); - for (Branch.NameKey branch : targets.keySet()) { - ret.put(branch.getParentKey(), branch); - } - - return ret; - } - 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)); - } - projects.add(project); - } - prev = project; + for (Project.NameKey project : branchesByProject.keySet()) { + addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects); } + for (Branch.NameKey branch : updatedBranches) { + projects.add(branch.getParentKey()); + } return ImmutableSet.copyOf(projects); } + private void addAllSubmoduleProjects(Project.NameKey project, + LinkedHashSet<Project.NameKey> current, + LinkedHashSet<Project.NameKey> projects) + throws SubmoduleException { + if (current.contains(project)) { + throw new SubmoduleException( + "Project level circular subscriptions detected: " + + printCircularPath(current, project)); + } + + if (projects.contains(project)) { + return; + } + + current.add(project); + Set<Project.NameKey> subprojects = new HashSet<>(); + for (Branch.NameKey branch : branchesByProject.get(project)) { + Collection<SubmoduleSubscription> subscriptions = targets.get(branch); + for (SubmoduleSubscription s : subscriptions) { + subprojects.add(s.getSubmodule().getParentKey()); + } + } + + for (Project.NameKey p : subprojects) { + addAllSubmoduleProjects(p, current, projects); + } + + current.remove(project); + projects.add(project); + } + 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/UserConfigSections.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java index a09466d..bbd55f0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -29,6 +29,10 @@ public static final String KEY_MATCH = "match"; public static final String KEY_TOKEN = "token"; + /** The table column user preferences. */ + public static final String CHANGE_TABLE = "changeTable"; + public static final String CHANGE_TABLE_COLUMN = "column"; + /** The edit user preferences. */ public static final String EDIT = "edit";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java index 6334cd2..f7752dc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -347,9 +347,11 @@ case FORCED: update.fireGitRefUpdatedEvent(ru); return; + case LOCK_FAILURE: + throw new LockFailureException("Cannot delete " + ru.getName() + + " in " + db.getDirectory() + ": " + ru.getResult()); case FAST_FORWARD: case IO_FAILURE: - case LOCK_FAILURE: case NEW: case NOT_ATTEMPTED: case NO_CHANGE: @@ -424,9 +426,11 @@ revision = rw.parseCommit(ru.getNewObjectId()); update.fireGitRefUpdatedEvent(ru); return revision; + case LOCK_FAILURE: + throw new LockFailureException("Cannot update " + ru.getName() + + " in " + db.getDirectory() + ": " + ru.getResult()); case FORCED: case IO_FAILURE: - case LOCK_FAILURE: case NOT_ATTEMPTED: case NO_CHANGE: case REJECTED:
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..af08b63 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
@@ -33,6 +33,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.ReceiveCommand; import java.io.IOException; @@ -48,14 +49,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 +69,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); @@ -114,15 +100,17 @@ args.rw.parseBody(toMerge); psId = ChangeUtil.nextPatchSetId( args.repo, toMerge.change().currentPatchSetId()); + RevCommit mergeTip = args.mergeTip.getCurrentTip(); + args.rw.parseBody(mergeTip); String cherryPickCmtMsg = - args.mergeUtil.createCherryPickCommitMessage(toMerge); + args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip); PersonIdent committer = args.caller.newCommitterIdent( ctx.getWhen(), args.serverIdent.getTimeZone()); try { newCommit = args.mergeUtil.createCherryPickFromCommit( args.repo, args.inserter, args.mergeTip.getCurrentTip(), toMerge, - committer, cherryPickCmtMsg, args.rw); + committer, cherryPickCmtMsg, args.rw, 0); } catch (MergeConflictException mce) { // Keep going in the case of a single merge failure; the goal is to // cherry-pick as many commits as possible. @@ -161,7 +149,7 @@ PatchSet newPs = args.psUtil.insert(ctx.getDb(), ctx.getRevWalk(), ctx.getUpdate(psId), psId, newCommit, false, prevPs != null ? prevPs.getGroups() : ImmutableList.<String> of(), - null); + null, null); ctx.getChange().setCurrentPatchSet(patchSetInfo); // Don't copy approvals, as this is already taken care of by @@ -191,8 +179,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 +189,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/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.java similarity index 70% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.java index ea0def0..26bb4c1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.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. @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.git.strategy; -public enum RecipientType { - TO, CC, BCC +public class RebaseAlways extends RebaseSubmitStrategy { + + RebaseAlways(SubmitStrategy.Arguments args) { + super(args, true); + } }
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 47b384b..104074a 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
@@ -14,223 +14,9 @@ package com.google.gerrit.server.git.strategy; -import com.google.gerrit.extensions.restapi.MergeConflictException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.server.change.RebaseChangeOp; -import com.google.gerrit.server.git.BatchUpdate.ChangeContext; -import com.google.gerrit.server.git.BatchUpdate.Context; -import com.google.gerrit.server.git.BatchUpdate.RepoContext; -import com.google.gerrit.server.git.CodeReviewCommit; -import com.google.gerrit.server.git.IntegrationException; -import com.google.gerrit.server.git.MergeTip; -import com.google.gerrit.server.git.RebaseSorter; -import com.google.gerrit.server.git.validators.CommitValidators; -import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gerrit.server.project.NoSuchChangeException; -import com.google.gwtorm.server.OrmException; - -import org.eclipse.jgit.revwalk.RevCommit; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class RebaseIfNecessary extends SubmitStrategy { +public class RebaseIfNecessary extends RebaseSubmitStrategy { RebaseIfNecessary(SubmitStrategy.Arguments args) { - super(args); - } - - @Override - public List<SubmitStrategyOp> buildOps( - Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = sort(toMerge, args.mergeTip.getCurrentTip()); - List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); - boolean first = true; - - for (CodeReviewCommit c : sorted) { - if (c.getParentCount() > 1) { - // Since there is a merge commit, sort and prune again using - // MERGE_IF_NECESSARY semantics to avoid creating duplicate - // commits. - // - sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted); - break; - } - } - - while (!sorted.isEmpty()) { - CodeReviewCommit n = sorted.remove(0); - if (first && args.mergeTip.getInitialTip() == null) { - ops.add(new RebaseUnbornRootOp(n)); - } else if (n.getParentCount() == 0) { - ops.add(new RebaseRootOp(n)); - } else if (n.getParentCount() == 1) { - ops.add(new RebaseOneOp(n)); - } else { - ops.add(new RebaseMultipleParentsOp(n)); - } - first = false; - } - 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); - } - - @Override - public void updateRepoImpl(RepoContext ctx) { - // Refuse to merge a root commit into an existing branch, we cannot obtain - // a delta for the cherry-pick to apply. - toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT); - } - } - - private class RebaseOneOp extends SubmitStrategyOp { - private RebaseChangeOp rebaseOp; - private CodeReviewCommit newCommit; - - private RebaseOneOp(CodeReviewCommit toMerge) { - super(RebaseIfNecessary.this.args, toMerge); - } - - @Override - public void updateRepoImpl(RepoContext ctx) - throws IntegrationException, InvalidChangeOperationException, - RestApiException, IOException, OrmException { - // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk. - // When hoisting BatchUpdate into MergeOp, we will need to teach - // BatchUpdate how to produce CodeReviewRevWalks. - if (args.mergeUtil.canFastForward(args.mergeSorter, - args.mergeTip.getCurrentTip(), args.rw, toMerge)) { - args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge); - acceptMergeTip(args.mergeTip); - return; - } - - // Stale read of patch set is ok; see comments in RebaseChangeOp. - PatchSet origPs = args.psUtil.get( - ctx.getDb(), toMerge.getControl().getNotes(), toMerge.getPatchsetId()); - rebaseOp = args.rebaseFactory.create( - toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name()) - .setFireRevisionCreated(false) - // Bypass approval copier since SubmitStrategyOp copy all approvals - // later anyway. - .setCopyApprovals(false) - .setValidatePolicy(CommitValidators.Policy.NONE); - try { - rebaseOp.updateRepo(ctx); - } catch (MergeConflictException | NoSuchChangeException e) { - toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT); - throw new IntegrationException( - "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e); - } - newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit()); - newCommit = amendGitlink(newCommit); - newCommit.copyFrom(toMerge); - newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE); - newCommit.setPatchsetId(rebaseOp.getPatchSetId()); - args.mergeTip.moveTipTo(newCommit, newCommit); - args.commits.put(args.mergeTip.getCurrentTip()); - acceptMergeTip(args.mergeTip); - } - - @Override - public PatchSet updateChangeImpl(ChangeContext ctx) - throws NoSuchChangeException, ResourceConflictException, - OrmException, IOException { - if (rebaseOp == null) { - // Took the fast-forward option, nothing to do. - return null; - } - - rebaseOp.updateChange(ctx); - ctx.getChange().setCurrentPatchSet( - args.patchSetInfoFactory.get( - args.rw, newCommit, rebaseOp.getPatchSetId())); - newCommit.setControl(ctx.getControl()); - return rebaseOp.getPatchSet(); - } - - @Override - public void postUpdateImpl(Context ctx) throws OrmException { - if (rebaseOp != null) { - rebaseOp.postUpdate(ctx); - } - } - } - - private class RebaseMultipleParentsOp extends SubmitStrategyOp { - private RebaseMultipleParentsOp(CodeReviewCommit toMerge) { - super(RebaseIfNecessary.this.args, toMerge); - } - - @Override - public void updateRepoImpl(RepoContext ctx) - throws IntegrationException, IOException { - // There are multiple parents, so this is a merge commit. We don't want - // to rebase the merge as clients can't easily rebase their history with - // that merge present and replaced by an equivalent merge with a different - // first parent. So instead behave as though MERGE_IF_NECESSARY was - // configured. - MergeTip mergeTip = args.mergeTip; - if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) { - mergeTip.moveTipTo(amendGitlink(toMerge), toMerge); - acceptMergeTip(mergeTip); - } else { - CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit( - args.serverIdent, args.serverIdent, args.repo, args.rw, - args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge); - mergeTip.moveTipTo(amendGitlink(newTip), toMerge); - } - args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, - mergeTip.getCurrentTip(), args.alreadyAccepted); - acceptMergeTip(mergeTip); - } - } - - private void acceptMergeTip(MergeTip mergeTip) { - args.alreadyAccepted.add(mergeTip.getCurrentTip()); - } - - private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort, - RevCommit initialTip) throws IntegrationException { - try { - return new RebaseSorter(args.rw, initialTip, args.alreadyAccepted, - args.canMergeFlag).sort(toSort); - } catch (IOException e) { - throw new IntegrationException("Commit sorting failed", e); - } - } - - static boolean dryRun(SubmitDryRun.Arguments args, - CodeReviewCommit mergeTip, CodeReviewCommit toMerge) - throws IntegrationException { - // Test for merge instead of cherry pick to avoid false negatives - // on commit chains. - return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge) - && args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip, - toMerge); + super(args, false); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java new file mode 100644 index 0000000..135db95 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -0,0 +1,293 @@ +// 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. + +package com.google.gerrit.server.git.strategy; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.git.strategy.CommitMergeStatus.SKIPPED_IDENTICAL_TREE; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.extensions.restapi.MergeConflictException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.change.RebaseChangeOp; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.BatchUpdate.Context; +import com.google.gerrit.server.git.BatchUpdate.RepoContext; +import com.google.gerrit.server.git.CodeReviewCommit; +import com.google.gerrit.server.git.IntegrationException; +import com.google.gerrit.server.git.MergeIdenticalTreeException; +import com.google.gerrit.server.git.MergeTip; +import com.google.gerrit.server.git.RebaseSorter; +import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gwtorm.server.OrmException; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * This strategy covers RebaseAlways and RebaseIfNecessary ones. + */ +public class RebaseSubmitStrategy extends SubmitStrategy { + private final boolean rebaseAlways; + + RebaseSubmitStrategy(SubmitStrategy.Arguments args, boolean rebaseAlways) { + super(args); + this.rebaseAlways = rebaseAlways; + } + + @Override + public List<SubmitStrategyOp> buildOps( + Collection<CodeReviewCommit> toMerge) throws IntegrationException { + List<CodeReviewCommit> sorted = sort(toMerge, args.mergeTip.getCurrentTip()); + List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); + boolean first = true; + + for (CodeReviewCommit c : sorted) { + if (c.getParentCount() > 1) { + // Since there is a merge commit, sort and prune again using + // MERGE_IF_NECESSARY semantics to avoid creating duplicate + // commits. + // + sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted); + break; + } + } + + while (!sorted.isEmpty()) { + CodeReviewCommit n = sorted.remove(0); + if (first && args.mergeTip.getInitialTip() == null) { + // TODO(tandrii): Cherry-Pick strategy does this too, but it's wrong + // and can be fixed. + ops.add(new FastForwardOp(args, n)); + } else if (n.getParentCount() == 0) { + ops.add(new RebaseRootOp(n)); + } else if (n.getParentCount() == 1) { + ops.add(new RebaseOneOp(n)); + } else { + ops.add(new RebaseMultipleParentsOp(n)); + } + first = false; + } + return ops; + } + + private class RebaseRootOp extends SubmitStrategyOp { + private RebaseRootOp(CodeReviewCommit toMerge) { + super(RebaseSubmitStrategy.this.args, toMerge); + } + + @Override + public void updateRepoImpl(RepoContext ctx) { + // Refuse to merge a root commit into an existing branch, we cannot obtain + // a delta for the cherry-pick to apply. + toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT); + } + } + + private class RebaseOneOp extends SubmitStrategyOp { + private RebaseChangeOp rebaseOp; + private CodeReviewCommit newCommit; + private PatchSet.Id newPatchSetId; + + private RebaseOneOp(CodeReviewCommit toMerge) { + super(RebaseSubmitStrategy.this.args, toMerge); + } + + @Override + public void updateRepoImpl(RepoContext ctx) + throws IntegrationException, InvalidChangeOperationException, + RestApiException, IOException, OrmException { + // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk. + // When hoisting BatchUpdate into MergeOp, we will need to teach + // BatchUpdate how to produce CodeReviewRevWalks. + if (args.mergeUtil + .canFastForward(args.mergeSorter, args.mergeTip.getCurrentTip(), + args.rw, toMerge)) { + if (!rebaseAlways){ + args.mergeTip.moveTipTo(amendGitlink(toMerge), toMerge); + toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE); + acceptMergeTip(args.mergeTip); + return; + } + // RebaseAlways means we modify commit message. + args.rw.parseBody(toMerge); + newPatchSetId = ChangeUtil.nextPatchSetId( + args.repo, toMerge.change().currentPatchSetId()); + RevCommit mergeTip = args.mergeTip.getCurrentTip(); + args.rw.parseBody(mergeTip); + String cherryPickCmtMsg = + args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip); + PersonIdent committer = args.caller.newCommitterIdent(ctx.getWhen(), + args.serverIdent.getTimeZone()); + try { + newCommit = args.mergeUtil.createCherryPickFromCommit(args.repo, + args.inserter, args.mergeTip.getCurrentTip(), toMerge, committer, + cherryPickCmtMsg, args.rw, 0); + } catch (MergeConflictException mce) { + // Unlike in Cherry-pick case, this should never happen. + toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT); + throw new IllegalStateException( + "MergeConflictException on message edit must not happen"); + } catch (MergeIdenticalTreeException mie) { + toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE); + return; + } + ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), newCommit, + newPatchSetId.toRefName())); + } else { + // Stale read of patch set is ok; see comments in RebaseChangeOp. + PatchSet origPs = args.psUtil.get(ctx.getDb(), + toMerge.getControl().getNotes(), toMerge.getPatchsetId()); + rebaseOp = args.rebaseFactory.create( + toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name()) + .setFireRevisionCreated(false) + // Bypass approval copier since SubmitStrategyOp copy all approvals + // later anyway. + .setCopyApprovals(false) + .setValidatePolicy(CommitValidators.Policy.NONE) + // RebaseAlways should set always modify commit message like + // Cherry-Pick strategy. + .setDetailedCommitMessage(rebaseAlways) + // Do not post message after inserting new patchset because there + // will be one about change being merged already. + .setPostMessage(false); + try { + rebaseOp.updateRepo(ctx); + } catch (MergeConflictException | NoSuchChangeException e) { + toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT); + throw new IntegrationException( + "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e); + } + newCommit = args.rw.parseCommit(rebaseOp.getRebasedCommit()); + newPatchSetId = rebaseOp.getPatchSetId(); + } + newCommit = amendGitlink(newCommit); + newCommit.copyFrom(toMerge); + newCommit.setPatchsetId(newPatchSetId); + newCommit.setStatusCode(CommitMergeStatus.CLEAN_REBASE); + args.mergeTip.moveTipTo(newCommit, newCommit); + args.commits.put(args.mergeTip.getCurrentTip()); + acceptMergeTip(args.mergeTip); + } + + @Override + public PatchSet updateChangeImpl(ChangeContext ctx) + throws NoSuchChangeException, ResourceConflictException, + OrmException, IOException { + if (newCommit == null) { + checkState(!rebaseAlways, "RebaseAlways must never fast forward"); + // Took the fast-forward option, nothing to do. + return null; + } + PatchSet newPs; + if (rebaseOp != null) { + rebaseOp.updateChange(ctx); + newPs = rebaseOp.getPatchSet(); + } else { + // CherryPick + PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes()); + newPs = args.psUtil.insert(ctx.getDb(), ctx.getRevWalk(), + ctx.getUpdate(newPatchSetId), newPatchSetId, newCommit, false, + prevPs != null ? prevPs.getGroups() : ImmutableList.<String> of(), + null, null); + } + ctx.getChange().setCurrentPatchSet(args.patchSetInfoFactory + .get(ctx.getRevWalk(), newCommit, newPatchSetId)); + newCommit.setControl(ctx.getControl()); + return newPs; + } + + @Override + public void postUpdateImpl(Context ctx) throws OrmException { + if (rebaseOp != null) { + rebaseOp.postUpdate(ctx); + } + } + } + + private class RebaseMultipleParentsOp extends SubmitStrategyOp { + private RebaseMultipleParentsOp(CodeReviewCommit toMerge) { + super(RebaseSubmitStrategy.this.args, toMerge); + } + + @Override + public void updateRepoImpl(RepoContext ctx) + throws IntegrationException, IOException { + // There are multiple parents, so this is a merge commit. We don't want + // to rebase the merge as clients can't easily rebase their history with + // that merge present and replaced by an equivalent merge with a different + // first parent. So instead behave as though MERGE_IF_NECESSARY was + // configured. + // TODO(tandrii): this is not in spirit of RebaseAlways strategy because + // the commit messages can not be modified in the process. It's also + // possible to implement rebasing of merge commits. E.g., the Cherry Pick + // REST endpoint already supports cherry-picking of merge commits. + // For now, users of RebaseAlways strategy for whom changed commit footers + // are important would be well advised to prohibit uploading patches with + // merge commits. + MergeTip mergeTip = args.mergeTip; + if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge) && + !args.submoduleOp.hasSubscription(args.destBranch)) { + mergeTip.moveTipTo(toMerge, toMerge); + } else { + CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit( + args.serverIdent, args.serverIdent, args.repo, args.rw, + args.inserter, args.destBranch, mergeTip.getCurrentTip(), toMerge); + mergeTip.moveTipTo(amendGitlink(newTip), toMerge); + } + RevCommit initialTip = mergeTip.getInitialTip(); + args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, + mergeTip.getCurrentTip(), initialTip == null ? + ImmutableSet.<RevCommit>of() : ImmutableSet.of(initialTip)); + acceptMergeTip(mergeTip); + } + } + + private void acceptMergeTip(MergeTip mergeTip) { + args.alreadyAccepted.add(mergeTip.getCurrentTip()); + } + + private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort, + RevCommit initialTip) throws IntegrationException { + try { + return new RebaseSorter(args.rw, initialTip, args.alreadyAccepted, + args.canMergeFlag).sort(toSort); + } catch (IOException e) { + throw new IntegrationException("Commit sorting failed", e); + } + } + + static boolean dryRun(SubmitDryRun.Arguments args, + CodeReviewCommit mergeTip, CodeReviewCommit toMerge) + throws IntegrationException { + // Test for merge instead of cherry pick to avoid false negatives + // on commit chains. + return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge) + && args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip, + toMerge); + } +}
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..a7dc367 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) @@ -128,6 +122,8 @@ return MergeIfNecessary.dryRun(args, tipCommit, toMergeCommit); case REBASE_IF_NECESSARY: return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit); + case REBASE_ALWAYS: + return RebaseAlways.dryRun(args, tipCommit, toMergeCommit); default: String errorMsg = "No submit strategy for: " + submitType; log.error(errorMsg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java index 36de70e..c77cb05 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
@@ -16,10 +16,13 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; @@ -96,7 +99,9 @@ Set<RevCommit> alreadyAccepted, RequestId submissionId, NotifyHandling notifyHandling, - SubmoduleOp submoduleOp); + Multimap<RecipientType, Account.Id> accountsToNotify, + SubmoduleOp submoduleOp, + boolean dryrun); } final AccountCache accountCache; @@ -128,11 +133,13 @@ final RequestId submissionId; final SubmitType submitType; final NotifyHandling notifyHandling; + final Multimap<RecipientType, Account.Id> accountsToNotify; final SubmoduleOp submoduleOp; final ProjectState project; final MergeSorter mergeSorter; final MergeUtil mergeUtil; + final boolean dryrun; @AssistedInject Arguments( @@ -165,7 +172,9 @@ @Assisted RequestId submissionId, @Assisted SubmitType submitType, @Assisted NotifyHandling notifyHandling, - @Assisted SubmoduleOp submoduleOp) { + @Assisted Multimap<RecipientType, Account.Id> accountsToNotify, + @Assisted SubmoduleOp submoduleOp, + @Assisted boolean dryrun) { this.accountCache = accountCache; this.approvalsUtil = approvalsUtil; this.batchUpdateFactory = batchUpdateFactory; @@ -195,7 +204,9 @@ this.submissionId = submissionId; this.submitType = submitType; this.notifyHandling = notifyHandling; + this.accountsToNotify = accountsToNotify; 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..4591c9e 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
@@ -14,8 +14,11 @@ package com.google.gerrit.server.git.strategy; +import com.google.common.collect.Multimap; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.SubmitType; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; @@ -54,12 +57,14 @@ 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) - throws IntegrationException { + CommitStatus commits, RequestId submissionId, + NotifyHandling notifyHandling, + Multimap<RecipientType, Account.Id> accountsToNotify, + 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, accountsToNotify, + submoduleOp, dryrun); switch (submitType) { case CHERRY_PICK: return new CherryPick(args); @@ -71,6 +76,8 @@ return new MergeIfNecessary(args); case REBASE_IF_NECESSARY: return new RebaseIfNecessary(args); + case REBASE_ALWAYS: + return new RebaseAlways(args); default: String errorMsg = "No submit strategy for: " + submitType; log.error(errorMsg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java index 72bfedf..79642fc 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; @@ -55,6 +56,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -182,13 +184,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; @@ -301,7 +297,7 @@ ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMerged); return args.psUtil.insert(ctx.getDb(), ctx.getRevWalk(), - ctx.getUpdate(psId), psId, alreadyMerged, false, groups, null); + ctx.getUpdate(psId), psId, alreadyMerged, false, groups, null, null); } private void setApproval(ChangeContext ctx, IdentifiedUser user) @@ -335,15 +331,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 +375,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 +390,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 +411,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) { @@ -448,6 +433,7 @@ case CHERRY_PICK: return message(ctx, commit, CommitMergeStatus.CLEAN_PICK); case REBASE_IF_NECESSARY: + case REBASE_ALWAYS: return message(ctx, commit, CommitMergeStatus.CLEAN_REBASE); default: throw new IllegalStateException("unexpected submit type " @@ -463,19 +449,10 @@ } 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, + ChangeMessagesUtil.TAG_MERGED); } private void setMerged(ChangeContext ctx, ChangeMessage msg) @@ -509,8 +486,11 @@ if (RefNames.REFS_CONFIG.equals(getDest().get())) { args.projectCache.evict(getProject()); ProjectState p = args.projectCache.get(getProject()); - args.repoManager.setProjectDescription( - p.getProject().getNameKey(), p.getProject().getDescription()); + try (Repository git = args.repoManager.openRepository(getProject())) { + git.setGitwebDescription(p.getProject().getDescription()); + } catch (IOException e) { + log.error("cannot update description of " + p.getProject().getName(), e); + } } } @@ -519,12 +499,12 @@ try { args.mergedSenderFactory .create(ctx.getProject(), getId(), submitter.getAccountId(), - args.notifyHandling) + args.notifyHandling, args.accountsToNotify) .sendAsync(); } 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 +544,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/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java index d5081b8..a53829e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -45,6 +45,7 @@ import org.kohsuke.args4j.Option; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; @@ -314,11 +315,11 @@ return groups; } - private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) { - final List<AccountGroup> filteredGroups = new ArrayList<>(); - final boolean isAdmin = + private List<AccountGroup> filterGroups(Collection<AccountGroup> groups) { + List<AccountGroup> filteredGroups = new ArrayList<>(groups.size()); + boolean isAdmin = identifiedUser.get().getCapabilities().canAdministrateServer(); - for (final AccountGroup group : groups) { + for (AccountGroup group : groups) { if (!Strings.isNullOrEmpty(matchSubstring)) { if (!group.getName().toLowerCase(Locale.US) .contains(matchSubstring.toLowerCase(Locale.US))) { @@ -333,7 +334,7 @@ continue; } if (!isAdmin) { - final GroupControl c = groupControlFactory.controlFor(group); + GroupControl c = groupControlFactory.controlFor(group); if (!c.isVisible()) { continue; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java index 386092d..92938cb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -18,6 +18,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; +import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.TrackingFooters; import com.google.gwtorm.server.OrmException; @@ -65,14 +66,17 @@ public static class FillArgs { public final TrackingFooters trackingFooters; public final boolean allowsDrafts; + public final AllUsersName allUsers; @Inject FillArgs(TrackingFooters trackingFooters, - @GerritServerConfig Config cfg) { + @GerritServerConfig Config cfg, + AllUsersName allUsers) { this.trackingFooters = trackingFooters; this.allowsDrafts = cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true); + this.allUsers = allUsers; } }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/GerritIndexStatus.java similarity index 83% rename from gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java rename to gerrit-server/src/main/java/com/google/gerrit/server/index/GerritIndexStatus.java index f43e385..5763185 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/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.server.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-server/src/main/java/com/google/gerrit/server/index/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java index 533e57c..84eb3bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Index.java
@@ -17,8 +17,11 @@ import com.google.gerrit.server.query.DataSource; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; +import com.google.gwtorm.server.OrmException; import java.io.IOException; +import java.util.List; +import java.util.Optional; /** * Secondary index implementation for arbitrary documents. @@ -91,6 +94,44 @@ throws QueryParseException; /** + * Get a single document from the index. + * + * @param key document key. + * @param opts query options. Options that do not make sense in the context of + * a single document, such as start, will be ignored. + * @return a single document if present. + * @throws IOException + */ + default Optional<V> get(K key, QueryOptions opts) throws IOException { + opts = opts.withStart(0).withLimit(2); + List<V> results; + try { + results = getSource(keyPredicate(key), opts).read().toList(); + } catch (QueryParseException e) { + throw new IOException("Unexpected QueryParseException during get()", e); + } catch (OrmException e) { + throw new IOException(e); + } + switch (results.size()) { + case 0: + return Optional.empty(); + case 1: + return Optional.of(results.get(0)); + default: + throw new IOException("Multiple results found in index for key " + + key + ": " + results); + } + } + + /** + * Get a predicate that looks up a single document by key. + * + * @param key document key. + * @return a single predicate. + */ + Predicate<V> keyPredicate(K key); + + /** * Mark whether this index is up-to-date and ready to serve reads. * * @param ready whether the index is ready
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/IndexUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java new file mode 100644 index 0000000..bcc7f7b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java
@@ -0,0 +1,74 @@ +// 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.index; + +import static com.google.gerrit.server.index.account.AccountField.ID; +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 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> accountFields(QueryOptions opts) { + Set<String> fs = opts.fields(); + return fs.contains(ID.getName()) + ? fs + : Sets.union(fs, ImmutableSet.of(ID.getName())); + } + + public static Set<String> changeFields(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-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java index 10f5ecb..faa6934 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -18,7 +18,6 @@ import com.google.common.base.Function; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; @@ -33,6 +32,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; /** Specific version of a secondary index schema. */ public class Schema<T> { @@ -149,15 +149,15 @@ FieldDef<T, ?>... rest) { FieldDef<T, ?> field = fields.get(first.getName()); if (field != null) { - return Optional.<FieldDef<T, ?>> of(checkSame(field, first)); + return Optional.of(checkSame(field, first)); } for (FieldDef<T, ?> f : rest) { field = fields.get(f.getName()); if (field != null) { - return Optional.<FieldDef<T, ?>> of(checkSame(field, f)); + return Optional.of(checkSame(field, f)); } } - return Optional.absent(); + return Optional.empty(); } /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java new file mode 100644 index 0000000..f996c3f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java
@@ -0,0 +1,103 @@ +// 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.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.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-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/account/AccountIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java index cb7b3ef..406982a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -18,9 +18,16 @@ import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexDefinition; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.account.AccountPredicates; public interface AccountIndex extends Index<Account.Id, AccountState> { public interface Factory extends IndexDefinition.IndexFactory<Account.Id, AccountState, AccountIndex> { } + + @Override + default Predicate<AccountState> keyPredicate(Account.Id id) { + return AccountPredicates.id(id); + } }
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..75dea17 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
@@ -18,11 +18,11 @@ import static org.eclipse.jgit.lib.RefDatabase.ALL; import com.google.common.base.Stopwatch; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; @@ -199,14 +199,15 @@ // trust the results. This is not an exact percentage since we bump the same // failure counter if a project can't be read, but close enough. int nFailed = failedTask.getCount(); - int nTotal = nFailed + doneTask.getCount(); + int nDone = doneTask.getCount(); + int nTotal = nFailed + nDone; double pctFailed = ((double) nFailed) / nTotal * 100; if (pctFailed > 10) { log.error("Failed {}/{} changes ({}%); not marking new index as ready", nFailed, nTotal, Math.round(pctFailed)); ok.set(false); } - return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount()); + return new Result(sw, ok.get(), nDone, nFailed); } private Callable<Void> reindexProject(final ChangeIndexer indexer, @@ -215,13 +216,18 @@ return new Callable<Void>() { @Override public Void call() throws Exception { - Multimap<ObjectId, ChangeData> byId = ArrayListMultimap.create(); + Multimap<ObjectId, ChangeData> byId = + MultimapBuilder.hashKeys().arrayListValues().build(); // TODO(dborowitz): Opening all repositories in a live server may be // wasteful; see if we can determine which ones it is safe to close // with RepositoryCache.close(repo). 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 +296,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..c943043 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
@@ -15,11 +15,12 @@ package com.google.gerrit.server.index.change; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +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; import com.google.common.collect.ImmutableSet; @@ -28,23 +29,32 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.collect.Table; +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.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.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.SchemaUtil; +import com.google.gerrit.server.index.change.StalenessChecker.RefState; +import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.notedb.RobotCommentNotes; +import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.ChangeData.ChangedLines; import com.google.gerrit.server.query.change.ChangeQueryBuilder; import com.google.gerrit.server.query.change.ChangeStatusPredicate; +import com.google.gson.Gson; import com.google.gwtorm.protobuf.CodecFactory; import com.google.gwtorm.protobuf.ProtobufCodec; import com.google.gwtorm.server.OrmException; @@ -75,6 +85,10 @@ * characters. */ public class ChangeField { + public static final int NO_ASSIGNEE = -1; + + private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson(); + /** Legacy change ID. */ public static final FieldDef<ChangeData, Integer> LEGACY_ID = new FieldDef.Single<ChangeData, Integer>("legacy_id", @@ -247,13 +261,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 +274,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 +306,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 +419,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 +555,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 +587,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()); @@ -622,10 +645,9 @@ @Override public Integer get(ChangeData input, FillArgs args) throws OrmException { - Optional<ChangedLines> changedLines = input.changedLines(); - return changedLines.isPresent() - ? changedLines.get().insertions + changedLines.get().deletions - : null; + return input.changedLines() + .map(c -> c.insertions + c.deletions) + .orElse(null); } }; @@ -642,31 +664,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 +680,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 +696,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 +741,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 +755,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()); } }; @@ -796,6 +789,239 @@ } }; + // Submit rule options in this class should never use fastEvalLabels. This + // slows down indexing slightly but produces correct search results. + public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT = + SubmitRuleOptions.defaults() + .allowClosed(true) + .allowDraft(true) + .build(); + + public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT = + SubmitRuleOptions.defaults().build(); + + /** + * JSON type for storing SubmitRecords. + * <p> + * Stored fields need to use a stable format over a long period; this type + * insulates the index from implementation changes in SubmitRecord itself. + */ + static class StoredSubmitRecord { + static class StoredLabel { + String label; + SubmitRecord.Label.Status status; + Integer appliedBy; + } + + SubmitRecord.Status status; + List<StoredLabel> labels; + String errorMessage; + + StoredSubmitRecord(SubmitRecord rec) { + this.status = rec.status; + this.errorMessage = rec.errorMessage; + if (rec.labels != null) { + this.labels = new ArrayList<>(rec.labels.size()); + for (SubmitRecord.Label label : rec.labels) { + StoredLabel sl = new StoredLabel(); + sl.label = label.label; + sl.status = label.status; + sl.appliedBy = + label.appliedBy != null ? label.appliedBy.get() : null; + this.labels.add(sl); + } + } + } + + private SubmitRecord toSubmitRecord() { + SubmitRecord rec = new SubmitRecord(); + rec.status = status; + rec.errorMessage = errorMessage; + if (labels != null) { + rec.labels = new ArrayList<>(labels.size()); + for (StoredLabel label : labels) { + SubmitRecord.Label srl = new SubmitRecord.Label(); + srl.label = label.label; + srl.status = label.status; + srl.appliedBy = label.appliedBy != null + ? new Account.Id(label.appliedBy) + : null; + rec.labels.add(srl); + } + } + return rec; + } + } + + public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD = + new FieldDef.Repeatable<ChangeData, String>( + "submit_record", FieldType.EXACT, false) { + @Override + public Iterable<String> get(ChangeData input, FillArgs args) + throws OrmException { + return formatSubmitRecordValues(input); + } + }; + + public static final FieldDef<ChangeData, Iterable<byte[]>> + STORED_SUBMIT_RECORD_STRICT = + new FieldDef.Repeatable<ChangeData, byte[]>( + "full_submit_record_strict", FieldType.STORED_ONLY, true) { + @Override + public Iterable<byte[]> get(ChangeData input, FillArgs args) + throws OrmException { + return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_STRICT); + } + }; + + public static final FieldDef<ChangeData, Iterable<byte[]>> + STORED_SUBMIT_RECORD_LENIENT = + new FieldDef.Repeatable<ChangeData, byte[]>( + "full_submit_record_lenient", FieldType.STORED_ONLY, true) { + @Override + public Iterable<byte[]> get(ChangeData input, FillArgs args) + throws OrmException { + return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_LENIENT); + } + }; + + public static void parseSubmitRecords( + Collection<String> values, SubmitRuleOptions opts, ChangeData out) { + checkArgument(!opts.fastEvalLabels()); + List<SubmitRecord> records = parseSubmitRecords(values); + if (records.isEmpty()) { + // Assume no values means the field is not in the index; + // SubmitRuleEvaluator ensures the list is non-empty. + return; + } + out.setSubmitRecords(opts, records); + + // Cache the fastEvalLabels variant as well so it can be used by + // ChangeJson. + out.setSubmitRecords( + opts.toBuilder().fastEvalLabels(true).build(), + records); + } + + @VisibleForTesting + static List<SubmitRecord> parseSubmitRecords(Collection<String> values) { + return values.stream() + .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord()) + .collect(toList()); + } + + @VisibleForTesting + static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) { + return Lists.transform( + records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8)); + } + + private static Iterable<byte[]> storedSubmitRecords( + ChangeData cd, SubmitRuleOptions opts) throws OrmException { + return storedSubmitRecords(cd.submitRecords(opts)); + } + + public static List<String> formatSubmitRecordValues(ChangeData cd) + throws OrmException { + return formatSubmitRecordValues( + cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), + cd.change().getOwner()); + } + + @VisibleForTesting + static List<String> formatSubmitRecordValues(List<SubmitRecord> records, + Account.Id changeOwner) { + List<String> result = new ArrayList<>(); + for (SubmitRecord rec : records) { + result.add(rec.status.name()); + if (rec.labels == null) { + continue; + } + for (SubmitRecord.Label label : rec.labels) { + String sl = label.status.toString() + ',' + label.label.toLowerCase(); + result.add(sl); + String slc = sl + ','; + if (label.appliedBy != null) { + result.add(slc + label.appliedBy.get()); + if (label.appliedBy.equals(changeOwner)) { + result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get()); + } + } + } + } + return result; + } + + /** + * All values of all refs that were used in the course of indexing this + * document. + * <p> + * Emitted as UTF-8 encoded strings of the form + * {@code project:ref/name:[hex sha]}. + */ + public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE = + new FieldDef.Repeatable<ChangeData, byte[]>( + "ref_state", FieldType.STORED_ONLY, true) { + @Override + public Iterable<byte[]> get(ChangeData input, FillArgs args) + throws OrmException { + List<byte[]> result = new ArrayList<>(); + Project.NameKey project = input.change().getProject(); + + input.editRefs().values().forEach( + r -> result.add(RefState.of(r).toByteArray(project))); + input.starRefs().values().forEach( + r -> result.add(RefState.of(r.ref()).toByteArray(args.allUsers))); + + if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) { + ChangeNotes notes = input.notes(); + result.add(RefState.create(notes.getRefName(), notes.getMetaId()) + .toByteArray(project)); + notes.getRobotComments(); // Force loading robot comments. + RobotCommentNotes robotNotes = notes.getRobotCommentNotes(); + result.add( + RefState.create(robotNotes.getRefName(), robotNotes.getMetaId()) + .toByteArray(project)); + input.draftRefs().values().forEach( + r -> result.add(RefState.of(r).toByteArray(args.allUsers))); + } + + return result; + } + }; + + /** + * All ref wildcard patterns that were used in the course of indexing this + * document. + * <p> + * Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}. + * See {@link RefStatePattern} for the pattern format. + */ + public static final FieldDef<ChangeData, Iterable<byte[]>> + REF_STATE_PATTERN = new FieldDef.Repeatable<ChangeData, byte[]>( + "ref_state_pattern", FieldType.STORED_ONLY, true) { + @Override + public Iterable<byte[]> get(ChangeData input, FillArgs args) + throws OrmException { + Change.Id id = input.getId(); + Project.NameKey project = input.change().getProject(); + List<byte[]> result = new ArrayList<>(3); + result.add(RefStatePattern.create( + RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*") + .toByteArray(project)); + result.add( + RefStatePattern.create( + RefNames.refsStarredChangesPrefix(id) + "*") + .toByteArray(args.allUsers)); + if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) { + result.add(RefStatePattern.create( + RefNames.refsDraftCommentsPrefix(id) + "*") + .toByteArray(args.allUsers)); + } + return result; + } + }; + public static final Integer NOT_REVIEWED = -1; private static String getTopic(ChangeData input) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java index 9545c0a..c56880f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -17,10 +17,17 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexDefinition; +import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.LegacyChangeIdPredicate; public interface ChangeIndex extends Index<Change.Id, ChangeData> { public interface Factory extends IndexDefinition.IndexFactory<Change.Id, ChangeData, ChangeIndex> { } + + @Override + default Predicate<ChangeData> keyPredicate(Change.Id id) { + return new LegacyChangeIdPredicate(id); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java index fa4f2fa..f256707 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -14,6 +14,9 @@ package com.google.gerrit.server.index.change; +import static com.google.gerrit.server.extensions.events.EventUtil.logEventListenerError; +import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH; + import com.google.common.base.Function; import com.google.common.util.concurrent.Atomics; import com.google.common.util.concurrent.CheckedFuture; @@ -26,7 +29,9 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.Index; +import com.google.gerrit.server.index.IndexExecutor; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.query.change.ChangeData; @@ -41,6 +46,7 @@ import com.google.inject.assistedinject.AssistedInject; import com.google.inject.util.Providers; +import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,16 +106,23 @@ private final ChangeNotes.Factory changeNotesFactory; private final ChangeData.Factory changeDataFactory; private final ThreadLocalRequestContext context; + private final ListeningExecutorService batchExecutor; private final ListeningExecutorService executor; - private final DynamicSet<ChangeIndexedListener> indexedListener; + private final DynamicSet<ChangeIndexedListener> indexedListeners; + private final StalenessChecker stalenessChecker; + private final boolean reindexAfterIndexUpdate; @AssistedInject - ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory, + ChangeIndexer( + @GerritServerConfig Config cfg, + SchemaFactory<ReviewDb> schemaFactory, NotesMigration notesMigration, ChangeNotes.Factory changeNotesFactory, ChangeData.Factory changeDataFactory, ThreadLocalRequestContext context, - DynamicSet<ChangeIndexedListener> indexedListener, + DynamicSet<ChangeIndexedListener> indexedListeners, + StalenessChecker stalenessChecker, + @IndexExecutor(BATCH) ListeningExecutorService batchExecutor, @Assisted ListeningExecutorService executor, @Assisted ChangeIndex index) { this.executor = executor; @@ -118,18 +131,24 @@ this.changeNotesFactory = changeNotesFactory; this.changeDataFactory = changeDataFactory; this.context = context; + this.indexedListeners = indexedListeners; + this.stalenessChecker = stalenessChecker; + this.batchExecutor = batchExecutor; + this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg); this.index = index; this.indexes = null; - this.indexedListener = indexedListener; } @AssistedInject ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory, + @GerritServerConfig Config cfg, NotesMigration notesMigration, ChangeNotes.Factory changeNotesFactory, ChangeData.Factory changeDataFactory, ThreadLocalRequestContext context, - DynamicSet<ChangeIndexedListener> indexedListener, + DynamicSet<ChangeIndexedListener> indexedListeners, + StalenessChecker stalenessChecker, + @IndexExecutor(BATCH) ListeningExecutorService batchExecutor, @Assisted ListeningExecutorService executor, @Assisted ChangeIndexCollection indexes) { this.executor = executor; @@ -138,9 +157,16 @@ this.changeNotesFactory = changeNotesFactory; this.changeDataFactory = changeDataFactory; this.context = context; + this.indexedListeners = indexedListeners; + this.stalenessChecker = stalenessChecker; + this.batchExecutor = batchExecutor; + this.reindexAfterIndexUpdate = reindexAfterIndexUpdate(cfg); this.index = null; this.indexes = indexes; - this.indexedListener = indexedListener; + } + + private static boolean reindexAfterIndexUpdate(Config cfg) { + return cfg.getBoolean("index", null, "testReindexAfterUpdate", true); } /** @@ -151,9 +177,7 @@ */ public CheckedFuture<?, IOException> indexAsync(Project.NameKey project, Change.Id id) { - return executor != null - ? submit(new IndexTask(project, id)) - : Futures.<Object, IOException> immediateCheckedFuture(null); + return submit(new IndexTask(project, id)); } /** @@ -181,17 +205,45 @@ i.replace(cd); } fireChangeIndexedEvent(cd.getId().get()); + + // Always double-check whether the change might be stale immediately after + // interactively indexing it. This fixes up the case where two writers write + // to the primary storage in one order, and the corresponding index writes + // happen in the opposite order: + // 1. Writer A writes to primary storage. + // 2. Writer B writes to primary storage. + // 3. Writer B updates index. + // 4. Writer A updates index. + // + // Without the extra reindexIfStale step, A has no way of knowing that it's + // about to overwrite the index document with stale data. It doesn't work to + // have A check for staleness before attempting its index update, because + // B's index update might not have happened when it does the check. + // + // With the extra reindexIfStale step after (3)/(4), we are able to detect + // and fix the staleness. It doesn't matter which order the two + // reindexIfStale calls actually execute in; we are guaranteed that at least + // one of them will execute after the second index write, (4). + reindexAfterIndexUpdate(cd); } private void fireChangeIndexedEvent(int id) { - for (ChangeIndexedListener listener : indexedListener) { - listener.onChangeIndexed(id); + for (ChangeIndexedListener listener : indexedListeners) { + try { + listener.onChangeIndexed(id); + } catch (Exception e) { + logEventListenerError(listener, e); + } } } private void fireChangeDeletedFromIndexEvent(int id) { - for (ChangeIndexedListener listener : indexedListener) { - listener.onChangeDeleted(id); + for (ChangeIndexedListener listener : indexedListeners) { + try { + listener.onChangeDeleted(id); + } catch (Exception e) { + logEventListenerError(listener, e); + } } } @@ -204,6 +256,8 @@ public void index(ReviewDb db, Change change) throws IOException, OrmException { index(newChangeData(db, change)); + // See comment in #index(ChangeData). + reindexAfterIndexUpdate(change.getProject(), change.getId()); } /** @@ -215,7 +269,10 @@ */ public void index(ReviewDb db, Project.NameKey project, Change.Id changeId) throws IOException, OrmException { - index(newChangeData(db, project, changeId)); + ChangeData cd = newChangeData(db, project, changeId); + index(cd); + // See comment in #index(ChangeData). + reindexAfterIndexUpdate(cd); } /** @@ -225,9 +282,7 @@ * @return future for the deleting task. */ public CheckedFuture<?, IOException> deleteAsync(Change.Id id) { - return executor != null - ? submit(new DeleteTask(id)) - : Futures.<Object, IOException> immediateCheckedFuture(null); + return submit(new DeleteTask(id)); } /** @@ -239,28 +294,68 @@ new DeleteTask(id).call(); } + /** + * Asynchronously check if a change is stale, and reindex if it is. + * <p> + * Always run on the batch executor, even if this indexer instance is + * configured to use a different executor. + * + * @param project the project to which the change belongs. + * @param id ID of the change to index. + * @return future for reindexing the change; returns true if the change was + * stale. + */ + public CheckedFuture<Boolean, IOException> reindexIfStale( + Project.NameKey project, Change.Id id) { + return submit(new ReindexIfStaleTask(project, id), batchExecutor); + } + + private void reindexAfterIndexUpdate(ChangeData cd) throws IOException { + try { + reindexAfterIndexUpdate(cd.project(), cd.getId()); + } catch (OrmException e) { + throw new IOException(e); + } + } + + private void reindexAfterIndexUpdate(Project.NameKey project, Change.Id id) { + if (reindexAfterIndexUpdate) { + reindexIfStale(project, id); + } + } + private Collection<ChangeIndex> getWriteIndexes() { return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index); } - private CheckedFuture<?, IOException> submit(Callable<?> task) { + private <T> CheckedFuture<T, IOException> submit(Callable<T> task) { + return submit(task, executor); + } + + private static <T> CheckedFuture<T, IOException> submit(Callable<T> task, + ListeningExecutorService executor) { return Futures.makeChecked( Futures.nonCancellationPropagating(executor.submit(task)), MAPPER); } - private class IndexTask implements Callable<Void> { - private final Project.NameKey project; - private final Change.Id id; + private abstract class AbstractIndexTask<T> implements Callable<T> { + protected final Project.NameKey project; + protected final Change.Id id; - private IndexTask(Project.NameKey project, Change.Id id) { + protected AbstractIndexTask(Project.NameKey project, Change.Id id) { this.project = project; this.id = id; } + protected abstract T callImpl(Provider<ReviewDb> db) throws Exception; + @Override - public Void call() throws Exception { + public abstract String toString(); + + @Override + public final T call() throws Exception { try { final AtomicReference<Provider<ReviewDb>> dbRef = Atomics.newReference(); @@ -289,10 +384,7 @@ }; RequestContext oldCtx = context.setContext(newCtx); try { - ChangeData cd = newChangeData( - newCtx.getReviewDbProvider().get(), project, id); - index(cd); - return null; + return callImpl(newCtx.getReviewDbProvider()); } finally { context.setContext(oldCtx); Provider<ReviewDb> db = dbRef.get(); @@ -301,17 +393,31 @@ } } } catch (Exception e) { - log.error(String.format("Failed to index change %d", id.get()), e); + log.error("Failed to execute " + this, e); throw e; } } + } + + private class IndexTask extends AbstractIndexTask<Void> { + private IndexTask(Project.NameKey project, Change.Id id) { + super(project, id); + } + + @Override + public Void callImpl(Provider<ReviewDb> db) throws Exception { + ChangeData cd = newChangeData(db.get(), project, id); + index(cd); + return null; + } @Override public String toString() { - return "index-change-" + id.get(); + return "index-change-" + id; } } + // Not AbstractIndexTask as it doesn't need ReviewDb. private class DeleteTask implements Callable<Void> { private final Change.Id id; @@ -327,11 +433,32 @@ for (ChangeIndex i : getWriteIndexes()) { i.delete(id); } + log.info("Deleted change {} from index.", id.get()); fireChangeDeletedFromIndexEvent(id.get()); return null; } } + private class ReindexIfStaleTask extends AbstractIndexTask<Boolean> { + private ReindexIfStaleTask(Project.NameKey project, Change.Id id) { + super(project, id); + } + + @Override + public Boolean callImpl(Provider<ReviewDb> db) throws Exception { + if (!stalenessChecker.isStale(id)) { + return false; + } + index(newChangeData(db.get(), project, id)); + return true; + } + + @Override + public String toString() { + return "reindex-if-stale-change-" + id; + } + } + // Avoid auto-rebuilding when reindexing if reading is disabled. This just // increases contention on the meta ref from a background indexing thread // with little benefit. The next actual write to the entity may still incur a
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..49bccf2 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,37 +55,35 @@ 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); + static final Schema<ChangeData> V33 = + schema(V32, ChangeField.ASSIGNEE); @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) + static final Schema<ChangeData> V34 = new Schema.Builder<ChangeData>() + .add(V33) + .remove(ChangeField.LABEL) + .add(ChangeField.LABEL2) .build(); - @SuppressWarnings("deprecation") - static final Schema<ChangeData> V32 = new Schema.Builder<ChangeData>() - .add(V31) - .remove(ChangeField.LEGACY_REVIEWER) - .add(ChangeField.REVIEWER) - .build(); + @Deprecated + static final Schema<ChangeData> V35 = + schema(V34, + ChangeField.SUBMIT_RECORD, + ChangeField.STORED_SUBMIT_RECORD_LENIENT, + ChangeField.STORED_SUBMIT_RECORD_STRICT); + + static final Schema<ChangeData> V36 = + schema(V35, + ChangeField.REF_STATE, + ChangeField.REF_STATE_PATTERN); public static final String NAME = "changes"; public static final ChangeSchemaDefinitions INSTANCE =
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/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java new file mode 100644 index 0000000..011b2474 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -0,0 +1,314 @@ +// 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.index.change; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.joining; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.Sets; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.IndexConfig; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.StreamSupport; + +@Singleton +public class StalenessChecker { + private static final Logger log = + LoggerFactory.getLogger(StalenessChecker.class); + + public static final ImmutableSet<String> FIELDS = ImmutableSet.of( + ChangeField.CHANGE.getName(), + ChangeField.REF_STATE.getName(), + ChangeField.REF_STATE_PATTERN.getName()); + + private final ChangeIndexCollection indexes; + private final GitRepositoryManager repoManager; + private final IndexConfig indexConfig; + private final Provider<ReviewDb> db; + + @Inject + StalenessChecker( + ChangeIndexCollection indexes, + GitRepositoryManager repoManager, + IndexConfig indexConfig, + Provider<ReviewDb> db) { + this.indexes = indexes; + this.repoManager = repoManager; + this.indexConfig = indexConfig; + this.db = db; + } + + public boolean isStale(Change.Id id) throws IOException, OrmException { + ChangeIndex i = indexes.getSearchIndex(); + if (i == null) { + return false; // No index; caller couldn't do anything if it is stale. + } + if (!i.getSchema().hasField(ChangeField.REF_STATE) + || !i.getSchema().hasField(ChangeField.REF_STATE_PATTERN)) { + return false; // Index version not new enough for this check. + } + + Optional<ChangeData> result = i.get( + id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, FIELDS)); + if (!result.isPresent()) { + return true; // Not in index, but caller wants it to be. + } + ChangeData cd = result.get(); + return isStale(repoManager, id, cd.change(), + ChangeNotes.readOneReviewDbChange(db.get(), id), + parseStates(cd), parsePatterns(cd)); + } + + public static boolean isStale( + GitRepositoryManager repoManager, + Change.Id id, + Change indexChange, + @Nullable Change reviewDbChange, + SetMultimap<Project.NameKey, RefState> states, + Multimap<Project.NameKey, RefStatePattern> patterns) { + return reviewDbChangeIsStale(indexChange, reviewDbChange) + || refsAreStale(repoManager, id, states, patterns); + } + + @VisibleForTesting + static boolean refsAreStale(GitRepositoryManager repoManager, + Change.Id id, + SetMultimap<Project.NameKey, RefState> states, + Multimap<Project.NameKey, RefStatePattern> patterns) { + Set<Project.NameKey> projects = + Sets.union(states.keySet(), patterns.keySet()); + + for (Project.NameKey p : projects) { + if (refsAreStale(repoManager, id, p, states, patterns)) { + return true; + } + } + + return false; + } + + @VisibleForTesting + static boolean reviewDbChangeIsStale( + Change indexChange, @Nullable Change reviewDbChange) { + if (reviewDbChange == null) { + return false; // Nothing the caller can do. + } + checkArgument(indexChange.getId().equals(reviewDbChange.getId()), + "mismatched change ID: %s != %s", + indexChange.getId(), reviewDbChange.getId()); + if (PrimaryStorage.of(reviewDbChange) != PrimaryStorage.REVIEW_DB) { + return false; // Not a ReviewDb change, don't check rowVersion. + } + return reviewDbChange.getRowVersion() != indexChange.getRowVersion(); + } + + private SetMultimap<Project.NameKey, RefState> parseStates(ChangeData cd) { + return parseStates(cd.getRefStates()); + } + + public static SetMultimap<Project.NameKey, RefState> parseStates( + Iterable<byte[]> states) { + RefState.check(states != null, null); + SetMultimap<Project.NameKey, RefState> result = + MultimapBuilder.hashKeys().hashSetValues().build(); + for (byte[] b : states) { + RefState.check(b != null, null); + String s = new String(b, UTF_8); + List<String> parts = Splitter.on(':').splitToList(s); + RefState.check( + parts.size() == 3 + && !parts.get(0).isEmpty() + && !parts.get(1).isEmpty(), + s); + result.put( + new Project.NameKey(parts.get(0)), + RefState.create(parts.get(1), parts.get(2))); + } + return result; + } + + private Multimap<Project.NameKey, RefStatePattern> parsePatterns( + ChangeData cd) { + return parsePatterns(cd.getRefStatePatterns()); + } + + public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns( + Iterable<byte[]> patterns) { + RefStatePattern.check(patterns != null, null); + ListMultimap<Project.NameKey, RefStatePattern> result = + MultimapBuilder.hashKeys().arrayListValues().build(); + for (byte[] b : patterns) { + RefStatePattern.check(b != null, null); + String s = new String(b, UTF_8); + List<String> parts = Splitter.on(':').splitToList(s); + RefStatePattern.check(parts.size() == 2, s); + result.put( + new Project.NameKey(parts.get(0)), + RefStatePattern.create(parts.get(1))); + } + return result; + } + + private static boolean refsAreStale(GitRepositoryManager repoManager, + Change.Id id, Project.NameKey project, + SetMultimap<Project.NameKey, RefState> allStates, + Multimap<Project.NameKey, RefStatePattern> allPatterns) { + try (Repository repo = repoManager.openRepository(project)) { + Set<RefState> states = allStates.get(project); + for (RefState state : states) { + if (!state.match(repo)) { + return true; + } + } + for (RefStatePattern pattern : allPatterns.get(project)) { + if (!pattern.match(repo, states)) { + return true; + } + } + return false; + } catch (IOException e) { + log.warn( + String.format("error checking staleness of %s in %s", id, project), + e); + return true; + } + } + + @AutoValue + public abstract static class RefState { + static RefState create(String ref, String sha) { + return new AutoValue_StalenessChecker_RefState( + ref, ObjectId.fromString(sha)); + } + + static RefState create(String ref, @Nullable ObjectId id) { + return new AutoValue_StalenessChecker_RefState( + ref, firstNonNull(id, ObjectId.zeroId())); + } + + static RefState of(Ref ref) { + return new AutoValue_StalenessChecker_RefState( + ref.getName(), ref.getObjectId()); + } + + byte[] toByteArray(Project.NameKey project) { + byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8); + byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH]; + System.arraycopy(a, 0, b, 0, a.length); + id().copyTo(b, a.length); + return b; + } + + private static void check(boolean condition, String str) { + checkArgument(condition, "invalid RefState: %s", str); + } + + abstract String ref(); + abstract ObjectId id(); + + private boolean match(Repository repo) throws IOException { + Ref ref = repo.exactRef(ref()); + ObjectId expected = ref != null ? ref.getObjectId() : ObjectId.zeroId(); + return id().equals(expected); + } + } + + /** + * Pattern for matching refs. + * <p> + * Similar to '*' syntax for native Git refspecs, but slightly more powerful: + * the pattern may contain arbitrarily many asterisks. There must be at least + * one '*' and the first one must immediately follow a '/'. + */ + @AutoValue + public abstract static class RefStatePattern { + static RefStatePattern create(String pattern) { + int star = pattern.indexOf('*'); + check(star > 0 && pattern.charAt(star - 1) == '/', pattern); + String prefix = pattern.substring(0, star); + check(Repository.isValidRefName(pattern.replace('*', 'x')), pattern); + + // Quote everything except the '*'s, which become ".*". + String regex = + StreamSupport.stream(Splitter.on('*').split(pattern).spliterator(), false) + .map(Pattern::quote) + .collect(joining(".*", "^", "$")); + return new AutoValue_StalenessChecker_RefStatePattern( + pattern, prefix, Pattern.compile(regex)); + } + + byte[] toByteArray(Project.NameKey project) { + return (project.toString() + ':' + pattern()).getBytes(UTF_8); + } + + private static void check(boolean condition, String str) { + checkArgument(condition, "invalid RefStatePattern: %s", str); + } + + abstract String pattern(); + abstract String prefix(); + abstract Pattern regex(); + + boolean match(String refName) { + return regex().matcher(refName).find(); + } + + private boolean match(Repository repo, Set<RefState> expected) + throws IOException { + for (Ref r : repo.getRefDatabase().getRefs(prefix()).values()) { + if (!match(r.getName())) { + continue; + } + if (!expected.contains(RefState.of(r))) { + return false; + } + } + return true; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java index 863cb82..2088261 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.mail; +import com.google.gerrit.server.mail.send.EmailHeader; + public class Address { public static Address parse(final String in) { final int lt = in.indexOf('<'); @@ -22,7 +24,16 @@ if (0 <= lt && lt < gt && lt + 1 < at && at + 1 < gt) { final String email = in.substring(lt + 1, gt).trim(); final String name = in.substring(0, lt).trim(); - return new Address(name.length() > 0 ? name : null, email); + int nameStart = 0; + int nameEnd = name.length(); + if (name.startsWith("\"")) { + nameStart++; + } + if (name.endsWith("\"")) { + nameEnd--; + } + return new Address(name.length() > 0 ? + name.substring(nameStart, nameEnd): null, email); } if (lt < 0 && gt < 0 && 0 < at && at < in.length() - 1) {
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 deleted file mode 100644 index b56b737..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java +++ /dev/null
@@ -1,286 +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.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.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.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.server.patch.PatchFile; -import com.google.gerrit.server.patch.PatchList; -import com.google.gerrit.server.patch.PatchListNotAvailableException; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import org.eclipse.jgit.lib.Repository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** 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); - - public interface Factory { - CommentSender create(Project.NameKey project, Change.Id id); - } - - private List<PatchLineComment> inlineComments = Collections.emptyList(); - private final PatchLineCommentsUtil plcUtil; - - @Inject - public CommentSender(EmailArguments ea, - PatchLineCommentsUtil plcUtil, - @Assisted Project.NameKey project, - @Assisted Change.Id id) throws OrmException { - super(ea, "comment", newChangeData(ea, project, id)); - this.plcUtil = plcUtil; - } - - public void setPatchLineComments(final List<PatchLineComment> plc) - throws OrmException { - inlineComments = plc; - - 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()); - } - } - changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) { - ccAllApprovals(); - } - if (notify.compareTo(NotifyHandling.ALL) >= 0) { - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS); - } - } - - @Override - public void formatChange() throws EmailException { - appendText(velocifyFile("Comment.vm")); - } - - @Override - public void formatFooter() throws EmailException { - appendText(velocifyFile("CommentFooter.vm")); - } - - public boolean hasInlineComments() { - return !inlineComments.isEmpty(); - } - - public String getInlineComments() { - return getInlineComments(1); - } - - public String getInlineComments(int lines) { - StringBuilder cmts = new StringBuilder(); - try (Repository repo = getRepository()) { - PatchList patchList = null; - if (repo != null) { - try { - patchList = getPatchList(); - } catch (PatchListNotAvailableException e) { - log.error("Failed to get patch list", e); - } - } - - Patch.Key currentFileKey = null; - PatchFile currentFileData = null; - for (final PatchLineComment c : inlineComments) { - final Patch.Key pk = c.getKey().getParentKey(); - - if (!pk.equals(currentFileKey)) { - String link = makeLink(pk); - if (link != null) { - cmts.append(link).append('\n'); - } - if (Patch.COMMIT_MSG.equals(pk.get())) { - cmts.append("Commit Message:\n\n"); - } else { - cmts.append("File ").append(pk.get()).append(":\n\n"); - } - currentFileKey = pk; - - if (patchList != null) { - try { - currentFileData = - new PatchFile(repo, patchList, pk.get()); - } catch (IOException e) { - log.warn(String.format( - "Cannot load %s from %s in %s", - pk.getFileName(), - patchList.getNewId().name(), - projectState.getProject().getName()), e); - currentFileData = null; - } - } - } - - if (currentFileData != null) { - appendComment(cmts, lines, currentFileData, c); - } - cmts.append("\n\n"); - } - } - return cmts.toString(); - } - - private void appendComment(StringBuilder out, int contextLines, - PatchFile currentFileData, PatchLineComment comment) { - short side = comment.getSide(); - CommentRange range = comment.getRange(); - 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() - ? 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. - } - out.append('\n'); - } - appendQuotedParent(out, comment); - out.append(comment.getMessage().trim()).append('\n'); - } else { - int lineNbr = comment.getLine(); - int maxLines; - try { - maxLines = currentFileData.getLineCount(side); - } catch (Throwable e) { - maxLines = lineNbr; - } - - final int startLine = Math.max(1, lineNbr - contextLines + 1); - final int stopLine = Math.min(maxLines, lineNbr + contextLines); - - for (int line = startLine; line <= lineNbr; ++line) { - appendFileLine(out, currentFileData, side, line); - } - appendQuotedParent(out, comment); - out.append(comment.getMessage().trim()).append('\n'); - - for (int line = lineNbr + 1; line < stopLine; ++line) { - appendFileLine(out, currentFileData, side, line); - } - } - } - - 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 appendQuotedParent(StringBuilder out, PatchLineComment child) { - if (child.getParentUuid() != null) { - Optional<PatchLineComment> parent; - PatchLineComment.Key key = new PatchLineComment.Key( - child.getKey().getParentKey(), - child.getParentUuid()); - try { - parent = plcUtil.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(); - if (msg.length() > 75) { - msg = msg.substring(0, 75); - } - int lf = msg.indexOf('\n'); - if (lf > 0) { - msg = msg.substring(0, lf); - } - out.append("> ").append(msg).append('\n'); - } - } - } - - // Makes a link back to the given patch set and file. - private String makeLink(Patch.Key patch) { - 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())) - .toString(); - } - - private Repository getRepository() { - try { - return args.server.openRepository(projectState.getProject().getNameKey()); - } catch (IOException e) { - return null; - } - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java index 7ceb0ae..9bf97dd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -15,6 +15,12 @@ package com.google.gerrit.server.mail; import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.server.mail.send.AbandonedSender; +import com.google.gerrit.server.mail.send.CommentSender; +import com.google.gerrit.server.mail.send.DeleteReviewerSender; +import com.google.gerrit.server.mail.send.DeleteVoteSender; +import com.google.gerrit.server.mail.send.RestoredSender; +import com.google.gerrit.server.mail.send.RevertedSender; public class EmailModule extends FactoryModule { @Override
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 deleted file mode 100644 index a7a1028..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java +++ /dev/null
@@ -1,46 +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.server.mail; - -import com.google.gerrit.common.errors.EmailException; - -import java.util.Collection; -import java.util.Map; - -/** Sends email messages to third parties. */ -public interface EmailSender { - boolean isEnabled(); - - /** - * Can the address receive messages from us? - * - * @param address the address to consider. - * @return true if this sender will deliver to the address. - */ - boolean canEmail(String address); - - /** - * Sends an email message. - * - * @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 body text to appear in the body of the message. - * @throws EmailException the message cannot be sent. - */ - void send(Address from, Collection<Address> rcpt, - Map<String, EmailHeader> headers, String body) throws EmailException; -}
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..b719193 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
@@ -15,19 +15,47 @@ package com.google.gerrit.server.mail; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.mail.receive.Protocol; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; +import java.util.concurrent.TimeUnit; + @Singleton public class EmailSettings { + private static final String SEND_EMAL = "sendemail"; + private static final String RECEIVE_EMAL = "receiveemail"; + // Send + public final boolean html; public final boolean includeDiff; public final int maximumDiffSize; + // Receive + public final Protocol protocol; + public final String host; + public final int port; + public final String username; + public final String password; + public final Encryption encryption; + public final long fetchInterval; // in milliseconds @Inject EmailSettings(@GerritServerConfig Config cfg) { - includeDiff = cfg.getBoolean("sendemail", "includeDiff", false); - maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10); + // Send + html = cfg.getBoolean(SEND_EMAL, "html", true); + includeDiff = cfg.getBoolean(SEND_EMAL, "includeDiff", false); + maximumDiffSize = cfg.getInt(SEND_EMAL, "maximumDiffSize", 256 << 10); + // Receive + protocol = cfg.getEnum(RECEIVE_EMAL, null, "protocol", Protocol.NONE); + host = cfg.getString(RECEIVE_EMAL, null, "host"); + port = cfg.getInt(RECEIVE_EMAL, "port", 0); + username = cfg.getString(RECEIVE_EMAL, null, "username"); + password = cfg.getString(RECEIVE_EMAL, null, "password"); + encryption = + cfg.getEnum(RECEIVE_EMAL, null, "encryption", Encryption.NONE); + fetchInterval = cfg.getTimeUnit(RECEIVE_EMAL, null, "fetchInterval", + TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS), + TimeUnit.MILLISECONDS); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java index 41e1e2c..488711a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -16,6 +16,7 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.mail.send.RegisterNewEmailSender; /** Verifies the token sent by {@link RegisterNewEmailSender}. */ public interface EmailTokenVerifier {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java similarity index 85% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java index ea0def0..a557532 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.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. @@ -14,6 +14,6 @@ package com.google.gerrit.server.mail; -public enum RecipientType { - TO, CC, BCC +public enum Encryption { + NONE, SSL, TLS }
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..ca8d101 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
@@ -28,12 +28,17 @@ import org.eclipse.jgit.revwalk.FooterKey; import org.eclipse.jgit.revwalk.FooterLine; +import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; public class MailUtil { + public static DateTimeFormatter rfcDateformatter = + DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ"); + public static MailRecipients getRecipientsFromFooters( ReviewDb db, AccountResolver accountResolver, boolean draftPatchSet, List<FooterLine> footerLines) throws OrmException { @@ -124,4 +129,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/MetadataName.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.java new file mode 100644 index 0000000..3db55c0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MetadataName.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.mail; + +public final class MetadataName { + public static final String CHANGE_ID = "Gerrit-Change-Id"; + public static final String PATCH_SET = "Gerrit-PatchSet"; + public static final String MESSAGE_TYPE = "Gerrit-MessageType"; + public static final String TIMESTAMP = "Gerrit-Comment-Date"; + + public static String toHeader(String metadataName) { + return "X-" + metadataName; + } + + public static String toHeaderWithDelimiter(String metadataName) { + return toHeader(metadataName) + ": "; + } + + public static String toFooterWithDelimiter(String metadataName) { + return metadataName + ": "; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java index f12859f..3dd98ea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -18,6 +18,7 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.mail.send.RegisterNewEmailSender; import com.google.gwtjsonrpc.server.SignedToken; import com.google.gwtjsonrpc.server.ValidToken; import com.google.gwtjsonrpc.server.XsrfException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java new file mode 100644 index 0000000..6b81d35 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
@@ -0,0 +1,125 @@ +// 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.receive; + +import com.google.common.base.Strings; +import com.google.common.collect.Iterators; +import com.google.common.collect.PeekingIterator; +import com.google.gerrit.reviewdb.client.Comment; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** HTMLParser provides parsing functionality for html email. */ +public class HtmlParser { + /** + * Parses comments from html email. + * + * @param email MailMessage as received from the email service. + * @param comments A specific set of comments as sent out in the original + * notification email. Comments are expected to be in the same + * order as they were sent out to in the email + * @param changeUrl Canonical change URL that points to the change on this + * Gerrit instance. + * Example: https://go-review.googlesource.com/#/c/91570 + * @return List of MailComments parsed from the html part of the email. + */ + public static List<MailComment> parse(MailMessage email, + Collection<Comment> comments, String changeUrl) { + // TODO(hiesel) Add support for Gmail Mobile + // TODO(hiesel) Add tests for other popular email clients + + // This parser goes though all html elements in the email and checks for + // matching patterns. It keeps track of the last file and comments it + // encountered to know in which context a parsed comment belongs. + // It uses the href attributes of <a> tags to identify comments sent out by + // Gerrit as these are generally more reliable then the text captions. + List<MailComment> parsedComments = new ArrayList<>(); + Document d = Jsoup.parse(email.htmlContent()); + PeekingIterator<Comment> iter = + Iterators.peekingIterator(comments.iterator()); + + String lastEncounteredFileName = null; + Comment lastEncounteredComment = null; + for (Element e : d.body().getAllElements()) { + String elementName = e.tagName(); + boolean isInBlockQuote = e.parents().stream() + .filter(p -> p.tagName().equals("blockquote")) + .findAny() + .isPresent(); + + if (elementName.equals("a")) { + String href = e.attr("href"); + // Check if there is still a next comment that could be contained in + // this <a> tag + if (!iter.hasNext()) { + continue; + } + Comment perspectiveComment = iter.peek(); + if (href.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) { + if (lastEncounteredFileName == null || !lastEncounteredFileName + .equals(perspectiveComment.key.filename)) { + // Not a file-level comment, but users could have typed a comment + // right after this file annotation to create a new file-level + // comment. If this file has a file-level comment, we have already + // set lastEncounteredComment to that file-level comment when we + // encountered the file link and should not reset it now. + lastEncounteredFileName = perspectiveComment.key.filename; + lastEncounteredComment = null; + } else if (perspectiveComment.lineNbr == 0) { + // This was originally a file-level comment + lastEncounteredComment = perspectiveComment; + iter.next(); + } + } else if (ParserUtil.isCommentUrl(href, changeUrl, + perspectiveComment)) { + // This is a regular inline comment + lastEncounteredComment = perspectiveComment; + iter.next(); + } + } else if (!isInBlockQuote && elementName.equals("div") && + !e.className().startsWith("gmail")) { + // This is a comment typed by the user + String content = e.ownText().trim(); + if (!Strings.isNullOrEmpty(content)) { + if (lastEncounteredComment == null && + lastEncounteredFileName == null) { + // Remove quotation line, email signature and + // "Sent from my xyz device" + content = ParserUtil.trimQuotationLine(content); + // TODO(hiesel) Add more sanitizer + if (!Strings.isNullOrEmpty(content)) { + parsedComments.add(new MailComment(content, null, null, + MailComment.CommentType.CHANGE_MESSAGE)); + } + } else if (lastEncounteredComment == null) { + parsedComments.add(new MailComment(content, lastEncounteredFileName, + null, MailComment.CommentType.FILE_COMMENT)); + } else { + parsedComments.add(new MailComment(content, null, + lastEncounteredComment, + MailComment.CommentType.INLINE_COMMENT)); + } + } + } + } + return parsedComments; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java new file mode 100644 index 0000000..ce2a834 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
@@ -0,0 +1,147 @@ +// 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.receive; + +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.mail.EmailSettings; +import com.google.gerrit.server.mail.Encryption; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.apache.commons.net.imap.IMAPClient; +import org.apache.commons.net.imap.IMAPSClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class ImapMailReceiver extends MailReceiver { + private static final Logger log = + LoggerFactory.getLogger(ImapMailReceiver.class); + private static final String INBOX_FOLDER = "INBOX"; + + @Inject + ImapMailReceiver(EmailSettings mailSettings, + MailProcessor mailProcessor, + WorkQueue workQueue) { + super(mailSettings, mailProcessor, workQueue); + } + + /** + * handleEmails will open a connection to the mail server, remove emails + * where deletion is pending, read new email and close the connection. + * @param async Determines if processing messages should happen asynchronous. + */ + @Override + public synchronized void handleEmails(boolean async) { + IMAPClient imap; + if (mailSettings.encryption != Encryption.NONE) { + imap = new IMAPSClient(mailSettings.encryption.name(), false); + } else { + imap = new IMAPClient(); + } + if (mailSettings.port > 0) { + imap.setDefaultPort(mailSettings.port); + } + // Set a 30s timeout for each operation + imap.setDefaultTimeout(30 * 1000); + try { + imap.connect(mailSettings.host); + try { + if (!imap.login(mailSettings.username, mailSettings.password)) { + log.error("Could not login to IMAP server"); + return; + } + try { + if (!imap.select(INBOX_FOLDER)){ + log.error("Could not select IMAP folder " + INBOX_FOLDER); + return; + } + // Fetch just the internal dates first to know how many messages we + // should fetch. + if (!imap.fetch("1:*", "(INTERNALDATE)")) { + log.error("IMAP fetch failed. Will retry in next fetch cycle."); + return; + } + // Format of reply is one line per email and one line to indicate + // that the fetch was successful. + // Example: + // * 1 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)") + // * 2 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)") + // AAAC OK FETCH completed. + int numMessages = imap.getReplyStrings().length - 1; + log.info("Fetched " + numMessages + " messages via IMAP"); + if (numMessages == 0) { + return; + } + // Fetch the full version of all emails + List<MailMessage> mailMessages = new ArrayList<>(numMessages); + for (int i = 1; i <= numMessages; i++) { + if (imap.fetch(i + ":" + i, "(BODY.PEEK[])")) { + // Obtain full reply + String[] rawMessage = imap.getReplyStrings(); + if (rawMessage.length < 2) { + continue; + } + // First and last line are IMAP status codes. We have already + // checked, that the fetch returned true (OK), so we safely ignore + // those two lines. + StringBuilder b = new StringBuilder(2 * (rawMessage.length - 2)); + for(int j = 1; j < rawMessage.length - 1; j++) { + if (j > 1) { + b.append("\n"); + } + b.append(rawMessage[j]); + } + try { + MailMessage mailMessage = RawMailParser.parse(b.toString()); + if (pendingDeletion.contains(mailMessage.id())) { + // Mark message as deleted + if (imap.store(i + ":" + i, "+FLAGS", "(\\Deleted)")) { + pendingDeletion.remove(mailMessage.id()); + } else { + log.error("Could not mark mail message as deleted: " + + mailMessage.id()); + } + } else { + mailMessages.add(mailMessage); + } + } catch (MailParsingException e) { + log.error("Exception while parsing email after IMAP fetch", e); + } + } else { + log.error("IMAP fetch failed. Will retry in next fetch cycle."); + } + } + // Permanently delete emails marked for deletion + if (!imap.expunge()) { + log.error("Could not expunge IMAP emails"); + } + dispatchMailProcessor(mailMessages, async); + } finally { + imap.logout(); + } + } finally { + imap.disconnect(); + } + } catch (IOException e) { + log.error("Error while talking to IMAP server", e); + return; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java new file mode 100644 index 0000000..4144cfc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.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.mail.receive; + +import com.google.gerrit.reviewdb.client.Comment; + +/** A comment parsed from inbound email */ +public class MailComment { + enum CommentType { + CHANGE_MESSAGE, + FILE_COMMENT, + INLINE_COMMENT + } + + CommentType type; + Comment inReplyTo; + String fileName; + String message; + + public MailComment() { } + + public MailComment(String message, String fileName, Comment inReplyTo, + CommentType type) { + this.message = message; + this.fileName = fileName; + this.inReplyTo = inReplyTo; + this.type = type; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java new file mode 100644 index 0000000..966f3e6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
@@ -0,0 +1,89 @@ +// 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.receive; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.server.mail.Address; + +import org.joda.time.DateTime; + +/** + * MailMessage is a simplified representation of an RFC 2045-2047 mime email + * message used for representing received emails inside Gerrit. It is populated + * by the MailParser after MailReceiver has received a message. Transformations + * done by the parser include stitching mime parts together, transforming all + * content to UTF-16 and removing attachments. + * + * A valid MailMessage contains at least the following fields: id, from, to, + * subject and dateReceived. + */ +@AutoValue +public abstract class MailMessage { + // Unique Identifier + public abstract String id(); + // Envelop Information + public abstract Address from(); + public abstract ImmutableList<Address> to(); + @Nullable + public abstract ImmutableList<Address> cc(); + // Metadata + public abstract DateTime dateReceived(); + public abstract ImmutableList<String> additionalHeaders(); + // Content + public abstract String subject(); + @Nullable + public abstract String textContent(); + @Nullable + public abstract String htmlContent(); + + public static Builder builder() { + return new AutoValue_MailMessage.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder id(String val); + public abstract Builder from(Address val); + public abstract ImmutableList.Builder<Address> toBuilder(); + + public Builder addTo(Address val) { + toBuilder().add(val); + return this; + } + + public abstract ImmutableList.Builder<Address> ccBuilder(); + + public Builder addCc(Address val) { + ccBuilder().add(val); + return this; + } + + public abstract Builder dateReceived(DateTime val); + public abstract ImmutableList.Builder<String> additionalHeadersBuilder(); + + public Builder addAdditionalHeader(String val) { + additionalHeadersBuilder().add(val); + return this; + } + + public abstract Builder subject(String val); + public abstract Builder textContent(String val); + public abstract Builder htmlContent(String val); + + public abstract MailMessage build(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java new file mode 100644 index 0000000..c353e54 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMetadata.java
@@ -0,0 +1,45 @@ +// 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.receive; + +import com.google.common.base.MoreObjects; + +import java.sql.Timestamp; + +/** MailMetadata represents metadata parsed from inbound email. */ +public class MailMetadata { + public String changeId; + public Integer patchSet; + public String author; // Author of the email + public Timestamp timestamp; + public String messageType; // we expect comment here + + + public boolean hasRequiredFields() { + return changeId != null && patchSet != null && author != null && + timestamp != null && messageType != null; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("Change-Id", changeId) + .add("Patch-Set", patchSet) + .add("Author", author) + .add("Timestamp", timestamp) + .add("Message-Type", messageType) + .toString(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.java new file mode 100644 index 0000000..edadef8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailParsingException.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.mail.receive; + +/** MailParsingException indicates that an email could not be parsed. */ +public class MailParsingException extends Exception { + private static final long serialVersionUID = 1L; + + public MailParsingException(String msg) { + super(msg); + } + + public MailParsingException(String msg, Throwable cause) { + super(msg, cause); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java new file mode 100644 index 0000000..9d8f332 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -0,0 +1,270 @@ +// 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.receive; + +import com.google.common.base.Strings; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.client.Side; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +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.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.account.AccountByEmailCache; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +public class MailProcessor { + private static final Logger log = + LoggerFactory.getLogger(MailProcessor.class.getName()); + + private final AccountByEmailCache accountByEmailCache; + private final BatchUpdate.Factory buf; + private final ChangeMessagesUtil changeMessagesUtil; + private final CommentsUtil commentsUtil; + private final OneOffRequestContext oneOffRequestContext; + private final PatchListCache patchListCache; + private final PatchSetUtil psUtil; + private final Provider<InternalChangeQuery> queryProvider; + private final Provider<ReviewDb> reviewDb; + private final Provider<String> canonicalUrl; + + @Inject + public MailProcessor(AccountByEmailCache accountByEmailCache, + BatchUpdate.Factory buf, + ChangeMessagesUtil changeMessagesUtil, + CommentsUtil commentsUtil, + OneOffRequestContext oneOffRequestContext, + PatchListCache patchListCache, + PatchSetUtil psUtil, + Provider<InternalChangeQuery> queryProvider, + Provider<ReviewDb> reviewDb, + @CanonicalWebUrl Provider<String> canonicalUrl) { + this.accountByEmailCache = accountByEmailCache; + this.buf = buf; + this.changeMessagesUtil = changeMessagesUtil; + this.commentsUtil = commentsUtil; + this.oneOffRequestContext = oneOffRequestContext; + this.patchListCache = patchListCache; + this.psUtil = psUtil; + this.queryProvider = queryProvider; + this.reviewDb = reviewDb; + this.canonicalUrl = canonicalUrl; + } + + /** + * Parse comments from MailMessage and persist them on the change. + * @param message MailMessage to process. + * @throws OrmException + */ + public void process(MailMessage message) throws OrmException { + MailMetadata metadata = MetadataParser.parse(message); + if (!metadata.hasRequiredFields()) { + log.error("Mail: Message " + message.id() + + " is missing required metadata, have " + metadata + + ". Will delete message."); + return; + } + + Set<Account.Id> accounts = accountByEmailCache.get(metadata.author); + if (accounts.size() != 1) { + log.error("Mail: Address " + metadata.author + + " could not be matched to a unique account. It was matched to " + + accounts + ". Will delete message."); + return; + } + Account.Id account = accounts.iterator().next(); + if (!reviewDb.get().accounts().get(account).isActive()) { + log.warn("Mail: Account " + account + + " is inactive. Will delete message."); + return; + } + + try (ManualRequestContext ctx = oneOffRequestContext.openAs(account)) { + ChangeData cd = queryProvider.get().setLimit(1) + .byKey(Change.Key.parse(metadata.changeId)).get(0); + if (existingMessageIds(cd).contains(message.id())) { + log.info("Mail: Message " + message.id() + + " was already processed. Will delete message."); + return; + } + // Get all comments; filter and sort them to get the original list of + // comments from the outbound email. + // TODO(hiesel) Also filter by original comment author. + Collection<Comment> comments = cd.publishedComments().stream() + .filter(c -> (c.writtenOn.getTime() / 1000) == + (metadata.timestamp.getTime() / 1000)) + .sorted(CommentsUtil.COMMENT_ORDER) + .collect(Collectors.toList()); + Project.NameKey project = cd.project(); + String changeUrl = canonicalUrl.get() + "#/c/" + cd.getId().get(); + + List<MailComment> parsedComments; + if (useHtmlParser(message)) { + parsedComments = HtmlParser.parse(message, comments, changeUrl); + } else { + parsedComments = TextParser.parse(message, comments, changeUrl); + } + + if (parsedComments.isEmpty()) { + log.warn("Mail: Could not parse any comments from " + message.id() + + ". Will delete message."); + return; + } + + Op o = new Op(new PatchSet.Id(cd.getId(), metadata.patchSet), + parsedComments, message.id()); + BatchUpdate batchUpdate = buf.create(cd.db(), project, ctx.getUser(), + TimeUtil.nowTs()); + batchUpdate.addOp(cd.getId(), o); + try { + batchUpdate.execute(); + } catch (UpdateException | RestApiException e) { + throw new OrmException(e); + } + } + } + + private class Op extends BatchUpdate.Op { + private final PatchSet.Id psId; + private final List<MailComment> parsedComments; + private final String tag; + + private Op(PatchSet.Id psId, List<MailComment> parsedComments, + String messageId) { + this.psId = psId; + this.parsedComments = parsedComments; + this.tag = "mailMessageId=" + messageId; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); + if (ps == null) { + throw new OrmException("patch set not found: " + psId); + } + + String changeMsg = "Patch Set " + psId.get() + ":"; + if (parsedComments.get(0).type == + MailComment.CommentType.CHANGE_MESSAGE) { + if (parsedComments.size() > 1) { + changeMsg += "\n" + numComments(parsedComments.size() - 1); + } + changeMsg += "\n" + parsedComments.get(0).message; + } else { + changeMsg += "\n" + numComments(parsedComments.size()); + } + + ChangeMessage msg = ChangeMessagesUtil.newMessage(ctx, changeMsg, tag); + changeMessagesUtil.addChangeMessage(ctx.getDb(), + ctx.getUpdate(psId), msg); + + List<Comment> comments = new ArrayList<>(); + for (MailComment c : parsedComments) { + if (c.type == MailComment.CommentType.CHANGE_MESSAGE) { + continue; + } + + String fileName; + // The patch set that this comment is based on is different if this + // comment was sent in reply to a comment on a previous patch set. + PatchSet psForComment; + Side side; + if (c.inReplyTo != null) { + fileName = c.inReplyTo.key.filename; + psForComment = psUtil.get(ctx.getDb(), ctx.getNotes(), + new PatchSet.Id(ctx.getChange().getId(), + c.inReplyTo.key.patchSetId)); + side = Side.fromShort(c.inReplyTo.side); + } else { + fileName = c.fileName; + psForComment = ps; + side = Side.REVISION; + } + + Comment comment = commentsUtil.newComment(ctx, fileName, + psForComment.getId(), (short) side.ordinal(), c.message); + comment.tag = tag; + if (c.inReplyTo != null) { + comment.parentUuid = c.inReplyTo.key.uuid; + comment.lineNbr = c.inReplyTo.lineNbr; + comment.range = c.inReplyTo.range; + } + CommentsUtil.setCommentRevId(comment, patchListCache, + ctx.getChange(), psForComment); + comments.add(comment); + } + commentsUtil.putComments(ctx.getDb(), + ctx.getUpdate(ctx.getChange().currentPatchSetId()), Status.PUBLISHED, + comments); + + return true; + } + } + + private static boolean useHtmlParser(MailMessage m) { + return !Strings.isNullOrEmpty(m.htmlContent()); + } + + private static String numComments(int numComments) { + return "(" + numComments + (numComments > 1 ? " comments)" : " comment)"); + } + + private Set<String> existingMessageIds(ChangeData cd) throws OrmException { + Set<String> existingMessageIds = new HashSet<>(); + cd.messages().stream().forEach(m -> { + String messageId = CommentsUtil.extractMessageId(m.getTag()); + if (messageId != null) { + existingMessageIds.add(messageId); + } + }); + cd.publishedComments().stream().forEach(c -> { + String messageId = CommentsUtil.extractMessageId(c.tag); + if (messageId != null) { + existingMessageIds.add(messageId); + } + }); + return existingMessageIds; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java new file mode 100644 index 0000000..24f4f48 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -0,0 +1,148 @@ +// 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.receive; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.mail.EmailSettings; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; + +/** MailReceiver implements base functionality for receiving emails. */ +public abstract class MailReceiver implements LifecycleListener { + private static final Logger log = + LoggerFactory.getLogger(MailReceiver.class.getName()); + + protected EmailSettings mailSettings; + protected Set<String> pendingDeletion; + private MailProcessor mailProcessor; + private WorkQueue workQueue; + private Timer timer; + + public static class Module extends LifecycleModule { + private final EmailSettings mailSettings; + + @Inject + Module(EmailSettings mailSettings) { + this.mailSettings = mailSettings; + } + + @Override + protected void configure() { + if (mailSettings.protocol == Protocol.NONE) { + return; + } + listener().to(MailReceiver.class); + switch (mailSettings.protocol) { + case IMAP: + bind(MailReceiver.class).to(ImapMailReceiver.class); + break; + case POP3: + bind(MailReceiver.class).to(Pop3MailReceiver.class); + break; + case NONE: + default: + } + } + } + + MailReceiver(EmailSettings mailSettings, MailProcessor mailProcessor, + WorkQueue workQueue) { + this.mailSettings = mailSettings; + this.mailProcessor = mailProcessor; + this.workQueue = workQueue; + pendingDeletion = Collections.synchronizedSet(new HashSet<>()); + } + + @Override + public void start() { + if (timer == null) { + timer = new Timer(); + } else { + timer.cancel(); + } + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + MailReceiver.this.handleEmails(true); + } + }, 0L, mailSettings.fetchInterval); + } + + @Override + public void stop() { + if (timer != null) { + timer.cancel(); + } + } + + /** + * requestDeletion will enqueue an email for deletion and delete it the + * next time we connect to the email server. This does not guarantee deletion + * as the Gerrit instance might fail before we connect to the email server. + * @param messageId + */ + public void requestDeletion(String messageId) { + pendingDeletion.add(messageId); + } + + /** + * handleEmails will open a connection to the mail server, remove emails + * where deletion is pending, read new email and close the connection. + * @param async Determines if processing messages should happen asynchronous. + */ + @VisibleForTesting + public abstract void handleEmails(boolean async); + + protected void dispatchMailProcessor(List<MailMessage> messages, + boolean async) { + for (MailMessage m : messages) { + if (async) { + Callable<?> task = () -> { + try { + mailProcessor.process(m); + requestDeletion(m.id()); + } catch (OrmException e) { + log.error("Mail: Can't process message " + m.id() + + " . Won't delete.", e); + } + return null; + }; + workQueue.getDefaultQueue().submit(task); + } else { + // Synchronous processing is used only in tests. + try { + mailProcessor.process(m); + requestDeletion(m.id()); + } catch (OrmException e) { + log.error("Mail: Can't process messages. Won't delete.", e); + } + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java new file mode 100644 index 0000000..1a3b14d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MetadataParser.java
@@ -0,0 +1,109 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.receive; + +import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter; +import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import com.google.gerrit.server.mail.MailUtil; +import com.google.gerrit.server.mail.MetadataName; + +import java.sql.Timestamp; +import java.time.Instant; + +/** Parse metadata from inbound email */ +public class MetadataParser { + public static MailMetadata parse(MailMessage m) { + MailMetadata metadata = new MailMetadata(); + // Find author + metadata.author = m.from().getEmail(); + + // Check email headers for X-Gerrit-<Name> + for (String header : m.additionalHeaders()) { + if (header.startsWith(toHeaderWithDelimiter(MetadataName.CHANGE_ID))) { + metadata.changeId = header + .substring(toHeaderWithDelimiter(MetadataName.CHANGE_ID).length()); + } else if (header.startsWith( + toHeaderWithDelimiter(MetadataName.PATCH_SET))) { + String ps = header.substring( + toHeaderWithDelimiter(MetadataName.PATCH_SET).length()); + metadata.patchSet = Ints.tryParse(ps); + } else if (header.startsWith( + toHeaderWithDelimiter(MetadataName.TIMESTAMP))) { + String ts = header.substring( + toHeaderWithDelimiter(MetadataName.TIMESTAMP).length()); + metadata.timestamp = Timestamp.from( + MailUtil.rfcDateformatter.parse(ts, Instant::from)); + } else if (header.startsWith( + toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE))) { + metadata.messageType = header.substring( + toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE).length()); + } + } + if (metadata.hasRequiredFields()) { + return metadata; + } + + // If the required fields were not yet found, continue to parse the text + if (!Strings.isNullOrEmpty(m.textContent())) { + String[] lines = m.textContent().split("\n"); + extractFooters(lines, metadata); + if (metadata.hasRequiredFields()) { + return metadata; + } + } + + // If the required fields were not yet found, continue to parse the HTML + // HTML footer are contained inside a <p> tag + if (!Strings.isNullOrEmpty(m.htmlContent())) { + String[] lines = m.htmlContent().split("</p>"); + extractFooters(lines, metadata); + if (metadata.hasRequiredFields()) { + return metadata; + } + } + + return metadata; + } + + private static void extractFooters(String[] lines, MailMetadata metadata) { + for (String line : lines) { + if (metadata.changeId == null && line.contains(MetadataName.CHANGE_ID)) { + metadata.changeId = + extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_ID), line); + } else if (metadata.patchSet == null && + line.contains(MetadataName.PATCH_SET)) { + metadata.patchSet = Ints.tryParse( + extractFooter(toFooterWithDelimiter(MetadataName.PATCH_SET), line)); + } else if (metadata.timestamp == null && + line.contains(MetadataName.TIMESTAMP)) { + String ts = + extractFooter(toFooterWithDelimiter(MetadataName.TIMESTAMP), line); + metadata.timestamp = Timestamp.from( + MailUtil.rfcDateformatter.parse(ts, Instant::from)); + } else if (metadata.messageType == null && + line.contains(MetadataName.MESSAGE_TYPE)) { + metadata.messageType = extractFooter( + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE), line); + } + } + } + + private static String extractFooter(String key, String line) { + return line.substring(line.indexOf(key) + key.length(), line.length()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java new file mode 100644 index 0000000..dbdea22 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ParserUtil.java
@@ -0,0 +1,73 @@ +// 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.receive; + +import com.google.gerrit.reviewdb.client.Comment; + +import java.util.regex.Pattern; + +public class ParserUtil { + private static final Pattern SIMPLE_EMAIL_PATTERN = Pattern.compile( + "[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+" + + "(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})"); + + /** + * Trims the quotation line that email clients add + * Example: On Sun, Nov 20, 2016 at 10:33 PM, <gerrit@hiesel.it> wrote: + * @param comment Comment parsed from an email. + * @return Trimmed comment. + */ + public static String trimQuotationLine(String comment) { + // Identifying the quotation line is hard, as it can be in any language. + // We identify this line by it's characteristics: It usually contains a + // valid email address, some digits for the date in groups of 1-4 in a row + // as well as some characters. + StringBuilder b = new StringBuilder(); + for (String line : comment.split("\n")) { + // Count occurrences of digit groups + int numConsecutiveDigits = 0; + int maxConsecutiveDigits = 0; + int numDigitGroups = 0; + for (char c : line.toCharArray()) { + if (c >= '0' && c <= '9') { + numConsecutiveDigits++; + } else if (numConsecutiveDigits > 0) { + maxConsecutiveDigits = Integer.max(maxConsecutiveDigits, + numConsecutiveDigits); + numConsecutiveDigits = 0; + numDigitGroups++; + } + } + if (numDigitGroups < 4 || maxConsecutiveDigits > 4 || + !SIMPLE_EMAIL_PATTERN.matcher(line).find()) { + b.append(line); + } + } + return b.toString().trim(); + } + + /** Check if string is an inline comment url on a patch set or the base */ + public static boolean isCommentUrl(String str, String changeUrl, + Comment comment) { + return str.equals(filePath(changeUrl, comment) + "@" + comment.lineNbr) || + str.equals(filePath(changeUrl, comment) + "@a" + comment.lineNbr); + } + + /** Generate the fully qualified filepath */ + public static String filePath(String changeUrl, Comment comment) { + return changeUrl + "/" + comment.key.patchSetId + "/" + + comment.key.filename; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java new file mode 100644 index 0000000..d1498fd --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -0,0 +1,138 @@ +// 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.receive; + +import com.google.common.primitives.Ints; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.mail.EmailSettings; +import com.google.gerrit.server.mail.Encryption; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.apache.commons.net.pop3.POP3Client; +import org.apache.commons.net.pop3.POP3MessageInfo; +import org.apache.commons.net.pop3.POP3SClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class Pop3MailReceiver extends MailReceiver { + private static final Logger log = + LoggerFactory.getLogger(Pop3MailReceiver.class); + + @Inject + Pop3MailReceiver(EmailSettings mailSettings, + MailProcessor mailProcessor, + WorkQueue workQueue) { + super(mailSettings, mailProcessor, workQueue); + } + + /** + * handleEmails will open a connection to the mail server, remove emails + * where deletion is pending, read new email and close the connection. + * @param async Determines if processing messages should happen asynchronous. + */ + @Override + public synchronized void handleEmails(boolean async) { + POP3Client pop3; + if (mailSettings.encryption != Encryption.NONE) { + pop3 = new POP3SClient(mailSettings.encryption.name()); + } else { + pop3 = new POP3Client(); + } + if (mailSettings.port > 0) { + pop3.setDefaultPort(mailSettings.port); + } + try { + pop3.connect(mailSettings.host); + } catch (IOException e) { + log.error("Could not connect to POP3 email server", e); + return; + } + try { + try { + if (!pop3.login(mailSettings.username, mailSettings.password)) { + log.error("Could not login to POP3 email server." + + " Check username and password"); + return; + } + try { + POP3MessageInfo[] messages = pop3.listMessages(); + if (messages == null) { + log.error("Could not retrieve message list via POP3"); + return; + } + log.info("Received " + messages.length + " messages via POP3"); + // Fetch messages + List<MailMessage> mailMessages = new ArrayList<>(); + for (POP3MessageInfo msginfo : messages) { + if (msginfo == null) { + // Message was deleted + continue; + } + try (BufferedReader reader = + (BufferedReader) pop3.retrieveMessage(msginfo.number)) { + if (reader == null) { + log.error( + "Could not retrieve POP3 message header for message {}", + msginfo.identifier); + return; + } + int[] message = fetchMessage(reader); + MailMessage mailMessage = RawMailParser.parse(message); + // Delete messages where deletion is pending. This requires + // knowing the integer message ID of the email. We therefore parse + // the message first and extract the Message-ID specified in RFC + // 822 and delete the message if deletion is pending. + if (pendingDeletion.contains(mailMessage.id())) { + if (pop3.deleteMessage(msginfo.number)) { + pendingDeletion.remove(mailMessage.id()); + } else { + log.error("Could not delete message " + msginfo.number); + } + } else { + // Process message further + mailMessages.add(mailMessage); + } + } catch (MailParsingException e) { + log.error("Could not parse message " + msginfo.number); + } + } + dispatchMailProcessor(mailMessages, async); + } finally { + pop3.logout(); + } + } finally { + pop3.disconnect(); + } + } catch (IOException e) { + log.error("Error while issuing POP3 command", e); + } + } + + public final int[] fetchMessage(BufferedReader reader) throws IOException { + List<Integer> character = new ArrayList<>(); + int ch; + while ((ch = reader.read()) != -1) { + character.add(ch); + } + return Ints.toArray(character); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java similarity index 79% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java index ea0def0..e8311f1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.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. @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.receive; -public enum RecipientType { - TO, CC, BCC +public enum Protocol { + NONE, POP3, IMAP }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java new file mode 100644 index 0000000..7315e44 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
@@ -0,0 +1,172 @@ +// 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.receive; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharStreams; +import com.google.gerrit.server.mail.Address; + +import org.apache.james.mime4j.MimeException; +import org.apache.james.mime4j.dom.Entity; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.MessageBuilder; +import org.apache.james.mime4j.dom.Multipart; +import org.apache.james.mime4j.dom.TextBody; +import org.apache.james.mime4j.dom.address.Mailbox; +import org.apache.james.mime4j.message.DefaultMessageBuilder; +import org.joda.time.DateTime; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * RawMailParser parses raw email content received through POP3 or IMAP into + * an internal {@link MailMessage}. + */ +public class RawMailParser { + private static final ImmutableSet<String> MAIN_HEADERS = + ImmutableSet.of("to", "from", "cc", "date", "message-id", + "subject", "content-type"); + + /** + * Parses a MailMessage from a string. + * @param raw String as received over the wire + * @return Parsed MailMessage + * @throws MailParsingException + */ + public static MailMessage parse(String raw) throws MailParsingException { + MailMessage.Builder messageBuilder = MailMessage.builder(); + Message mimeMessage; + try { + MessageBuilder builder = new DefaultMessageBuilder(); + mimeMessage = + builder.parseMessage(new ByteArrayInputStream(raw.getBytes(UTF_8))); + } catch (IOException | MimeException e) { + throw new MailParsingException("Can't parse email", e); + } + // Add general headers + messageBuilder.id(mimeMessage.getMessageId()); + messageBuilder.subject(mimeMessage.getSubject()); + messageBuilder.dateReceived(new DateTime(mimeMessage.getDate())); + + // Add From, To and Cc + if (mimeMessage.getFrom() != null && mimeMessage.getFrom().size() > 0) { + Mailbox from = mimeMessage.getFrom().get(0); + messageBuilder.from(new Address(from.getName(), from.getAddress())); + } + if (mimeMessage.getTo() != null) { + for (Mailbox m : mimeMessage.getTo().flatten()) { + messageBuilder.addTo(new Address(m.getName(), m.getAddress())); + } + } + if (mimeMessage.getCc() != null) { + for (Mailbox m : mimeMessage.getCc().flatten()) { + messageBuilder.addCc(new Address(m.getName(), m.getAddress())); + } + } + + // Add additional headers + mimeMessage.getHeader().getFields().stream() + .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase())) + .forEach(f -> messageBuilder.addAdditionalHeader( + f.getName() + ": " + f.getBody())); + + // Add text and html body parts + StringBuilder textBuilder = new StringBuilder(); + StringBuilder htmlBuilder = new StringBuilder(); + try { + handleMimePart(mimeMessage, textBuilder, htmlBuilder); + } catch (IOException e) { + throw new MailParsingException("Can't parse email", e); + } + messageBuilder.textContent(Strings.emptyToNull(textBuilder.toString())); + messageBuilder.htmlContent(Strings.emptyToNull(htmlBuilder.toString())); + + try { + // build() will only succeed if all required attributes were set. We wrap + // the IllegalStateException in a MailParsingException indicating that + // required attributes are missing, so that the caller doesn't fall over. + return messageBuilder.build(); + } catch (IllegalStateException e) { + throw new MailParsingException( + "Missing required attributes after email was parsed", e); + } + } + + /** + * Parses a MailMessage from an array of characters. Note that the character + * array is int-typed. This method is only used by POP3, which specifies that + * all transferred characters are US-ASCII (RFC 6856). When reading the input + * in Java, io.Reader yields ints. These can be safely converted to chars + * as all US-ASCII characters fit in a char. If emails contain non-ASCII + * characters, such as UTF runes, these will be encoded in ASCII using either + * Base64 or quoted-printable encoding. + * @param chars Array as received over the wire + * @return Parsed MailMessage + * @throws MailParsingException + */ + public static MailMessage parse(int[] chars) throws MailParsingException { + StringBuilder b = new StringBuilder(chars.length); + for (int c : chars) { + b.append((char) c); + } + return parse(b.toString()); + } + + /** + * Traverses a mime tree and parses out text and html parts. All other parts + * will be dropped. + * @param part MimePart to parse + * @param textBuilder StringBuilder to append all plaintext parts + * @param htmlBuilder StringBuilder to append all html parts + * @throws IOException + */ + private static void handleMimePart(Entity part, StringBuilder textBuilder, + StringBuilder htmlBuilder) throws IOException { + if (isPlainOrHtml(part.getMimeType()) && + !isAttachment(part.getDispositionType())) { + TextBody tb = (TextBody) part.getBody(); + String result = CharStreams.toString(new InputStreamReader( + tb.getInputStream(), tb.getMimeCharset())); + if (part.getMimeType().equals("text/plain")) { + textBuilder.append(result); + } else if (part.getMimeType().equals("text/html")) { + htmlBuilder.append(result); + } + } else if (isMixedOrAlternative(part.getMimeType())) { + Multipart multipart = (Multipart) part.getBody(); + for (Entity e : multipart.getBodyParts()) { + handleMimePart(e, textBuilder, htmlBuilder); + } + } + } + + private static boolean isPlainOrHtml(String mimeType) { + return (mimeType.equals("text/plain") || mimeType.equals("text/html")); + } + + private static boolean isMixedOrAlternative(String mimeType) { + return mimeType.equals("multipart/alternative") || + mimeType.equals("multipart/mixed"); + } + + private static boolean isAttachment(String dispositionType) { + return dispositionType != null && dispositionType.equals("attachment"); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java new file mode 100644 index 0000000..8b28df5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/TextParser.java
@@ -0,0 +1,141 @@ +// 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.receive; + +import com.google.common.collect.Iterators; +import com.google.common.collect.PeekingIterator; +import com.google.gerrit.reviewdb.client.Comment; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** TextParser provides parsing functionality for plaintext email. */ +public class TextParser { + /** + * Parses comments from plaintext email. + * + * @param email MailMessage as received from the email service. + * @param comments Comments previously persisted on the change that caused the + * original notification email to be sent out. Ordering must + * be the same as in the outbound email + * @param changeUrl Canonical change url that points to the change on this + * Gerrit instance. + * Example: https://go-review.googlesource.com/#/c/91570 + * @return List of MailComments parsed from the plaintext part of the email. + */ + public static List<MailComment> parse( + MailMessage email, Collection<Comment> comments, String changeUrl) { + String body = email.textContent(); + // Replace CR-LF by \n + body = body.replace("\r\n", "\n"); + + List<MailComment> parsedComments = new ArrayList<>(); + + // Some email clients (like GMail) use >> for enquoting text when there are + // inline comments that the users typed. These will then be enquoted by a + // single >. We sanitize this by unifying it into >. Inline comments typed + // by the user will not be enquoted. + // + // Example: + // Some comment + // >> Quoted Text + // >> Quoted Text + // > A comment typed in the email directly + String singleQuotePattern = "\n> "; + String doubleQuotePattern = "\n>> "; + if (countOccurrences(body, doubleQuotePattern) > + countOccurrences(body, singleQuotePattern)) { + body = body.replace(doubleQuotePattern, singleQuotePattern); + } + + PeekingIterator<Comment> iter = + Iterators.peekingIterator(comments.iterator()); + + String[] lines = body.split("\n"); + MailComment currentComment = null; + String lastEncounteredFileName = null; + Comment lastEncounteredComment = null; + for (String line : lines) { + if (line.startsWith("> ")) { + line = line.substring("> ".length()).trim(); + // This is not a comment, try to advance the file/comment pointers and + // add previous comment to list if applicable + if (currentComment != null) { + parsedComments.add(currentComment); + currentComment = null; + } + + if (!iter.hasNext()) { + continue; + } + Comment perspectiveComment = iter.peek(); + if (line.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) { + if (lastEncounteredFileName == null || + !lastEncounteredFileName + .equals(perspectiveComment.key.filename)) { + // This is the annotation of a file + lastEncounteredFileName = perspectiveComment.key.filename; + lastEncounteredComment = null; + } else if (perspectiveComment.lineNbr == 0) { + // This was originally a file-level comment + lastEncounteredComment = perspectiveComment; + iter.next(); + } + } else if (ParserUtil.isCommentUrl(line, changeUrl, + perspectiveComment)) { + lastEncounteredComment = perspectiveComment; + iter.next(); + } + } else { + // This is a comment. Try to append to previous comment if applicable or + // create a new comment. + if (currentComment == null) { + // Start new comment + currentComment = new MailComment(); + currentComment.message = line; + if (lastEncounteredComment == null) { + if (lastEncounteredFileName == null) { + // Change message + currentComment.type = MailComment.CommentType.CHANGE_MESSAGE; + } else { + // File comment not sent in reply to another comment + currentComment.type = MailComment.CommentType.FILE_COMMENT; + currentComment.fileName = lastEncounteredFileName; + } + } else { + // Comment sent in reply to another comment + currentComment.inReplyTo = lastEncounteredComment; + currentComment.type = MailComment.CommentType.INLINE_COMMENT; + } + } else { + // Attach to previous comment + currentComment.message += "\n" + line; + } + } + } + // There is no need to attach the currentComment after this loop as all + // emails have footers and other enquoted text after the last comment + // appeared and the last comment will have already been added to the list + // at this point. + + return parsedComments; + } + + /** Counts the occurrences of pattern in s */ + private static int countOccurrences(String s, String pattern) { + return (s.length() - s.replace(pattern, "").length()) / pattern.length(); + } +}
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/send/AbandonedSender.java similarity index 84% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AbandonedSender.java index 1e8bdf4..254d3f1 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/send/AbandonedSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; @@ -35,7 +35,7 @@ @Assisted Project.NameKey project, @Assisted Change.Id id) throws OrmException { - super(ea, "abandon", newChangeData(ea, project, id)); + super(ea, "abandon", ChangeEmail.newChangeData(ea, project, id)); } @Override @@ -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/send/AddKeySender.java similarity index 81% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java index f825d1c..47068eb 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/send/AddKeySender.java
@@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.common.base.Joiner; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.mail.Address; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -80,7 +82,10 @@ @Override protected void format() throws EmailException { - appendText(velocifyFile("AddKey.vm")); + appendText(textTemplate("AddKey")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("AddKeyHtml")); + } } public String getEmail() { @@ -110,4 +115,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/send/AddReviewerSender.java similarity index 96% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddReviewerSender.java index c9e42ad..67577de 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/send/AddReviewerSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.reviewdb.client.Change;
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/send/ChangeEmail.java similarity index 81% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java index badc706..1cb6922 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/send/ChangeEmail.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. @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; - -import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; +package com.google.gerrit.server.mail.send; import com.google.common.collect.Multimap; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; @@ -31,7 +30,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.mail.ProjectWatch.Watchers; +import com.google.gerrit.server.mail.send.ProjectWatch.Watchers; +import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListEntry; import com.google.gerrit.server.patch.PatchListNotAvailableException; @@ -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,17 +121,9 @@ @Override protected void format() throws EmailException { formatChange(); - appendText(velocifyFile("ChangeFooter.vm")); - try { - TreeSet<String> names = new TreeSet<>(); - for (Account.Id who : changeData.reviewers().all()) { - names.add(getNameEmailFor(who)); - } - for (String name : names) { - appendText("Gerrit-Reviewer: " + name + "\n"); - } - } catch (OrmException e) { - log.warn("Cannot get change reviewers", e); + appendText(textTemplate("ChangeFooter")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("ChangeFooterHtml")); } formatFooter(); } @@ -163,12 +156,15 @@ } } - if (patchSet != null && patchSetInfo == null) { - try { - patchSetInfo = args.patchSetInfoFactory.get( - args.db.get(), changeData.notes(), patchSet.getId()); - } catch (PatchSetInfoNotAvailableException | OrmException err) { - patchSetInfo = null; + if (patchSet != null) { + setHeader("X-Gerrit-PatchSet", patchSet.getPatchSetId() + ""); + if (patchSetInfo == null) { + try { + patchSetInfo = args.patchSetInfoFactory.get( + args.db.get(), changeData.notes(), patchSet.getId()); + } catch (PatchSetInfoNotAvailableException | OrmException err) { + patchSetInfo = null; + } } } authors = getAuthors(); @@ -199,7 +195,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 +251,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()) @@ -372,7 +368,8 @@ } try { - for (Account.Id id : changeData.reviewers().byState(REVIEWER)) { + for (Account.Id id : changeData.reviewers() + .byState(ReviewerStateInternal.REVIEWER)) { add(RecipientType.CC, id); } } catch (OrmException err) { @@ -435,11 +432,74 @@ 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())); + changeData.put("changeNumber", Integer.toString(change.getChangeId())); + 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 + + footers.add("Gerrit-MessageType: " + messageClass); + footers.add("Gerrit-Change-Id: " + change.getKey().get()); + footers.add("Gerrit-Change-Number: " + + Integer.toString(change.getChangeId())); + footers.add("Gerrit-PatchSet: " + patchSet.getPatchSetId()); + footers.add("Gerrit-Owner: " + getNameEmailFor(change.getOwner())); + for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) { + footers.add("Gerrit-Reviewer: " + reviewer); + } + for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) { + footers.add("Gerrit-CC: " + reviewer); + } + } + + private Set<String> getEmailsByState(ReviewerStateInternal state) { + Set<String> reviewers = new TreeSet<>(); + try { + for (Account.Id who : changeData.reviewers().byState(state)) { + reviewers.add(getNameEmailFor(who)); + } + } catch (OrmException e) { + log.warn("Cannot get change reviewers", e); + } + return reviewers; + } + 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/send/CommentFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java new file mode 100644 index 0000000..722fe1f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -0,0 +1,190 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +import static com.google.common.base.Strings.isNullOrEmpty; + +import com.google.gerrit.common.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CommentFormatter { + public enum BlockType { + LIST, + PARAGRAPH, + PRE_FORMATTED, + QUOTE + } + + public static class Block { + public BlockType type; + public String text; + public List<String> items; // For the items of list blocks. + public List<Block> quotedBlocks; // For the contents of quote blocks. + } + + /** + * Take a string of comment text that was written using the wiki-Like format + * and emit a list of blocks that can be rendered to block-level HTML. This + * method does not escape HTML. + * + * Adapted from the {@code wikify} method found in: + * com.google.gwtexpui.safehtml.client.SafeHtml + * + * @param source The raw, unescaped comment in the Gerrit wiki-like format. + * @return List of block objects, each with unescaped comment content. + */ + public static List<Block> parse(@Nullable String source) { + if (isNullOrEmpty(source)) { + return Collections.emptyList(); + } + + List<Block> result = new ArrayList<>(); + for (String p : source.split("\n\n")) { + if (isQuote(p)) { + result.add(makeQuote(p)); + } else if (isPreFormat(p)) { + result.add(makePre(p)); + } else if (isList(p)) { + makeList(p, result); + } else if (!p.isEmpty()) { + result.add(makeParagraph(p)); + } + } + return result; + } + + /** + * Take a block of comment text that contains a list and potentially + * paragraphs (but does not contain blank lines), generate appropriate block + * elements and append them to the output list. + * + * In simple cases, this will generate a single list block. For example, on + * the following input. + * + * * Item one. + * * Item two. + * * item three. + * + * However, if the list is adjacent to a paragraph, it will need to also + * generate that paragraph. Consider the following input. + * + * A bit of text describing the context of the list: + * * List item one. + * * List item two. + * * Et cetera. + * + * In this case, {@code makeList} generates a paragraph block object + * containing the non-bullet-prefixed text, followed by a list block. + * + * Adapted from the {@code wikifyList} method found in: + * com.google.gwtexpui.safehtml.client.SafeHtml + * + * @param p The block containing the list (as well as potential paragraphs). + * @param out The list of blocks to append to. + */ + private static void makeList(String p, List<Block> out) { + Block block = null; + StringBuilder textBuilder = null; + boolean inList = false; + boolean inParagraph = false; + + for (String line : p.split("\n")) { + if (line.startsWith("-") || line.startsWith("*")) { + // The next line looks like a list item. If not building a list already, + // then create one. Remove the list item marker (* or -) from the line. + if (!inList) { + if (inParagraph) { + // Add the finished paragraph block to the result. + inParagraph = false; + block.text = textBuilder.toString(); + out.add(block); + } + + inList = true; + block = new Block(); + block.type = BlockType.LIST; + block.items = new ArrayList<>(); + } + line = line.substring(1).trim(); + + } else if (!inList) { + // Otherwise, if a list has not yet been started, but the next line does + // not look like a list item, then add the line to a paragraph block. If + // a paragraph block has not yet been started, then create one. + if (!inParagraph) { + inParagraph = true; + block = new Block(); + block.type = BlockType.PARAGRAPH; + textBuilder = new StringBuilder(); + } else { + textBuilder.append(" "); + } + textBuilder.append(line); + continue; + } + + block.items.add(line); + } + + if (block != null) { + out.add(block); + } + } + + private static Block makeQuote(String p) { + String quote = p.replaceAll("\n\\s?>\\s?", "\n"); + if (quote.startsWith("> ")) { + quote = quote.substring(2); + } else if (quote.startsWith(" > ")) { + quote = quote.substring(3); + } + + Block block = new Block(); + block.type = BlockType.QUOTE; + block.quotedBlocks = CommentFormatter.parse(quote); + return block; + } + + private static Block makePre(String p) { + Block block = new Block(); + block.type = BlockType.PRE_FORMATTED; + block.text = p; + return block; + } + + private static Block makeParagraph(String p) { + Block block = new Block(); + block.type = BlockType.PARAGRAPH; + block.text = p; + return block; + } + + private static boolean isQuote(String p) { + return p.startsWith("> ") || p.startsWith(" > "); + } + + private static boolean isPreFormat(String p) { + return p.startsWith(" ") || p.startsWith("\t") + || p.contains("\n ") || p.contains("\n\t"); + } + + private static boolean isList(String p) { + return p.startsWith("- ") || p.startsWith("* ") + || p.contains("\n- ") || p.contains("\n* "); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java new file mode 100644 index 0000000..a4d0e24 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -0,0 +1,665 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +import com.google.common.base.Strings; +import com.google.common.collect.Ordering; +import com.google.gerrit.common.data.FilenameComparator; +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.Comment; +import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RobotComment; +import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.mail.MailUtil; +import com.google.gerrit.server.patch.PatchFile; +import com.google.gerrit.server.patch.PatchList; +import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gerrit.server.util.LabelVote; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** 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); + + public interface Factory { + CommentSender create(Project.NameKey project, Change.Id id); + } + + private class FileCommentGroup { + public String filename; + public int patchSetId; + public PatchFile fileData; + public List<Comment> comments = new ArrayList<>(); + + /** + * @return a web link to the given patch set and file. + */ + public String getLink() { + String url = getGerritUrl(); + if (url == null) { + return null; + } + + return new StringBuilder() + .append(url) + .append("#/c/").append(change.getId()) + .append('/').append(patchSetId) + .append('/').append(KeyUtil.encode(filename)) + .toString(); + } + + /** + * @return A title for the group, i.e. "Commit Message", "Merge List", or + * "File [[filename]]". + */ + public String getTitle() { + if (Patch.COMMIT_MSG.equals(filename)) { + return "Commit Message"; + } else if (Patch.MERGE_LIST.equals(filename)) { + return "Merge List"; + } else { + return "File " + filename; + } + } + } + + private List<Comment> inlineComments = Collections.emptyList(); + private String patchSetComment = null; + private List<LabelVote> labels = Collections.emptyList(); + private final CommentsUtil commentsUtil; + + @Inject + public CommentSender(EmailArguments ea, + CommentsUtil commentsUtil, + @Assisted Project.NameKey project, + @Assisted Change.Id id) throws OrmException { + super(ea, "comment", newChangeData(ea, project, id)); + this.commentsUtil = commentsUtil; + } + + public void setComments(List<Comment> comments) throws OrmException { + inlineComments = comments; + + Set<String> paths = new HashSet<>(); + for (Comment c : comments) { + if (!Patch.isMagic(c.key.filename)) { + paths.add(c.key.filename); + } + } + changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths)); + } + + public void setPatchSetComment(String comment) { + this.patchSetComment = comment; + } + + public void setLabels(List<LabelVote> labels) { + this.labels = labels; + } + + @Override + protected void init() throws EmailException { + super.init(); + + if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) { + ccAllApprovals(); + } + if (notify.compareTo(NotifyHandling.ALL) >= 0) { + bccStarredBy(); + includeWatchers(NotifyType.ALL_COMMENTS); + } + + // Add header that enables identifying comments on parsed email. + // Grouping is currently done by timestamp. + setHeader("X-Gerrit-Comment-Date", timestamp); + } + + @Override + public void formatChange() throws EmailException { + appendText(textTemplate("Comment")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("CommentHtml")); + } + } + + @Override + public void formatFooter() throws EmailException { + appendText(textTemplate("CommentFooter")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("CommentFooterHtml")); + } + } + + /** + * No longer used outside Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + public boolean hasInlineComments() { + return !inlineComments.isEmpty(); + } + + /** + * No longer used outside Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + public String getInlineComments() { + return getInlineComments(1); + } + + /** + * No longer used outside Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + public String getInlineComments(int lines) { + try (Repository repo = getRepository()) { + StringBuilder cmts = new StringBuilder(); + for (FileCommentGroup group : getGroupedInlineComments(repo)) { + String link = group.getLink(); + if (link != null) { + cmts.append(link).append('\n'); + } + cmts.append(group.getTitle()).append(":\n\n"); + for (Comment c : group.comments) { + appendComment(cmts, lines, group.fileData, c); + } + cmts.append("\n\n"); + } + return cmts.toString(); + } + } + + /** + * @return a list of FileCommentGroup objects representing the inline comments + * grouped by the file. + */ + private List<CommentSender.FileCommentGroup> getGroupedInlineComments( + Repository repo) { + List<CommentSender.FileCommentGroup> groups = new ArrayList<>(); + // Get the patch list: + PatchList patchList = null; + if (repo != null) { + try { + patchList = getPatchList(); + } catch (PatchListNotAvailableException e) { + log.error("Failed to get patch list", e); + } + } + + // Loop over the comments and collect them into groups based on the file + // location of the comment. + FileCommentGroup currentGroup = null; + for (Comment c : inlineComments) { + // If it's a new group: + if (currentGroup == null + || !c.key.filename.equals(currentGroup.filename) + || c.key.patchSetId != currentGroup.patchSetId) { + currentGroup = new FileCommentGroup(); + currentGroup.filename = c.key.filename; + currentGroup.patchSetId = c.key.patchSetId; + groups.add(currentGroup); + if (patchList != null) { + try { + currentGroup.fileData = + new PatchFile(repo, patchList, c.key.filename); + } catch (IOException e) { + log.warn(String.format( + "Cannot load %s from %s in %s", + c.key.filename, + patchList.getNewId().name(), + projectState.getProject().getName()), e); + currentGroup.fileData = null; + } + } + } + + if (currentGroup.fileData != null) { + currentGroup.comments.add(c); + } + } + + Collections.sort(groups, + Comparator.comparing(g -> g.filename, FilenameComparator.INSTANCE)); + return groups; + } + + /** + * No longer used except for Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + private void appendComment(StringBuilder out, int contextLines, + 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"); + } + if (comment.range != null) { + appendRangedComment(out, currentFileData, comment); + } else { + appendLineComment(out, contextLines, currentFileData, comment); + } + } + + /** + * No longer used except for Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + private void appendRangedComment(StringBuilder out, PatchFile fileData, + Comment comment) { + String prefix = getCommentLinePrefix(comment); + String emptyPrefix = Strings.padStart(": ", prefix.length(), ' '); + boolean firstLine = true; + for (String line : getLinesByRange(comment.range, fileData, comment.side)) { + out.append(firstLine ? prefix : emptyPrefix) + .append(line) + .append('\n'); + firstLine = false; + } + appendQuotedParent(out, comment); + out.append(comment.message.trim()).append('\n'); + } + + private String getCommentLinePrefix(Comment comment) { + int lineNbr = comment.range == null ? + comment.lineNbr : comment.range.startLine; + StringBuilder sb = new StringBuilder(); + sb.append("PS").append(comment.key.patchSetId); + if (lineNbr != 0) { + sb.append(", Line ").append(lineNbr); + } + sb.append(": "); + return sb.toString(); + } + + /** + * @return the lines of file content in fileData that are encompassed by range + * on the given side. + */ + private List<String> getLinesByRange(Comment.Range range, + PatchFile fileData, short side) { + List<String> lines = new ArrayList<>(); + + for (int n = range.startLine; n <= range.endLine; n++) { + String s = getLine(fileData, 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())); + } + lines.add(s); + } + return lines; + } + + /** + * No longer used except for Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + private void appendLineComment(StringBuilder out, int contextLines, + PatchFile currentFileData, Comment comment) { + short side = comment.side; + int lineNbr = comment.lineNbr; + + // Initialize maxLines to the known line number. + int maxLines = lineNbr; + + try { + maxLines = currentFileData.getLineCount(side); + } 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); + } + + int startLine = Math.max(1, lineNbr - contextLines + 1); + int stopLine = Math.min(maxLines, lineNbr + contextLines); + + for (int line = startLine; line <= lineNbr; ++line) { + appendFileLine(out, currentFileData, side, line); + } + appendQuotedParent(out, comment); + out.append(comment.message.trim()).append('\n'); + + for (int line = lineNbr + 1; line < stopLine; ++line) { + appendFileLine(out, currentFileData, side, line); + } + } + + /** + * No longer used except for Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + 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"); + } + + /** + * No longer used except for Velocity. Remove this method when VTL support is + * removed. + */ + @Deprecated + private void appendQuotedParent(StringBuilder out, Comment child) { + Optional<Comment> parent = getParent(child); + if (parent.isPresent()) { + out.append("> ") + .append(getShortenedCommentMessage(parent.get())) + .append('\n'); + } + } + + /** + * Get the parent comment of a given comment. + * @param child the comment with a potential parent comment. + * @return an optional comment that will be present if the given comment has + * a parent, and is empty if it does not. + */ + private Optional<Comment> getParent(Comment child) { + if (child.parentUuid == null) { + return Optional.empty(); + } + + Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename, + child.key.patchSetId); + try { + return commentsUtil.get(args.db.get(), changeData.notes(), key); + } catch (OrmException e) { + log.warn("Could not find the parent of this comment: " + + child.toString()); + return Optional.empty(); + } + } + + /** + * Retrieve the file lines refered to by a comment. + * @param comment The comment that refers to some file contents. The comment + * may be a line comment or a ranged comment. + * @param fileData The file on which the comment appears. + * @return file contents referred to by the comment. If the comment is a line + * comment, the result will be a list of one string. Otherwise it will be + * a list of one or more strings. + */ + private List<String> getLinesOfComment(Comment comment, PatchFile fileData) { + List<String> lines = new ArrayList<>(); + if (comment.lineNbr == 0) { + // file level comment has no line + return lines; + } + if (comment.range == null) { + lines.add(getLine(fileData, comment.side, comment.lineNbr)); + } else { + lines.addAll(getLinesByRange(comment.range, fileData, comment.side)); + } + return lines; + } + + /** + * @return a shortened version of the given comment's message. Will be + * shortened to 75 characters or the first line, whichever is shorter. + */ + private String getShortenedCommentMessage(Comment comment) { + String msg = comment.message.trim(); + if (msg.length() > 75) { + msg = msg.substring(0, 75); + } + int lf = msg.indexOf('\n'); + if (lf > 0) { + msg = msg.substring(0, lf); + } + return msg; + } + + /** + * @return grouped inline comment data mapped to data structures that are + * suitable for passing into Soy. + */ + private List<Map<String, Object>> getCommentGroupsTemplateData( + Repository repo) { + List<Map<String, Object>> commentGroups = new ArrayList<>(); + + for ( + CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) { + Map<String, Object> groupData = new HashMap<>(); + groupData.put("link", group.getLink()); + groupData.put("title", group.getTitle()); + groupData.put("patchSetId", group.patchSetId); + + List<Map<String, Object>> commentsList = new ArrayList<>(); + for (Comment comment : group.comments) { + Map<String, Object> commentData = new HashMap<>(); + commentData.put("lines", getLinesOfComment(comment, group.fileData)); + commentData.put("message", comment.message.trim()); + List<CommentFormatter.Block> blocks = + CommentFormatter.parse(comment.message); + commentData.put("messageBlocks", commentBlocksToSoyData(blocks)); + + // Set the prefix. + String prefix = getCommentLinePrefix(comment); + commentData.put("linePrefix", prefix); + commentData.put("linePrefixEmpty", + Strings.padStart(": ", prefix.length(), ' ')); + + // Set line numbers. + int startLine; + if (comment.range == null) { + startLine = comment.lineNbr; + } else { + startLine = comment.range.startLine; + commentData.put("endLine", comment.range.endLine); + } + commentData.put("startLine", startLine); + + // Set the comment link. + if (comment.lineNbr == 0) { + commentData.put("link", group.getLink()); + } else if (comment.side == 0) { + commentData.put("link", group.getLink() + "@a" + startLine); + } else { + commentData.put("link", group.getLink() + '@' + startLine); + } + + // Set robot comment data. + if (comment instanceof RobotComment) { + RobotComment robotComment = (RobotComment) comment; + commentData.put("isRobotComment", true); + commentData.put("robotId", robotComment.robotId); + commentData.put("robotRunId", robotComment.robotRunId); + commentData.put("robotUrl", robotComment.url); + } else { + commentData.put("isRobotComment", false); + } + + // If the comment has a quote, don't bother loading the parent message. + if (!hasQuote(blocks)) { + // Set parent comment info. + Optional<Comment> parent = getParent(comment); + if (parent.isPresent()) { + commentData.put("parentMessage", + getShortenedCommentMessage(parent.get())); + } + } + + commentsList.add(commentData); + } + groupData.put("comments", commentsList); + + commentGroups.add(groupData); + } + return commentGroups; + } + + private List<Map<String, Object>> commentBlocksToSoyData( + List<CommentFormatter.Block> blocks) { + return blocks.stream() + .map(b -> { + Map<String, Object> map = new HashMap<>(); + switch (b.type) { + case PARAGRAPH: + map.put("type", "paragraph"); + map.put("text", b.text); + break; + case PRE_FORMATTED: + map.put("type", "pre"); + map.put("text", b.text); + break; + case QUOTE: + map.put("type", "quote"); + map.put("quotedBlocks", commentBlocksToSoyData(b.quotedBlocks)); + break; + case LIST: + map.put("type", "list"); + map.put("items", b.items); + break; + } + return map; + }) + .collect(Collectors.toList()); + } + + private boolean hasQuote(List<CommentFormatter.Block> blocks) { + for (CommentFormatter.Block block : blocks) { + if (block.type == CommentFormatter.BlockType.QUOTE) { + return true; + } + } + return false; + } + + private Repository getRepository() { + try { + return args.server.openRepository(projectState.getProject().getNameKey()); + } catch (IOException e) { + return null; + } + } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + boolean hasComments = false; + try (Repository repo = getRepository()) { + List<Map<String, Object>> files = getCommentGroupsTemplateData(repo); + soyContext.put("commentFiles", files); + hasComments = !files.isEmpty(); + } + + soyContext.put("patchSetCommentBlocks", + commentBlocksToSoyData(CommentFormatter.parse(patchSetComment))); + soyContext.put("labels", getLabelVoteSoyData(labels)); + soyContext.put("commentCount", inlineComments.size()); + soyContext.put("commentTimestamp", getCommentTimestamp()); + soyContext.put("coverLetterBlocks", + commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter()))); + + footers.add("Gerrit-Comment-Date: " + getCommentTimestamp()); + footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No")); + } + + 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 ""; + } + } + + private List<Map<String, Object>> getLabelVoteSoyData(List<LabelVote> votes) { + List<Map<String, Object>> result = new ArrayList<>(); + for (LabelVote vote : votes) { + Map<String, Object> data = new HashMap<>(); + data.put("label", vote.label()); + + // Soy needs the short to be cast as an int for it to get converted to the + // correct tamplate type. + data.put("value", (int) vote.value()); + result.add(data); + } + return result; + } + + private String getCommentTimestamp() { + // Grouping is currently done by timestamp. + return MailUtil.rfcDateformatter.format( + ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC"))); + } + + @Override + protected boolean supportsHtml() { + return true; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java similarity index 94% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java index 2110e37..d01e89d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -12,15 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.common.collect.Iterables; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.mail.ProjectWatch.Watchers; +import com.google.gerrit.server.mail.send.ProjectWatch.Watchers; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted;
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/send/DeleteReviewerSender.java similarity index 83% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java index 75f9f82..c456828 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/send/DeleteReviewerSender.java
@@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Change; @@ -65,7 +66,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 +82,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/send/DeleteVoteSender.java similarity index 87% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java index d861109..f8d1745 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/send/DeleteVoteSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; @@ -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/send/EmailArguments.java similarity index 93% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java index 68e5e50..818023f 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/send/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. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.registration.DynamicSet; @@ -30,8 +30,10 @@ 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.mail.EmailSettings; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchSetInfoFactory; @@ -43,6 +45,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 +72,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 +105,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 +135,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/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java similarity index 95% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java index 6a964a3..43d365c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java
@@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.MoreObjects; +import com.google.gerrit.server.mail.Address; import java.io.IOException; import java.io.Writer; @@ -75,7 +76,7 @@ } } - static boolean needsQuotedPrintable(java.lang.String value) { + public static boolean needsQuotedPrintable(java.lang.String value) { for (int i = 0; i < value.length(); i++) { if (value.charAt(i) < ' ' || '~' < value.charAt(i)) { return true; @@ -104,7 +105,7 @@ } } - static java.lang.String quotedPrintable(java.lang.String value) { + public static java.lang.String quotedPrintable(java.lang.String value) { final StringBuilder r = new StringBuilder(); r.append("=?UTF-8?Q?"); @@ -191,7 +192,7 @@ void remove(java.lang.String email) { for (Iterator<Address> i = list.iterator(); i.hasNext();) { - if (i.next().email.equals(email)) { + if (i.next().getEmail().equals(email)) { i.remove(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailSender.java new file mode 100644 index 0000000..0bfb6f2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailSender.java
@@ -0,0 +1,77 @@ +// 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.server.mail.send; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.server.mail.Address; + +import java.util.Collection; +import java.util.Map; + +/** Sends email messages to third parties. */ +public interface EmailSender { + boolean isEnabled(); + + /** + * Can the address receive messages from us? + * + * @param address the address to consider. + * @return true if this sender will deliver to the address. + */ + boolean canEmail(String address); + + /** + * 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. + * This list overrides any To or CC headers in {@code headers}. + * @param headers message headers. + * @param body text to appear in the body of the message. + * @throws EmailException the message cannot be sent. + */ + void send(Address from, Collection<Address> rcpt, + Map<String, EmailHeader> headers, String body) throws EmailException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java similarity index 90% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java index 9bcabc3..2489063 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
@@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.mail.Address; /** Constructs an address to send email from. */ public interface FromAddressGenerator {
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/send/FromAddressGeneratorProvider.java similarity index 64% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java index 51f7ad1..3326b38 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/send/FromAddressGeneratorProvider.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import static java.nio.charset.StandardCharsets.UTF_8; @@ -22,6 +22,8 @@ import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.MailUtil; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -32,6 +34,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */ @Singleton @@ -52,23 +55,26 @@ ParameterizedString name = new ParameterizedString("${user} (Code Review)"); generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, - srvAddr.email); - + srvAddr.getEmail()); } 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; + final ParameterizedString name = + a.getName() != null ? new ParameterizedString(a.getName()) : null; if (name == null || name.getParameterNames().isEmpty()) { generator = new ServerGen(a); } else { generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, - a.email); + a.getEmail()); } } } @@ -84,11 +90,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 +124,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.getName(); } - return srvAddr; + + String senderEmail; + ParameterizedString senderEmailPattern = + new ParameterizedString(serverAddress.getEmail()); + 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; } } @@ -162,7 +218,7 @@ senderName = namePattern.replace("user", fullName).toString(); } else { - senderName = serverAddress.name; + senderName = serverAddress.getName(); } String senderEmail;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java new file mode 100644 index 0000000..72bafde --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -0,0 +1,109 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +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", + "CommentHtml.soy", + "CommentFooter.soy", + "CommentFooterHtml.soy", + "DeleteReviewer.soy", + "DeleteReviewerHtml.soy", + "DeleteVote.soy", + "DeleteVoteHtml.soy", + "Footer.soy", + "FooterHtml.soy", + "HeaderHtml.soy", + "Merged.soy", + "MergedHtml.soy", + "NewChange.soy", + "NewChangeHtml.soy", + "Private.soy", + "RegisterNewEmail.soy", + "ReplacePatchSet.soy", + "ReplacePatchSetHtml.soy", + "Restored.soy", + "RestoredHtml.soy", + "Reverted.soy", + "RevertedHtml.soy", + "SetAssignee.soy", + "SetAssigneeHtml.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/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailTemplates.java similarity index 63% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailTemplates.java index ea0def0..b92567f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailTemplates.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. @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; -public enum RecipientType { - TO, CC, BCC -} +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/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java similarity index 91% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java index f6c3d0f..a5c8940 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/send/MergedSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; @@ -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/send/NewChangeSender.java similarity index 83% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java index 62385d9..c858b8e 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/send/NewChangeSender.java
@@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; @@ -67,7 +68,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 +84,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/send/NotificationEmail.java similarity index 78% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java index de338ec..c08f24d 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/send/NotificationEmail.java
@@ -12,19 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.common.collect.Iterables; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Branch; -import com.google.gerrit.server.mail.ProjectWatch.Watchers; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.ProjectWatch.Watchers; import com.google.gwtorm.server.OrmException; 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 +108,23 @@ 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); + + footers.add("Gerrit-Project: " + branch.getParentKey().get()); + footers.add("Gerrit-Branch: " + branch.getShortName()); + } }
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/send/OutgoingEmail.java similarity index 77% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java index 6200688..9f25897 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/send/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. @@ -12,22 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS; import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.Multimap; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.UserIdentity; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.mail.EmailHeader.AddressList; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.EmailHeader.AddressList; 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,11 +49,16 @@ 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.ArrayList; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -63,9 +74,14 @@ 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; + private Multimap<RecipientType, Account.Id> accountsToNotify = + ImmutableListMultimap.of(); protected VelocityContext velocityContext; - + protected Map<String, Object> soyContext; + protected Map<String, Object> soyContextEmailData; + protected List<String> footers; protected final EmailArguments args; protected Account.Id fromId; protected NotifyHandling notify = NotifyHandling.ALL; @@ -81,7 +97,12 @@ } public void setNotify(NotifyHandling notify) { - this.notify = notify; + this.notify = checkNotNull(notify); + } + + public void setAccountsToNotify( + Multimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); } /** @@ -90,7 +111,7 @@ * @throws EmailException */ public void send() throws EmailException { - if (NotifyHandling.NONE.equals(notify)) { + if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) { return; } @@ -101,8 +122,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(); @@ -115,7 +142,8 @@ // on their behalf to others. // add(RecipientType.CC, fromId); - } else if (rcptTo.remove(fromId)) { + } else if (!accountsToNotify.containsValue(fromId) + && rcptTo.remove(fromId)) { // If they don't want a copy, but we queued one up anyway, // drop them from the recipient lists. // @@ -136,12 +164,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 +186,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 +201,7 @@ */ protected void init() throws EmailException { setupVelocityContext(); + setupSoyContext(); smtpFromAddress = args.fromAddressGenerator.from(fromId); setHeader("Date", new Date()); @@ -172,6 +210,10 @@ headers.put(HDR_CC, new EmailHeader.AddressList()); setHeader("Message-ID", ""); + for (RecipientType recipientType : accountsToNotify.keySet()) { + add(recipientType, accountsToNotify.get(recipientType)); + } + if (fromId != null) { // If we have a user that this message is supposedly caused by // but the From header on the email does not match the user as @@ -179,13 +221,14 @@ // Reply-To header with the current user's email address. // final Address a = toAddress(fromId); - if (a != null && !smtpFromAddress.email.equals(a.email)) { - setHeader("Reply-To", a.email); + if (a != null && !smtpFromAddress.getEmail().equals(a.getEmail())) { + setHeader("Reply-To", a.getEmail()); } } 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 +303,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 +384,7 @@ } protected boolean shouldSendMessage() { - if (body.length() == 0) { + if (textBody.length() == 0) { // If we have no message body, don't send. return false; } @@ -346,7 +396,9 @@ return false; } - if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) { + if ((accountsToNotify == null || accountsToNotify.isEmpty()) + && smtpRcptTo.size() == 1 && rcptTo.size() == 1 + && rcptTo.contains(fromId)) { // If the only recipient is also the sender, don't bother. // return false; @@ -391,11 +443,11 @@ /** Schedule delivery of this message to the given account. */ protected void add(final RecipientType rt, final Address addr) { - if (addr != null && addr.email != null && addr.email.length() > 0) { - if (!OutgoingEmailValidator.isValid(addr.email)) { - log.warn("Not emailing " + addr.email + " (invalid email address)"); - } else if (!args.emailSender.canEmail(addr.email)) { - log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)"); + if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) { + if (!OutgoingEmailValidator.isValid(addr.getEmail())) { + log.warn("Not emailing " + addr.getEmail() + " (invalid email address)"); + } else if (!args.emailSender.canEmail(addr.getEmail())) { + log.warn("Not emailing " + addr.getEmail() + " (prohibited by allowrcpt)"); } else if (smtpRcptTo.add(addr)) { switch (rt) { case TO: @@ -428,6 +480,20 @@ velocityContext.put("StringUtils", StringUtils.class); } + protected void setupSoyContext() { + soyContext = new HashMap<>(); + footers = new ArrayList<>(); + + soyContext.put("messageClass", messageClass); + soyContext.put("footers", footers); + + 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 +529,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); } @@ -488,7 +585,7 @@ protected void removeUser(Account user) { String fromEmail = user.getPreferredEmail(); for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext();) { - if (j.next().email.equals(fromEmail)) { + if (j.next().getEmail().equals(fromEmail)) { j.remove(); } } @@ -504,4 +601,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/OutgoingEmailValidator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java similarity index 95% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java index 5ab5f4e..1e92c83 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java similarity index 98% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java index f19b2a8..3a43691 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.common.base.Strings; import com.google.gerrit.common.data.GroupDescription; @@ -30,6 +30,7 @@ import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import com.google.gerrit.server.git.NotifyConfig; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException;
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/send/RegisterNewEmailSender.java similarity index 80% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java index cfdeb8f..b665690 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/send/RegisterNewEmailSender.java
@@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import static com.google.common.base.Preconditions.checkNotNull; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.EmailTokenVerifier; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -51,7 +54,7 @@ @Override protected void format() throws EmailException { - appendText(velocifyFile("RegisterNewEmail.vm")); + appendText(textTemplate("RegisterNewEmail")); } public String getUserNameEmail() { @@ -69,4 +72,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/send/ReplacePatchSetSender.java similarity index 82% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java index df9f20e..39affb7 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/send/ReplacePatchSetSender.java
@@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Change; @@ -72,17 +73,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/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java similarity index 93% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java index dd922d3..a6e2fa7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
@@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.query.change.ChangeData;
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/send/RestoredSender.java similarity index 84% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RestoredSender.java index d946eb2..1b1823e 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/send/RestoredSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; @@ -35,7 +35,7 @@ @Assisted Project.NameKey project, @Assisted Change.Id id) throws OrmException { - super(ea, "restore", newChangeData(ea, project, id)); + super(ea, "restore", ChangeEmail.newChangeData(ea, project, id)); } @Override @@ -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/send/RevertedSender.java similarity index 83% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RevertedSender.java index 2c9c37e..b297ee1 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/send/RevertedSender.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; @@ -33,7 +33,7 @@ @Assisted Project.NameKey project, @Assisted Change.Id id) throws OrmException { - super(ea, "revert", newChangeData(ea, project, id)); + super(ea, "revert", ChangeEmail.newChangeData(ea, project, id)); } @Override @@ -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/send/SetAssigneeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java new file mode 100644 index 0000000..11d9a6e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SetAssigneeSender.java
@@ -0,0 +1,73 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +public class SetAssigneeSender extends ChangeEmail { + public interface Factory { + SetAssigneeSender create(Project.NameKey project, Change.Id id, + Account.Id assignee); + } + + private final Account.Id assignee; + + @Inject + public SetAssigneeSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id, + @Assisted Account.Id assignee) + throws OrmException { + super(ea, "setassignee", newChangeData(ea, project, id)); + this.assignee = assignee; + } + + @Override + protected void init() throws EmailException { + super.init(); + + add(RecipientType.TO, assignee); + } + + @Override + protected void formatChange() throws EmailException { + appendText(textTemplate("SetAssignee")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("SetAssigneeHtml")); + } + } + + public String getAssigneeName() { + return getNameFor(assignee); + } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("assigneeName", getAssigneeName()); + } + + @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/send/SmtpEmailSender.java similarity index 77% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java index e263c6a..d5622bf 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/send/SmtpEmailSender.java
@@ -12,16 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; 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; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.Encryption; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -42,6 +46,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. */ @@ -57,10 +62,6 @@ } } - public enum Encryption { - NONE, SSL, TLS - } - private final boolean enabled; private final int connectTimeout; @@ -146,8 +147,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 +163,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,13 +176,25 @@ 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(); try { - if (!client.setSender(from.email)) { + if (!client.setSender(from.getEmail())) { throw new EmailException("Server " + smtpHost - + " rejected from address " + from.email); + + " rejected from address " + from.getEmail()); } /* Do not prevent the email from being sent to "good" users simply @@ -186,7 +205,7 @@ * error(s) logged. */ for (Address addr : rcpt) { - if (!client.addRecipient(addr.email)) { + if (!client.addRecipient(addr.getEmail())) { String error = client.getReplyString(); rejected.append("Server ").append(smtpHost) .append(" rejected recipient ").append(addr) @@ -214,7 +233,7 @@ } w.write("\r\n"); - w.write(body); + w.write(encodedBody); w.flush(); } @@ -235,6 +254,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/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java similarity index 98% rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java rename to gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java index 3fdc550..03d4f7a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.mail.send; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject;
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..c00035c 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,8 @@ 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.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -108,16 +110,20 @@ } protected final Args args; + protected final PrimaryStorage primaryStorage; protected final boolean autoRebuild; private final Change.Id changeId; private ObjectId revision; private boolean loaded; - AbstractChangeNotes(Args args, Change.Id changeId, boolean autoRebuild) { + AbstractChangeNotes(Args args, Change.Id changeId, + @Nullable PrimaryStorage primaryStorage, boolean autoRebuild) { this.args = checkNotNull(args); this.changeId = checkNotNull(changeId); - this.autoRebuild = autoRebuild; + this.primaryStorage = primaryStorage; + this.autoRebuild = primaryStorage == PrimaryStorage.REVIEW_DB + && autoRebuild; } public Change.Id getChangeId() { @@ -134,6 +140,9 @@ return self(); } boolean read = args.migration.readChanges(); + if (!read && primaryStorage == PrimaryStorage.NOTE_DB) { + throw new OrmException("NoteDb is required to read change " + changeId); + } boolean readOrWrite = read || args.migration.writeChanges(); if (!readOrWrite && !autoRebuild) { loadDefaults();
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..29b6f2b 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(PatchSet.class, 1, 2, 3, 4, 5, 6, 8, 9); checkColumns(PatchSetApproval.Key.class, 1, 2, 3); - checkColumns(PatchSetApproval.class, 1, 2, 3, 6); + checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7, 8); 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, 12); } private final Change change; @@ -347,16 +327,16 @@ private Timestamp getLatestTimestamp() { Ordering<Timestamp> o = Ordering.natural().nullsFirst(); Timestamp ts = null; - for (ChangeMessage cm : getChangeMessages()) { + for (ChangeMessage cm : filterChangeMessages()) { ts = o.max(ts, cm.getWrittenOn()); } for (PatchSet ps : getPatchSets()) { ts = o.max(ts, ps.getCreatedOn()); } - for (PatchSetApproval psa : getPatchSetApprovals()) { + for (PatchSetApproval psa : filterPatchSetApprovals().values()) { ts = o.max(ts, psa.getGranted()); } - for (PatchLineComment plc : getPatchLineComments()) { + for (PatchLineComment plc : filterPatchLineComments().values()) { // Ignore draft comments, as they do not show up in the change meta graph. if (plc.getStatus() != PatchLineComment.Status.DRAFT) { ts = o.max(ts, plc.getWrittenOn()); @@ -367,75 +347,38 @@ 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); - } - }; + return patchSets::containsKey; } 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); }); } - private Predicate<PatchSet.Id> upToCurrentPredicate() { - PatchSet.Id current = change.currentPatchSetId(); - 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; - } - }; - } - - private Map<PatchSet.Id, PatchSet> filterPatchSets() { - return Maps.filterKeys(patchSets, upToCurrentPredicate()); - } - private static void diffChanges(List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) { Change a = bundleA.change; @@ -669,17 +612,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); @@ -688,8 +645,8 @@ private static void diffPatchSets(List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) { - Map<PatchSet.Id, PatchSet> as = bundleA.filterPatchSets(); - Map<PatchSet.Id, PatchSet> bs = bundleB.filterPatchSets(); + Map<PatchSet.Id, PatchSet> as = bundleA.patchSets; + Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets; for (PatchSet.Id id : diffKeySets(diffs, as, bs)) { PatchSet a = as.get(id); PatchSet b = bs.get(id); @@ -718,7 +675,35 @@ 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. + // + // ChangeRebuilder ensures all post-submit approvals happen after the + // actual submit, so the timestamps may not line up. This shouldn't really + // happen, because postSubmit shouldn't be set in ReviewDb until after the + // change is submitted in ReviewDb, but you never know. + 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())) + || ta.compareTo(tb) < 0; + } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { + excludeGranted = + tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()) + || tb.compareTo(ta) < 0; + } + 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/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java similarity index 60% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java index ea0def0..9e7a1fe1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.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. @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.notedb; -public enum RecipientType { - TO, CC, BCC +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..607fdf1 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,26 @@ 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_CURRENT = new FooterKey("Current"); + 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_PATCH_SET_DESCRIPTION = + new FooterKey("Patch-set-description"); + 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,8 +92,10 @@ 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 UNRESOLVED = "Unresolved"; private static final String TAG = FOOTER_TAG.getName(); public static String formatTime(PersonIdent ident, Timestamp t) { @@ -99,16 +109,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 +134,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 +168,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 +205,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,14 +238,28 @@ } 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; String parentUUID = null; + boolean unresolved = false; if (hasParent) { parentUUID = parseStringField(note, curr, changeId, PARENT); } + boolean hasUnresolved = + (RawParseUtils.match(note, curr.value, + UNRESOLVED.getBytes(UTF_8))) != -1; + if (hasUnresolved) { + unresolved = parseBooleanField(note, curr, changeId, UNRESOLVED); + } String uuid = parseStringField(note, curr, changeId, UUID); @@ -236,27 +276,31 @@ 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, + unresolved); + 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 +435,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, @@ -422,6 +466,17 @@ return checkResult(commentLength, "comment length", changeId); } + private boolean parseBooleanField(byte[] note, MutableInteger curr, + Change.Id changeId, String fieldName) throws ConfigInvalidException { + String str = parseStringField(note, curr, changeId, fieldName); + if ("true".equalsIgnoreCase(str)) { + return true; + } else if ("false".equalsIgnoreCase(str)) { + return false; + } + throw parseException(changeId, "invalid boolean for %s: %s", fieldName, str); + } + private static <T> T checkResult(T o, String fieldName, Change.Id changeId) throws ConfigInvalidException { if (o == null) { @@ -470,47 +525,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 +578,62 @@ } } - 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, UNRESOLVED, Boolean.toString(c.unresolved)); + 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..fe830bc 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,19 +19,18 @@ 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; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; import com.google.gerrit.common.Nullable; @@ -40,18 +39,21 @@ 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.NoteDbChangeState.PrimaryStorage; +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 +71,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 +78,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) { @@ -106,6 +96,11 @@ + String.format(fmt, args)); } + public static Change readOneReviewDbChange(ReviewDb db, Change.Id id) + throws OrmException { + return ReviewDbUtil.unwrapDb(db).changes().get(id); + } + @Singleton public static class Factory { private final Args args; @@ -129,7 +124,7 @@ public ChangeNotes createChecked(ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException, NoSuchChangeException { - Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId); + Change change = readOneReviewDbChange(db, changeId); if (change == null || !change.getProject().equals(project)) { throw new NoSuchChangeException(changeId); } @@ -153,7 +148,8 @@ private Change loadChangeFromDb(ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException { - Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId); + Change change = readOneReviewDbChange(db, changeId); + checkArgument(project != null, "project is required"); checkNotNull(change, "change %s not found in ReviewDb", changeId); checkArgument(change.getProject().equals(project), @@ -193,17 +189,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 +233,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 +243,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); } } @@ -268,13 +253,14 @@ public ListMultimap<Project.NameKey, ChangeNotes> create(ReviewDb db, Predicate<ChangeNotes> predicate) throws IOException, OrmException { - ListMultimap<Project.NameKey, ChangeNotes> m = ArrayListMultimap.create(); + ListMultimap<Project.NameKey, ChangeNotes> m = + MultimapBuilder.hashKeys().arrayListValues().build(); if (args.migration.readChanges()) { for (Project.NameKey project : projectCache.all()) { 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 +269,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); } } @@ -318,9 +304,8 @@ Project.NameKey project) throws OrmException, IOException { Set<Change.Id> ids = scan(repo); List<ChangeNotes> changeNotes = new ArrayList<>(ids.size()); - db = ReviewDbUtil.unwrapDb(db); for (Change.Id id : ids) { - Change change = db.changes().get(id); + Change change = readOneReviewDbChange(db, id); if (change == null) { log.warn("skipping change {} found in project {} " + "but not in ReviewDb", @@ -361,10 +346,16 @@ // 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; + + // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the + // ChangeNotesCache from handlers. + private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets; + private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals; @VisibleForTesting public ChangeNotes(Args args, Change change) { @@ -373,7 +364,7 @@ private ChangeNotes(Args args, Change change, boolean autoRebuild, @Nullable RefCache refs) { - super(args, change.getId(), autoRebuild); + super(args, change.getId(), PrimaryStorage.of(change), autoRebuild); this.change = new Change(change); this.refs = refs; } @@ -382,12 +373,32 @@ return change; } - public ImmutableMap<PatchSet.Id, PatchSet> getPatchSets() { - return state.patchSets(); + public ObjectId getMetaId() { + return state.metaId(); + } + + public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() { + if (patchSets == null) { + ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = + ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get)); + for (Map.Entry<PatchSet.Id, PatchSet> e : state.patchSets()) { + b.put(e.getKey(), new PatchSet(e.getValue())); + } + patchSets = b.build(); + } + return patchSets; } public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() { - return state.approvals(); + if (approvals == null) { + ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> b = + ImmutableListMultimap.builder(); + for (Map.Entry<PatchSet.Id, PatchSetApproval> e : state.approvals()) { + b.put(e.getKey(), new PatchSetApproval(e.getValue())); + } + approvals = b.build(); + } + return approvals; } public ReviewerSet getReviewers() { @@ -399,8 +410,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 +455,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 +503,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 { + public 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; } } @@ -504,13 +537,13 @@ } @Override - protected String getRefName() { + public String getRefName() { return changeMetaRef(getChangeId()); } public PatchSet getCurrentPatchSet() { PatchSet.Id psId = change.currentPatchSetId(); - return checkNotNull(state.patchSets().get(psId), + return checkNotNull(getPatchSets().get(psId), "missing current patch set %s", psId.get()); } @@ -543,7 +576,7 @@ @Override protected ObjectId readRef(Repository repo) throws IOException { return refs != null - ? refs.get(getRefName()).orNull() + ? refs.get(getRefName()).orElse(null) : super.readRef(repo); }
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..92ad17d 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,155 @@ 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 + P + O + 20 // metaId + + 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 + list(state.patchSets(), patchSet()) + + P + list(state.allPastReviewers(), approval()) + + 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 approval() { + return O + + P + patchSetId() + P + K + P + O + str(10) + + 2 // value + + P + T // granted + + P // tag + + P; // realAccountId + } + + 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 +226,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/ChangeNotesCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java index 272f3a6..4dd272d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCommit.java
@@ -16,8 +16,8 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.server.git.InMemoryInserter; import com.google.gerrit.server.git.InsertedObject; @@ -118,7 +118,8 @@ public List<String> getFooterLineValues(FooterKey key) { if (footerLines == null) { List<FooterLine> src = getFooterLines(); - footerLines = ArrayListMultimap.create(src.size(), 1); + footerLines = + MultimapBuilder.hashKeys(src.size()).arrayListValues(1).build(); for (FooterLine fl : src) { footerLines.put(fl.getKey().toLowerCase(), fl.getValue()); }
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..719b51c 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,13 +14,17 @@ 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; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS; 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_PATCH_SET_DESCRIPTION; +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,20 +32,17 @@ 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.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; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.common.collect.Table; import com.google.common.collect.Tables; @@ -52,6 +53,7 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.LabelId; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchSet; @@ -72,6 +74,8 @@ import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.FooterKey; import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.Charset; @@ -85,18 +89,34 @@ 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.Optional; import java.util.Set; -import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Function; class ChangeNotesParser { + private static final Logger log = + LoggerFactory.getLogger(ChangeNotesParser.class); + // Sentinel RevId indicating a mutable field on a patch set was parsed, but // the parser does not yet know its commit SHA-1. 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(); + + private static ApprovalKey create(PatchSet.Id psId, Account.Id accountId, + String label) { + return new AutoValue_ChangeNotesParser_ApprovalKey( + psId, accountId, label); + } + } + // Private final members initialized in the constructor. private final ChangeNoteUtil noteUtil; private final NoteDbMetrics metrics; @@ -110,12 +130,13 @@ private final List<Account.Id> allPastReviewers; private final List<ReviewerStatusUpdate> reviewerUpdates; private final List<SubmitRecord> submitRecords; - private final Multimap<RevId, PatchLineComment> comments; - private final TreeMap<PatchSet.Id, PatchSet> patchSets; + private final Multimap<RevId, Comment> comments; + private final Map<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 List<PatchSet.Id> currentPatchSets; + private final Map<ApprovalKey, PatchSetApproval> approvals; + private final List<PatchSetApproval> bufferedApprovals; private final List<ChangeMessage> allChangeMessages; private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet; @@ -123,6 +144,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; @@ -132,8 +155,7 @@ private String originalSubject; 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,17 +164,19 @@ this.walk = walk; this.noteUtil = noteUtil; this.metrics = metrics; - approvals = new HashMap<>(); + approvals = new LinkedHashMap<>(); + bufferedApprovals = new ArrayList<>(); reviewers = HashBasedTable.create(); allPastReviewers = new ArrayList<>(); reviewerUpdates = new ArrayList<>(); submitRecords = Lists.newArrayListWithExpectedSize(1); allChangeMessages = new ArrayList<>(); changeMessagesByPatchSet = LinkedListMultimap.create(); - comments = ArrayListMultimap.create(); - patchSets = Maps.newTreeMap(ReviewDbUtil.intKeyOrdering()); + comments = MultimapBuilder.hashKeys().arrayListValues().build(); + patchSets = new HashMap<>(); deletedPatchSets = new HashSet<>(); patchSetStates = new HashMap<>(); + currentPatchSets = new ArrayList<>(); } ChangeNotesState parseAll() @@ -171,6 +195,7 @@ parseNotes(); allPastReviewers.addAll(reviewers.rowKeySet()); pruneReviewers(); + updatePatchSetStates(); checkMandatoryFooters(); } @@ -178,25 +203,28 @@ return buildState(); } - RevisionNoteMap getRevisionNoteMap() { + RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() { return revisionNoteMap; } private ChangeNotesState buildState() { return ChangeNotesState.create( + tip.copy(), id, new Change.Key(changeId), createdOn, lastUpdatedOn, ownerId, branch, - currentPatchSetId, + buildCurrentPatchSetId(), subject, topic, originalSubject, submissionId, + assignee != null ? assignee.orElse(null) : null, status, + Sets.newLinkedHashSet(Lists.reverse(pastAssignees)), hashtags, patchSets, buildApprovals(), @@ -209,15 +237,28 @@ comments); } + private PatchSet.Id buildCurrentPatchSetId() { + // currentPatchSets are in parse order, i.e. newest first. Pick the first + // patch set that was marked as current, excluding deleted patch sets. + for (PatchSet.Id psId : currentPatchSets) { + if (patchSets.containsKey(psId)) { + return psId; + } + } + return null; + } + 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()); - } + MultimapBuilder.hashKeys().arrayListValues().build(); + for (PatchSetApproval a : approvals.values()) { + if (!patchSets.containsKey(a.getPatchSetId())) { + continue; // Patch set deleted or missing. + } 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); @@ -260,15 +301,8 @@ if (branch == null) { branch = parseBranch(commit); } - if (status == null) { - status = parseStatus(commit); - } PatchSet.Id psId = parsePatchSetId(commit); - if (currentPatchSetId == null || psId.get() > currentPatchSetId.get()) { - currentPatchSetId = psId; - } - PatchSetState psState = parsePatchSetState(commit); if (psState != null) { if (!patchSetStates.containsKey(psId)) { @@ -283,6 +317,7 @@ if (accountId != null) { ownerId = accountId; } + Account.Id realAccountId = parseRealAccountId(commit, accountId); if (changeId == null) { changeId = parseChangeId(commit); @@ -296,12 +331,13 @@ 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); @@ -312,6 +348,7 @@ parsePatchSet(psId, currRev, accountId, ts); } parseGroups(psId, commit); + parseCurrentPatchSet(psId, commit); if (submitRecords.isEmpty()) { // Only parse the most recent set of submit records; any older ones are @@ -319,8 +356,14 @@ parseSubmitRecords(commit.getFooterLineValues(FOOTER_SUBMITTED_WITH)); } + if (status == null) { + status = parseStatus(commit); + } + + // Parse approvals after status to treat approvals in the same commit as + // "Status: merged" as non-post-submit. for (String line : commit.getFooterLineValues(FOOTER_LABEL)) { - parseApproval(psId, accountId, ts, line); + parseApproval(psId, accountId, realAccountId, ts, line); } for (ReviewerStateInternal state : ReviewerStateInternal.values()) { @@ -334,6 +377,8 @@ if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) { lastUpdatedOn = ts; } + + parseDescription(psId, commit); } private String parseSubmissionId(ChangeNotesCommit commit) @@ -357,6 +402,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); @@ -440,6 +495,28 @@ ps.setGroups(PatchSet.splitGroups(groupsStr)); } + private void parseCurrentPatchSet(PatchSet.Id psId, ChangeNotesCommit commit) + throws ConfigInvalidException { + // This commit implies a new current patch set if either it creates a new + // patch set, or sets the current field explicitly. + boolean current = false; + if (parseOneFooter(commit, FOOTER_COMMIT) != null) { + current = true; + } else { + String currentStr = parseOneFooter(commit, FOOTER_CURRENT); + if (Boolean.TRUE.toString().equalsIgnoreCase(currentStr)) { + current = true; + } else if (currentStr != null) { + // Only "true" is allowed; unsetting the current patch set makes no + // sense. + throw invalidFooter(FOOTER_CURRENT, currentStr); + } + } + if (current) { + currentPatchSets.add(psId); + } + } + private void parseHashtags(ChangeNotesCommit commit) throws ConfigInvalidException { // Commits are parsed in reverse order and only the last set of hashtags @@ -459,6 +536,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.empty(); + } else { + PersonIdent ident = RawParseUtils.parsePersonIdent(assigneeValue); + parsedAssignee = Optional.ofNullable(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; @@ -480,12 +581,21 @@ } else if (statusLines.size() > 1) { throw expectedOneFooter(FOOTER_STATUS, statusLines); } - Optional<Change.Status> status = Enums.getIfPresent( - Change.Status.class, statusLines.get(0).toUpperCase()); - if (!status.isPresent()) { + Change.Status status = Enums.getIfPresent( + Change.Status.class, statusLines.get(0).toUpperCase()).orNull(); + if (status == null) { throw invalidFooter(FOOTER_STATUS, statusLines.get(0)); } - return status.get(); + // All approvals after MERGED and before the next status change get the + // postSubmit bit. (Currently the state can't change from MERGED to + // something else, but just in case.) + if (status == Change.Status.MERGED) { + for (PatchSetApproval psa : bufferedApprovals) { + psa.setPostSubmit(true); + } + } + bufferedApprovals.clear(); + return status; } private PatchSet.Id parsePatchSetId(ChangeNotesCommit commit) @@ -509,17 +619,41 @@ } String withParens = psIdLine.substring(s + 1); if (withParens.startsWith("(") && withParens.endsWith(")")) { - Optional<PatchSetState> state = Enums.getIfPresent(PatchSetState.class, - withParens.substring(1, withParens.length() - 1).toUpperCase()); - if (state.isPresent()) { - return state.get(); + PatchSetState state = Enums.getIfPresent(PatchSetState.class, + withParens.substring(1, withParens.length() - 1).toUpperCase()) + .orNull(); + if (state != null) { + return state; } } throw invalidFooter(FOOTER_PATCH_SET, psIdLine); } + private void parseDescription(PatchSet.Id psId, ChangeNotesCommit commit) + throws ConfigInvalidException { + List<String> descLines = + commit.getFooterLineValues(FOOTER_PATCH_SET_DESCRIPTION); + if (descLines.isEmpty()) { + return; + } else if (descLines.size() == 1) { + String desc = descLines.get(0).trim(); + PatchSet ps = patchSets.get(psId); + if (ps == null) { + ps = new PatchSet(psId); + ps.setRevision(PARTIAL_PATCH_SET); + patchSets.put(psId, ps); + } + if (ps.getDescription() == null) { + ps.setDescription(desc); + } + } else { + throw expectedOneFooter(FOOTER_PATCH_SET_DESCRIPTION, descLines); + } + } + 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 +702,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 +715,67 @@ 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()); } + PatchSetApproval psa; if (line.startsWith("-")) { - parseRemoveApproval(psId, accountId, line); + psa = parseRemoveApproval(psId, accountId, realAccountId, ts, line); } else { - parseAddApproval(psId, accountId, ts, line); + psa = parseAddApproval(psId, accountId, realAccountId, ts, line); } + bufferedApprovals.add(psa); } - private void parseAddApproval(PatchSet.Id psId, Account.Id committerId, - Timestamp ts, String line) throws ConfigInvalidException { - Account.Id accountId; + private PatchSetApproval parseAddApproval(PatchSet.Id psId, + Account.Id committerId, 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 +788,44 @@ 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()); + if (!approvals.containsKey(k)) { + approvals.put(k, psa); + } + return psa; } - private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId, + private PatchSetApproval parseRemoveApproval(PatchSet.Id psId, + Account.Id committerId, Account.Id realAccountId, Timestamp ts, String line) throws ConfigInvalidException { - Account.Id accountId; - Entry<String, String> label; + // 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 +833,27 @@ 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); + if (!approvals.containsKey(k)) { + approvals.put(k, remove); } - return curr; + return remove; } private void parseSubmitRecords(List<String> lines) @@ -720,10 +867,9 @@ submitRecords.add(rec); int s = line.indexOf(' '); String statusStr = s >= 0 ? line.substring(0, s) : line; - Optional<SubmitRecord.Status> status = - Enums.getIfPresent(SubmitRecord.Status.class, statusStr); - checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line); - rec.status = status.get(); + rec.status = + Enums.getIfPresent(SubmitRecord.Status.class, statusStr).orNull(); + checkFooter(rec.status != null, FOOTER_SUBMITTED_WITH, line); if (s >= 0) { rec.errorMessage = line.substring(s); } @@ -735,10 +881,9 @@ } rec.labels.add(label); - Optional<SubmitRecord.Label.Status> status = Enums.getIfPresent( - SubmitRecord.Label.Status.class, line.substring(0, c)); - checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line); - label.status = status.get(); + label.status = Enums.getIfPresent( + SubmitRecord.Label.Status.class, line.substring(0, c)).orNull(); + checkFooter(label.status != null, FOOTER_SUBMITTED_WITH, line); int c2 = line.indexOf(": ", c + 2); if (c2 >= 0) { label.label = line.substring(c + 2, c2); @@ -787,25 +932,20 @@ 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()); - } } } } - private void updatePatchSetStates() throws ConfigInvalidException { - for (PatchSet ps : patchSets.values()) { + private void updatePatchSetStates() { + Set<PatchSet.Id> missing = new TreeSet<>(ReviewDbUtil.intKeyOrdering()); + for (Iterator<PatchSet> it = patchSets.values().iterator(); + it.hasNext();) { + PatchSet ps = it.next(); if (ps.getRevision().equals(PARTIAL_PATCH_SET)) { - throw parseException("No %s found for patch set %s", - FOOTER_COMMIT, ps.getPatchSetId()); + missing.add(ps.getId()); + it.remove(); } } - if (patchSetStates.isEmpty()) { - return; - } - - boolean deleted = false; for (Map.Entry<PatchSet.Id, PatchSetState> e : patchSetStates.entrySet()) { switch (e.getValue()) { case PUBLISHED: @@ -813,7 +953,6 @@ break; case DELETED: - deleted = true; patchSets.remove(e.getKey()); break; @@ -825,35 +964,42 @@ break; } } - if (!deleted) { - return; - } // 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. - NavigableSet<PatchSet.Id> all = patchSets.navigableKeySet(); - if (!all.isEmpty()) { - currentPatchSetId = all.last(); - } else { - currentPatchSetId = null; - } - approvals.keySet().retainAll(all); - changeMessagesByPatchSet.keys().retainAll(all); + // (or otherwise missing) 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. + changeMessagesByPatchSet.keys().retainAll(patchSets.keySet()); - for (Iterator<ChangeMessage> it = allChangeMessages.iterator(); - it.hasNext();) { - if (!all.contains(it.next().getPatchSetId())) { + int pruned = pruneEntitiesForMissingPatchSets( + allChangeMessages, ChangeMessage::getPatchSetId, missing); + pruned += pruneEntitiesForMissingPatchSets( + comments.values(), c -> new PatchSet.Id(id, c.key.patchSetId), missing); + pruned += pruneEntitiesForMissingPatchSets( + approvals.values(), PatchSetApproval::getPatchSetId, missing); + + if (!missing.isEmpty()) { + log.warn( + "ignoring {} additional entities due to missing patch sets: {}", + pruned, missing); + } + } + + private <T> int pruneEntitiesForMissingPatchSets( + Iterable<T> ents, Function<T, PatchSet.Id> psIdFunc, + Set<PatchSet.Id> missing) { + int pruned = 0; + for (Iterator<T> it = ents.iterator(); it.hasNext();) { + PatchSet.Id psId = psIdFunc.apply(it.next()); + if (!patchSets.containsKey(psId)) { + pruned++; + missing.add(psId); it.remove(); + } else if (deletedPatchSets.contains(psId)) { + it.remove(); // Not an error we need to report, don't increment pruned. } } - for (Iterator<PatchLineComment> it = comments.values().iterator(); - it.hasNext();) { - PatchSet.Id psId = it.next().getKey().getParentKey().getParentKey(); - if (!all.contains(psId)) { - it.remove(); - } - } + return pruned; } private void checkMandatoryFooters() throws ConfigInvalidException { @@ -868,13 +1014,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..337bf93 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,13 +15,13 @@ package com.google.gerrit.server.notedb; 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.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Multimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.SubmitRecord; @@ -29,14 +29,18 @@ 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.Project; 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.NoteDbChangeState.PrimaryStorage; +import org.eclipse.jgit.lib.ObjectId; + +import java.io.IOException; import java.sql.Timestamp; import java.util.List; import java.util.Map; @@ -57,21 +61,24 @@ public abstract class ChangeNotesState { static ChangeNotesState empty(Change change) { return new AutoValue_ChangeNotesState( + null, change.getId(), null, - ImmutableSet.<String>of(), - ImmutableSortedMap.<PatchSet.Id, PatchSet>of(), - ImmutableListMultimap.<PatchSet.Id, PatchSetApproval>of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableList.of(), + ImmutableList.of(), ReviewerSet.empty(), - ImmutableList.<Account.Id>of(), - ImmutableList.<ReviewerStatusUpdate>of(), - ImmutableList.<SubmitRecord>of(), - ImmutableList.<ChangeMessage>of(), - ImmutableListMultimap.<PatchSet.Id, ChangeMessage>of(), - ImmutableListMultimap.<RevId, PatchLineComment>of()); + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ImmutableListMultimap.of(), + ImmutableListMultimap.of()); } static ChangeNotesState create( + @Nullable ObjectId metaId, Change.Id changeId, Change.Key changeKey, Timestamp createdOn, @@ -83,7 +90,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,11 +102,12 @@ 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(); } return new AutoValue_ChangeNotesState( + metaId, changeId, new AutoValue_ChangeNotesState_ChangeColumns( changeKey, @@ -110,10 +120,12 @@ topic, originalSubject, submissionId, + assignee, status), + ImmutableSet.copyOf(pastAssignees), ImmutableSet.copyOf(hashtags), - ImmutableSortedMap.copyOf(patchSets, ReviewDbUtil.intKeyOrdering()), - ImmutableListMultimap.copyOf(approvals), + ImmutableList.copyOf(patchSets.entrySet()), + ImmutableList.copyOf(approvals.entries()), reviewers, ImmutableList.copyOf(allPastReviewers), ImmutableList.copyOf(reviewerUpdates), @@ -138,24 +150,33 @@ abstract Timestamp createdOn(); abstract Timestamp lastUpdatedOn(); abstract Account.Id owner(); - abstract String branch(); // Project not included. + + // Project not included, as it's not stored anywhere in the meta ref. + abstract String branch(); + @Nullable abstract PatchSet.Id currentPatchSetId(); abstract String subject(); @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(); } + // Only null if NoteDb is disabled. + @Nullable abstract ObjectId metaId(); + abstract Change.Id changeId(); + // Only null if NoteDb is disabled. @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(); + abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets(); + abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals(); abstract ReviewerSet reviewers(); abstract ImmutableList<Account.Id> allPastReviewers(); @@ -165,20 +186,60 @@ 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()); + Change newChange(Project.NameKey project) { + ChangeColumns c = checkNotNull(columns(), "columns are required"); + Change change = new Change( + c.changeKey(), + changeId(), + c.owner(), + new Branch.NameKey(project, c.branch()), + c.createdOn()); + copyNonConstructorColumnsTo(change); + change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + return change; + } + + void copyColumnsTo(Change change) throws IOException { + ChangeColumns c = columns(); + checkState(c != null && metaId() != null, + "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s", + this); + checkMetaId(change); + change.setKey(c.changeKey()); + change.setOwner(c.owner()); + change.setDest(new Branch.NameKey(change.getProject(), c.branch())); + change.setCreatedOn(c.createdOn()); + copyNonConstructorColumnsTo(change); + } + + private void checkMetaId(Change change) throws IOException { + NoteDbChangeState state = NoteDbChangeState.parse(change); + if (state == null) { + return; // Can happen during small NoteDb tests. + } else if (state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) { + return; + } + checkState(state.getRefState().isPresent(), "expected RefState: %s", state); + ObjectId idFromState = state.getRefState().get().changeMetaId(); + if (!idFromState.equals(metaId())) { + throw new IOException( + "cannot copy ChangeNotesState into Change " + changeId() + + "; this ChangeNotesState was created from " + metaId() + + ", but change requires state " + idFromState); + } + } + + private void copyNonConstructorColumnsTo(Change change) { + ChangeColumns c = checkNotNull(columns(), "columns are required"); if (c.status() != null) { change.setStatus(c.status()); } - change.setKey(c.changeKey()); - change.setDest(new Branch.NameKey(change.getProject(), c.branch())); change.setTopic(Strings.emptyToNull(c.topic())); - change.setCreatedOn(c.createdOn()); 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..b78178f 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,24 +19,28 @@ 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; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS; 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_PATCH_SET_DESCRIPTION; +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; import com.google.common.base.Joiner; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Table; @@ -45,10 +49,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 +61,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 +84,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; /** @@ -96,8 +104,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 +120,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 +136,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; @@ -129,8 +144,11 @@ private Iterable<String> groups; private String pushCert; private boolean isAllowWriteToNewtRef; + private String psDescription; + private boolean currentPatchSet; private ChangeDraftUpdate draftUpdate; + private RobotCommentUpdate robotCommentUpdate; @AssistedInject private ChangeUpdate( @@ -140,11 +158,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 +175,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 +193,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 +204,7 @@ AccountCache accountCache, NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, + RobotCommentUpdate.Factory robotCommentUpdateFactory, @Assisted ChangeControl ctl, @Assisted Date when, @Assisted Comparator<String> labelNameComparator, @@ -192,6 +213,7 @@ anonymousCowardName, noteUtil, when); this.accountCache = accountCache; this.draftUpdateFactory = draftUpdateFactory; + this.robotCommentUpdateFactory = robotCommentUpdateFactory; this.updateManagerFactory = updateManagerFactory; this.approvals = approvals(labelNameComparator); } @@ -204,16 +226,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); } @@ -263,7 +288,7 @@ } public void removeApprovalFor(Account.Id reviewer, String label) { - approvals.put(label, reviewer, Optional.<Short> absent()); + approvals.put(label, reviewer, Optional.empty()); } public void merge(RequestId submissionId, @@ -284,7 +309,7 @@ this.commitSubject = commitSubject; } - void setSubject(String subject) { + public void setSubject(String subject) { this.subject = subject; } @@ -301,10 +326,14 @@ this.tag = tag; } - public void putComment(PatchLineComment c) { + public void setPsDescription(String psDescription) { + this.psDescription = psDescription; + } + + 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 +345,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 +361,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 +416,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.empty(); + } + public Map<Account.Id, ReviewerStateInternal> getReviewers() { return reviewers; } @@ -396,6 +442,10 @@ this.psState = psState; } + public void setCurrentPatchSet() { + this.currentPatchSet = true; + } + public void setGroups(List<String> groups) { checkNotNull(groups, "groups may not be null"); this.groups = groups; @@ -407,12 +457,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 +473,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 +502,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 +532,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); } @@ -519,6 +573,14 @@ addPatchSetFooter(msg, ps); + if (currentPatchSet) { + addFooter(msg, FOOTER_CURRENT, Boolean.TRUE); + } + + if (psDescription != null) { + addFooter(msg, FOOTER_PATCH_SET_DESCRIPTION, psDescription); + } + if (changeId != null) { addFooter(msg, FOOTER_CHANGE_ID, changeId); } @@ -543,6 +605,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 +666,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 +675,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,18 +717,25 @@ && status == null && submissionId == null && submitRecords == null + && assignee == null && hashtags == null && topic == null && commit == null && psState == null && groups == null - && tag == null; + && tag == null + && psDescription == null + && !currentPatchSet; } ChangeDraftUpdate getDraftUpdate() { 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..5240ce1 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
@@ -18,19 +18,22 @@ import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; 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; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.git.RepoRefCache; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; 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 +70,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( @@ -83,7 +86,9 @@ Args args, @Assisted Change.Id changeId, @Assisted Account.Id author) { - super(args, changeId, true); + // PrimaryStorage is unknown; this should only called by + // PatchLineCommentsUtil#draftByAuthor, which can live with this. + super(args, changeId, null, false); this.change = null; this.author = author; this.rebuildResult = null; @@ -95,13 +100,13 @@ Account.Id author, boolean autoRebuild, NoteDbUpdateManager.Result rebuildResult) { - super(args, change.getId(), autoRebuild); + super(args, change.getId(), PrimaryStorage.of(change), autoRebuild); this.change = change; this.author = author; this.rebuildResult = rebuildResult; } - RevisionNoteMap getRevisionNoteMap() { + RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() { return revisionNoteMap; } @@ -109,13 +114,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 +145,12 @@ 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 = + MultimapBuilder.hashKeys().arrayListValues().build(); + 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..dcc4213 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
@@ -16,16 +16,18 @@ 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.reviewdb.client.RefNames.refsDraftComments; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; +import static org.eclipse.jgit.lib.ObjectId.zeroId; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Optional; -import com.google.common.base.Predicates; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; @@ -36,10 +38,10 @@ import org.eclipse.jgit.lib.ObjectId; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; /** * The state of all relevant NoteDb refs across all repos corresponding to a @@ -48,13 +50,37 @@ * Stored serialized in the {@code Change#noteDbState} field, and used to * determine whether the state in NoteDb is out of date. * <p> - * Serialized in the form: - * <pre> - * [meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... - * </pre> + * Serialized in one of the forms: + * <ul> + * <li>[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... + * <li>R[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... + * <li>N + * </ul> + * * in numeric account ID order, with hex SHA-1s for human readability. */ public class NoteDbChangeState { + public static final String NOTE_DB_PRIMARY_STATE = "N"; + + public enum PrimaryStorage { + REVIEW_DB('R'), + NOTE_DB('N'); + + private final char code; + + PrimaryStorage(char code) { + this.code = code; + } + + public static PrimaryStorage of(Change c) { + return of(NoteDbChangeState.parse(c)); + } + + public static PrimaryStorage of(NoteDbChangeState s) { + return s != null ? s.getPrimaryStorage() : REVIEW_DB; + } + } + @AutoValue public abstract static class Delta { static Delta create(Change.Id changeId, Optional<ObjectId> newChangeMetaId, @@ -73,31 +99,89 @@ abstract ImmutableMap<Account.Id, ObjectId> newDraftIds(); } + @AutoValue + public abstract static class RefState { + @VisibleForTesting + public static RefState create(ObjectId changeMetaId, + Map<Account.Id, ObjectId> draftIds) { + return new AutoValue_NoteDbChangeState_RefState( + changeMetaId.copy(), + ImmutableMap.copyOf( + Maps.filterValues(draftIds, id -> !zeroId().equals(id)))); + } + + private static Optional<RefState> parse(Change.Id changeId, + List<String> parts) { + checkArgument(!parts.isEmpty(), + "missing state string for change %s", changeId); + ObjectId changeMetaId = ObjectId.fromString(parts.get(0)); + Map<Account.Id, ObjectId> draftIds = + Maps.newHashMapWithExpectedSize(parts.size() - 1); + Splitter s = Splitter.on('='); + for (int i = 1; i < parts.size(); i++) { + String p = parts.get(i); + List<String> draftParts = s.splitToList(p); + checkArgument(draftParts.size() == 2, + "invalid draft state part for change %s: %s", changeId, p); + draftIds.put(Account.Id.parse(draftParts.get(0)), + ObjectId.fromString(draftParts.get(1))); + } + return Optional.of(create(changeMetaId, draftIds)); + } + + abstract ObjectId changeMetaId(); + abstract ImmutableMap<Account.Id, ObjectId> draftIds(); + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + StringBuilder appendTo(StringBuilder sb) { + sb.append(changeMetaId().name()); + for (Account.Id id : ReviewDbUtil.intKeyOrdering() + .sortedCopy(draftIds().keySet())) { + sb.append(',') + .append(id.get()) + .append('=') + .append(draftIds().get(id).name()); + } + return sb; + } + } + public static NoteDbChangeState parse(Change c) { - return parse(c.getId(), c.getNoteDbState()); + return c != null ? parse(c.getId(), c.getNoteDbState()) : null; } @VisibleForTesting - static NoteDbChangeState parse(Change.Id id, String str) { - if (str == null) { + public static NoteDbChangeState parse(Change.Id id, String str) { + if (Strings.isNullOrEmpty(str)) { + // Return null rather than Optional as this is what goes in the field in + // ReviewDb. return null; } List<String> parts = Splitter.on(',').splitToList(str); - checkArgument(!parts.isEmpty(), - "invalid state string for change %s: %s", id, str); - ObjectId changeMetaId = ObjectId.fromString(parts.get(0)); - Map<Account.Id, ObjectId> draftIds = - Maps.newHashMapWithExpectedSize(parts.size() - 1); - Splitter s = Splitter.on('='); - for (int i = 1; i < parts.size(); i++) { - String p = parts.get(i); - List<String> draftParts = s.splitToList(p); - checkArgument(draftParts.size() == 2, - "invalid draft state part for change %s: %s", id, p); - draftIds.put(Account.Id.parse(draftParts.get(0)), - ObjectId.fromString(draftParts.get(1))); + + // Only valid NOTE_DB state is "N". + String first = parts.get(0); + if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) { + return new NoteDbChangeState(id, NOTE_DB, Optional.empty()); } - return new NoteDbChangeState(id, changeMetaId, draftIds); + + // Otherwise it must be REVIEW_DB, either "R,<RefState>" or just + // "<RefState>". Allow length > 0 for forward compatibility. + if (first.length() > 0) { + Optional<RefState> refState; + if (first.charAt(0) == REVIEW_DB.code) { + refState = RefState.parse(id, parts.subList(1, parts.size())); + } else { + refState = RefState.parse(id, parts); + } + return new NoteDbChangeState(id, REVIEW_DB, refState); + } + throw new IllegalArgumentException( + "invalid state string for change " + id + ": " + str); } public static NoteDbChangeState applyDelta(Change change, Delta delta) { @@ -112,6 +196,10 @@ return null; } NoteDbChangeState oldState = parse(change.getId(), oldStr); + if (oldState != null && oldState.getPrimaryStorage() == NOTE_DB) { + // NOTE_DB state doesn't include RefState, so applying a delta is a no-op. + return oldState; + } ObjectId changeMetaId; if (delta.newChangeMetaId().isPresent()) { @@ -121,12 +209,12 @@ return null; } } else { - changeMetaId = oldState.changeMetaId; + changeMetaId = oldState.getChangeMetaId(); } Map<Account.Id, ObjectId> draftIds = new HashMap<>(); if (oldState != null) { - draftIds.putAll(oldState.draftIds); + draftIds.putAll(oldState.getDraftIds()); } for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) { if (e.getValue().equals(ObjectId.zeroId())) { @@ -137,13 +225,20 @@ } NoteDbChangeState state = new NoteDbChangeState( - change.getId(), changeMetaId, draftIds); + change.getId(), + oldState != null + ? oldState.getPrimaryStorage() + : REVIEW_DB, + Optional.of(RefState.create(changeMetaId, draftIds))); change.setNoteDbState(state.toString()); return state; } public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state, RefCache changeRepoRefs, Change.Id changeId) throws IOException { + if (PrimaryStorage.of(state) == NOTE_DB) { + return true; // Primary storage is NoteDb, up to date by definition. + } if (state == null) { return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent(); } @@ -153,6 +248,9 @@ public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state, RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId) throws IOException { + if (PrimaryStorage.of(state) == NOTE_DB) { + return true; // Primary storage is NoteDb, up to date by definition. + } if (state == null) { return !draftsRepoRefs.get(refsDraftComments(changeId, accountId)) .isPresent(); @@ -160,56 +258,74 @@ return state.areDraftsUpToDate(draftsRepoRefs, accountId); } - public static String toString(ObjectId changeMetaId, - Map<Account.Id, ObjectId> draftIds) { - List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet()); - Collections.sort(accountIds, ReviewDbUtil.intKeyOrdering()); - StringBuilder sb = new StringBuilder(changeMetaId.name()); - for (Account.Id id : accountIds) { - sb.append(',') - .append(id.get()) - .append('=') - .append(draftIds.get(id).name()); + private final Change.Id changeId; + private final PrimaryStorage primaryStorage; + private final Optional<RefState> refState; + + public NoteDbChangeState( + Change.Id changeId, + PrimaryStorage primaryStorage, + Optional<RefState> refState) { + this.changeId = checkNotNull(changeId); + this.primaryStorage = checkNotNull(primaryStorage); + this.refState = refState; + + switch (primaryStorage) { + case REVIEW_DB: + checkArgument( + refState.isPresent(), + "expected RefState for change %s with primary storage %s", + changeId, primaryStorage); + break; + case NOTE_DB: + checkArgument( + !refState.isPresent(), + "expected no RefState for change %s with primary storage %s", + changeId, primaryStorage); + break; + default: + throw new IllegalStateException( + "invalid PrimaryStorage: " + primaryStorage); } - return sb.toString(); } - private final Change.Id changeId; - private final ObjectId changeMetaId; - private final ImmutableMap<Account.Id, ObjectId> draftIds; - - public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId, - Map<Account.Id, ObjectId> draftIds) { - this.changeId = checkNotNull(changeId); - this.changeMetaId = checkNotNull(changeMetaId); - this.draftIds = ImmutableMap.copyOf(Maps.filterValues( - draftIds, Predicates.not(Predicates.equalTo(ObjectId.zeroId())))); + public PrimaryStorage getPrimaryStorage() { + return primaryStorage; } public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException { + if (primaryStorage == NOTE_DB) { + return true; // Primary storage is NoteDb, up to date by definition. + } Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId)); if (!id.isPresent()) { - return changeMetaId.equals(ObjectId.zeroId()); + return getChangeMetaId().equals(ObjectId.zeroId()); } - return id.get().equals(changeMetaId); + return id.get().equals(getChangeMetaId()); } public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId) throws IOException { + if (primaryStorage == NOTE_DB) { + return true; // Primary storage is NoteDb, up to date by definition. + } Optional<ObjectId> id = draftsRepoRefs.get(refsDraftComments(changeId, accountId)); if (!id.isPresent()) { - return !draftIds.containsKey(accountId); + return !getDraftIds().containsKey(accountId); } - return id.get().equals(draftIds.get(accountId)); + return id.get().equals(getDraftIds().get(accountId)); } - boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) + public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) throws IOException { + if (primaryStorage == NOTE_DB) { + return true; // Primary storage is NoteDb, up to date by definition. + } if (!isChangeUpToDate(changeRepoRefs)) { return false; } - for (Account.Id accountId : draftIds.keySet()) { + for (Account.Id accountId : getDraftIds().keySet()) { if (!areDraftsUpToDate(draftsRepoRefs, accountId)) { return false; } @@ -224,16 +340,36 @@ @VisibleForTesting public ObjectId getChangeMetaId() { - return changeMetaId; + return refState().changeMetaId(); } @VisibleForTesting ImmutableMap<Account.Id, ObjectId> getDraftIds() { - return draftIds; + return refState().draftIds(); + } + + @VisibleForTesting + Optional<RefState> getRefState() { + return refState; + } + + private RefState refState() { + checkState(refState.isPresent(), + "state for %s has no RefState: %s", changeId, this); + return refState.get(); } @Override public String toString() { - return toString(changeMetaId, draftIds); + switch (primaryStorage) { + case REVIEW_DB: + // Don't include enum field, just IDs (though parse would accept it). + return refState().toString(); + case NOTE_DB: + return NOTE_DB_PRIMARY_STATE; + default: + throw new IllegalArgumentException( + "Unsupported PrimaryStorage: " + primaryStorage); + } } }
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..c4d7277 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
@@ -16,18 +16,16 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableMultimap; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Change.Id; -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; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Repository; public class NoteDbModule extends FactoryModule { private final Config cfg; @@ -51,6 +49,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()); @@ -75,13 +75,6 @@ } @Override - public boolean rebuildProject(ReviewDb db, - ImmutableMultimap<NameKey, Id> allChanges, NameKey project, - Repository allUsersRepo) { - return false; - } - - @Override public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) { return null; } @@ -91,6 +84,12 @@ NoteDbUpdateManager manager) { return null; } + + @Override + public void buildUpdates(NoteDbUpdateManager manager, + ChangeBundle bundle) { + // Do nothing. + } }); bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {}) .annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
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..2eaa459 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
@@ -14,6 +14,7 @@ package com.google.gerrit.server.notedb; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; @@ -21,11 +22,10 @@ import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; import com.google.auto.value.AutoValue; -import com.google.common.base.Optional; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Table; import com.google.gerrit.common.Nullable; import com.google.gerrit.metrics.Timer1; @@ -38,6 +38,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.InMemoryInserter; import com.google.gerrit.server.git.InsertedObject; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gwtorm.server.OrmConcurrencyException; import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; @@ -58,6 +59,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -70,7 +72,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 +80,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 +122,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 +146,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 +182,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; @@ -197,8 +201,9 @@ this.allUsersName = allUsersName; this.metrics = metrics; this.projectName = projectName; - changeUpdates = ArrayListMultimap.create(); - draftUpdates = ArrayListMultimap.create(); + changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); + draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); + robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); toDelete = new HashSet<>(); } @@ -233,17 +238,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 +278,7 @@ } return changeUpdates.isEmpty() && draftUpdates.isEmpty() + && robotCommentUpdates.isEmpty() && toDelete.isEmpty(); } @@ -294,6 +300,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) { @@ -354,7 +364,7 @@ StagedResult r = StagedResult.create( e.getKey(), NoteDbChangeState.Delta.create( - e.getKey(), Optional.<ObjectId>absent(), e.getValue()), + e.getKey(), Optional.empty(), e.getValue()), changeRepo, allUsersRepo); checkState(r.changeCommands().isEmpty(), "should not have change commands when updating only drafts: %s", r); @@ -453,6 +463,9 @@ if (!draftUpdates.isEmpty()) { addUpdates(draftUpdates, allUsersRepo); } + if (!robotCommentUpdates.isEmpty()) { + addUpdates(robotCommentUpdates, changeRepo); + } for (Change.Id id : toDelete) { doDelete(id); } @@ -478,6 +491,17 @@ } } + public static class MismatchedStateException extends OrmException { + private static final long serialVersionUID = 1L; + + private MismatchedStateException(Change.Id id, NoteDbChangeState expectedState) { + super(String.format( + "cannot apply NoteDb updates for change %s;" + + " change meta ref does not match %s", + id, expectedState.getChangeMetaId().name())); + } + } + private void checkExpectedState() throws OrmException, IOException { if (!checkExpectedState) { return; @@ -502,15 +526,17 @@ // - We short-circuited before adding any commands that update this // ref, and we won't stage a delta for this change either. // Either way, it is safe to proceed here rather than throwing - // OrmConcurrencyException. + // MismatchedStateException. + continue; + } + + if (expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) { + // NoteDb is primary, no need to compare state to ReviewDb. continue; } if (!expectedState.isChangeUpToDate(changeRepo.cmds.getRepoRefCache())) { - throw new OrmConcurrencyException(String.format( - "cannot apply NoteDb updates for change %s;" - + " change meta ref does not match %s", - u.getId(), expectedState.getChangeMetaId().name())); + throw new MismatchedStateException(u.getId(), expectedState); } } @@ -518,17 +544,20 @@ ChangeDraftUpdate u = us.iterator().next(); NoteDbChangeState expectedState = NoteDbChangeState.parse(u.getChange()); - if (expectedState == null) { + if (expectedState == null + || expectedState.getPrimaryStorage() == PrimaryStorage.NOTE_DB) { continue; // See above. } Account.Id accountId = u.getAccountId(); if (!expectedState.areDraftsUpToDate( allUsersRepo.cmds.getRepoRefCache(), accountId)) { + ObjectId expectedDraftId = firstNonNull( + expectedState.getDraftIds().get(accountId), ObjectId.zeroId()); throw new OrmConcurrencyException(String.format( "cannot apply NoteDb updates for change %s;" + " draft ref for account %s does not match %s", - u.getId(), accountId, expectedState.getChangeMetaId().name())); + u.getId(), accountId, expectedDraftId.name())); } } } @@ -539,7 +568,7 @@ for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) { String refName = e.getKey(); Collection<U> updates = e.getValue(); - ObjectId old = or.cmds.get(refName).or(ObjectId.zeroId()); + ObjectId old = or.cmds.get(refName).orElse(ObjectId.zeroId()); // Only actually write to the ref if one of the updates explicitly allows // us to do so, i.e. it is known to represent a new change. This avoids // writing partial change meta if the change hasn't been backfilled yet.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java index 071e12c..e4a6f7c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -83,7 +83,8 @@ .withStopStrategy(StopStrategies.stopAfterDelay(30, TimeUnit.SECONDS)); } - private static Retryer<RefUpdate.Result> RETRYER = retryerBuilder().build(); + private static final Retryer<RefUpdate.Result> RETRYER = + retryerBuilder().build(); private final GitRepositoryManager repoManager; private final Project.NameKey projectName; @@ -197,7 +198,9 @@ limit = counter + count; acquireCount++; } catch (ExecutionException | RetryException e) { - Throwables.propagateIfInstanceOf(e.getCause(), OrmException.class); + if (e.getCause() != null) { + Throwables.throwIfInstanceOf(e.getCause(), OrmException.class); + } throw new OrmException(e); } catch (IOException e) { throw new OrmException(e);
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..69fcc02 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.common.collect.MultimapBuilder; +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,47 @@ this.pushCert = pushCert; } - byte[] build(ChangeNoteUtil noteUtil) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); + private Multimap<Integer, Comment> buildCommentMap() { + Multimap<Integer, Comment> all = + MultimapBuilder.hashKeys().arrayListValues().build(); + + 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/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java similarity index 61% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java index ea0def0..e0ee934 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.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. @@ -12,8 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.notedb; -public enum RecipientType { - TO, CC, BCC +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..c89bf33 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -0,0 +1,118 @@ +// 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.ImmutableListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.client.RevId; +import com.google.gerrit.reviewdb.client.RobotComment; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +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; + private ObjectId metaId; + + @AssistedInject + RobotCommentNotes( + Args args, + @Assisted Change change) { + super(args, change.getId(), PrimaryStorage.of(change), 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 + public String getRefName() { + return RefNames.robotCommentsRef(getChangeId()); + } + + @Nullable + public ObjectId getMetaId() { + return metaId; + } + + @Override + protected void onLoad(LoadHandle handle) + throws IOException, ConfigInvalidException { + metaId = handle.id(); + if (metaId == null) { + loadDefaults(); + return; + } + metaId = metaId.copy(); + + RevCommit tipCommit = handle.walk().parseCommit(metaId); + ObjectReader reader = handle.walk().getObjectReader(); + revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader, + NoteMap.read(reader, tipCommit)); + Multimap<RevId, RobotComment> cs = + MultimapBuilder.hashKeys().arrayListValues().build(); + 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/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java similarity index 68% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java index ea0def0..ea3a149 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.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. @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.notedb; -public enum RecipientType { - TO, CC, BCC +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..653cd9d0 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
@@ -15,20 +15,17 @@ package com.google.gerrit.server.notedb; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMultimap; 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.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; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.Repository; - import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; @@ -58,8 +55,7 @@ @Override public Result rebuild(ReviewDb db, Change.Id changeId) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException { + throws NoSuchChangeException, IOException, OrmException { if (failNextUpdate.getAndSet(false)) { throw new IOException("Update failed"); } @@ -73,7 +69,7 @@ @Override public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle) throws NoSuchChangeException, IOException, - OrmException, ConfigInvalidException { + OrmException { // stealNextUpdate doesn't really apply in this case because the IOException // would normally come from the manager.execute() method, which isn't called // here. @@ -81,23 +77,6 @@ } @Override - public boolean rebuildProject(ReviewDb db, - ImmutableMultimap<Project.NameKey, Change.Id> allChanges, - Project.NameKey project, Repository allUsersRepo) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException { - if (failNextUpdate.getAndSet(false)) { - throw new IOException("Update failed"); - } - boolean result = - delegate.rebuildProject(db, allChanges, project, allUsersRepo); - if (stealNextUpdate.getAndSet(false)) { - throw new IOException("Update stolen"); - } - return result; - } - - @Override public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) throws NoSuchChangeException, IOException, OrmException { // Don't inspect stealNextUpdate; that happens in execute() below. @@ -117,4 +96,11 @@ } return result; } + + @Override + public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle) + throws IOException, OrmException { + // Don't check for manual failure; that happens in execute(). + delegate.buildUpdates(manager, bundle); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java similarity index 63% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java index ea0def0..0e6d3e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.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. @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.notedb.rebuild; -public enum RecipientType { - TO, CC, BCC +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..4fed25d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
@@ -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. + +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()); + } + + @Override + protected boolean isPostSubmitApproval() { + return psa.isPostSubmit(); + } +}
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 79% 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..24d62c0 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,22 +12,19 @@ // 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; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.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; import com.google.gwtorm.server.SchemaFactory; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.Repository; - import java.io.IOException; import java.util.concurrent.Callable; @@ -60,18 +57,14 @@ } public abstract Result rebuild(ReviewDb db, Change.Id changeId) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException; + throws NoSuchChangeException, IOException, OrmException; public abstract Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle) throws NoSuchChangeException, IOException, - OrmException, ConfigInvalidException; + OrmException; - public abstract boolean rebuildProject(ReviewDb db, - ImmutableMultimap<Project.NameKey, Change.Id> allChanges, - Project.NameKey project, Repository allUsersRepo) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException; + public abstract void buildUpdates(NoteDbUpdateManager manager, + ChangeBundle bundle) throws IOException, OrmException; public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) throws NoSuchChangeException, IOException, 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..0583060 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -0,0 +1,601 @@ +// 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.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.Splitter; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +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.common.collect.Table; +import com.google.common.primitives.Ints; +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.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.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +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.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.io.IOException; +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.Optional; +import java.util.Set; + +public class ChangeRebuilderImpl extends ChangeRebuilder { + /** + * 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 { + db = ReviewDbUtil.unwrapDb(db); + // Read change just to get project; this instance is then discarded so we + // can read a consistent ChangeBundle inside a transaction. + 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 { + 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 = + checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, 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 = + checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, 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; + } + + private static Change checkNoteDbState(Change c) throws OrmException { + // Can only rebuild a change if its primary storage is ReviewDb. + NoteDbChangeState s = NoteDbChangeState.parse(c); + if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) { + throw new OrmException(String.format( + "cannot rebuild change " + c.getId() + " with state " + s)); + } + return c; + } + + @Override + public 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()); + } + + // 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 = + MultimapBuilder.hashKeys().arrayListValues().build(); + + 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()) { + 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) { + continue; // Ignore events for missing patch sets. + } + for (Event e : msgEvents) { + e.addDep(pse); + } + } + events.addAll(msgEvents); + } + + sortAndFillEvents( + change, noteDbChange, bundle.getPatchSets(), 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, + ImmutableCollection<PatchSet> patchSets, + List<Event> events, Integer minPsNum) { + Event finalUpdates = new FinalUpdatesEvent(change, noteDbChange, patchSets); + events.add(finalUpdates); + setPostSubmitDeps(events); + 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 setPostSubmitDeps(List<Event> events) { + Optional<Event> submitEvent = Lists.reverse(events).stream() + .filter(Event::isSubmit) + .findFirst(); + if (submitEvent.isPresent()) { + events.stream() + .filter(Event::isPostSubmitApproval) + .forEach(e -> e.addDep(submitEvent.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.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..147a467 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
@@ -0,0 +1,135 @@ +// 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; + + /** + * Dependencies of this event; other events that must happen before this + * one. + */ + 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; + + protected boolean isPostSubmitApproval() { + return false; + } + + protected boolean isSubmit() { + return false; + } + + @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..59ff49e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
@@ -0,0 +1,152 @@ +// 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.Iterator; +import java.util.Objects; + +class EventList<E extends Event> implements Iterable<E> { + private final ArrayList<E> list = new ArrayList<>(); + private boolean isSubmit; + + @Override + public Iterator<E> iterator() { + return list.iterator(); + } + + void add(E e) { + list.add(e); + if (e.isSubmit()) { + isSubmit = true; + } + } + + void clear() { + list.clear(); + isSubmit = false; + } + + boolean isEmpty() { + return list.isEmpty(); + } + + 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. + } + if (e.isPostSubmitApproval() && isSubmit) { + // Post-submit approvals must come after the update that submits. + return false; + } + + 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; + } + + private E get(int i) { + return list.get(i); + } + + private int size() { + return list.size(); + } + + private E getLast() { + return list.get(list.size() - 1); + } + + private long getLastTime() { + return getLast().when.getTime(); + } + + private long getFirstTime() { + return list.get(0).when.getTime(); + } +}
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..d0847cd --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventSorter.java
@@ -0,0 +1,113 @@ +// 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.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.SetMultimap; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.PriorityQueue; + +/** + * 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. + PriorityQueue<Event> todo = new PriorityQueue<>(out); + + // Populate waiting map after initial sort to preserve natural order. + waiting = MultimapBuilder.hashKeys().arrayListValues().build(); + deps = MultimapBuilder.hashKeys().hashSetValues().build(); + for (Event e : todo) { + for (Event d : e.deps) { + deps.put(e, d); + waiting.put(d, e); + } + } + + // Second pass: enforce dependencies. + int size = out.size(); + while (!todo.isEmpty()) { + process(todo.remove(), todo); + } + 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, PriorityQueue<Event> todo) { + if (sorted.contains(e)) { + return; // Already emitted. + } + if (!deps.get(e).isEmpty()) { + // Not all events that e depends on have been emitted yet. Ignore e for + // now; it will get added back to the queue in the block below once its + // last dependency is processed. + return; + } + + // All events that e depends on have been emitted, so e can be emitted. + sorted.add(e); + + // Remove e from the dependency set of all events waiting on e, and add + // those events back to the queue in the original priority order for + // reconsideration. + for (Event w : waiting.get(e)) { + deps.get(w).remove(e); + todo.add(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..a9b51a4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -0,0 +1,82 @@ +// 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.reviewdb.server.ReviewDbUtil.intKeyOrdering; + +import com.google.common.collect.ImmutableCollection; +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.util.Objects; + +class FinalUpdatesEvent extends Event { + private final Change change; + private final Change noteDbChange; + private final ImmutableCollection<PatchSet> patchSets; + + FinalUpdatesEvent(Change change, Change noteDbChange, + ImmutableCollection<PatchSet> patchSets) { + super(change.currentPatchSetId(), change.getOwner(), change.getOwner(), + change.getLastUpdatedOn(), change.getCreatedOn(), null); + this.change = change; + this.noteDbChange = noteDbChange; + this.patchSets = patchSets; + } + + @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 (!patchSets.isEmpty() && !highestNumberedPatchSetIsCurrent()) { + update.setCurrentPatchSet(); + } + if (!update.isEmpty()) { + update.setSubjectForCommit("Final NoteDb migration updates"); + } + } + + private boolean highestNumberedPatchSetIsCurrent() { + PatchSet.Id max = + patchSets.stream().map(PatchSet::getId).max(intKeyOrdering()).get(); + return max.equals(change.currentPatchSetId()); + } + + @Override + protected boolean isSubmit() { + return change.getStatus() == Change.Status.MERGED; + } +}
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..eeeec55 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
@@ -0,0 +1,88 @@ +// 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); + update.setPsDescription(ps.getDescription()); + 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..5bc05d0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
@@ -0,0 +1,95 @@ +// 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.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.Optional; +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.empty(); + } + 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.empty(); + } + + private final Change.Status status; + private final Change change; + private final Change noteDbChange; + + 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()); + } + } + + @Override + protected boolean isSubmit() { + return status == Change.Status.MERGED; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java index c4af9fd..aa8879a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -57,7 +57,7 @@ public class AutoMerger { private static final Logger log = LoggerFactory.getLogger(AutoMerger.class); - static boolean cacheAutomerge(Config cfg) { + public static boolean cacheAutomerge(Config cfg) { return cfg.getBoolean("change", null, "cacheAutomerge", true); } @@ -96,11 +96,7 @@ } rw.parseHeaders(merge); - String hash = merge.name(); - String refName = RefNames.REFS_CACHE_AUTOMERGE - + hash.substring(0, 2) - + "/" - + hash.substring(2); + String refName = RefNames.refsCacheAutomerge(merge.name()); Ref ref = repo.getRefDatabase().exactRef(refName); if (ref != null && ref.getObjectId() != null) { RevObject obj = rw.parseAny(ref.getObjectId());
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/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java index dd15cfc..ae37c01 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -93,7 +93,7 @@ } catch (ExecutionException e) { // If there was an error computing the result, carry it // up to the caller so the cache knows this key is invalid. - Throwables.propagateIfInstanceOf(e.getCause(), Exception.class); + Throwables.throwIfInstanceOf(e.getCause(), Exception.class); throw new Exception(e.getMessage(), e.getCause()); } }
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..e1829bc 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, @@ -269,7 +279,7 @@ } catch (ExecutionException e) { // If there was an error computing the result, carry it // up to the caller so the cache knows this key is invalid. - Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + Throwables.throwIfInstanceOf(e.getCause(), IOException.class); throw new IOException(e.getMessage(), e.getCause()); } } @@ -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..42324f5 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,9 +15,7 @@ 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; import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.PatchScript; @@ -26,14 +24,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; @@ -60,6 +58,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; @@ -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..3dc4c0b 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,16 +17,12 @@ 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; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import org.eclipse.jgit.util.IO; import org.objectweb.asm.AnnotationVisitor; @@ -50,6 +46,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; @@ -59,15 +56,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 { @@ -79,7 +67,8 @@ String pluginName, Iterable<Class<? extends Annotation>> annotations) throws InvalidPluginException { Set<String> descriptors = new HashSet<>(); - Multimap<String, JarScanner.ClassData> rawMap = ArrayListMultimap.create(); + Multimap<String, JarScanner.ClassData> rawMap = + MultimapBuilder.hashKeys().arrayListValues().build(); Map<Class<? extends Annotation>, String> classObjToClassDescr = new HashMap<>(); @@ -128,8 +117,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(); @@ -199,10 +191,10 @@ String annotationName; String annotationValue; String[] interfaces; - Iterable<String> exports; + Collection<String> exports; - private ClassData(Iterable<String> exports) { - super(Opcodes.ASM4); + private ClassData(Collection<String> exports) { + super(Opcodes.ASM5); this.exports = exports; } @@ -221,9 +213,12 @@ @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (!visible) { + return null; + } Optional<String> found = - Iterables.tryFind(exports, Predicates.equalTo(desc)); - if (visible && found.isPresent()) { + exports.stream().filter(x -> x.equals(desc)).findAny(); + if (found.isPresent()) { annotationName = desc; return new AbstractAnnotationVisitor() { @Override @@ -271,7 +266,7 @@ private abstract static class AbstractAnnotationVisitor extends AnnotationVisitor { AbstractAnnotationVisitor() { - super(Opcodes.ASM4); + super(Opcodes.ASM5); } @Override @@ -294,10 +289,11 @@ } @Override - public Optional<PluginEntry> getEntry(String resourcePath) throws IOException { + public Optional<PluginEntry> getEntry(String resourcePath) + throws IOException { JarEntry jarEntry = jarFile.getJarEntry(resourcePath); if (jarEntry == null || jarEntry.getSize() == 0) { - return Optional.absent(); + return Optional.empty(); } return Optional.of(resourceOf(jarEntry)); @@ -307,15 +303,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/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java index 4fe0c2a..63a254a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -90,7 +90,8 @@ this.snapshot = snapshot; this.pluginUser = pluginUser; this.cacheKey = new Plugin.CacheKey(name); - this.disabled = srcPath.getFileName().toString().endsWith(".disabled"); + this.disabled = srcPath != null + && srcPath.getFileName().toString().endsWith(".disabled"); } public CleanupHandle getCleanupHandle() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java index 15bb92f..c333638 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -14,8 +14,6 @@ package com.google.gerrit.server.plugins; -import com.google.common.base.Optional; - import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; @@ -23,6 +21,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.Map; +import java.util.Optional; import java.util.jar.Manifest; /** @@ -51,9 +50,8 @@ } @Override - public Optional<PluginEntry> getEntry(String resourcePath) - throws IOException { - return Optional.absent(); + public Optional<PluginEntry> getEntry(String resourcePath) { + return Optional.empty(); } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java index 74ded73..c6077f4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginEntry.java
@@ -11,13 +11,13 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.plugins; -import com.google.common.base.Optional; +package com.google.gerrit.server.plugins; import java.util.Collections; import java.util.Comparator; import java.util.Map; +import java.util.Optional; /** * Plugin static resource entry @@ -38,7 +38,7 @@ }; private static final Map<Object, String> EMPTY_ATTRS = Collections.emptyMap(); - private static final Optional<Long> NO_SIZE = Optional.absent(); + private static final Optional<Long> NO_SIZE = Optional.empty(); private final String name; private final long time;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java index 2c5354e..6dc6dea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -265,7 +265,7 @@ } } - void onStopPlugin(Plugin plugin) { + public void onStopPlugin(Plugin plugin) { for (StopPluginListener l : onStop) { l.onStopPlugin(plugin); }
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/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java index 59ed261..7f89f71 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -42,9 +42,9 @@ private final Path dataDir; private final String pluginCanonicalWebUrl; private final ClassLoader classLoader; - private Class<? extends Module> sysModule; - private Class<? extends Module> sshModule; - private Class<? extends Module> httpModule; + protected Class<? extends Module> sysModule; + protected Class<? extends Module> sshModule; + protected Class<? extends Module> httpModule; private Injector sysInjector; private Injector sshInjector; @@ -61,13 +61,17 @@ Path dataDir, ClassLoader classLoader) throws InvalidPluginException { super(name, srcJar, pluginUser, snapshot, - Plugin.getApiType(getPluginManifest(scanner))); + scanner == null + ? ApiType.PLUGIN + : Plugin.getApiType(getPluginManifest(scanner))); this.pluginCanonicalWebUrl = pluginCanonicalWebUrl; this.scanner = scanner; this.dataDir = dataDir; this.classLoader = classLoader; - this.manifest = getPluginManifest(scanner); - loadGuiceModules(manifest, classLoader); + this.manifest = scanner == null ? null : getPluginManifest(scanner); + if (manifest != null) { + loadGuiceModules(manifest, classLoader); + } } private void loadGuiceModules(Manifest manifest, ClassLoader classLoader) throws InvalidPluginException { @@ -92,7 +96,7 @@ } @SuppressWarnings("unchecked") - private static Class<? extends Module> load(String name, ClassLoader pluginLoader) + protected static Class<? extends Module> load(String name, ClassLoader pluginLoader) throws ClassNotFoundException { if (Strings.isNullOrEmpty(name)) { return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/TestServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/TestServerPlugin.java new file mode 100644 index 0000000..98cd3d2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/TestServerPlugin.java
@@ -0,0 +1,73 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.plugins; + +import com.google.gerrit.server.PluginUser; + +public class TestServerPlugin extends ServerPlugin { + private final ClassLoader classLoader; + private String sysName; + private String httpName; + private String sshName; + + public TestServerPlugin(String name, String pluginCanonicalWebUrl, + PluginUser user, ClassLoader classloader, String sysName, + String httpName, String sshName) + throws InvalidPluginException { + super(name, pluginCanonicalWebUrl, user, null, null, null, null, classloader); + this.classLoader = classloader; + this.sysName = sysName; + this.httpName = httpName; + this.sshName = sshName; + loadGuiceModules(); + } + + private void loadGuiceModules() throws InvalidPluginException { + try { + this.sysModule = load(sysName, classLoader); + this.httpModule = load(httpName, classLoader); + this.sshModule = load(sshName, classLoader); + } catch (ClassNotFoundException e) { + throw new InvalidPluginException("Unable to load plugin Guice Modules", e); + } + } + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + protected boolean canReload() { + return false; + } + + @Override + // Widen access modifier in derived class + public void start(PluginGuiceEnvironment env) throws Exception { + super.start(env); + } + + @Override + // Widen access modifier in derived class + public void stop(PluginGuiceEnvironment env) { + super.stop(env); + } + + @Override + public PluginContentScanner getContentScanner() { + return null; + } +}
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/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java index 7168b1b2..db23967 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -19,7 +19,7 @@ import com.google.gerrit.reviewdb.client.Branch; import com.google.inject.TypeLiteral; -public class BranchResource extends ProjectResource { +public class BranchResource extends RefResource { public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND = new TypeLiteral<RestView<BranchResource>>() {}; @@ -38,10 +38,12 @@ return new Branch.NameKey(getNameKey(), branchInfo.ref); } + @Override public String getRef() { return branchInfo.ref; } + @Override public String getRevision() { return branchInfo.revision; }
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..145d1c8 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
@@ -261,15 +261,28 @@ && isVisible(db); } - /** Can this user delete this draft change or any draft patch set of this change? */ - public boolean canDeleteDraft(final ReviewDb db) throws OrmException { - return (isOwner() || getRefControl().canDeleteDrafts()) - && isVisible(db); + /** Can this user delete this change or any patch set of this change? */ + public boolean canDelete(ReviewDb db, Change.Status status) + throws OrmException { + if (!isVisible(db)) { + return false; + } + + switch (status) { + case DRAFT: + return (isOwner() || getRefControl().canDeleteDrafts()); + case NEW: + case ABANDONED: + return isAdmin(); + case MERGED: + default: + return false; + } } /** 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 +365,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); + } + return false; + } + /** Is this user a reviewer for the change? */ public boolean isReviewer(ReviewDb db) throws OrmException { return isReviewer(db, null); @@ -367,6 +390,10 @@ return false; } + public boolean isAdmin() { + return getUser().getCapabilities().canAdministrateServer(); + } + /** @return true if the user is allowed to remove this reviewer. */ public boolean canRemoveReviewer(PatchSetApproval approval) { return canRemoveReviewer(approval.getAccountId(), approval.getValue()); @@ -414,6 +441,25 @@ return getRefControl().canForceEditTopicName(); } + /** Can this user edit the description? */ + public boolean canEditDescription() { + if (getChange().getStatus().isOpen()) { + return isOwner() // owner (aka creator) of the change can edit desc + || getRefControl().isOwner() // branch owner can edit desc + || getProjectControl().isOwner() // project owner can edit desc + || getUser().getCapabilities().canAdministrateServer() // site administers are god + ; + } + return false; + } + + 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 +470,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/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java index 939d8d4..41392fb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -307,10 +307,9 @@ md.setMessage("Created project\n"); config.commit(md); + md.getRepository().setGitwebDescription(args.projectDescription); } projectCache.onCreateProject(args.getProject()); - repoManager.setProjectDescription(args.getProject(), - args.projectDescription); } private List<String> normalizeBranchNames(List<String> branches)
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..31e59de 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
@@ -14,7 +14,6 @@ package com.google.gerrit.server.project; -import static org.eclipse.jgit.lib.Constants.R_REFS; import static org.eclipse.jgit.lib.Constants.R_TAGS; import com.google.common.base.Strings; @@ -90,18 +89,8 @@ if (input.revision == null) { input.revision = Constants.HEAD; } - while (ref.startsWith("/")) { - ref = ref.substring(1); - } - if (ref.startsWith(R_REFS) && !ref.startsWith(R_TAGS)) { - throw new BadRequestException("invalid tag name \"" + ref + "\""); - } - if (!ref.startsWith(R_TAGS)) { - ref = R_TAGS + ref; - } - if (!Repository.isValidRefName(ref)) { - throw new BadRequestException("invalid tag name \"" + ref + "\""); - } + + ref = RefUtil.normalizeTagRef(ref); RefControl refControl = resource.getControl().controlForRef(ref); try (Repository repo = repoManager.openRepository(resource.getNameKey())) { @@ -116,7 +105,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/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java index 091cba3..e9741ef 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -17,10 +17,8 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.extensions.events.GitReferenceUpdated; -import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.project.DeleteBranch.Input; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; @@ -28,107 +26,37 @@ import com.google.inject.Provider; import com.google.inject.Singleton; -import org.eclipse.jgit.errors.LockFailedException; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ReceiveCommand; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; @Singleton public class DeleteBranch implements RestModifyView<BranchResource, Input> { - private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class); - private static final int MAX_LOCK_FAILURE_CALLS = 10; - private static final long SLEEP_ON_LOCK_FAILURE_MS = 15; - public static class Input { } - private final Provider<IdentifiedUser> identifiedUser; - private final GitRepositoryManager repoManager; private final Provider<InternalChangeQuery> queryProvider; - private final GitReferenceUpdated referenceUpdated; - private final RefValidationHelper refDeletionValidator; + private final DeleteRef.Factory deleteRefFactory; @Inject - DeleteBranch(Provider<IdentifiedUser> identifiedUser, - GitRepositoryManager repoManager, - Provider<InternalChangeQuery> queryProvider, - GitReferenceUpdated referenceUpdated, - RefValidationHelper.Factory refHelperFactory) { - this.identifiedUser = identifiedUser; - this.repoManager = repoManager; + DeleteBranch(Provider<InternalChangeQuery> queryProvider, + DeleteRef.Factory deleteRefFactory) { this.queryProvider = queryProvider; - this.referenceUpdated = referenceUpdated; - this.refDeletionValidator = - refHelperFactory.create(ReceiveCommand.Type.DELETE); + this.deleteRefFactory = deleteRefFactory; } @Override - public Response<?> apply(BranchResource rsrc, Input input) throws AuthException, - ResourceConflictException, OrmException, IOException { + public Response<?> apply(BranchResource rsrc, Input input) + throws RestApiException, OrmException, IOException { if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) { throw new AuthException("Cannot delete branch"); } + if (!queryProvider.get().setLimit(1) .byBranchOpen(rsrc.getBranchKey()).isEmpty()) { throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes"); } - try (Repository r = repoManager.openRepository(rsrc.getNameKey())) { - RefUpdate.Result result; - RefUpdate u = r.updateRef(rsrc.getRef()); - u.setForceUpdate(true); - refDeletionValidator.validateRefOperation( - rsrc.getName(), identifiedUser.get(), u); - int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; - for (;;) { - try { - result = u.delete(); - } catch (LockFailedException e) { - result = RefUpdate.Result.LOCK_FAILURE; - } catch (IOException e) { - log.error("Cannot delete " + rsrc.getBranchKey(), e); - throw e; - } - if (result == RefUpdate.Result.LOCK_FAILURE - && --remainingLockFailureCalls > 0) { - try { - Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS); - } catch (InterruptedException ie) { - // ignore - } - } else { - break; - } - } - - switch (result) { - case NEW: - case NO_CHANGE: - case FAST_FORWARD: - case FORCED: - referenceUpdated.fire(rsrc.getNameKey(), u, ReceiveCommand.Type.DELETE, - identifiedUser.get().getAccount()); - break; - - case REJECTED_CURRENT_BRANCH: - log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name()); - throw new ResourceConflictException("cannot delete current branch"); - - case IO_FAILURE: - case LOCK_FAILURE: - case NOT_ATTEMPTED: - case REJECTED: - case RENAMED: - default: - log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name()); - throw new ResourceConflictException("cannot delete branch: " + result.name()); - } - } + deleteRefFactory.create(rsrc).ref(rsrc.getRef()).delete(); return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java index f4fa446..07e5032 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -14,154 +14,36 @@ package com.google.gerrit.server.project; -import static java.lang.String.format; - -import com.google.common.collect.Lists; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; -import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.reviewdb.client.Branch; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.extensions.events.GitReferenceUpdated; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.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.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.RefUpdate; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.ReceiveCommand; -import org.eclipse.jgit.transport.ReceiveCommand.Result; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; @Singleton public class DeleteBranches implements RestModifyView<ProjectResource, DeleteBranchesInput> { - private static final Logger log = LoggerFactory.getLogger(DeleteBranches.class); - - private final Provider<IdentifiedUser> identifiedUser; - private final GitRepositoryManager repoManager; - private final Provider<InternalChangeQuery> queryProvider; - private final GitReferenceUpdated referenceUpdated; - private final RefValidationHelper refDeletionValidator; + private final DeleteRef.Factory deleteRefFactory; @Inject - DeleteBranches(Provider<IdentifiedUser> identifiedUser, - GitRepositoryManager repoManager, - Provider<InternalChangeQuery> queryProvider, - GitReferenceUpdated referenceUpdated, - RefValidationHelper.Factory refHelperFactory) { - this.identifiedUser = identifiedUser; - this.repoManager = repoManager; - this.queryProvider = queryProvider; - this.referenceUpdated = referenceUpdated; - this.refDeletionValidator = - refHelperFactory.create(ReceiveCommand.Type.DELETE); + DeleteBranches(DeleteRef.Factory deleteRefFactory) { + this.deleteRefFactory = deleteRefFactory; } @Override public Response<?> apply(ProjectResource project, DeleteBranchesInput input) - throws OrmException, IOException, ResourceConflictException { + throws OrmException, IOException, RestApiException { - if (input == null) { - input = new DeleteBranchesInput(); - } - if (input.branches == null) { - input.branches = Lists.newArrayListWithCapacity(1); + if (input == null || input.branches == null || input.branches.isEmpty()) { + throw new BadRequestException("branches must be specified"); } - try (Repository r = repoManager.openRepository(project.getNameKey())) { - BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate(); - for (String branch : input.branches) { - batchUpdate.addCommand(createDeleteCommand(project, r, branch)); - } - try (RevWalk rw = new RevWalk(r)) { - batchUpdate.execute(rw, NullProgressMonitor.INSTANCE); - } - StringBuilder errorMessages = new StringBuilder(); - for (ReceiveCommand command : batchUpdate.getCommands()) { - if (command.getResult() == Result.OK) { - postDeletion(project, command); - } else { - appendAndLogErrorMessage(errorMessages, command); - } - } - if (errorMessages.length() > 0) { - throw new ResourceConflictException(errorMessages.toString()); - } - } + deleteRefFactory.create(project).refs(input.branches).delete(); return Response.none(); } - - private ReceiveCommand createDeleteCommand(ProjectResource project, - Repository r, String branch) - throws OrmException, IOException, ResourceConflictException { - Ref ref = r.getRefDatabase().getRef(branch); - ReceiveCommand command; - if (ref == null) { - command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), branch); - command.setResult(Result.REJECTED_OTHER_REASON, - "it doesn't exist or you do not have permission to delete it"); - return command; - } - command = - new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()); - Branch.NameKey branchKey = - new Branch.NameKey(project.getNameKey(), ref.getName()); - if (!project.getControl().controlForRef(branchKey).canDelete()) { - command.setResult(Result.REJECTED_OTHER_REASON, - "it doesn't exist or you do not have permission to delete it"); - } - if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) { - command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes"); - } - RefUpdate u = r.updateRef(branch); - u.setForceUpdate(true); - refDeletionValidator.validateRefOperation( - project.getName(), identifiedUser.get(), u); - return command; - } - - private void appendAndLogErrorMessage(StringBuilder errorMessages, - ReceiveCommand cmd) { - String msg = null; - switch (cmd.getResult()) { - case REJECTED_CURRENT_BRANCH: - msg = format("Cannot delete %s: it is the current branch", - cmd.getRefName()); - break; - case REJECTED_OTHER_REASON: - msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage()); - break; - case LOCK_FAILURE: - case NOT_ATTEMPTED: - case OK: - case REJECTED_MISSING_OBJECT: - case REJECTED_NOCREATE: - case REJECTED_NODELETE: - case REJECTED_NONFASTFORWARD: - default: - msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult()); - break; - } - log.error(msg); - errorMessages.append(msg); - errorMessages.append("\n"); - } - - private void postDeletion(ProjectResource project, ReceiveCommand cmd) { - referenceUpdated.fire(project.getNameKey(), cmd, - identifiedUser.get().getAccount()); - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java new file mode 100644 index 0000000..5623efc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
@@ -0,0 +1,263 @@ +// 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.project; + +import static java.lang.String.format; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; + +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import org.eclipse.jgit.errors.LockFailedException; +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.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceiveCommand.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class DeleteRef { + private static final Logger log = LoggerFactory.getLogger(DeleteRef.class); + + private static final int MAX_LOCK_FAILURE_CALLS = 10; + private static final long SLEEP_ON_LOCK_FAILURE_MS = 15; + + private final Provider<IdentifiedUser> identifiedUser; + private final GitRepositoryManager repoManager; + private final GitReferenceUpdated referenceUpdated; + private final RefValidationHelper refDeletionValidator; + private final Provider<InternalChangeQuery> queryProvider; + private final ProjectResource resource; + private final List<String> refsToDelete; + private String prefix; + + public interface Factory { + public DeleteRef create(ProjectResource r); + } + + @AssistedInject + DeleteRef(Provider<IdentifiedUser> identifiedUser, + GitRepositoryManager repoManager, + GitReferenceUpdated referenceUpdated, + RefValidationHelper.Factory refDeletionValidatorFactory, + Provider<InternalChangeQuery> queryProvider, + @Assisted ProjectResource resource) { + this.identifiedUser = identifiedUser; + this.repoManager = repoManager; + this.referenceUpdated = referenceUpdated; + this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE); + this.queryProvider = queryProvider; + this.resource = resource; + this.refsToDelete = new ArrayList<>(); + } + + public DeleteRef ref(String ref) { + this.refsToDelete.add(ref); + return this; + } + + public DeleteRef refs(List<String> refs) { + this.refsToDelete.addAll(refs); + return this; + } + + public DeleteRef prefix(String prefix) { + this.prefix = prefix; + return this; + } + + public void delete() + throws OrmException, IOException, ResourceConflictException { + if (!refsToDelete.isEmpty()) { + try (Repository r = repoManager.openRepository(resource.getNameKey())) { + if (refsToDelete.size() == 1) { + deleteSingleRef(r); + } else { + deleteMultipleRefs(r); + } + } + } + } + + private void deleteSingleRef(Repository r) + throws IOException, ResourceConflictException { + String ref = refsToDelete.get(0); + if (prefix != null && !ref.startsWith(prefix)) { + ref = prefix + ref; + } + RefUpdate.Result result; + RefUpdate u = r.updateRef(ref); + u.setForceUpdate(true); + refDeletionValidator.validateRefOperation( + ref, identifiedUser.get(), u); + int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; + for (;;) { + try { + result = u.delete(); + } catch (LockFailedException e) { + result = RefUpdate.Result.LOCK_FAILURE; + } catch (IOException e) { + log.error("Cannot delete " + ref, e); + throw e; + } + if (result == RefUpdate.Result.LOCK_FAILURE + && --remainingLockFailureCalls > 0) { + try { + Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS); + } catch (InterruptedException ie) { + // ignore + } + } else { + break; + } + } + + switch (result) { + case NEW: + case NO_CHANGE: + case FAST_FORWARD: + case FORCED: + referenceUpdated.fire(resource.getNameKey(), u, ReceiveCommand.Type.DELETE, + identifiedUser.get().getAccount()); + break; + + case REJECTED_CURRENT_BRANCH: + log.error("Cannot delete " + ref + ": " + result.name()); + throw new ResourceConflictException("cannot delete current branch"); + + case IO_FAILURE: + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case REJECTED: + case RENAMED: + default: + log.error("Cannot delete " + ref + ": " + result.name()); + throw new ResourceConflictException("cannot delete: " + result.name()); + } + } + + private void deleteMultipleRefs(Repository r) + throws OrmException, IOException, ResourceConflictException { + BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate(); + List<String> refs = prefix == null + ? refsToDelete + : refsToDelete.stream().map( + ref -> ref.startsWith(prefix) + ? ref + : prefix + ref).collect(Collectors.toList()); + for (String ref : refs) { + batchUpdate.addCommand(createDeleteCommand(resource, r, ref)); + } + try (RevWalk rw = new RevWalk(r)) { + batchUpdate.execute(rw, NullProgressMonitor.INSTANCE); + } + StringBuilder errorMessages = new StringBuilder(); + for (ReceiveCommand command : batchUpdate.getCommands()) { + if (command.getResult() == Result.OK) { + postDeletion(resource, command); + } else { + appendAndLogErrorMessage(errorMessages, command); + } + } + if (errorMessages.length() > 0) { + throw new ResourceConflictException(errorMessages.toString()); + } + } + + private ReceiveCommand createDeleteCommand(ProjectResource project, + Repository r, String refName) + throws OrmException, IOException, ResourceConflictException { + Ref ref = r.getRefDatabase().getRef(refName); + ReceiveCommand command; + if (ref == null) { + command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), refName); + command.setResult(Result.REJECTED_OTHER_REASON, + "it doesn't exist or you do not have permission to delete it"); + return command; + } + command = new ReceiveCommand( + ref.getObjectId(), ObjectId.zeroId(), ref.getName()); + + if (!project.getControl().controlForRef(refName).canDelete()) { + command.setResult(Result.REJECTED_OTHER_REASON, + "it doesn't exist or you do not have permission to delete it"); + } + + if (!refName.startsWith(R_TAGS)) { + Branch.NameKey branchKey = + new Branch.NameKey(project.getNameKey(), ref.getName()); + if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) { + command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes"); + } + } + + RefUpdate u = r.updateRef(refName); + u.setForceUpdate(true); + refDeletionValidator.validateRefOperation( + project.getName(), identifiedUser.get(), u); + return command; + } + + private void appendAndLogErrorMessage(StringBuilder errorMessages, + ReceiveCommand cmd) { + String msg = null; + switch (cmd.getResult()) { + case REJECTED_CURRENT_BRANCH: + msg = format("Cannot delete %s: it is the current branch", + cmd.getRefName()); + break; + case REJECTED_OTHER_REASON: + msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage()); + break; + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case OK: + case REJECTED_MISSING_OBJECT: + case REJECTED_NOCREATE: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + default: + msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult()); + break; + } + log.error(msg); + errorMessages.append(msg); + errorMessages.append("\n"); + } + + private void postDeletion(ProjectResource project, ReceiveCommand cmd) { + referenceUpdated.fire(project.getNameKey(), cmd, + identifiedUser.get().getAccount()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java new file mode 100644 index 0000000..bcc433b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.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.project; + +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.io.IOException; + +@Singleton +public class DeleteTag implements RestModifyView<TagResource, DeleteTag.Input> { + private final DeleteRef.Factory deleteRefFactory; + + public static class Input { + } + + @Inject + DeleteTag(DeleteRef.Factory deleteRefFactory) { + this.deleteRefFactory = deleteRefFactory; + } + + @Override + public Response<?> apply(TagResource resource, Input input) + throws OrmException, RestApiException, IOException { + String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref); + RefControl refControl = resource.getControl().controlForRef(tag); + + if (!refControl.canDelete()) { + throw new AuthException("Cannot delete tag"); + } + + deleteRefFactory.create(resource).ref(tag).delete(); + return Response.none(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java new file mode 100644 index 0000000..813012b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.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.project; + +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import com.google.gerrit.extensions.api.projects.DeleteTagsInput; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.io.IOException; + +@Singleton +public class DeleteTags + implements RestModifyView<ProjectResource, DeleteTagsInput> { + private final DeleteRef.Factory deleteRefFactory; + + @Inject + DeleteTags(DeleteRef.Factory deleteRefFactory) { + this.deleteRefFactory = deleteRefFactory; + } + + @Override + public Response<?> apply(ProjectResource project, DeleteTagsInput input) + throws OrmException, RestApiException, IOException { + + if (input == null || input.tags == null || input.tags.isEmpty()) { + throw new BadRequestException("tags must be specified"); + } + + deleteRefFactory.create(project).refs(input.tags).prefix(R_TAGS).delete(); + return Response.none(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/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/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java index a50705d..2da0e01 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -15,7 +15,6 @@ package com.google.gerrit.server.project; import com.google.common.collect.ComparisonChain; -import com.google.common.collect.FluentIterable; import com.google.common.collect.Sets; import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.common.ActionInfo; @@ -191,10 +190,10 @@ } info.actions.put(d.getId(), new ActionInfo(d)); } - FluentIterable<WebLinkInfo> links = + List<WebLinkInfo> links = webLinks.getBranchLinks( refControl.getProjectControl().getProject().getName(), ref.getName()); - info.webLinks = links.isEmpty() ? null : links.toList(); + info.webLinks = links.isEmpty() ? null : links; return info; } }
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..92189dd 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,9 +16,7 @@ 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; import com.google.common.collect.Iterables; import com.google.gerrit.common.data.GroupReference; @@ -379,9 +377,9 @@ log.warn("Unexpected error reading " + projectName, err); continue; } - FluentIterable<WebLinkInfo> links = + List<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get()); - info.webLinks = links.isEmpty() ? null : links.toList(); + info.webLinks = links.isEmpty() ? null : links; } if (foundIndex++ < start) { @@ -445,13 +443,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/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java index 8a6145a..7579398 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -81,6 +81,8 @@ child(PROJECT_KIND, "tags").to(TagsCollection.class); get(TAG_KIND).to(GetTag.class); put(TAG_KIND).to(PutTag.class); + delete(TAG_KIND).to(DeleteTag.class); + post(PROJECT_KIND, "tags:delete").to(DeleteTags.class); factory(CreateTag.Factory.class); child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class); @@ -91,5 +93,7 @@ get(PROJECT_KIND, "config").to(GetConfig.class); put(PROJECT_KIND, "config").to(PutConfig.class); + + factory(DeleteRef.Factory.class); } }
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..f0ea487 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
@@ -20,6 +20,8 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; 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 = + MultimapBuilder.hashKeys().arrayListValues().build(); 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/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java index 8a08052..41b8721 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,12 +14,11 @@ package com.google.gerrit.server.project; -import com.google.common.base.Predicate; +import static java.util.stream.Collectors.toSet; + import com.google.common.base.Throwables; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.reviewdb.client.AccountGroup; @@ -43,9 +42,9 @@ import java.io.IOException; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ExecutionException; @@ -61,14 +60,6 @@ private static final String CACHE_NAME = "projects"; private static final String CACHE_LIST = "project_list"; - private static final Predicate<AccountGroup.UUID> NON_NULL_UUID = - new Predicate<AccountGroup.UUID>() { - @Override - public boolean apply(AccountGroup.UUID uuid) { - return uuid != null && uuid.get() != null; - } - }; - public static Module module() { return new CacheModule() { @Override @@ -159,7 +150,7 @@ } catch (ExecutionException e) { if (!(e.getCause() instanceof RepositoryNotFoundException)) { log.warn(String.format("Cannot read project %s", projectName.get()), e); - Throwables.propagateIfInstanceOf(e.getCause(), IOException.class); + Throwables.throwIfInstanceOf(e.getCause(), IOException.class); throw new IOException(e); } return null; @@ -216,23 +207,19 @@ return list.get(ListKey.ALL); } catch (ExecutionException e) { log.warn("Cannot list available projects", e); - return ImmutableSortedSet.of(); + return Collections.emptySortedSet(); } } @Override public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() { - Set<AccountGroup.UUID> groups = new HashSet<>(); - for (Project.NameKey n : all()) { - ProjectState p = byName.getIfPresent(n.get()); - if (p != null) { - groups.addAll(FluentIterable - .from(p.getConfig().getAllGroupUUIDs()) - .filter(NON_NULL_UUID) - .toSet()); - } - } - return groups; + return all().stream().map(n -> byName.getIfPresent(n.get())) + .filter(Objects::nonNull) + .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream()) + // getAllGroupUUIDs shouldn't really return null UUIDs, but harden + // against them just in case there is a bug or corner case. + .filter(id -> id != null && id.get() != null) + .collect(toSet()); } @Override
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..ca01630 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
@@ -42,6 +42,8 @@ import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.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; @@ -153,6 +155,7 @@ private final Collection<ContributorAgreement> contributorAgreements; private final TagCache tagCache; @Nullable private final SearchingChangeCacheImpl changeCache; + private final Provider<InternalChangeQuery> queryProvider; private List<SectionMatcher> allSections; private List<SectionMatcher> localSections; @@ -168,6 +171,7 @@ ChangeNotes.Factory changeNotesFactory, ChangeControl.Factory changeControlFactory, TagCache tagCache, + Provider<InternalChangeQuery> queryProvider, @Nullable SearchingChangeCacheImpl changeCache, @CanonicalWebUrl @Nullable String canonicalWebUrl, @Assisted CurrentUser who, @@ -181,6 +185,7 @@ this.permissionFilter = permissionFilter; this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements(); this.canonicalWebUrl = canonicalWebUrl; + this.queryProvider = queryProvider; user = who; state = ps; } @@ -307,8 +312,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 +333,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 + "'"); } @@ -512,7 +518,27 @@ return false; } + /** @return whether a commit is visible to user. */ public boolean canReadCommit(ReviewDb db, Repository repo, RevCommit commit) { + // Look for changes associated with the commit. + try { + List<ChangeData> changes = queryProvider.get() + .byProjectCommit(getProject().getNameKey(), commit); + for (ChangeData change : changes) { + if (controlFor(db, change.change()).isVisible(db)) { + return true; + } + } + } catch (OrmException e) { + log.error("Cannot look up change for commit " + commit.name() + " in " + + getProject().getName(), e); + } + // Scan all visible refs. + return canReadCommitFromVisibleRef(db, repo, commit); + } + + private boolean canReadCommitFromVisibleRef(ReviewDb db, Repository repo, + RevCommit commit) { try (RevWalk rw = new RevWalk(repo)) { return isMergedIntoVisibleRef(repo, db, rw, commit, repo.getAllRefs().values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java index 5b1d521..767e36a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
@@ -15,7 +15,6 @@ package com.google.gerrit.server.project; import com.google.common.base.Strings; -import com.google.common.collect.FluentIterable; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.common.WebLinkInfo; import com.google.gerrit.extensions.restapi.Url; @@ -25,6 +24,8 @@ import com.google.inject.Inject; import com.google.inject.Singleton; +import java.util.List; + @Singleton public class ProjectJson { @@ -50,9 +51,9 @@ info.description = Strings.emptyToNull(p.getDescription()); info.state = p.getState(); info.id = Url.encode(info.name); - FluentIterable<WebLinkInfo> links = + List<WebLinkInfo> links = webLinks.getProjectLinks(p.getName()); - info.webLinks = links.isEmpty() ? null : links.toList(); + info.webLinks = links.isEmpty() ? null : links; return info; } }
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/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java index 19b5b26..bf4cbbf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -34,7 +34,6 @@ import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.ProjectConfigEntry; -import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.TransferConfig; @@ -60,7 +59,6 @@ private final boolean serverEnableSignedPush; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; private final ProjectCache projectCache; - private final GitRepositoryManager gitMgr; private final ProjectState.Factory projectStateFactory; private final TransferConfig config; private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; @@ -73,7 +71,6 @@ PutConfig(@EnableSignedPush boolean serverEnableSignedPush, Provider<MetaDataUpdate.User> metaDataUpdateFactory, ProjectCache projectCache, - GitRepositoryManager gitMgr, ProjectState.Factory projectStateFactory, TransferConfig config, DynamicMap<ProjectConfigEntry> pluginConfigEntries, @@ -84,7 +81,6 @@ this.serverEnableSignedPush = serverEnableSignedPush; this.metaDataUpdateFactory = metaDataUpdateFactory; this.projectCache = projectCache; - this.gitMgr = gitMgr; this.projectStateFactory = projectStateFactory; this.config = config; this.pluginConfigEntries = pluginConfigEntries; @@ -170,7 +166,7 @@ try { projectConfig.commit(md); projectCache.evict(projectConfig.getProject()); - gitMgr.setProjectDescription(projectName, p.getDescription()); + md.getRepository().setGitwebDescription(p.getDescription()); } catch (IOException e) { if (e.getCause() instanceof ConfigInvalidException) { throw new ResourceConflictException("Cannot update " + projectName
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java index 17401fe..99f0b83 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -24,7 +24,6 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.inject.Inject; @@ -39,15 +38,12 @@ public class PutDescription implements RestModifyView<ProjectResource, DescriptionInput> { private final ProjectCache cache; private final MetaDataUpdate.Server updateFactory; - private final GitRepositoryManager gitMgr; @Inject PutDescription(ProjectCache cache, - MetaDataUpdate.Server updateFactory, - GitRepositoryManager gitMgr) { + MetaDataUpdate.Server updateFactory) { this.cache = cache; this.updateFactory = updateFactory; - this.gitMgr = gitMgr; } @Override @@ -79,9 +75,7 @@ md.setMessage(msg); config.commit(md); cache.evict(ctl.getProject()); - gitMgr.setProjectDescription( - resource.getNameKey(), - project.getDescription()); + md.getRepository().setGitwebDescription(project.getDescription()); return Strings.isNullOrEmpty(project.getDescription()) ? Response.<String>none()
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/RefPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java index ed50a54..8c850fb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
@@ -49,7 +49,7 @@ try { return exampleCache.get(refPattern); } catch (ExecutionException e) { - Throwables.propagateIfPossible(e.getCause()); + Throwables.throwIfUnchecked(e.getCause()); throw new RuntimeException(e); } } else if (refPattern.endsWith("/*")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.java new file mode 100644 index 0000000..9300d43 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefResource.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.server.project; + +public abstract class RefResource extends ProjectResource { + + public RefResource(ProjectControl control) { + super(control); + } + + /** + * @return the ref's name + */ + public abstract String getRef(); + + /** + * @return the ref's revision + */ + public abstract String getRevision(); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefUtil.java index 9d8fe10..680bba1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefUtil.java
@@ -14,7 +14,11 @@ package com.google.gerrit.server.project; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + import com.google.common.collect.Iterables; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; @@ -99,6 +103,23 @@ return Constants.R_HEADS; } + public static String normalizeTagRef(String tag) throws BadRequestException { + String result = tag; + while (result.startsWith("/")) { + result = result.substring(1); + } + if (result.startsWith(R_REFS) && !result.startsWith(R_TAGS)) { + throw new BadRequestException("invalid tag name \"" + result + "\""); + } + if (!result.startsWith(R_TAGS)) { + result = R_TAGS + result; + } + if (!Repository.isValidRefName(result)) { + throw new BadRequestException("invalid tag name \"" + result + "\""); + } + return result; + } + /** Error indicating the revision is invalid as supplied. */ static class InvalidRevisionException extends Exception { private static final long serialVersionUID = 1L;
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..48cd7ee 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
@@ -91,12 +91,9 @@ private final ChangeData cd; private final ChangeControl control; + private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.defaults(); + private SubmitRuleOptions opts; private PatchSet patchSet; - private boolean fastEvalLabels; - private boolean allowDraft; - private boolean allowClosed; - private boolean skipFilters; - private String rule; private boolean logErrors = true; private long reductionsConsumed; @@ -108,6 +105,29 @@ } /** + * @return immutable snapshot of options configured so far. If neither {@link + * #getSubmitRule()} nor {@link #getSubmitType()} have been called yet, + * state within this instance is still mutable, so may change before + * evaluation. The instance's options are frozen at evaluation time. + */ + public SubmitRuleOptions getOptions() { + if (opts != null) { + return opts; + } + return optsBuilder.build(); + } + + public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) { + checkNotStarted(); + if (opts != null) { + optsBuilder = opts.toBuilder(); + } else { + optsBuilder = SubmitRuleOptions.defaults(); + } + return this; + } + + /** * @param ps patch set of the change to evaluate. If not set, the current * patch set will be loaded from {@link #evaluate()} or {@link * #getSubmitType}. @@ -121,12 +141,14 @@ } /** - * @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) { - fastEvalLabels = fast; + checkNotStarted(); + optsBuilder.fastEvalLabels(fast); return this; } @@ -135,7 +157,8 @@ * @return this */ public SubmitRuleEvaluator setAllowClosed(boolean allow) { - allowClosed = allow; + checkNotStarted(); + optsBuilder.allowClosed(allow); return this; } @@ -144,7 +167,8 @@ * @return this */ public SubmitRuleEvaluator setAllowDraft(boolean allow) { - allowDraft = allow; + checkNotStarted(); + optsBuilder.allowDraft(allow); return this; } @@ -153,7 +177,8 @@ * @return this */ public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) { - skipFilters = skip; + checkNotStarted(); + optsBuilder.skipFilters(skip); return this; } @@ -162,7 +187,8 @@ * @return this */ public SubmitRuleEvaluator setRule(@Nullable String rule) { - this.rule = rule; + checkNotStarted(); + optsBuilder.rule(rule); return this; } @@ -187,23 +213,21 @@ * rules, including any errors. */ public List<SubmitRecord> evaluate() { + initOptions(); Change c = control.getChange(); - if (!allowClosed && c.getStatus().isClosed()) { + if (!opts.allowClosed() && c.getStatus().isClosed()) { SubmitRecord rec = new SubmitRecord(); rec.status = SubmitRecord.Status.CLOSED; return Collections.singletonList(rec); } - if (!allowDraft) { - if (c.getStatus() == Change.Status.DRAFT) { - return cannotSubmitDraft(); - } + if (!opts.allowDraft()) { try { initPatchSet(); } catch (OrmException e) { return ruleError("Error looking up patch set " - + control.getChange().currentPatchSetId()); + + control.getChange().currentPatchSetId(), e); } - if (patchSet.isDraft()) { + if (c.getStatus() == Change.Status.DRAFT || patchSet.isDraft()) { return cannotSubmitDraft(); } } @@ -235,13 +259,15 @@ if (!control.isDraftVisible(cd.db(), cd)) { return createRuleError("Patch set " + patchSet.getId() + " not found"); } - initPatchSet(); if (patchSet.isDraft()) { return createRuleError("Cannot submit draft patch sets"); } return createRuleError("Cannot submit draft changes"); } catch (OrmException err) { - String msg = "Cannot check visibility of patch set " + patchSet.getId(); + PatchSet.Id psId = patchSet != null + ? patchSet.getId() + : control.getChange().currentPatchSetId(); + String msg = "Cannot check visibility of patch set " + psId; log.error(msg, err); return createRuleError(msg); } @@ -368,11 +394,12 @@ * @return record from the evaluated rules. */ public SubmitTypeRecord getSubmitType() { + initOptions(); try { initPatchSet(); } catch (OrmException e) { return typeError("Error looking up patch set " - + control.getChange().currentPatchSetId()); + + control.getChange().currentPatchSetId(), e); } try { @@ -452,7 +479,7 @@ PrologEnvironment env = getPrologEnvironment(user); try { Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm()); - if (fastEvalLabels) { + if (opts.fastEvalLabels()) { env.once("gerrit", "assume_range_from_label"); } @@ -475,7 +502,7 @@ } Term resultsTerm = toListTerm(results); - if (!skipFilters) { + if (!opts.skipFilters()) { resultsTerm = runSubmitFilters( resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName); } @@ -502,18 +529,19 @@ ProjectState projectState = control.getProjectControl().getProjectState(); PrologEnvironment env; try { - if (rule == null) { + if (opts.rule() == null) { env = projectState.newPrologEnvironment(); } else { - env = projectState.newPrologEnvironment("stdin", new StringReader(rule)); + env = projectState.newPrologEnvironment( + "stdin", new StringReader(opts.rule())); } } catch (CompileException err) { String msg; - if (rule == null && control.getProjectControl().isOwner()) { + if (opts.rule() == null && control.getProjectControl().isOwner()) { msg = String.format( "Cannot load rules.pl for %s: %s", getProjectName(), err.getMessage()); - } else if (rule != null) { + } else if (opts.rule() != null) { msg = err.getMessage(); } else { msg = String.format("Cannot load rules.pl for %s", getProjectName()); @@ -547,7 +575,7 @@ Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm()); try { - if (fastEvalLabels) { + if (opts.fastEvalLabels()) { env.once("gerrit", "assume_range_from_label"); } @@ -607,6 +635,17 @@ return submitRule != null ? submitRule.toString() : "<unknown rule>"; } + private void checkNotStarted() { + checkState(opts == null, "cannot set options after starting evaluation"); + } + + private void initOptions() { + if (opts == null) { + opts = optsBuilder.build(); + optsBuilder = null; + } + } + private void initPatchSet() throws OrmException { if (patchSet == null) { patchSet = cd.currentPatchSet();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.java new file mode 100644 index 0000000..97155ac --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.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.project; + +import com.google.auto.value.AutoValue; +import com.google.gerrit.common.Nullable; + +/** + * Stable identifier for options passed to a particular submit rule evaluator. + * <p> + * Used to test whether it is ok to reuse a cached list of submit records. Does + * not include a change or patch set ID; callers are responsible for checking + * those on their own. + */ +@AutoValue +public abstract class SubmitRuleOptions { + public static Builder builder() { + return new AutoValue_SubmitRuleOptions.Builder(); + } + + public static Builder defaults() { + return builder() + .fastEvalLabels(false) + .allowDraft(false) + .allowClosed(false) + .skipFilters(false) + .rule(null); + } + + public abstract boolean fastEvalLabels(); + public abstract boolean allowDraft(); + public abstract boolean allowClosed(); + public abstract boolean skipFilters(); + @Nullable public abstract String rule(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract SubmitRuleOptions.Builder fastEvalLabels(boolean fastEvalLabels); + public abstract SubmitRuleOptions.Builder allowDraft(boolean allowDraft); + public abstract SubmitRuleOptions.Builder allowClosed(boolean allowClosed); + public abstract SubmitRuleOptions.Builder skipFilters(boolean skipFilters); + public abstract SubmitRuleOptions.Builder rule(@Nullable String rule); + + public abstract SubmitRuleOptions build(); + } + + public Builder toBuilder() { + return builder() + .fastEvalLabels(fastEvalLabels()) + .allowDraft(allowDraft()) + .allowClosed(allowClosed()) + .skipFilters(skipFilters()) + .rule(rule()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java index afbd3be..fe4d68d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -18,18 +18,28 @@ import com.google.gerrit.extensions.restapi.RestView; import com.google.inject.TypeLiteral; -public class TagResource extends ProjectResource { +public class TagResource extends RefResource { public static final TypeLiteral<RestView<TagResource>> TAG_KIND = new TypeLiteral<RestView<TagResource>>() {}; - private final TagInfo tag; + private final TagInfo tagInfo; - public TagResource(ProjectControl control, TagInfo tag) { + public TagResource(ProjectControl control, TagInfo tagInfo) { super(control); - this.tag = tag; + this.tagInfo = tagInfo; } public TagInfo getTagInfo() { - return tag; + return tagInfo; + } + + @Override + public String getRef() { + return tagInfo.ref; + } + + @Override + public String getRevision() { + return tagInfo.revision; } }
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..4acd2ba 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; @@ -84,7 +83,9 @@ try { return readImpl(); } catch (OrmRuntimeException err) { - Throwables.propagateIfInstanceOf(err.getCause(), OrmException.class); + if (err.getCause() != null) { + Throwables.throwIfInstanceOf(err.getCause(), OrmException.class); + } throw new OrmException(err); } } @@ -157,12 +158,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..a0f66db 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 { @@ -140,7 +136,9 @@ } catch (OrmRuntimeException e) { throw new OrmException(e.getMessage(), e); } catch (OrmException e) { - Throwables.propagateIfInstanceOf(e.getCause(), QueryParseException.class); + if (e.getCause() != null) { + Throwables.throwIfInstanceOf(e.getCause(), QueryParseException.class); + } throw e; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java index b3f92ff..9a9ec5d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -47,7 +47,7 @@ return Predicate.or(preds); } - static Predicate<AccountState> id(Account.Id accountId) { + public static Predicate<AccountState> id(Account.Id accountId) { return new AccountPredicate(AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString()); }
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 ba58113..085f34c 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
@@ -17,13 +17,13 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.server.ApprovalsUtil.sortApprovals; +import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoValue; import com.google.common.base.MoreObjects; -import com.google.common.base.Optional; -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.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; @@ -36,35 +36,37 @@ 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; import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.StarredChangesUtil.StarRef; import com.google.gerrit.server.change.MergeabilityCache; import com.google.gerrit.server.git.GitRepositoryManager; 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; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.SubmitRuleEvaluator; +import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; import com.google.inject.assistedinject.Assisted; @@ -87,11 +89,13 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; public class ChangeData { private static final int BATCH_SIZE = 50; @@ -106,12 +110,8 @@ } public static Map<Change.Id, ChangeData> asMap(List<ChangeData> changes) { - Map<Change.Id, ChangeData> result = - Maps.newHashMapWithExpectedSize(changes.size()); - for (ChangeData cd : changes) { - result.put(cd.getId(), cd); - } - return result; + return changes.stream().collect( + Collectors.toMap(ChangeData::getId, cd -> cd)); } public static void ensureChangeLoaded(Iterable<ChangeData> changes) @@ -307,6 +307,7 @@ return cd; } + private boolean lazyLoad = true; private final ReviewDb db; private final GitRepositoryManager repoManager; private final ChangeControl.GenericFactory changeControlFactory; @@ -316,13 +317,16 @@ 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; private final MergeabilityCache mergeabilityCache; private final StarredChangesUtil starredChangesUtil; private final Change.Id legacyId; + private final Map<SubmitRuleOptions, List<SubmitRecord>> + submitRecords = Maps.newLinkedHashMapWithExpectedSize(1); + private Project.NameKey project; private Change change; private ChangeNotes notes; @@ -334,26 +338,30 @@ 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; - private List<SubmitRecord> submitRecords; private Optional<ChangedLines> changedLines; private SubmitTypeRecord submitTypeRecord; private Boolean mergeable; private Set<String> hashtags; - private Set<Account.Id> editsByUser; + private Map<Account.Id, Ref> editsByUser; private Set<Account.Id> reviewedBy; - private Set<Account.Id> draftsByUser; + private Map<Account.Id, Ref> draftsByUser; @Deprecated private Set<Account.Id> starredByUser; private ImmutableMultimap<Account.Id, String> stars; + private ImmutableMap<Account.Id, StarRef> starRefs; private ReviewerSet reviewers; private List<ReviewerStatusUpdate> reviewerUpdates; private PersonIdent author; private PersonIdent committer; + private ImmutableList<byte[]> refStates; + private ImmutableList<byte[]> refStatePatterns; + @AssistedInject private ChangeData( GitRepositoryManager repoManager, @@ -364,7 +372,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -382,7 +390,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 +410,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -419,7 +427,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 +448,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -457,7 +465,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 +487,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -496,7 +504,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 +527,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -538,7 +546,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 +556,11 @@ this.project = null; } + public ChangeData setLazyLoad(boolean load) { + lazyLoad = load; + return this; + } + public ReviewDb db() { return db; } @@ -568,10 +581,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 +593,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,35 +615,57 @@ } Optional<PatchList> r = patchLists.get(psId); if (r == null) { + if (!lazyLoad) { + return Optional.empty(); + } try { r = Optional.of(patchListCache.get(c, ps)); } catch (PatchListNotAvailableException e) { - r = Optional.absent(); + r = Optional.empty(); } patchLists.put(psId, r); } 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.empty(); + } + try { + r = Optional.of(patchListCache.getDiffSummary(c, ps)); + } catch (PatchListNotAvailableException e) { + r = Optional.empty(); + } + diffSummaries.put(psId, r); + } + return r; + } + private Optional<ChangedLines> computeChangedLines() throws OrmException { Change c = change(); if (c == null) { - return Optional.absent(); + return Optional.empty(); } PatchSet ps = currentPatchSet(); if (ps == null) { - return Optional.absent(); + return Optional.empty(); } - Optional<PatchList> p = getPatchList(c, ps); - if (!p.isPresent()) { - return Optional.absent(); - } - return Optional.of( - new ChangedLines(p.get().getInsertions(), p.get().getDeletions())); + return getPatchList(c, ps).map( + p -> new ChangedLines(p.getInsertions(), p.getDeletions())); } public Optional<ChangedLines> changedLines() throws OrmException { if (changedLines == null) { + if (!lazyLoad) { + return Optional.empty(); + } changedLines = computeChangedLines(); } return changedLines; @@ -663,7 +676,7 @@ } public void setNoChangedLines() { - changedLines = Optional.absent(); + changedLines = Optional.empty(); } public Change.Id getId() { @@ -703,10 +716,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 +735,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 +765,21 @@ } public Change reloadChange() throws OrmException { - if (project == null) { - notes = notesFactory.createFromIdOnlyWhenNoteDbDisabled(db, legacyId); - } else { - notes = notesFactory.create(db, project, legacyId); + try { + notes = notesFactory.createChecked(db, project, legacyId); + } catch (NoSuchChangeException e) { + throw new OrmException("Unable to load change " + legacyId, e); } 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 +804,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,17 +901,14 @@ * @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<? super PatchSet> predicate = ps -> { + try { + return changeControl().isPatchVisible(ps, db); + } catch (OrmException e) { + return false; } }; - return FluentIterable.from(patchSets()).filter(predicate).toList(); + return patchSets().stream().filter(predicate).collect(toList()); } public void setPatchSets(Collection<PatchSet> patchSets) { @@ -908,6 +940,9 @@ public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() throws OrmException { if (allApprovals == null) { + if (!lazyLoad) { + return ImmutableListMultimap.of(); + } allApprovals = approvalsUtil.byChange(db, notes()); } return allApprovals; @@ -918,17 +953,17 @@ * @throws OrmException an error occurred reading the database. */ public Optional<PatchSetApproval> getSubmitApproval() - throws OrmException { - for (PatchSetApproval psa : currentApprovals()) { - if (psa.isLegacySubmit()) { - return Optional.fromNullable(psa); - } - } - return Optional.absent(); + throws OrmException { + return currentApprovals().stream() + .filter(PatchSetApproval::isLegacySubmit) + .findFirst(); } public ReviewerSet reviewers() throws OrmException { if (reviewers == null) { + if (!lazyLoad) { + return ReviewerSet.empty(); + } reviewers = approvalsUtil.getReviewers(notes(), approvals().values()); } return reviewers; @@ -944,6 +979,9 @@ public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException { if (reviewerUpdates == null) { + if (!lazyLoad) { + return Collections.emptyList(); + } reviewerUpdates = approvalsUtil.getReviewerUpdates(notes()); } return reviewerUpdates; @@ -957,10 +995,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,17 +1009,38 @@ public List<ChangeMessage> messages() throws OrmException { if (messages == null) { + if (!lazyLoad) { + return Collections.emptyList(); + } messages = cmUtil.byChange(db, notes()); } return messages; } - public void setSubmitRecords(List<SubmitRecord> records) { - submitRecords = records; + public List<SubmitRecord> submitRecords( + SubmitRuleOptions options) throws OrmException { + List<SubmitRecord> records = submitRecords.get(options); + if (records == null) { + if (!lazyLoad) { + return Collections.emptyList(); + } + records = new SubmitRuleEvaluator(this) + .setOptions(options) + .evaluate(); + submitRecords.put(options, records); + } + return records; } - public List<SubmitRecord> getSubmitRecords() { - return submitRecords; + @Nullable + public List<SubmitRecord> getSubmitRecords( + SubmitRuleOptions options) { + return submitRecords.get(options); + } + + public void setSubmitRecords(SubmitRuleOptions options, + List<SubmitRecord> records) { + submitRecords.put(options, records); } public SubmitTypeRecord submitTypeRecord() throws OrmException { @@ -1001,10 +1063,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(); @@ -1028,18 +1101,25 @@ } public Set<Account.Id> editsByUser() throws OrmException { + return editRefs().keySet(); + } + + public Map<Account.Id, Ref> editRefs() throws OrmException { if (editsByUser == null) { + if (!lazyLoad) { + return Collections.emptyMap(); + } Change c = change(); if (c == null) { - return Collections.emptySet(); + return Collections.emptyMap(); } - editsByUser = new HashSet<>(); + editsByUser = new HashMap<>(); Change.Id id = checkNotNull(change.getId()); try (Repository repo = repoManager.openRepository(project())) { - for (String ref - : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) { - if (id.equals(Change.Id.fromEditRefPart(ref))) { - editsByUser.add(Account.Id.fromRefPart(ref)); + for (Map.Entry<String, Ref> e + : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).entrySet()) { + if (id.equals(Change.Id.fromEditRefPart(e.getKey()))) { + editsByUser.put(Account.Id.fromRefPart(e.getKey()), e.getValue()); } } } catch (IOException e) { @@ -1050,14 +1130,31 @@ } public Set<Account.Id> draftsByUser() throws OrmException { + return draftRefs().keySet(); + } + + public Map<Account.Id, Ref> draftRefs() throws OrmException { if (draftsByUser == null) { + if (!lazyLoad) { + return Collections.emptyMap(); + } Change c = change(); if (c == null) { - return Collections.emptySet(); + return Collections.emptyMap(); } - draftsByUser = new HashSet<>(); - for (PatchLineComment sc : plcUtil.draftByChange(db, notes())) { - draftsByUser.add(sc.getAuthor()); + + draftsByUser = new HashMap<>(); + if (notesMigration.readChanges()) { + for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) { + Account.Id account = Account.Id.fromRefSuffix(ref.getName()); + if (account != null) { + draftsByUser.put(account, ref); + } + } + } else { + for (Comment sc : commentsUtil.draftByChange(db, notes())) { + draftsByUser.put(sc.author.getId(), null); + } } } return draftsByUser; @@ -1065,6 +1162,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 +1194,9 @@ public Set<String> hashtags() throws OrmException { if (hashtags == null) { + if (!lazyLoad) { + return Collections.emptySet(); + } hashtags = notes().getHashtags(); } return hashtags; @@ -1106,6 +1209,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,7 +1225,15 @@ public ImmutableMultimap<Account.Id, String> stars() throws OrmException { if (stars == null) { - stars = checkNotNull(starredChangesUtil).byChange(legacyId); + if (!lazyLoad) { + return ImmutableMultimap.of(); + } + ImmutableMultimap.Builder<Account.Id, String> b = + ImmutableMultimap.builder(); + for (Map.Entry<Account.Id, StarRef> e : starRefs().entrySet()) { + b.putAll(e.getKey(), e.getValue().labels()); + } + return b.build(); } return stars; } @@ -1128,6 +1242,16 @@ this.stars = ImmutableMultimap.copyOf(stars); } + public ImmutableMap<Account.Id, StarRef> starRefs() throws OrmException { + if (starRefs == null) { + if (!lazyLoad) { + return ImmutableMap.of(); + } + starRefs = checkNotNull(starredChangesUtil).byChange(legacyId); + } + return starRefs; + } + @AutoValue abstract static class ReviewedByEvent { private static ReviewedByEvent create(ChangeMessage msg) { @@ -1159,4 +1283,20 @@ this.deletions = deletions; } } + + public ImmutableList<byte[]> getRefStates() { + return refStates; + } + + public void setRefStates(Iterable<byte[]> refStates) { + this.refStates = ImmutableList.copyOf(refStates); + } + + public ImmutableList<byte[]> getRefStatePatterns() { + return refStatePatterns; + } + + public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) { + this.refStatePatterns = ImmutableList.copyOf(refStatePatterns); + } }
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..73951c4 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,17 +16,17 @@ 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.base.Enums; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.common.data.SubmitRecord; 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 +34,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; @@ -86,6 +86,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; @@ -97,6 +98,27 @@ extends OperatorFactory<ChangeData, ChangeQueryBuilder> { } + /** + * Converts a operand (operator value) passed to an operator into a + * {@link Predicate}. + * + * Register a ChangeOperandFactory in a config Module like this (note, for + * an example we are using the has predicate, when other predicate plugin + * operands are created they can be registered in a similar manner): + * + * bind(ChangeHasOperandFactory.class) + * .annotatedWith(Exports.named("your has operand")) + * .to(YourClass.class); + * + */ + private interface ChangeOperandFactory { + Predicate<ChangeData> create(ChangeQueryBuilder builder) + throws QueryParseException; + } + + public interface ChangeHasOperandFactory extends ChangeOperandFactory { + } + private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$"); private static final Pattern PAT_CHANGE_ID = Pattern.compile(CHANGE_ID_PATTERN); private static final Pattern DEF_CHANGE = Pattern.compile( @@ -107,6 +129,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 +175,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); @@ -163,13 +187,14 @@ final Provider<InternalChangeQuery> queryProvider; final ChangeIndexRewriter rewriter; final DynamicMap<ChangeOperatorFactory> opFactories; + final DynamicMap<ChangeHasOperandFactory> hasOperands; final IdentifiedUser.GenericFactory userFactory; final CapabilityControl.Factory capabilityControlFactory; final ChangeControl.GenericFactory changeControlGenericFactory; 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; @@ -196,6 +221,7 @@ Provider<InternalChangeQuery> queryProvider, ChangeIndexRewriter rewriter, DynamicMap<ChangeOperatorFactory> opFactories, + DynamicMap<ChangeHasOperandFactory> hasOperands, IdentifiedUser.GenericFactory userFactory, Provider<CurrentUser> self, CapabilityControl.Factory capabilityControlFactory, @@ -203,7 +229,7 @@ ChangeNotes.Factory notesFactory, ChangeData.Factory changeDataFactory, FieldDef.FillArgs fillArgs, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, AccountResolver accountResolver, GroupBackend groupBackend, AllProjectsName allProjectsName, @@ -221,13 +247,13 @@ StarredChangesUtil starredChangesUtil, AccountCache accountCache, @GerritServerConfig Config cfg) { - this(db, queryProvider, rewriter, opFactories, userFactory, self, - capabilityControlFactory, changeControlGenericFactory, notesFactory, - changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend, - allProjectsName, allUsersName, patchListCache, repoManager, - projectCache, listChildProjects, submitDryRun, conflictsCache, - trackingFooters, indexes != null ? indexes.getSearchIndex() : null, - indexConfig, listMembers, starredChangesUtil, accountCache, + this(db, queryProvider, rewriter, opFactories, hasOperands, userFactory, + self, capabilityControlFactory, changeControlGenericFactory, notesFactory, + changeDataFactory, fillArgs, commentsUtil, accountResolver, groupBackend, + allProjectsName, allUsersName, patchListCache, repoManager, projectCache, + listChildProjects, submitDryRun, conflictsCache, trackingFooters, + indexes != null ? indexes.getSearchIndex() : null, indexConfig, listMembers, + starredChangesUtil, accountCache, cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true)); } @@ -236,6 +262,7 @@ Provider<InternalChangeQuery> queryProvider, ChangeIndexRewriter rewriter, DynamicMap<ChangeOperatorFactory> opFactories, + DynamicMap<ChangeHasOperandFactory> hasOperands, IdentifiedUser.GenericFactory userFactory, Provider<CurrentUser> self, CapabilityControl.Factory capabilityControlFactory, @@ -243,7 +270,7 @@ ChangeNotes.Factory notesFactory, ChangeData.Factory changeDataFactory, FieldDef.FillArgs fillArgs, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, AccountResolver accountResolver, GroupBackend groupBackend, AllProjectsName allProjectsName, @@ -272,7 +299,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; @@ -290,15 +317,16 @@ this.starredChangesUtil = starredChangesUtil; this.accountCache = accountCache; this.allowsDrafts = allowsDrafts; + this.hasOperands = hasOperands; } Arguments asUser(CurrentUser otherUser) { - return new Arguments(db, queryProvider, rewriter, opFactories, userFactory, - Providers.of(otherUser), + return new Arguments(db, queryProvider, rewriter, opFactories, + hasOperands, userFactory, Providers.of(otherUser), capabilityControlFactory, changeControlGenericFactory, notesFactory, - changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend, - allProjectsName, allUsersName, patchListCache, repoManager, - projectCache, listChildProjects, submitDryRun, + changeDataFactory, fillArgs, commentsUtil, accountResolver, + groupBackend, allProjectsName, allUsersName, patchListCache, + repoManager, projectCache, listChildProjects, submitDryRun, conflictsCache, trackingFooters, index, indexConfig, listMembers, starredChangesUtil, accountCache, allowsDrafts); } @@ -364,6 +392,10 @@ } } + public Arguments getArgs() { + return args; + } + public ChangeQueryBuilder asUser(CurrentUser user) { return new ChangeQueryBuilder(builderDef, args.asUser(user)); } @@ -417,7 +449,8 @@ } @Operator - public Predicate<ChangeData> status(String statusName) { + public Predicate<ChangeData> status(String statusName) + throws QueryParseException { if ("reviewed".equalsIgnoreCase(statusName)) { return IsReviewedPredicate.create(); } @@ -445,6 +478,16 @@ if ("edit".equalsIgnoreCase(value)) { return new EditByPredicate(self()); } + + // for plugins the value will be operandName_pluginName + String[] names = value.split("_"); + if (names.length == 2) { + ChangeHasOperandFactory op = args.hasOperands.get(names[1], names[0]); + if (op != null) { + return op.create(this); + } + } + throw new IllegalArgumentException(); } @@ -478,6 +521,18 @@ 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)); + } + + if ("submittable".equalsIgnoreCase(value)) { + return new SubmittablePredicate(SubmitRecord.Status.OK); + } + try { return status(value); } catch (IllegalArgumentException e) { @@ -592,6 +647,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 +659,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,14 +678,18 @@ 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) try { group = parseGroup(value).getUUID(); } catch (QueryParseException e) { - throw error("Neither user nor group " + value + " found"); + throw error("Neither user nor group " + value + " found", e); } } } @@ -632,14 +698,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 +710,33 @@ } } - return new LabelPredicate(args.projectCache, - args.changeControlGenericFactory, args.userFactory, args.db, - name, accounts, group); + // If the vote piece looks like Code-Review=NEED with a valid non-numeric + // submit record status, interpret as a submit record query. + int eq = name.indexOf('='); + if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) { + String statusName = name.substring(eq + 1).toUpperCase(); + if (!isInt(statusName)) { + SubmitRecord.Label.Status status = Enums.getIfPresent( + SubmitRecord.Label.Status.class, statusName).orNull(); + if (status == null) { + throw error("Invalid label status " + statusName + " in " + name); + } + return SubmitRecordPredicate.create( + name.substring(0, eq), status, accounts); + } + } + + return new LabelPredicate(args, name, accounts, group); + } + + private static boolean isInt(String s) { + if (s == null) { + return false; + } + if (s.startsWith("+")) { + s = s.substring(1); + } + return Ints.tryParse(s) != null; } @Operator @@ -670,8 +755,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 +763,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 +803,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 +866,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); @@ -957,6 +1035,17 @@ return new CommitterPredicate(who); } + @Operator + public Predicate<ChangeData> submittable(String str) + throws QueryParseException { + SubmitRecord.Status status = Enums.getIfPresent( + SubmitRecord.Status.class, str.toUpperCase()).orNull(); + if (status == null) { + throw error("invalid value for submittable:" + str); + } + return new SubmittablePredicate(status); + } + @Override protected Predicate<ChangeData> defaultField(String query) throws QueryParseException { if (query.startsWith("refs/")) {
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..0bd1800 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)); @@ -251,6 +242,11 @@ } public List<ChangeData> byProjectCommit(Project.NameKey project, + ObjectId id) throws OrmException { + return byProjectCommit(project, id.name()); + } + + public List<ChangeData> byProjectCommit(Project.NameKey project, String hash) throws OrmException { return query(and(project(project), commit(hash))); } @@ -276,7 +272,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 +286,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/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java index 425eb00..f7f98d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -22,7 +22,7 @@ public class LegacyChangeIdPredicate extends ChangeIndexPredicate { private final Change.Id id; - LegacyChangeIdPredicate(Change.Id id) { + public LegacyChangeIdPredicate(Change.Id id) { super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString()); this.id = id; }
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..62ca0e0 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,11 @@ package com.google.gerrit.server.query.change; +import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; +import static com.google.gerrit.extensions.client.ListChangesOption.LABELS; + +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; @@ -21,6 +26,7 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.server.change.ChangeJson; +import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.QueryResult; import com.google.gwtorm.server.OrmException; @@ -101,7 +107,7 @@ String op = m.group(1); throw new AuthException("Must be signed-in to use " + op); } - throw new BadRequestException(e.getMessage()); + throw new BadRequestException(e.getMessage(), e); } return out.size() == 1 ? out.get(0) : out; } @@ -121,7 +127,13 @@ int cnt = queries.size(); List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries)); + boolean requireLazyLoad = + containsAnyOf(options, ImmutableSet.of(DETAILED_LABELS, LABELS)) + && !qb.getArgs().getSchema() + .hasField(ChangeField.STORED_SUBMIT_RECORD_LENIENT); List<List<ChangeInfo>> res = json.create(options) + .lazyLoad(requireLazyLoad + || containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD)) .formatQueryResults(results); for (int n = 0; n < cnt; n++) { List<ChangeInfo> info = res.get(n); @@ -131,4 +143,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/query/change/SubmitRecordPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java new file mode 100644 index 0000000..ec3c56f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -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. + +package com.google.gerrit.server.query.change; + +import static java.util.stream.Collectors.toList; + +import com.google.gerrit.common.data.SubmitRecord; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gerrit.server.query.Predicate; +import com.google.gwtorm.server.OrmException; + +import java.util.Set; + +class SubmitRecordPredicate extends ChangeIndexPredicate { + static Predicate<ChangeData> create(String label, + SubmitRecord.Label.Status status, Set<Account.Id> accounts) { + String lowerLabel = label.toLowerCase(); + if (accounts == null || accounts.isEmpty()) { + return new SubmitRecordPredicate(status.name() + ',' + lowerLabel); + } + return Predicate.or( + accounts.stream() + .map(a -> new SubmitRecordPredicate( + status.name() + ',' + lowerLabel + ',' + a.get())) + .collect(toList())); + } + + private SubmitRecordPredicate(String value) { + super(ChangeField.SUBMIT_RECORD, value); + } + + @Override + public boolean match(ChangeData in) throws OrmException { + return ChangeField.formatSubmitRecordValues(in).contains(getValue()); + } + + @Override + public int getCost() { + return 1; + } +}
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/SubmittablePredicate.java similarity index 64% copy from gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java copy to gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java index cd93ed3..8782cfd 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/SubmittablePredicate.java
@@ -14,26 +14,22 @@ package com.google.gerrit.server.query.change; -import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -@Deprecated -class LegacyReviewerPredicate extends ChangeIndexPredicate { - private final Account.Id id; +class SubmittablePredicate extends ChangeIndexPredicate { + private final SubmitRecord.Status status; - LegacyReviewerPredicate(Account.Id id) { - super(ChangeField.LEGACY_REVIEWER, id.toString()); - this.id = id; - } - - Account.Id getAccountId() { - return id; + SubmittablePredicate(SubmitRecord.Status status) { + super(ChangeField.SUBMIT_RECORD, status.name()); + this.status = status; } @Override - public boolean match(ChangeData object) throws OrmException { - return object.reviewers().all().contains(id); + public boolean match(ChangeData cd) throws OrmException { + return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream() + .anyMatch(r -> r.status == status); } @Override
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..b486bf1 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); @@ -174,6 +175,7 @@ meta.getPermission(Permission.READ, true).setExclusiveGroup(true); grant(config, meta, Permission.READ, admin, owners); grant(config, meta, cr, -2, 2, admin, owners); + grant(config, meta, Permission.CREATE, admin, owners); grant(config, meta, Permission.PUSH, admin, owners); grant(config, meta, Permission.SUBMIT, admin, owners);
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..504767c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -0,0 +1,110 @@ +// 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); + } + } + + public boolean isUpdated() { + return updated; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java index 0b7e8b0..e1b1185 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.schema; +import com.google.common.annotations.VisibleForTesting; import com.google.gerrit.reviewdb.client.CurrentSchemaVersion; import com.google.gerrit.reviewdb.client.SystemConfig; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -116,6 +117,11 @@ } } + @VisibleForTesting + public SchemaVersion getLatestSchemaVersion() { + return updater.get(); + } + private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) { try { return db.schemaVersion().get(new CurrentSchemaVersion.Key());
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..f0a063e 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
@@ -14,6 +14,8 @@ package com.google.gerrit.server.schema; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; import com.google.common.collect.Lists; import com.google.gerrit.reviewdb.client.CurrentSchemaVersion; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -29,11 +31,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; /** 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_138> C = Schema_138.class; public static int getBinaryVersion() { return guessVersion(C); @@ -61,6 +64,11 @@ return versionNbr; } + @VisibleForTesting + public final SchemaVersion getPrior() { + return prior.get(); + } + public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException { if (curr.versionNbr == versionNbr) { @@ -136,11 +144,14 @@ private void migrateData(List<SchemaVersion> pending, UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException { for (SchemaVersion v : pending) { + Stopwatch sw = Stopwatch.createStarted(); ui.message(String.format( "Migrating data to schema %d ...", v.getVersionNbr())); v.migrateData(db, ui); v.finish(curr, db); + ui.message(String.format("\t> Done (%.3f s)", + sw.elapsed(TimeUnit.MILLISECONDS) / 1000d)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java index 946ddcf..cde6b42 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -15,10 +15,9 @@ package com.google.gerrit.server.schema; import com.google.common.base.Joiner; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Change; @@ -102,8 +101,10 @@ } } - Multimap<ObjectId, Ref> changeRefsBySha = ArrayListMultimap.create(); - Multimap<ObjectId, PatchSet.Id> patchSetsBySha = ArrayListMultimap.create(); + Multimap<ObjectId, Ref> changeRefsBySha = + MultimapBuilder.hashKeys().arrayListValues().build(); + Multimap<ObjectId, PatchSet.Id> patchSetsBySha = + MultimapBuilder.hashKeys().arrayListValues().build(); for (Ref ref : refdb.getRefs(RefNames.REFS_CHANGES).values()) { ObjectId id = ref.getObjectId(); if (ref.getObjectId() == null) { @@ -154,7 +155,7 @@ SortedSet<NameKey> projects = repoManager.list(); SortedSet<NameKey> nonExistentProjects = Sets.newTreeSet(); SetMultimap<Project.NameKey, Change.Id> openByProject = - HashMultimap.create(); + MultimapBuilder.hashKeys().hashSetValues().build(); for (Change c : db.changes().all()) { Status status = c.getStatus(); if (status != null && status.isClosed()) {
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_123.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java index d698974..1594829 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.schema; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.RefNames; @@ -57,7 +57,8 @@ @Override protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { - Multimap<Account.Id, Change.Id> imports = ArrayListMultimap.create(); + Multimap<Account.Id, Change.Id> imports = + MultimapBuilder.hashKeys().arrayListValues().build(); try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement(); ResultSet rs = stmt.executeQuery( "SELECT "
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..4f4a866 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,10 +14,11 @@ 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; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Ordering; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountSshKey; @@ -70,7 +71,8 @@ @Override protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { - Multimap<Account.Id, AccountSshKey> imports = ArrayListMultimap.create(); + Multimap<Account.Id, AccountSshKey> imports = + MultimapBuilder.hashKeys().arrayListValues().build(); try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement(); ResultSet rs = stmt.executeQuery( "SELECT " @@ -124,13 +126,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..5dcd981 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.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.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; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +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 { + SortedSet<Project.NameKey> repoList = repoManager.list(); + SortedSet<Project.NameKey> repoUpgraded = new TreeSet<>(); + ui.message("\tMigrating " + repoList.size() + " repositories ..."); + for (Project.NameKey projectName : repoList) { + try (Repository git = repoManager.openRepository(projectName); + MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, + projectName, git)) { + ProjectConfigSchemaUpdate cfg = ProjectConfigSchemaUpdate.read(md); + cfg.removeForceFromPermission("pushTag"); + if (cfg.isUpdated()) { + repoUpgraded.add(projectName); + } + cfg.save(serverUser, COMMIT_MSG); + } catch (ConfigInvalidException | IOException ex) { + throw new OrmException("Cannot migrate project " + projectName, ex); + } + } + ui.message("\tMigration completed: " + repoUpgraded.size() + + " repositories updated:"); + ui.message("\t" + + repoUpgraded.stream().map(n -> n.get()) + .collect(Collectors.joining(" "))); + } +}
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..4e581c8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.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.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; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +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 { + SortedSet<Project.NameKey> repoList = repoManager.list(); + SortedSet<Project.NameKey> repoUpgraded = new TreeSet<>(); + ui.message("\tMigrating " + repoList.size() + " repositories ..."); + for (Project.NameKey projectName : repoList) { + 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); + repoUpgraded.add(projectName); + } + } catch (ConfigInvalidException | IOException ex) { + throw new OrmException("Cannot migrate project " + projectName, ex); + } + } + ui.message("\tMigration completed: " + repoUpgraded.size() + + " repositories updated:"); + ui.message("\t" + + repoUpgraded.stream().map(n -> n.get()) + .collect(Collectors.joining(" "))); + } +}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java similarity index 66% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java index a0fed9e..7c1cde8 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.server.schema; -import com.google.inject.servlet.ServletModule; +import com.google.inject.Inject; +import com.google.inject.Provider; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +public class Schema_132 extends SchemaVersion { + @Inject + Schema_132(Provider<Schema_131> prior) { + super(prior); } }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java similarity index 66% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java index a0fed9e..31d330b 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.server.schema; -import com.google.inject.servlet.ServletModule; +import com.google.inject.Inject; +import com.google.inject.Provider; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +public class Schema_133 extends SchemaVersion { + @Inject + Schema_133(Provider<Schema_132> prior) { + super(prior); } }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java similarity index 66% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java index a0fed9e..fa01ff3 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.server.schema; -import com.google.inject.servlet.ServletModule; +import com.google.inject.Inject; +import com.google.inject.Provider; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +public class Schema_134 extends SchemaVersion { + @Inject + Schema_134(Provider<Schema_133> prior) { + super(prior); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java new file mode 100644 index 0000000..92f150f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java
@@ -0,0 +1,101 @@ +// 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.group.SystemGroupBackend.PROJECT_OWNERS; +import static java.util.stream.Collectors.toSet; + +import com.google.gerrit.common.data.AccessSection; +import com.google.gerrit.common.data.GlobalCapability; +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.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.config.AllProjectsName; +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.gerrit.server.group.SystemGroupBackend; +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; +import java.util.Set; +import java.util.stream.Stream; + +public class Schema_135 extends SchemaVersion { + private static final String COMMIT_MSG = + "Allow admins and project owners to create refs/meta/config"; + + private final GitRepositoryManager repoManager; + private final AllProjectsName allProjectsName; + private final PersonIdent serverUser; + + @Inject + Schema_135(Provider<Schema_134> prior, + GitRepositoryManager repoManager, + AllProjectsName allProjectsName, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.allProjectsName = allProjectsName; + this.serverUser = serverUser; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + try (Repository git = repoManager.openRepository(allProjectsName); + MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, + allProjectsName, git)) { + ProjectConfig config = ProjectConfig.read(md); + + AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true); + Permission createRefsMetaConfigPermission = + meta.getPermission(Permission.CREATE, true); + + Set<GroupReference> groups = + Stream.concat( + config + .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true) + .getPermission(GlobalCapability.ADMINISTRATE_SERVER, true) + .getRules() + .stream() + .map(PermissionRule::getGroup), + Stream.of(SystemGroupBackend.getGroup(PROJECT_OWNERS))) + .filter(g -> createRefsMetaConfigPermission.getRule(g) == null) + .collect(toSet()); + + for (GroupReference group : groups) { + createRefsMetaConfigPermission + .add(new PermissionRule(config.resolve(group))); + } + + md.getCommitBuilder().setAuthor(serverUser); + md.getCommitBuilder().setCommitter(serverUser); + md.setMessage(COMMIT_MSG); + config.commit(md); + } catch (ConfigInvalidException | IOException ex) { + throw new OrmException(ex); + } + } +}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java similarity index 66% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java index a0fed9e..a4b1c82 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.server.schema; -import com.google.inject.servlet.ServletModule; +import com.google.inject.Inject; +import com.google.inject.Provider; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +public class Schema_136 extends SchemaVersion { + @Inject + Schema_136(Provider<Schema_135> prior) { + super(prior); } }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.java similarity index 62% copy from gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.java index a0fed9e..1b4102f 100644 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2015 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. @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ${package}; +package com.google.gerrit.server.schema; -import com.google.inject.servlet.ServletModule; +import com.google.inject.Inject; +import com.google.inject.Provider; -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO +/* change the type of SystemConfig#sitePath to CLOB */ +public class Schema_137 extends SchemaVersion { + @Inject + Schema_137(Provider<Schema_136> prior) { + super(prior); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_138.java similarity index 62% copy from gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_138.java index ea0def0..f824ee1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_138.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. @@ -12,8 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.mail; +package com.google.gerrit.server.schema; -public enum RecipientType { - TO, CC, BCC +import com.google.inject.Inject; +import com.google.inject.Provider; + +/* Add resolved field to PatchLineComment */ +public class Schema_138 extends SchemaVersion { + @Inject + Schema_138(Provider<Schema_137> 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/HostPlatform.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java index 86b3b7364..074df0c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
@@ -18,14 +18,19 @@ import java.security.PrivilegedAction; public final class HostPlatform { - private static final boolean win32 = computeWin32(); + private static final boolean win32 = compute("windows"); + private static final boolean mac = compute("mac"); /** @return true if this JVM is running on a Windows platform. */ public static boolean isWin32() { return win32; } - private static boolean computeWin32() { + public static boolean isMac() { + return mac; + } + + private static boolean compute(String platform) { final String osDotName = AccessController.doPrivileged(new PrivilegedAction<String>() { @Override @@ -34,7 +39,7 @@ } }); return osDotName != null - && osDotName.toLowerCase().contains("windows"); + && osDotName.toLowerCase().contains(platform); } private HostPlatform() {
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/OneOffRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java index 6feb182..f4719aa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java
@@ -14,7 +14,9 @@ package com.google.gerrit.server.util; +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.InternalUser; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; @@ -27,25 +29,34 @@ * Each call to {@link #open()} opens a new {@link ReviewDb}, so this class * should only be used in a bounded try/finally block. * <p> - * The user in the request context is {@link InternalUser}. + * The user in the request context is {@link InternalUser} or the + * {@link IdentifiedUser} associated to the userId passed as parameter. */ @Singleton public class OneOffRequestContext { private final InternalUser.Factory userFactory; private final SchemaFactory<ReviewDb> schemaFactory; private final ThreadLocalRequestContext requestContext; + private final IdentifiedUser.GenericFactory identifiedUserFactory; @Inject OneOffRequestContext(InternalUser.Factory userFactory, SchemaFactory<ReviewDb> schemaFactory, - ThreadLocalRequestContext requestContext) { + ThreadLocalRequestContext requestContext, + IdentifiedUser.GenericFactory identifiedUserFactory) { this.userFactory = userFactory; this.schemaFactory = schemaFactory; this.requestContext = requestContext; + this.identifiedUserFactory = identifiedUserFactory; } public ManualRequestContext open() throws OrmException { return new ManualRequestContext(userFactory.create(), schemaFactory, requestContext); } + + public ManualRequestContext openAs(Account.Id userId) throws OrmException { + return new ManualRequestContext(identifiedUserFactory.create(userId), + schemaFactory, requestContext); + } }
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/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java index 13142fa..ac69ecf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -130,7 +130,7 @@ try { wrapped.call(); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new RuntimeException(e); // Not possible. } }
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..667ef0d 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,9 +14,10 @@ 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; +import com.google.gerrit.server.mail.send.EmailHeader; import java.util.Map; import java.util.Set; @@ -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/java/gerrit/PRED_uploader_1.java b/gerrit-server/src/main/java/gerrit/PRED_uploader_1.java index bea7c8b..06977b3 100644 --- a/gerrit-server/src/main/java/gerrit/PRED_uploader_1.java +++ b/gerrit-server/src/main/java/gerrit/PRED_uploader_1.java
@@ -15,6 +15,7 @@ package gerrit; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.rules.StoredValues; import com.googlecode.prolog_cafe.exceptions.PrologException; @@ -26,7 +27,13 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm; import com.googlecode.prolog_cafe.lang.Term; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class PRED_uploader_1 extends Predicate.P1 { + private static final Logger log = + LoggerFactory.getLogger(PRED_uploader_1.class); + private static final SymbolTerm user = SymbolTerm.intern("user", 1); public PRED_uploader_1(Term a1, Operation n) { @@ -39,7 +46,14 @@ engine.setB0(); Term a1 = arg1.dereference(); - Account.Id uploaderId = StoredValues.getPatchSet(engine).getUploader(); + PatchSet patchSet = StoredValues.getPatchSet(engine); + if (patchSet == null) { + log.error("Failed to load current patch set of change " + + StoredValues.getChange(engine).getChangeId()); + return engine.fail(); + } + + Account.Id uploaderId = patchSet.getUploader(); if (!a1.unify(new StructureTerm(user, new IntegerTerm(uploaderId.get())), engine.trail)) {
diff --git a/gerrit-server/src/main/prolog/BUILD b/gerrit-server/src/main/prolog/BUILD index 555cd90..603a0bf 100644 --- a/gerrit-server/src/main/prolog/BUILD +++ b/gerrit-server/src/main/prolog/BUILD
@@ -1,8 +1,8 @@ -load('//lib/prolog:prolog.bzl', 'prolog_cafe_library') +load("//lib/prolog:prolog.bzl", "prolog_cafe_library") prolog_cafe_library( - name = 'common', - srcs = ['gerrit_common.pl'], - deps = ['//gerrit-server:server'], - visibility = ['//visibility:public'], + name = "common", + srcs = ["gerrit_common.pl"], + visibility = ["//visibility:public"], + deps = ["//gerrit-server:server"], )
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties index f05f23b..f34c992 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,7 +1,8 @@ # Changes to this file should also be made in # gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}. -reviewerNotFound = {0} does not identify a registered user or group +reviewerNotFoundUser = {0} does not identify a registered user +reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group groupIsNotAllowed = The group {0} cannot be added as reviewer. groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
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..c7d4699 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param coverLetter + * @param email + * @param fromName + */ +{template .AbandonedHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>abandoned</strong> this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + {if $coverLetter} + <div style="white-space:pre-wrap">{$coverLetter}</div> + {/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..a034872 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.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 .ChangeFooter template will determine the contents of the footer text + * that will be appended to ALL emails related to changes. + * @param email + */ +{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} +{/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..5091cfe --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -0,0 +1,43 @@ +/** + * 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 .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} + + {if $email.changeUrl} + <div itemscope itemtype="http://schema.org/EmailMessage"> + <div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"> + <link itemprop="url" href="{$email.changeUrl}"/> + <meta itemprop="name" content="View Change"/> + </div> + </div> + {/if} +{/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..0e1f153 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.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} + +/** + * 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 + * @param commentFiles + */ +{template .Comment autoescape="strict" kind="text"} + {$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}{\n} + {\n} + {/if} + + {foreach $group in $commentFiles} + {$group.link}{\n} + {$group.title}:{\n} + {\n} + + {foreach $comment in $group.comments} + {if $comment.isRobotComment} + Robot Comment from {$comment.robotId} (run ID {$comment.robotRunId}): + {\n} + {/if} + + {foreach $line in $comment.lines} + {if isFirst($line)} + {if $comment.startLine != 0} + {$comment.link} + {/if}{\n} + {$comment.linePrefix} + {else} + {$comment.linePrefixEmpty} + {/if} + {$line}{\n} + {/foreach} + {if $comment.parentMessage} + >{sp}{$comment.parentMessage}{\n} + {/if} + {$comment.message}{\n} + {\n} + {\n} + {/foreach} + {/foreach} + {\n} +{/template}
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..73fdfba --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.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 .CommentFooter template will determine the contents of the footer text + * that will be appended to emails related to a user submitting comments on + * changes. + */ +{template .CommentFooter autoescape="strict" kind="text"} +{/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/CommentFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy new file mode 100644 index 0000000..7bf28e7 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.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 .CommentFooterHtml autoescape="strict" kind="html"} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy new file mode 100644 index 0000000..4e67d4e --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -0,0 +1,170 @@ +/** + * 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 commentFiles + * @param commentCount + * @param email + * @param fromName + * @param labels + * @param patchSet + * @param patchSetCommentBlocks + */ +{template .CommentHtml autoescape="strict" kind="html"} + {let $commentHeaderStyle kind="css"} + margin-bottom: 4px; + {/let} + + {let $blockquoteStyle kind="css"} + border-left: 1px solid #aaa; + margin: 10px 0; + padding: 0 10px; + {/let} + + {let $ulStyle kind="css"} + list-style: none; + padding-left: 20px; + {/let} + + {let $voteStyle kind="css"} + border-radius: 3px; + display: inline-block; + margin: 0 2px; + padding: 4px; + {/let} + + {let $positiveVoteStyle kind="css"} + {$voteStyle} + background-color: #d4ffd4; + {/let} + + {let $negativeVoteStyle kind="css"} + {$voteStyle} + background-color: #ffd4d4; + {/let} + + {let $neutralVoteStyle kind="css"} + {$voteStyle} + background-color: #ddd; + {/let} + + <p> + {$fromName} <strong>posted comments</strong> on this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + <p> + Patch set {$patchSet.patchSetId}: + {foreach $label in $labels} + {if $label.value > 0} + <span style="{$positiveVoteStyle}"> + {$label.label}{sp}+{$label.value} + </span> + {elseif $label.value < 0} + <span style="{$negativeVoteStyle}"> + {$label.label}{sp}{$label.value} + </span> + {else} + <span style="{$neutralVoteStyle}"> + -{$label.label} + </span> + {/if} + {/foreach} + </p> + + {if $patchSetCommentBlocks} + {call .WikiFormat}{param content: $patchSetCommentBlocks /}{/call} + {/if} + + {if $commentCount == 1} + <p>(1 comment)</p> + {elseif $commentCount > 1} + <p>({$commentCount} comments)</p> + {/if} + + <ul style="{$ulStyle}"> + {foreach $group in $commentFiles} + <li> + <p> + <a href="{$group.link}">{$group.title}:</a> + </p> + + <ul style="{$ulStyle}"> + {foreach $comment in $group.comments} + <li> + {if $comment.isRobotComment} + <p style="{$commentHeaderStyle}"> + Robot Comment from{sp} + {if $comment.robotUrl}<a href="{$comment.robotUrl}">{/if} + {$comment.robotId} + {if $comment.robotUrl}</a>{/if}{sp} + (run ID {$comment.robotRunId}): + </p> + {/if} + + <p style="{$commentHeaderStyle}"> + {if length($comment.lines) > 0} + <a href="{$comment.link}"> + {if $comment.startLine == 0} + Patch Set #{$group.patchSetId}: + {else} + Patch Set #{$group.patchSetId},{sp} + Line {$comment.startLine}: + {/if} + </a>{sp} + {/if} + {if length($comment.lines) == 1} + <code style="font-family:monospace,monospace"> + {$comment.lines[0]} + </code> + {/if} + </p> + + {if length($comment.lines) > 1} + <p> + <blockquote style="{$blockquoteStyle}"> + {call .Pre}{param content kind="html"} + {foreach $line in $comment.lines} + {$line}{\n} + {/foreach} + {/param}{/call} + </blockquote> + </p> + {/if} + + {if $comment.parentMessage} + <p> + <blockquote style="{$blockquoteStyle}"> + {$comment.parentMessage} + </blockquote> + </p> + {/if} + + {call .WikiFormat}{param content: $comment.messageBlocks /}{/call} + </li> + {/foreach} + </ul> + </li> + {/foreach} + </ul> +{/template}
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..5faa411 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -0,0 +1,43 @@ +/** + * 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 + * @param fromName + */ +{template .DeleteReviewerHtml autoescape="strict" kind="html"} + <p> + {$fromName}{sp} + <strong> + removed{sp} + {foreach $reviewerName in $email.reviewerNames} + {if not isFirst($reviewerName)} + {if isLast($reviewerName)}{sp}and{else},{/if}{sp} + {/if} + {$reviewerName} + {/foreach} + </strong>{sp} + from this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/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..3d76ae2 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param coverLetter + * @param email + * @param fromName + */ +{template .DeleteVoteHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>removed a vote</strong> from this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + {if $coverLetter} + <div style="white-space:pre-wrap">{$coverLetter}</div> + {/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..24db2fd --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
@@ -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. +*/ + +{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. + * @param footers + */ +{template .Footer autoescape="strict" kind="text"} + {foreach $footer in $footers} + {$footer}{\n} + {/foreach} +{/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..9f9c503 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -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. +*/ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param footers + */ +{template .FooterHtml autoescape="strict" kind="html"} + {\n} + {\n} + {foreach $footer in $footers} + <div style="display:none">{sp}{$footer}{sp}</div>{\n} + {/foreach} + {\n} +{/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..fa2b44d --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param email + * @param fromName + */ +{template .MergedHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>merged</strong> this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + <div style="white-space:pre-wrap">{$email.approvals}</div> + + {call .Pre}{param content: $email.changeDetail /}{/call} + + {if $email.includeDiff} + {call .Pre}{param content: $email.unifiedDiff /}{/call} + {/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..eef3a7e --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.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} + +/** + * @param email + * @param fromName + * @param patchSet + * @param projectName + */ +{template .NewChangeHtml autoescape="strict" kind="html"} + <p> + {if $email.reviewerNames} + {$fromName} would like{sp} + {foreach $reviewerName in $email.reviewerNames} + {if not isFirst($reviewerName)} + {if isLast($reviewerName)}{sp}and{else},{/if}{sp} + {/if} + {$reviewerName} + {/foreach}{sp} + to <strong>review</strong> this change. + {else} + {$fromName} uploaded this change for <strong>review</strong>. + {/if} + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + {call .Pre}{param content: $email.changeDetail /}{/call} + + {if $email.sshHost} + {call .Pre}{param content kind="html"} + git pull ssh:{print '//'}{$email.sshHost}/{$projectName} + {sp}{$patchSet.refName} + {/param}{/call} + {/if} + + {if $email.includeDiff} + {call .Pre}{param content: $email.unifiedDiff /}{/call} + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy new file mode 100644 index 0000000..42711c8 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
@@ -0,0 +1,88 @@ +/** + * 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 templates that cannot be overridden. + */ + +/** + * 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} + +/** + * Private template to render PRE block with consistent font-sizing. + * @param content + */ +{template .Pre private="true" autoescape="strict" kind="html"} + {let $preStyle kind="css"} + font-family: monospace,monospace; // Use this to avoid browsers scaling down + // monospace text. + white-space: pre-wrap; + {/let} + <pre style="{$preStyle}">{$content}</pre> +{/template} + +/** + * Take a list of unescaped comment blocks and emit safely escaped HTML to + * render it nicely with wiki-like format. + * + * Each block is a map with a type key. When the type is 'paragraph', or 'pre', + * it also has a 'text' key that maps to the unescaped text content for the + * block. If the type is 'list', the map will have a 'items' key which maps to + * list of unescaped list item strings. If the type is quote, the map will have + * a 'quotedBlocks' key which maps to the blocks contained within the quote. + * + * This mechanism encodes as little structure as possible in order to depend on + * the Soy autoescape mechanism for all of the content. + * + * @param content + */ +{template .WikiFormat private="true" autoescape="strict" kind="html"} + {let $blockquoteStyle kind="css"} + border-left: 1px solid #aaa; + margin: 10px 0; + padding: 0 10px; + {/let} + + {let $pStyle kind="css"} + white-space: pre-wrap; + word-wrap: break-word; + {/let} + + {foreach $block in $content} + {if $block.type == 'paragraph'} + <p style="{$pStyle}">{$block.text}</p> + {elseif $block.type == 'quote'} + <blockquote style="{$blockquoteStyle}"> + {call .WikiFormat}{param content: $block.quotedBlocks /}{/call} + </blockquote> + {elseif $block.type == 'pre'} + {call .Pre}{param content: $block.text /}{/call} + {elseif $block.type == 'list'} + <ul> + {foreach $item in $block.items} + <li>{$item}</li> + {/foreach} + </ul> + {/if} + {/foreach} +{/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..0d19f3f --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -0,0 +1,45 @@ +/** + * 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 + * @param fromName + * @param patchSet + * @param projectName + */ +{template .ReplacePatchSetHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>uploaded patch set #{$patchSet.patchSetId}</strong>{sp} + to this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + {call .Pre}{param content: $email.changeDetail /}{/call} + + {if $email.sshHost} + {call .Pre}{param content kind="html"} + git pull ssh:{print '//'}{$email.sshHost}/{$projectName}{sp} + {$patchSet.refName} + {/param}{/call} + {/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..ea4f615 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param email + * @param fromName + */ +{template .RestoredHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>restored</strong> this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/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..d6407e7 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param email + * @param fromName + */ +{template .RevertedHtml autoescape="strict" kind="html"} + <p> + {$fromName} <strong>reverted</strong> this change. + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.soy new file mode 100644 index 0000000..ca4f267 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssignee.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 .SetAssignee template will determine the contents of the email related + * to a user being assigned to a change. + * @param change + * @param email + * @param fromName + * @param patchSet + * @param projectName + */ +{template .SetAssignee autoescape="strict" kind="text"} + Hello{sp} + {$email.assigneeName}, + + {\n} + {\n} + + {$fromName} has assigned a change to you. + + {sp}Please visit + + {\n} + {\n} + + {sp}{sp}{sp}{sp}{$email.changeUrl} + + {\n} + {\n} + + to view the change. + + {\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/SetAssigneeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy new file mode 100644 index 0000000..bbf16d6 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.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 email + * @param fromName + * @param patchSet + * @param projectName + */ +{template .SetAssigneeHtml autoescape="strict" kind="html"} + <p> + {$fromName} has <strong>assigned</strong> a change to{sp} + {$email.assigneeName}.{sp} + </p> + + {if $email.changeUrl} + <p> + {call .ViewChangeButton data="all" /} + </p> + {/if} + + {call .Pre}{param content: $email.changeDetail /}{/call} + + {if $email.sshHost} + {call .Pre}{param content kind="html"} + git pull ssh:{print '//'}{$email.sshHost}/{$projectName} + {sp}{$patchSet.refName} + {/param}{/call} + {/if} + + {if $email.includeDiff} + {call .Pre}{param content: $email.unifiedDiff /}{/call} + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties index 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/metrics/proc/ProcMetricModuleTest.java b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java index aa4c4eb..70ad137 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -54,13 +54,13 @@ MetricRegistry registry; @Test - public void testConstantBuildLabel() { + public void constantBuildLabel() { Gauge<String> buildLabel = gauge("build/label"); assertThat(buildLabel.getValue()).isEqualTo(Version.getVersion()); } @Test - public void testProcUptime() { + public void procUptime() { Gauge<Long> birth = gauge("proc/birth_timestamp"); assertThat(birth.getValue()).isAtMost( TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())); @@ -70,7 +70,7 @@ } @Test - public void testCounter0() { + public void counter0() { Counter0 cntr = metrics.newCounter( "test/count", new Description("simple test") @@ -87,7 +87,7 @@ } @Test - public void testCounter1() { + public void counter1() { Counter1<String> cntr = metrics.newCounter( "test/count", new Description("simple test") @@ -110,7 +110,7 @@ } @Test - public void testCounterPrefixFields() { + public void counterPrefixFields() { Counter1<String> cntr = metrics.newCounter( "test/count", new Description("simple test") @@ -134,7 +134,7 @@ } @Test - public void testCallbackMetric0() { + public void callbackMetric0() { final CallbackMetric0<Long> cntr = metrics.newCallbackMetric( "test/count", Long.class, @@ -161,13 +161,13 @@ } @Test - public void testInvalidName1() { + public void invalidName1() { exception.expect(IllegalArgumentException.class); metrics.newCounter("invalid name", new Description("fail")); } @Test - public void testInvalidName2() { + public void invalidName2() { exception.expect(IllegalArgumentException.class); metrics.newCounter("invalid/ name", new Description("fail")); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java index e23867f..279ad61 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -70,12 +70,12 @@ } @Test - public void testGerritCommon() { + public void gerritCommon() { runPrologBasedTests(); } @Test - public void testReductionLimit() throws CompileException { + public void reductionLimit() throws CompileException { PrologEnvironment env = envFactory.create(machine); setUpEnvironment(env);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java index 0d2de399..c746c63 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -111,7 +111,7 @@ } @Test - public void testEmailsExistence() { + public void emailsExistence() { assertThat(identifiedUser.hasEmailAddress(TEST_CASES[0])).isTrue(); assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1].toLowerCase())).isTrue(); assertThat(identifiedUser.hasEmailAddress(TEST_CASES[1])).isTrue();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java index 0646eef0..ed42c40 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -24,7 +24,7 @@ * should be escaped. */ @Test - public void testEscapeFirstChar() { + public void escapeFirstChar() { assertEquals(StringUtil.escapeString("\tLeading tab"), "\\tLeading tab"); } @@ -33,7 +33,7 @@ * should be escaped. */ @Test - public void testEscapeLastChar() { + public void escapeLastChar() { assertEquals(StringUtil.escapeString("Trailing tab\t"), "Trailing tab\\t"); } @@ -42,7 +42,7 @@ * in the expected way. */ @Test - public void testEscapeString() { + public void escapeString() { final String[] testPairs = { "", "", "plain string", "plain string",
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java index f5849c1..585dd04 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; -import com.google.common.base.Optional; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountSshKey; @@ -24,6 +23,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; public class AuthorizedKeysTest { private static final String KEY1 = @@ -85,7 +85,7 @@ } @Test - public void testParseWindowsLineEndings() throws Exception { + public void parseWindowsLineEndings() throws Exception { List<Optional<AccountSshKey>> keys = new ArrayList<>(); StringBuilder authorizedKeys = new StringBuilder(); authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY1))); @@ -168,7 +168,7 @@ * @return the expected line for this key in the authorized_keys file */ private static String addDeletedKey(List<Optional<AccountSshKey>> keys) { - keys.add(Optional.<AccountSshKey> absent()); + keys.add(Optional.empty()); return AuthorizedKeys.DELETED_KEY_COMMENT + "\n"; } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java index 3cec25c..f3269c5 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -35,8 +35,7 @@ import com.google.gerrit.reviewdb.client.AccountGroup.UUID; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.group.SystemGroupBackend; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; +import com.google.gerrit.testutil.GerritBaseTests; import org.easymock.IAnswer; import org.junit.Before; @@ -44,11 +43,7 @@ import java.util.Set; -public class UniversalGroupBackendTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - +public class UniversalGroupBackendTest extends GerritBaseTests { private static final AccountGroup.UUID OTHER_UUID = new AccountGroup.UUID("other"); @@ -67,14 +62,14 @@ } @Test - public void testHandles() { + public void handles() { assertTrue(backend.handles(ANONYMOUS_USERS)); assertTrue(backend.handles(PROJECT_OWNERS)); assertFalse(backend.handles(OTHER_UUID)); } @Test - public void testGet() { + public void get() { assertEquals("Registered Users", backend.get(REGISTERED_USERS).getName()); assertEquals("Project Owners", @@ -83,14 +78,14 @@ } @Test - public void testSuggest() { + public void suggest() { assertTrue(backend.suggest("X", null).isEmpty()); assertEquals(1, backend.suggest("project", null).size()); assertEquals(1, backend.suggest("reg", null).size()); } @Test - public void testSytemGroupMemberships() { + public void sytemGroupMemberships() { GroupMembership checker = backend.membershipsOf(user); assertTrue(checker.contains(REGISTERED_USERS)); assertFalse(checker.contains(OTHER_UUID)); @@ -98,7 +93,7 @@ } @Test - public void testKnownGroups() { + public void knownGroups() { GroupMembership checker = backend.membershipsOf(user); Set<UUID> knownGroups = checker.getKnownGroups(); assertEquals(2, knownGroups.size()); @@ -107,7 +102,7 @@ } @Test - public void testOtherMemberships() { + public void otherMemberships() { final AccountGroup.UUID handled = new AccountGroup.UUID("handled"); final AccountGroup.UUID notHandled = new AccountGroup.UUID("not handled"); final IdentifiedUser member = createNiceMock(IdentifiedUser.class);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java index 4f2166d..7f6bb5e 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/WalkSorterTest.java
@@ -26,11 +26,10 @@ import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.server.change.WalkSorter.PatchSetData; import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.testutil.GerritBaseTests; import com.google.gerrit.testutil.InMemoryRepositoryManager; import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo; import com.google.gerrit.testutil.TestChanges; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; @@ -42,11 +41,7 @@ import java.util.ArrayList; import java.util.List; -public class WalkSorterTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - +public class WalkSorterTest extends GerritBaseTests { private Account.Id userId; private InMemoryRepositoryManager repoManager;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java index 6282415..8f7e5b2 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -75,7 +75,7 @@ } @Test - public void testStoreLoadSection() throws Exception { + public void storeLoadSection() throws Exception { SectionInfo d = SectionInfo.defaults(); SectionInfo in = new SectionInfo(); in.missing = "42"; @@ -142,7 +142,7 @@ } @Test - public void testTimeUnit() { + public void timeUnit() { assertEquals(ms(0, MILLISECONDS), parse("0")); assertEquals(ms(2, MILLISECONDS), parse("2ms")); assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java index 12e563f..ab7da99 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -25,14 +25,14 @@ private static final String SOME_INVALID_CHARACTERS = "09AZaz$-_.+!',"; @Test - public void testValidPathSeparator() { + public void validPathSeparator() { for (char c : VALID_CHARACTERS.toCharArray()) { assertTrue("valid character rejected: " + c, GitwebConfig.isValidPathSeparator(c)); } } @Test - public void testInalidPathSeparator() { + public void inalidPathSeparator() { for (char c : SOME_INVALID_CHARACTERS.toCharArray()) { assertFalse("invalid character accepted: " + c, GitwebConfig.isValidPathSeparator(c)); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java index 992502f..bd9f463 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -55,7 +55,7 @@ } @Test - public void testList() throws Exception { + public void list() throws Exception { Map<String, CapabilityInfo> m = injector.getInstance(ListCapabilities.class) .apply(new ConfigResource());
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..88eec7e 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
@@ -40,13 +40,13 @@ } @Test - public void testDefaultSubmitTypeWhenNotConfigured() { + public void defaultSubmitTypeWhenNotConfigured() { assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject"))) .isEqualTo(SubmitType.MERGE_IF_NECESSARY); } @Test - public void testDefaultSubmitTypeForStarFilter() { + public void defaultSubmitTypeForStarFilter() { configureDefaultSubmitType("*", SubmitType.CHERRY_PICK); assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject"))) .isEqualTo(SubmitType.CHERRY_PICK); @@ -58,10 +58,14 @@ configureDefaultSubmitType("*", SubmitType.REBASE_IF_NECESSARY); assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject"))) .isEqualTo(SubmitType.REBASE_IF_NECESSARY); + + configureDefaultSubmitType("*", SubmitType.REBASE_ALWAYS); + assertThat(repoCfg.getDefaultSubmitType(new NameKey("someProject"))) + .isEqualTo(SubmitType.REBASE_ALWAYS); } @Test - public void testDefaultSubmitTypeForSpecificFilter() { + public void defaultSubmitTypeForSpecificFilter() { configureDefaultSubmitType("someProject", SubmitType.CHERRY_PICK); assertThat(repoCfg.getDefaultSubmitType(new NameKey("someOtherProject"))) .isEqualTo(SubmitType.MERGE_IF_NECESSARY); @@ -70,7 +74,7 @@ } @Test - public void testDefaultSubmitTypeForStartWithFilter() { + public void defaultSubmitTypeForStartWithFilter() { configureDefaultSubmitType("somePath/somePath/*", SubmitType.REBASE_IF_NECESSARY); configureDefaultSubmitType("somePath/*", SubmitType.CHERRY_PICK); @@ -96,12 +100,12 @@ } @Test - public void testOwnerGroupsWhenNotConfigured() { + public void ownerGroupsWhenNotConfigured() { assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))).isEmpty(); } @Test - public void testOwnerGroupsForStarFilter() { + public void ownerGroupsForStarFilter() { ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2"); configureOwnerGroups("*", ownerGroups); assertThat(repoCfg.getOwnerGroups(new NameKey("someProject"))) @@ -109,7 +113,7 @@ } @Test - public void testOwnerGroupsForSpecificFilter() { + public void ownerGroupsForSpecificFilter() { ImmutableList<String> ownerGroups = ImmutableList.of("group1", "group2"); configureOwnerGroups("someProject", ownerGroups); assertThat(repoCfg.getOwnerGroups(new NameKey("someOtherProject"))) @@ -119,7 +123,7 @@ } @Test - public void testOwnerGroupsForStartWithFilter() { + public void ownerGroupsForStartWithFilter() { ImmutableList<String> ownerGroups1 = ImmutableList.of("group1"); ImmutableList<String> ownerGroups2 = ImmutableList.of("group2"); ImmutableList<String> ownerGroups3 = ImmutableList.of("group3"); @@ -146,12 +150,12 @@ } @Test - public void testBasePathWhenNotConfigured() { + public void basePathWhenNotConfigured() { assertThat((Object)repoCfg.getBasePath(new NameKey("someProject"))).isNull(); } @Test - public void testBasePathForStarFilter() { + public void basePathForStarFilter() { String basePath = "/someAbsolutePath/someDirectory"; configureBasePath("*", basePath); assertThat(repoCfg.getBasePath(new NameKey("someProject")).toString()) @@ -159,7 +163,7 @@ } @Test - public void testBasePathForSpecificFilter() { + public void basePathForSpecificFilter() { String basePath = "/someAbsolutePath/someDirectory"; configureBasePath("someProject", basePath); assertThat((Object) repoCfg.getBasePath(new NameKey("someOtherProject"))) @@ -169,7 +173,7 @@ } @Test - public void testBasePathForStartWithFilter() { + public void basePathForStartWithFilter() { String basePath1 = "/someAbsolutePath1/someDirectory"; String basePath2 = "someRelativeDirectory2"; String basePath3 = "/someAbsolutePath3/someDirectory"; @@ -192,7 +196,7 @@ } @Test - public void testAllBasePath() { + public void allBasePath() { ImmutableList<Path> allBasePaths = ImmutableList.of( Paths.get("/someBasePath1"), Paths.get("/someBasePath2"),
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java index d5f68cc..e93de50 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -32,7 +32,7 @@ private static final DateTime NOW = DateTime.parse("2014-06-13T10:00:00-00:00"); @Test - public void testInitialDelay() throws Exception { + public void initialDelay() throws Exception { assertEquals(ms(1, HOURS), initialDelay("11:00", "1h")); assertEquals(ms(30, MINUTES), initialDelay("05:30", "1h")); assertEquals(ms(30, MINUTES), initialDelay("09:30", "1h")); @@ -56,7 +56,7 @@ } @Test - public void testCustomKeys() { + public void customKeys() { Config rc = new Config(); rc.setString("a", "b", "i", "1h"); rc.setString("a", "b", "s", "01:00");
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java index 8cdd42b..29a7dde 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
@@ -33,7 +33,7 @@ public class SitePathsTest extends GerritBaseTests { @Test - public void testCreate_NotExisting() throws IOException { + public void create_NotExisting() throws IOException { final Path root = random(); final SitePaths site = new SitePaths(root); assertTrue(site.isNew); @@ -42,7 +42,7 @@ } @Test - public void testCreate_Empty() throws IOException { + public void create_Empty() throws IOException { final Path root = random(); try { Files.createDirectory(root); @@ -56,7 +56,7 @@ } @Test - public void testCreate_NonEmpty() throws IOException { + public void create_NonEmpty() throws IOException { final Path root = random(); final Path txt = root.resolve("test.txt"); try { @@ -73,7 +73,7 @@ } @Test - public void testCreate_NotDirectory() throws IOException { + public void create_NotDirectory() throws IOException { final Path root = random(); try { Files.createFile(root); @@ -85,7 +85,7 @@ } @Test - public void testResolve() throws IOException { + public void resolve() throws IOException { final Path root = random(); final SitePaths site = new SitePaths(root);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java index 6a006cd..b6e73ff 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -28,7 +28,7 @@ public class EventDeserializerTest { @Test - public void testRefUpdatedEvent() { + public void refUpdatedEvent() { RefUpdatedEvent refUpdatedEvent = new RefUpdatedEvent(); RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java index 3cbb59c..de13fd8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java
@@ -34,7 +34,7 @@ } @Test - public void testEventTypeRegistration() { + public void eventTypeRegistration() { EventTypes.register(TestEvent.TYPE, TestEvent.class); EventTypes.register(AnotherTestEvent.TYPE, AnotherTestEvent.class); assertThat(EventTypes.getClass(TestEvent.TYPE)).isEqualTo(TestEvent.class); @@ -43,7 +43,7 @@ } @Test - public void testGetClassForNonExistingType() { + public void getClassForNonExistingType() { Class<?> clazz = EventTypes.getClass("does-not-exist-event"); assertThat(clazz).isNull(); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java index fde86a5..6cdf6c9 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
@@ -27,6 +27,7 @@ import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; import org.junit.Before; import org.junit.Test; @@ -37,7 +38,7 @@ import java.util.Set; public class GroupListTest { - + private static final Project.NameKey PROJECT = new Project.NameKey("project"); private static final String TEXT = "# UUID \tGroup Name\n" + "#\n" + "d96b998f8a66ff433af50befb975d0e2bb6e0999\tNon-Interactive Users\n" @@ -49,11 +50,11 @@ public void setup() throws IOException { ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class); replay(sink); - groupList = GroupList.parse(TEXT, sink); + groupList = GroupList.parse(PROJECT, TEXT, sink); } @Test - public void testByUUID() throws Exception { + public void byUUID() throws Exception { AccountGroup.UUID uuid = new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999"); @@ -64,7 +65,7 @@ } @Test - public void testPut() { + public void put() { AccountGroup.UUID uuid = new AccountGroup.UUID("abc"); GroupReference groupReference = new GroupReference(uuid, "Hutzliputz"); @@ -76,7 +77,7 @@ } @Test - public void testReferences() throws Exception { + public void references() throws Exception { Collection<GroupReference> result = groupList.references(); assertEquals(2, result.size()); @@ -88,7 +89,7 @@ } @Test - public void testUUIDs() throws Exception { + public void uUIDs() throws Exception { Set<AccountGroup.UUID> result = groupList.uuids(); assertEquals(2, result.size()); @@ -98,17 +99,17 @@ } @Test - public void testValidationError() throws Exception { + public void validationError() throws Exception { ValidationError.Sink sink = createMock(ValidationError.Sink.class); sink.error(anyObject(ValidationError.class)); expectLastCall().times(2); replay(sink); - groupList = GroupList.parse(TEXT.replace("\t", " "), sink); + groupList = GroupList.parse(PROJECT, TEXT.replace("\t", " "), sink); verify(sink); } @Test - public void testRetainAll() throws Exception { + public void retainAll() throws Exception { AccountGroup.UUID uuid = new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999"); groupList.retainUUIDs(Collections.singleton(uuid)); @@ -119,7 +120,7 @@ } @Test - public void testAsText() throws Exception { + public void asText() throws Exception { assertTrue(TEXT.equals(groupList.asText())); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java index 86fa0db..e30e3fc 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -15,9 +15,11 @@ package com.google.gerrit.server.git; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.util.HostPlatform; import com.google.gerrit.testutil.TempFileUtil; import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.server.StandardKeyEncoder; @@ -34,6 +36,7 @@ import org.junit.Test; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; public class LocalDiskRepositoryManagerTest extends EasyMockSupport { @@ -53,7 +56,6 @@ cfg = new Config(); cfg.setString("gerrit", null, "basePath", "git"); repoManager = new LocalDiskRepositoryManager(site, cfg); - repoManager.start(); } @Test(expected = IllegalStateException.class) @@ -62,7 +64,7 @@ } @Test - public void testProjectCreation() throws Exception { + public void projectCreation() throws Exception { Project.NameKey projectA = new Project.NameKey("projectA"); try (Repository repo = repoManager.createRepository(projectA)) { assertThat(repo).isNotNull(); @@ -164,8 +166,22 @@ repoManager.createRepository(new Project.NameKey("project\\rA")); } + @Test(expected = IllegalStateException.class) + public void testProjectRecreation() throws Exception { + repoManager.createRepository(new Project.NameKey("a")); + repoManager.createRepository(new Project.NameKey("a")); + } + + @Test(expected = IllegalStateException.class) + public void testProjectRecreationAfterRestart() throws Exception { + repoManager.createRepository(new Project.NameKey("a")); + LocalDiskRepositoryManager newRepoManager = + new LocalDiskRepositoryManager(site, cfg); + newRepoManager.createRepository(new Project.NameKey("a")); + } + @Test - public void testOpenRepositoryCreatedDirectlyOnDisk() throws Exception { + public void openRepositoryCreatedDirectlyOnDisk() throws Exception { Project.NameKey projectA = new Project.NameKey("projectA"); createRepository(repoManager.getBasePath(projectA), projectA.get()); try (Repository repo = repoManager.openRepository(projectA)) { @@ -174,13 +190,48 @@ assertThat(repoManager.list()).containsExactly(projectA); } + @Test(expected = RepositoryCaseMismatchException.class) + public void testNameCaseMismatch() throws Exception { + assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue(); + repoManager.createRepository(new Project.NameKey("a")); + repoManager.createRepository(new Project.NameKey("A")); + } + + @Test(expected = RepositoryCaseMismatchException.class) + public void testNameCaseMismatchWithSymlink() throws Exception { + assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue(); + Project.NameKey name = new Project.NameKey("a"); + repoManager.createRepository(name); + createSymLink(name, "b.git"); + repoManager.createRepository(new Project.NameKey("B")); + } + + @Test(expected = RepositoryCaseMismatchException.class) + public void testNameCaseMismatchAfterRestart() throws Exception { + assume().that(HostPlatform.isWin32() || HostPlatform.isMac()).isTrue(); + Project.NameKey name = new Project.NameKey("a"); + repoManager.createRepository(name); + + LocalDiskRepositoryManager newRepoManager = + new LocalDiskRepositoryManager(site, cfg); + newRepoManager.createRepository(new Project.NameKey("A")); + } + + private void createSymLink(Project.NameKey project, String link) + throws IOException { + Path base = repoManager.getBasePath(project); + Path projectDir = base.resolve(project.get() + ".git"); + Path symlink = base.resolve(link); + Files.createSymbolicLink(symlink, projectDir); + } + @Test(expected = RepositoryNotFoundException.class) public void testOpenRepositoryInvalidName() throws Exception { repoManager.openRepository(new Project.NameKey("project%?|<>A")); } @Test - public void testList() throws Exception { + public void list() throws Exception { Project.NameKey projectA = new Project.NameKey("projectA"); createRepository(repoManager.getBasePath(projectA), projectA.get()); @@ -197,28 +248,6 @@ .containsExactly(projectA, projectB, projectC); } - @Test - public void testGetSetProjectDescription() throws Exception { - Project.NameKey projectA = new Project.NameKey("projectA"); - try (Repository repo = repoManager.createRepository(projectA)) { - assertThat(repo).isNotNull(); - } - - assertThat(repoManager.getProjectDescription(projectA)).isNull(); - repoManager.setProjectDescription(projectA, "projectA description"); - assertThat(repoManager.getProjectDescription(projectA)).isEqualTo( - "projectA description"); - - repoManager.setProjectDescription(projectA, ""); - assertThat(repoManager.getProjectDescription(projectA)).isNull(); - } - - @Test(expected = RepositoryNotFoundException.class) - public void testGetProjectDescriptionFromUnexistingRepository() - throws Exception { - repoManager.getProjectDescription(new Project.NameKey("projectA")); - } - private void createRepository(Path directory, String projectName) throws IOException { String n = projectName + Constants.DOT_GIT_EXT;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java index b26a228..accf778 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -23,9 +23,8 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.config.RepositoryConfig; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.testutil.GerritBaseTests; import com.google.gerrit.testutil.TempFileUtil; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; @@ -45,12 +44,7 @@ import java.util.Arrays; import java.util.SortedSet; -public class MultiBaseLocalDiskRepositoryManagerTest { - - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - +public class MultiBaseLocalDiskRepositoryManagerTest extends GerritBaseTests { private Config cfg; private SitePaths site; private MultiBaseLocalDiskRepositoryManager repoManager; @@ -75,8 +69,9 @@ } @Test - public void testDefaultRepositoryLocation() - throws RepositoryCaseMismatchException, RepositoryNotFoundException { + public void defaultRepositoryLocation() + throws RepositoryCaseMismatchException, RepositoryNotFoundException, + IOException { Project.NameKey someProjectKey = new Project.NameKey("someProject"); Repository repo = repoManager.createRepository(someProjectKey); assertThat(repo.getDirectory()).isNotNull(); @@ -102,7 +97,7 @@ } @Test - public void testAlternateRepositoryLocation() throws IOException { + public void alternateRepositoryLocation() throws IOException { Path alternateBasePath = TempFileUtil.createTempDirectory().toPath(); Project.NameKey someProjectKey = new Project.NameKey("someProject"); reset(configMock); @@ -135,7 +130,7 @@ } @Test - public void testListReturnRepoFromProperLocation() throws IOException { + public void listReturnRepoFromProperLocation() throws IOException { Project.NameKey basePathProject = new Project.NameKey("basePathProject"); Project.NameKey altPathProject = new Project.NameKey("altPathProject"); Project.NameKey misplacedProject1 =
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java index 0757a26..94220f6 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -78,7 +78,7 @@ } @Test - public void testReadConfig() throws Exception { + public void readConfig() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -125,7 +125,7 @@ } @Test - public void testReadConfigLabelDefaultValue() throws Exception { + public void readConfigLabelDefaultValue() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -142,7 +142,7 @@ } @Test - public void testReadConfigLabelDefaultValueInRange() throws Exception { + public void readConfigLabelDefaultValueInRange() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -160,7 +160,7 @@ } @Test - public void testReadConfigLabelDefaultValueNotInRange() throws Exception { + public void readConfigLabelDefaultValueNotInRange() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -179,7 +179,7 @@ } @Test - public void testReadConfigLabelScores() throws Exception { + public void readConfigLabelScores() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -203,7 +203,7 @@ } @Test - public void testEditConfig() throws Exception { + public void editConfig() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""// @@ -256,7 +256,7 @@ } @Test - public void testEditConfigMissingGroupTableEntry() throws Exception { + public void editConfigMissingGroupTableEntry() throws Exception { RevCommit rev = util.commit(util.tree( // util.file("groups", util.blob(group(developers))), // util.file("project.config", util.blob(""//
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java index 839d349..693abfb 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -15,17 +15,19 @@ package com.google.gerrit.server.index.change; import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Table; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.testutil.GerritBaseTests; import com.google.gerrit.testutil.TestTimeUtil; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; import org.junit.After; import org.junit.Before; @@ -36,10 +38,6 @@ import java.util.concurrent.TimeUnit; public class ChangeFieldTest extends GerritBaseTests { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - @Before public void setUp() { TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS); @@ -70,4 +68,63 @@ assertThat(ChangeField.parseReviewerFieldValues(values)) .isEqualTo(reviewers); } + + @Test + public void formatSubmitRecordValues() { + assertThat( + ChangeField.formatSubmitRecordValues( + ImmutableList.of( + record( + SubmitRecord.Status.OK, + label(SubmitRecord.Label.Status.MAY, "Label-1", null), + label(SubmitRecord.Label.Status.OK, "Label-2", 1))), + new Account.Id(1))) + .containsExactly( + "OK", + "MAY,label-1", + "OK,label-2", + "OK,label-2,0", + "OK,label-2,1"); + } + + @Test + public void storedSubmitRecords() { + assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED)); + assertStoredRecordRoundTrip( + record( + SubmitRecord.Status.OK, + label(SubmitRecord.Label.Status.MAY, "Label-1", null), + label(SubmitRecord.Label.Status.OK, "Label-2", 1))); + } + + private static SubmitRecord record(SubmitRecord.Status status, + SubmitRecord.Label... labels) { + SubmitRecord r = new SubmitRecord(); + r.status = status; + if (labels.length > 0) { + r.labels = ImmutableList.copyOf(labels); + } + return r; + } + + private static SubmitRecord.Label label(SubmitRecord.Label.Status status, + String label, Integer appliedBy) { + SubmitRecord.Label l = new SubmitRecord.Label(); + l.status = status; + l.label = label; + if (appliedBy != null) { + l.appliedBy = new Account.Id(appliedBy); + } + return l; + } + + private static void assertStoredRecordRoundTrip(SubmitRecord... records) { + List<SubmitRecord> recordList = ImmutableList.copyOf(records); + List<String> stored = ChangeField.storedSubmitRecords(recordList).stream() + .map(s -> new String(s, UTF_8)) + .collect(toList()); + assertThat(ChangeField.parseSubmitRecords(stored)) + .named("JSON %s" + stored) + .isEqualTo(recordList); + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java index ac7aed7..acf6b14 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -65,13 +65,13 @@ } @Test - public void testIndexPredicate() throws Exception { + public void indexPredicate() throws Exception { Predicate<ChangeData> in = parse("file:a"); assertThat(rewrite(in)).isEqualTo(query(in)); } @Test - public void testNonIndexPredicate() throws Exception { + public void nonIndexPredicate() throws Exception { Predicate<ChangeData> in = parse("foo:a"); Predicate<ChangeData> out = rewrite(in); assertThat(AndChangeSource.class).isSameAs(out.getClass()); @@ -81,13 +81,13 @@ } @Test - public void testIndexPredicates() throws Exception { + public void indexPredicates() throws Exception { Predicate<ChangeData> in = parse("file:a file:b"); assertThat(rewrite(in)).isEqualTo(query(in)); } @Test - public void testNonIndexPredicates() throws Exception { + public void nonIndexPredicates() throws Exception { Predicate<ChangeData> in = parse("foo:a OR foo:b"); Predicate<ChangeData> out = rewrite(in); assertThat(AndChangeSource.class).isSameAs(out.getClass()); @@ -97,7 +97,7 @@ } @Test - public void testOneIndexPredicate() throws Exception { + public void oneIndexPredicate() throws Exception { Predicate<ChangeData> in = parse("foo:a file:b"); Predicate<ChangeData> out = rewrite(in); assertThat(AndChangeSource.class).isSameAs(out.getClass()); @@ -109,7 +109,7 @@ } @Test - public void testThreeLevelTreeWithAllIndexPredicates() throws Exception { + public void threeLevelTreeWithAllIndexPredicates() throws Exception { Predicate<ChangeData> in = parse("-status:abandoned (file:a OR file:b)"); assertThat(rewrite.rewrite(in, options(0, DEFAULT_MAX_QUERY_LIMIT))) @@ -117,7 +117,7 @@ } @Test - public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception { + public void threeLevelTreeWithSomeIndexPredicates() throws Exception { Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)"); Predicate<ChangeData> out = rewrite(in); assertThat(out.getClass()).isSameAs(AndChangeSource.class); @@ -129,7 +129,7 @@ } @Test - public void testMultipleIndexPredicates() throws Exception { + public void multipleIndexPredicates() throws Exception { Predicate<ChangeData> in = parse("file:a OR foo:b OR file:c OR foo:d"); Predicate<ChangeData> out = rewrite(in); @@ -143,7 +143,7 @@ } @Test - public void testIndexAndNonIndexPredicates() throws Exception { + public void indexAndNonIndexPredicates() throws Exception { Predicate<ChangeData> in = parse("status:new bar:p file:a"); Predicate<ChangeData> out = rewrite(in); assertThat(AndChangeSource.class).isSameAs(out.getClass()); @@ -155,7 +155,7 @@ } @Test - public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception { + public void duplicateCompoundNonIndexOnlyPredicates() throws Exception { Predicate<ChangeData> in = parse("(status:new OR status:draft) bar:p file:a"); Predicate<ChangeData> out = rewrite(in); @@ -168,7 +168,7 @@ } @Test - public void testDuplicateCompoundIndexOnlyPredicates() throws Exception { + public void duplicateCompoundIndexOnlyPredicates() throws Exception { Predicate<ChangeData> in = parse("(status:new OR file:a) bar:p file:b"); Predicate<ChangeData> out = rewrite(in); @@ -181,7 +181,7 @@ } @Test - public void testOptionsArgumentOverridesAllLimitPredicates() + public void optionsArgumentOverridesAllLimitPredicates() throws Exception { Predicate<ChangeData> in = parse("limit:1 file:a limit:3"); Predicate<ChangeData> out = rewrite(in, options(0, 5)); @@ -195,7 +195,7 @@ } @Test - public void testStartIncreasesLimitInQueryButNotPredicate() throws Exception { + public void startIncreasesLimitInQueryButNotPredicate() throws Exception { int n = 3; Predicate<ChangeData> f = parse("file:a"); Predicate<ChangeData> l = parse("limit:" + n); @@ -209,7 +209,7 @@ } @Test - public void testGetPossibleStatus() throws Exception { + public void getPossibleStatus() throws Exception { assertThat(status("file:a")).isEqualTo(EnumSet.allOf(Change.Status.class)); assertThat(status("is:new")).containsExactly(NEW); assertThat(status("-is:new")) @@ -225,7 +225,7 @@ } @Test - public void testUnsupportedIndexOperator() throws Exception { + public void unsupportedIndexOperator() throws Exception { Predicate<ChangeData> in = parse("status:merged file:a"); assertThat(rewrite(in)).isEqualTo(query(in)); @@ -240,7 +240,7 @@ } @Test - public void testTooManyTerms() throws Exception { + public void tooManyTerms() throws Exception { String q = "file:a OR file:b OR file:c"; Predicate<ChangeData> in = parse(q); assertEquals(query(in), rewrite(in)); @@ -258,7 +258,7 @@ } @Test - public void testAddingStartToLimitDoesNotExceedBackendLimit() throws Exception { + public void addingStartToLimitDoesNotExceedBackendLimit() throws Exception { int max = CONFIG.maxLimit(); assertEquals(options(0, max), convertOptions(options(0, max))); assertEquals(options(0, max), convertOptions(options(1, max)));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java index 545fd08..e59067a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -29,8 +29,8 @@ FakeQueryBuilder.class), new ChangeQueryBuilder.Arguments(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, indexes, null, null, null, null, null, null, null, - null)); + null, null, null, null, indexes, null, null, null, null, null, null, + null, null)); } @Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java new file mode 100644 index 0000000..913ce93 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -0,0 +1,359 @@ +// 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.index.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; +import static com.google.gerrit.server.index.change.StalenessChecker.refsAreStale; +import static com.google.gerrit.testutil.TestChanges.newChange; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.ListMultimap; +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.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.change.StalenessChecker.RefState; +import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.testutil.GerritBaseTests; +import com.google.gerrit.testutil.InMemoryRepositoryManager; +import com.google.gwtorm.protobuf.CodecFactory; +import com.google.gwtorm.protobuf.ProtobufCodec; + +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; +import org.junit.Test; + +import java.util.stream.Stream; + +public class StalenessCheckerTest extends GerritBaseTests { + private static final String SHA1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + private static final String SHA2 = "badc0feebadc0feebadc0feebadc0feebadc0fee"; + + private static final Project.NameKey P1 = new Project.NameKey("project1"); + private static final Project.NameKey P2 = new Project.NameKey("project2"); + + private static final Change.Id C = new Change.Id(1234); + + private static final ProtobufCodec<Change> CHANGE_CODEC = + CodecFactory.encoder(Change.class); + + private GitRepositoryManager repoManager; + private Repository r1; + private Repository r2; + private TestRepository<Repository> tr1; + private TestRepository<Repository> tr2; + + @Before + public void setUp() throws Exception { + repoManager = new InMemoryRepositoryManager(); + r1 = repoManager.createRepository(P1); + tr1 = new TestRepository<>(r1); + r2 = repoManager.createRepository(P2); + tr2 = new TestRepository<>(r2); + } + + @Test + public void parseStates() { + assertInvalidState(null); + assertInvalidState(""); + assertInvalidState("project1:refs/heads/foo"); + assertInvalidState("project1:refs/heads/foo:notasha"); + assertInvalidState("project1:refs/heads/foo:"); + + assertThat( + StalenessChecker.parseStates( + byteArrays( + P1 + ":refs/heads/foo:" + SHA1, + P1 + ":refs/heads/bar:" + SHA2, + P2 + ":refs/heads/baz:" + SHA1))) + .isEqualTo( + ImmutableSetMultimap.of( + P1, RefState.create("refs/heads/foo", SHA1), + P1, RefState.create("refs/heads/bar", SHA2), + P2, RefState.create("refs/heads/baz", SHA1))); + } + + private static void assertInvalidState(String state) { + try { + StalenessChecker.parseStates(byteArrays(state)); + assert_().fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void refStateToByteArray() { + assertThat( + new String( + RefState.create("refs/heads/foo", ObjectId.fromString(SHA1)) + .toByteArray(P1), + UTF_8)) + .isEqualTo(P1 + ":refs/heads/foo:" + SHA1); + assertThat( + new String( + RefState.create("refs/heads/foo", (ObjectId) null) + .toByteArray(P1), + UTF_8)) + .isEqualTo(P1 + ":refs/heads/foo:" + ObjectId.zeroId().name()); + } + + @Test + public void parsePatterns() { + assertInvalidPattern(null); + assertInvalidPattern(""); + assertInvalidPattern("project:"); + assertInvalidPattern("project:refs/heads/foo"); + assertInvalidPattern("project:refs/he*ds/bar"); + assertInvalidPattern("project:refs/(he)*ds/bar"); + assertInvalidPattern("project:invalidrefname"); + + ListMultimap<Project.NameKey, RefStatePattern> r = + StalenessChecker.parsePatterns( + byteArrays( + P1 + ":refs/heads/*", + P2 + ":refs/heads/foo/*/bar", + P2 + ":refs/heads/foo/*-baz/*/quux")); + + assertThat(r.keySet()).containsExactly(P1, P2); + RefStatePattern p = r.get(P1).get(0); + assertThat(p.pattern()).isEqualTo("refs/heads/*"); + assertThat(p.prefix()).isEqualTo("refs/heads/"); + assertThat(p.regex().pattern()).isEqualTo("^\\Qrefs/heads/\\E.*\\Q\\E$"); + assertThat(p.match("refs/heads/foo")).isTrue(); + assertThat(p.match("xrefs/heads/foo")).isFalse(); + assertThat(p.match("refs/tags/foo")).isFalse(); + + p = r.get(P2).get(0); + assertThat(p.pattern()).isEqualTo("refs/heads/foo/*/bar"); + assertThat(p.prefix()).isEqualTo("refs/heads/foo/"); + assertThat(p.regex().pattern()) + .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q/bar\\E$"); + assertThat(p.match("refs/heads/foo//bar")).isTrue(); + assertThat(p.match("refs/heads/foo/x/bar")).isTrue(); + assertThat(p.match("refs/heads/foo/x/y/bar")).isTrue(); + assertThat(p.match("refs/heads/foo/x/baz")).isFalse(); + + p = r.get(P2).get(1); + assertThat(p.pattern()).isEqualTo("refs/heads/foo/*-baz/*/quux"); + assertThat(p.prefix()).isEqualTo("refs/heads/foo/"); + assertThat(p.regex().pattern()) + .isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q-baz/\\E.*\\Q/quux\\E$"); + assertThat(p.match("refs/heads/foo/-baz//quux")).isTrue(); + assertThat(p.match("refs/heads/foo/x-baz/x/quux")).isTrue(); + assertThat(p.match("refs/heads/foo/x/y-baz/x/y/quux")).isTrue(); + assertThat(p.match("refs/heads/foo/x-baz/x/y")).isFalse(); + } + + @Test + public void refStatePatternToByteArray() { + assertThat( + new String(RefStatePattern.create("refs/*").toByteArray(P1), UTF_8)) + .isEqualTo(P1 + ":refs/*"); + } + + private static void assertInvalidPattern(String state) { + try { + StalenessChecker.parsePatterns(byteArrays(state)); + assert_().fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + @Test + public void isStaleRefStatesOnly() throws Exception { + String ref1 = "refs/heads/foo"; + ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1")); + String ref2 = "refs/heads/bar"; + ObjectId id2 = tr2.update(ref2, tr2.commit().message("commit 2")); + + // Not stale. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P2, RefState.create(ref2, id2.name())), + ImmutableMultimap.of())) + .isFalse(); + + // Wrong ref value. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, SHA1), + P2, RefState.create(ref2, id2.name())), + ImmutableMultimap.of())) + .isTrue(); + + // Swapped repos. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id2.name()), + P2, RefState.create(ref2, id1.name())), + ImmutableMultimap.of())) + .isTrue(); + + // Two refs in same repo, not stale. + String ref3 = "refs/heads/baz"; + ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3")); + tr1.update(ref3, id3); + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, id3.name())), + ImmutableMultimap.of())) + .isFalse(); + + // Ignore ref not mentioned. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableMultimap.of())) + .isFalse(); + + // One ref wrong. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, SHA1)), + ImmutableMultimap.of())) + .isTrue(); + } + + @Test + public void isStaleWithRefStatePatterns() throws Exception { + String ref1 = "refs/heads/foo"; + ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1")); + + // ref1 is only ref matching pattern. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/heads/*")))) + .isFalse(); + + // Now ref2 matches pattern, so stale unless ref2 is present in state map. + String ref2 = "refs/heads/bar"; + ObjectId id2 = tr1.update(ref2, tr1.commit().message("commit 2")); + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/heads/*")))) + .isTrue(); + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref2, id2.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/heads/*")))) + .isFalse(); + } + + @Test + public void isStaleWithNonPrefixPattern() throws Exception { + String ref1 = "refs/heads/foo"; + ObjectId id1 = tr1.update(ref1, tr1.commit().message("commit 1")); + tr1.update("refs/heads/bar", tr1.commit().message("commit 2")); + + // ref1 is only ref matching pattern. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/*/foo")))) + .isFalse(); + + // Now ref2 matches pattern, so stale unless ref2 is present in state map. + String ref3 = "refs/other/foo"; + ObjectId id3 = tr1.update(ref3, tr1.commit().message("commit 3")); + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/*/foo")))) + .isTrue(); + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, id3.name())), + ImmutableMultimap.of( + P1, RefStatePattern.create("refs/*/foo")))) + .isFalse(); + } + + @Test + public void reviewDbChangeIsStale() throws Exception { + Change indexChange = newChange(P1, new Account.Id(1)); + indexChange.setNoteDbState(SHA1); + + assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null)) + .isFalse(); + + Change noteDbPrimary = clone(indexChange); + noteDbPrimary.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + assertThat( + StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary)) + .isFalse(); + + assertThat( + StalenessChecker.reviewDbChangeIsStale( + indexChange, clone(indexChange))) + .isFalse(); + + // Can't easily change row version to check true case. + } + + private static Iterable<byte[]> byteArrays(String... strs) { + return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null) + .collect(toList()); + } + + private static Change clone(Change change) { + return CHANGE_CODEC.decode(CHANGE_CODEC.encodeToByteArray(change)); + } + +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java index 049e17d..51abe2b 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -48,7 +48,7 @@ * Test that only lines with at least one column of text emit output. */ @Test - public void testEmptyLine() { + public void emptyLine() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t'); @@ -67,7 +67,7 @@ * Test that there is no output if no columns are ever added. */ @Test - public void testEmptyOutput() { + public void emptyOutput() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t'); @@ -82,7 +82,7 @@ * the output immediately after the creation of the {@link ColumnFormatter}. */ @Test - public void testNoNextLine() { + public void noNextLine() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t'); @@ -95,7 +95,7 @@ * (which of course shouldn't be escaped) is left alone. */ @Test - public void testEscapingTakesPlace() { + public void escapingTakesPlace() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t'); @@ -112,7 +112,7 @@ * of columns in each line varies. */ @Test - public void testMultiLineDifferentColumnCount() { + public void multiLineDifferentColumnCount() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t'); @@ -131,7 +131,7 @@ * Test that we get the correct output with a single column of input. */ @Test - public void testOneColumn() { + public void oneColumn() { final PrintWriterComparator comparator = new PrintWriterComparator(); final ColumnFormatter formatter = new ColumnFormatter(comparator.getPrintWriter(), '\t');
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java index d5f3132..6d33f50 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -23,63 +23,63 @@ public class AddressTest extends GerritBaseTests { @Test - public void testParse_NameEmail1() { + public void parse_NameEmail1() { final Address a = Address.parse("A U Thor <author@example.com>"); assertThat(a.name).isEqualTo("A U Thor"); assertThat(a.email).isEqualTo("author@example.com"); } @Test - public void testParse_NameEmail2() { + public void parse_NameEmail2() { final Address a = Address.parse("A <a@b>"); assertThat(a.name).isEqualTo("A"); assertThat(a.email).isEqualTo("a@b"); } @Test - public void testParse_NameEmail3() { + public void parse_NameEmail3() { final Address a = Address.parse("<a@b>"); assertThat(a.name).isNull(); assertThat(a.email).isEqualTo("a@b"); } @Test - public void testParse_NameEmail4() { + public void parse_NameEmail4() { final Address a = Address.parse("A U Thor<author@example.com>"); assertThat(a.name).isEqualTo("A U Thor"); assertThat(a.email).isEqualTo("author@example.com"); } @Test - public void testParse_NameEmail5() { + public void parse_NameEmail5() { final Address a = Address.parse("A U Thor <author@example.com>"); assertThat(a.name).isEqualTo("A U Thor"); assertThat(a.email).isEqualTo("author@example.com"); } @Test - public void testParse_Email1() { + public void parse_Email1() { final Address a = Address.parse("author@example.com"); assertThat(a.name).isNull(); assertThat(a.email).isEqualTo("author@example.com"); } @Test - public void testParse_Email2() { + public void parse_Email2() { final Address a = Address.parse("a@b"); assertThat(a.name).isNull(); assertThat(a.email).isEqualTo("a@b"); } @Test - public void testParse_NewTLD() { + public void parse_NewTLD() { Address a = Address.parse("A U Thor <author@example.systems>"); assertThat(a.name).isEqualTo("A U Thor"); assertThat(a.email).isEqualTo("author@example.systems"); } @Test - public void testParseInvalid() { + public void parseInvalid() { assertInvalid(""); assertInvalid("a"); assertInvalid("a<"); @@ -107,49 +107,49 @@ } @Test - public void testToHeaderString_NameEmail1() { + public void toHeaderString_NameEmail1() { assertThat(format("A", "a@a")).isEqualTo("A <a@a>"); } @Test - public void testToHeaderString_NameEmail2() { + public void toHeaderString_NameEmail2() { assertThat(format("A B", "a@a")).isEqualTo("A B <a@a>"); } @Test - public void testToHeaderString_NameEmail3() { + public void toHeaderString_NameEmail3() { assertThat(format("A B. C", "a@a")).isEqualTo("\"A B. C\" <a@a>"); } @Test - public void testToHeaderString_NameEmail4() { + public void toHeaderString_NameEmail4() { assertThat(format("A B, C", "a@a")).isEqualTo("\"A B, C\" <a@a>"); } @Test - public void testToHeaderString_NameEmail5() { + public void toHeaderString_NameEmail5() { assertThat(format("A \" C", "a@a")).isEqualTo("\"A \\\" C\" <a@a>"); } @Test - public void testToHeaderString_NameEmail6() { + public void toHeaderString_NameEmail6() { assertThat(format("A \u20ac B", "a@a")) .isEqualTo("=?UTF-8?Q?A_=E2=82=AC_B?= <a@a>"); } @Test - public void testToHeaderString_NameEmail7() { + public void toHeaderString_NameEmail7() { assertThat(format("A \u20ac B (Code Review)", "a@a")) .isEqualTo("=?UTF-8?Q?A_=E2=82=AC_B_=28Code_Review=29?= <a@a>"); } @Test - public void testToHeaderString_Email1() { + public void toHeaderString_Email1() { assertThat(format(null, "a@a")).isEqualTo("a@a"); } @Test - public void testToHeaderString_Email2() { + public void toHeaderString_Email2() { assertThat(format(null, "a,b@a")).isEqualTo("<a,b@a>"); }
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 deleted file mode 100644 index 11f1d54..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java +++ /dev/null
@@ -1,308 +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.server.mail; - -import static com.google.common.truth.Truth.assertThat; -import static org.easymock.EasyMock.createStrictMock; -import static org.easymock.EasyMock.eq; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; - -import com.google.gerrit.common.TimeUtil; -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.AccountProjectWatch.NotifyType; -import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.PersonIdent; -import org.junit.Before; -import org.junit.Test; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Set; - -public class FromAddressGeneratorProviderTest { - private Config config; - private PersonIdent ident; - private AccountCache accountCache; - - @Before - public void setUp() throws Exception { - config = new Config(); - ident = new PersonIdent("NAME", "e@email", 0, 0); - accountCache = createStrictMock(AccountCache.class); - } - - private FromAddressGenerator create() { - return new FromAddressGeneratorProvider(config, "Anonymous Coward", ident, - accountCache).get(); - } - - private void setFrom(final String newFrom) { - config.setString("sendemail", null, "from", newFrom); - } - - @Test - public void testDefaultIsMIXED() { - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); - } - - @Test - public void testSelectUSER() { - setFrom("USER"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); - - setFrom("user"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); - - setFrom("uSeR"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); - } - - @Test - public void testUSER_FullyConfiguredUser() { - setFrom("USER"); - - 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 testUSER_NoFullNameUser() { - setFrom("USER"); - - final String email = "a.u.thor@test.example.com"; - final Account.Id user = user(null, email); - - replay(accountCache); - final Address r = create().from(user); - assertThat(r).isNotNull(); - assertThat(r.name).isNull(); - assertThat(r.email).isEqualTo(email); - verify(accountCache); - } - - @Test - public void testUSER_NoPreferredEmailUser() { - setFrom("USER"); - - final String name = "A U. Thor"; - final Account.Id user = user(name, null); - - replay(accountCache); - final Address r = create().from(user); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(name); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testUSER_NullUser() { - setFrom("USER"); - replay(accountCache); - final Address r = create().from(null); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(ident.getName()); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testSelectSERVER() { - setFrom("SERVER"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); - - setFrom("server"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); - - setFrom("sErVeR"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); - } - - @Test - public void testSERVER_FullyConfiguredUser() { - setFrom("SERVER"); - - final String name = "A U. Thor"; - final String email = "a.u.thor@test.example.com"; - final Account.Id user = userNoLookup(name, email); - - replay(accountCache); - final Address r = create().from(user); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(ident.getName()); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testSERVER_NullUser() { - setFrom("SERVER"); - replay(accountCache); - final Address r = create().from(null); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(ident.getName()); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testSelectMIXED() { - setFrom("MIXED"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); - - setFrom("mixed"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); - - setFrom("mIxEd"); - assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); - } - - @Test - public void testMIXED_FullyConfiguredUser() { - setFrom("MIXED"); - - 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 + " (Code Review)"); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testMIXED_NoFullNameUser() { - setFrom("MIXED"); - - final String email = "a.u.thor@test.example.com"; - final Account.Id user = user(null, email); - - replay(accountCache); - final Address r = create().from(user); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo("Anonymous Coward (Code Review)"); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testMIXED_NoPreferredEmailUser() { - setFrom("MIXED"); - - final String name = "A U. Thor"; - final Account.Id user = user(name, null); - - 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 testMIXED_NullUser() { - setFrom("MIXED"); - replay(accountCache); - final Address r = create().from(null); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(ident.getName()); - assertThat(r.email).isEqualTo(ident.getEmailAddress()); - verify(accountCache); - } - - @Test - public void testCUSTOM_FullyConfiguredUser() { - setFrom("A ${user} B <my.server@email.address>"); - - 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("A " + name + " B"); - assertThat(r.email).isEqualTo("my.server@email.address"); - verify(accountCache); - } - - @Test - public void testCUSTOM_NoFullNameUser() { - setFrom("A ${user} B <my.server@email.address>"); - - final String email = "a.u.thor@test.example.com"; - final Account.Id user = user(null, email); - - replay(accountCache); - final Address r = create().from(user); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo("A Anonymous Coward B"); - assertThat(r.email).isEqualTo("my.server@email.address"); - verify(accountCache); - } - - @Test - public void testCUSTOM_NullUser() { - setFrom("A ${user} B <my.server@email.address>"); - - replay(accountCache); - final Address r = create().from(null); - assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(ident.getName()); - assertThat(r.email).isEqualTo("my.server@email.address"); - verify(accountCache); - } - - private Account.Id user(final String name, final String email) { - final AccountState s = makeUser(name, email); - expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(s); - return s.getAccount().getId(); - } - - private Account.Id userNoLookup(final String name, final String email) { - final AccountState s = makeUser(name, email); - return s.getAccount().getId(); - } - - private AccountState makeUser(final String name, final String email) { - final Account.Id userId = new Account.Id(42); - final Account account = new Account(userId, TimeUtil.nowTs()); - account.setFullName(name); - account.setPreferredEmail(email); - return new AccountState(account, Collections.<AccountGroup.UUID> emptySet(), - Collections.<AccountExternalId> emptySet(), - new HashMap<ProjectWatchKey, Set<NotifyType>>()); - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java index 4f2c776..4d7bf08 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/ValidatorTest.java
@@ -17,6 +17,8 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; + import org.junit.Test; import java.io.BufferedReader;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java new file mode 100644 index 0000000..dc1c054 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/AbstractParserTest.java
@@ -0,0 +1,86 @@ +// 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.receive; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.server.mail.Address; + +import org.joda.time.DateTime; +import org.junit.Ignore; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +@Ignore +public class AbstractParserTest { + protected static final String changeURL = + "https://gerrit-review.googlesource.com/#/changes/123"; + + protected static void assertChangeMessage(String message, + MailComment comment) { + assertThat(comment.fileName).isNull(); + assertThat(comment.message).isEqualTo(message); + assertThat(comment.inReplyTo).isNull(); + assertThat(comment.type).isEqualTo(MailComment.CommentType.CHANGE_MESSAGE); + } + + protected static void assertInlineComment(String message, MailComment comment, + Comment inReplyTo) { + assertThat(comment.fileName).isNull(); + assertThat(comment.message).isEqualTo(message); + assertThat(comment.inReplyTo).isEqualTo(inReplyTo); + assertThat(comment.type).isEqualTo(MailComment.CommentType.INLINE_COMMENT); + } + + protected static void assertFileComment(String message, MailComment comment, + String file) { + assertThat(comment.fileName).isEqualTo(file); + assertThat(comment.message).isEqualTo(message); + assertThat(comment.inReplyTo).isNull(); + assertThat(comment.type).isEqualTo(MailComment.CommentType.FILE_COMMENT); + } + + protected static Comment newComment(String uuid, String file, + String message, int line) { + Comment c = new Comment(new Comment.Key(uuid, file, 1), + new Account.Id(0), new Timestamp(0L), (short) 0, message, "", false); + c.lineNbr = line; + return c; + } + + /** Returns a MailMessage.Builder with all required fields populated. */ + protected static MailMessage.Builder newMailMessageBuilder() { + MailMessage.Builder b = MailMessage.builder(); + b.id("id"); + b.from(new Address("Foo Bar", "foo@bar.com")); + b.dateReceived(new DateTime()); + b.subject(""); + return b; + } + + /** Returns a List of default comments for testing. */ + protected static List<Comment> defaultComments() { + List<Comment> comments = new ArrayList<>(); + comments.add(newComment("c1", "gerrit-server/test.txt", "comment", 0)); + comments.add(newComment("c2", "gerrit-server/test.txt", "comment", 2)); + comments.add(newComment("c3", "gerrit-server/test.txt", "comment", 3)); + comments.add(newComment("c4", "gerrit-server/readme.txt", "comment", 3)); + return comments; + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java new file mode 100644 index 0000000..7eadf01 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GenericHtmlParserTest.java
@@ -0,0 +1,95 @@ +// 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.receive; + +/** Test parser for a generic Html email client response */ +public class GenericHtmlParserTest extends HtmlParserTest { + @Override + protected String newHtmlBody(String changeMessage, String c1, + String c2, String c3, String f1, String f2, String fc1) { + String email = "" + + "<div dir=\"ltr\">" + (changeMessage != null ? changeMessage : "") + + "<div class=\"extra\"><br><div class=\"quote\">" + + "On Fri, Nov 18, 2016 at 11:15 AM, foobar (Gerrit) noreply@gerrit.com" + + "<span dir=\"ltr\"><<a href=\"mailto:noreply@gerrit.com\" " + + "target=\"_blank\">noreply@gerrit.com</a>></span> wrote:<br>" + + "<blockquote class=\"quote\" " + + "<p>foobar <strong>posted comments</strong> on this change.</p>" + + "<p><a href=\"" + changeURL + "/1\" " + + "target=\"_blank\">View Change</a></p><div>Patch Set 2: CR-1\n" + + "\n" + + "(3 comments)</div><ul><li>" + + "<p>" + // File #1: test.txt + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt\">" + + "File gerrit-server/<wbr>test.txt:</a></p>" + + commentBlock(f1) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt\">" + + "Patch Set #2:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some comment on file 1</p>" + + "</li>" + + commentBlock(fc1) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@2\">" + + "Patch Set #2, Line 31:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some text from original comment</p>" + + "</li>" + + commentBlock(c1) + + "" + // Inline comment #2 + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@3\">" + + "Patch Set #2, Line 47:</a> </p>" + + "<blockquote><pre>Some comment posted on Gerrit</pre>" + + "</blockquote><p>Some more comments from Gerrit</p>" + + "</li>" + + commentBlock(c2) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@115\">" + + "Patch Set #2, Line 115:</a> <code>some code</code></p>" + + "<p>some comment</p></li></ul></li>" + + "" + + "<li><p>" + // File #2: test.txt + "<a href=\"" + changeURL + "/1/gerrit-server/readme.txt\">" + + "File gerrit-server/<wbr>readme.txt:</a></p>" + + commentBlock(f2) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/readme.txt@3\">" + + "Patch Set #2, Line 31:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some text from original comment</p>" + + "</li>" + + commentBlock(c3) + + "" + // Inline comment #2 + "</ul></li></ul>" + + "" + // Footer + "<p>To view, visit <a href=\"" + changeURL + "/1\">this change</a>. " + + "To unsubscribe, visit <a href=\"https://someurl\">settings</a>." + + "</p><p>Gerrit-MessageType: comment<br>" + + "Footer omitted</p>" + + "<div><div></div></div>" + + "<p>Gerrit-HasComments: Yes</p></blockquote></div><br></div></div>"; + return email; + } + + private static String commentBlock(String comment) { + if (comment == null) { + return ""; + } + return "</ul></li></ul></blockquote><div>" + comment + + "</div><blockquote class=\"quote\"><ul><li><ul>"; + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java new file mode 100644 index 0000000..7000e46 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/GmailHtmlParserTest.java
@@ -0,0 +1,94 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.receive; + +public class GmailHtmlParserTest extends HtmlParserTest { + @Override + protected String newHtmlBody(String changeMessage, String c1, + String c2, String c3, String f1, String f2, String fc1) { + String email = "" + + "<div dir=\"ltr\">" + (changeMessage != null ? changeMessage : "") + + "<div class=\"gmail_extra\"><br><div class=\"gmail_quote\">" + + "On Fri, Nov 18, 2016 at 11:15 AM, foobar (Gerrit) noreply@gerrit.com" + + "<span dir=\"ltr\"><<a href=\"mailto:noreply@gerrit.com\" " + + "target=\"_blank\">noreply@gerrit.com</a>></span> wrote:<br>" + + "<blockquote class=\"gmail_quote\" " + + "<p>foobar <strong>posted comments</strong> on this change.</p>" + + "<p><a href=\"" + changeURL + "/1\" " + + "target=\"_blank\">View Change</a></p><div>Patch Set 2: CR-1\n" + + "\n" + + "(3 comments)</div><ul><li>" + + "<p>" + // File #1: test.txt + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt\">" + + "File gerrit-server/<wbr>test.txt:</a></p>" + + commentBlock(f1) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt\">" + + "Patch Set #2:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some comment on file 1</p>" + + "</li>" + + commentBlock(fc1) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@2\">" + + "Patch Set #2, Line 31:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some text from original comment</p>" + + "</li>" + + commentBlock(c1) + + "" + // Inline comment #2 + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@3\">" + + "Patch Set #2, Line 47:</a> </p>" + + "<blockquote><pre>Some comment posted on Gerrit</pre>" + + "</blockquote><p>Some more comments from Gerrit</p>" + + "</li>" + + commentBlock(c2) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/test.txt@115\">" + + "Patch Set #2, Line 115:</a> <code>some code</code></p>" + + "<p>some comment</p></li></ul></li>" + + "" + + "<li><p>" + // File #2: test.txt + "<a href=\"" + changeURL + "/1/gerrit-server/readme.txt\">" + + "File gerrit-server/<wbr>readme.txt:</a></p>" + + commentBlock(f2) + + "<li><p>" + + "<a href=\"" + changeURL + "/1/gerrit-server/readme.txt@3\">" + + "Patch Set #2, Line 31:</a> </p>" + + "<blockquote><pre>Some inline comment from Gerrit</pre>" + + "</blockquote><p>Some text from original comment</p>" + + "</li>" + + commentBlock(c3) + + "" + // Inline comment #2 + "</ul></li></ul>" + + "" + // Footer + "<p>To view, visit <a href=\"" + changeURL + "/1\">this change</a>. " + + "To unsubscribe, visit <a href=\"https://someurl\">settings</a>." + + "</p><p>Gerrit-MessageType: comment<br>" + + "Footer omitted</p>" + + "<div><div></div></div>" + + "<p>Gerrit-HasComments: Yes</p></blockquote></div><br></div></div>"; + return email; + } + + private static String commentBlock(String comment) { + if (comment == null) { + return ""; + } + return "</ul></li></ul></blockquote><div>" + comment + + "</div><blockquote class=\"gmail_quote\"><ul><li><ul>"; + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java new file mode 100644 index 0000000..198e827 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java
@@ -0,0 +1,123 @@ +// 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.receive; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.reviewdb.client.Comment; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.List; + +@Ignore +public abstract class HtmlParserTest extends AbstractParserTest { + @Test + public void simpleChangeMessage() { + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody("Looks good to me", null, null, + null, null, null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + HtmlParser.parse(b.build(), comments, ""); + + assertThat(parsedComments).hasSize(1); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + } + + @Test + public void simpleInlineComments() { + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody("Looks good to me", + "I have a comment on this.", null, "Also have a comment here.", + null, null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + HtmlParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertInlineComment("I have a comment on this.", parsedComments.get(1), + comments.get(1)); + assertInlineComment("Also have a comment here.", parsedComments.get(2), + comments.get(3)); + } + + @Test + public void simpleFileComment() { + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody("Looks good to me", + null, null, "Also have a comment here.", + "This is a nice file", null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + HtmlParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertFileComment("This is a nice file", parsedComments.get(1), + comments.get(1).key.filename); + assertInlineComment("Also have a comment here.", parsedComments.get(2), + comments.get(3)); + } + + @Test + public void noComments() { + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody(null, null, null, null, null, null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + HtmlParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).isEmpty(); + } + + @Test + public void noChangeMessage() { + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody(null, null, null, + "Also have a comment here.", "This is a nice file", null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + HtmlParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(2); + assertFileComment("This is a nice file", parsedComments.get(0), + comments.get(1).key.filename); + assertInlineComment("Also have a comment here.", parsedComments.get(1), + comments.get(3)); + } + + /** + * Create an html message body with the specified comments. + * + * @param changeMessage + * @param c1 Comment in reply to first comment. + * @param c2 Comment in reply to second comment. + * @param c3 Comment in reply to third comment. + * @param f1 Comment on file one. + * @param f2 Comment on file two. + * @param fc1 Comment in reply to a comment on file 1. + * @return A string with all inline comments and the original quoted email. + */ + protected abstract String newHtmlBody(String changeMessage, String c1, + String c2, String c3, String f1, String f2, String fc1); +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java new file mode 100644 index 0000000..67f3e46 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.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.mail.receive; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter; +import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.MetadataName; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Test; + +public class MetadataParserTest { + @Test + public void parseMetadataFromHeader() { + // This tests if the metadata parser is able to parse metadata from the + // email headers of the message. + MailMessage.Builder b = MailMessage.builder(); + b.id(""); + b.dateReceived(new DateTime()); + b.subject(""); + + b.addAdditionalHeader( + toHeaderWithDelimiter(MetadataName.CHANGE_ID) + "cid"); + b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.PATCH_SET) + "1"); + b.addAdditionalHeader( + toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE) +"comment"); + b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.TIMESTAMP) + + "Tue, 25 Oct 2016 02:11:35 -0700"); + + Address author = new Address("Diffy", "test@gerritcodereview.com"); + b.from(author); + + MailMetadata meta = MetadataParser.parse(b.build()); + assertThat(meta.author).isEqualTo(author.getEmail()); + assertThat(meta.changeId).isEqualTo("cid"); + assertThat(meta.patchSet).isEqualTo(1); + assertThat(meta.messageType).isEqualTo("comment"); + assertThat(meta.timestamp.getTime()).isEqualTo( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis()); + } + + @Test + public void parseMetadataFromText() { + // This tests if the metadata parser is able to parse metadata from the + // the text body of the message. + MailMessage.Builder b = MailMessage.builder(); + b.id(""); + b.dateReceived(new DateTime()); + b.subject(""); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append( + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "\n"); + stringBuilder.append( + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "\n"); + stringBuilder.append( + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "\n"); + stringBuilder.append(toFooterWithDelimiter(MetadataName.TIMESTAMP) + + "Tue, 25 Oct 2016 02:11:35 -0700" + "\n"); + b.textContent(stringBuilder.toString()); + + Address author = new Address("Diffy", "test@gerritcodereview.com"); + b.from(author); + + MailMetadata meta = MetadataParser.parse(b.build()); + assertThat(meta.author).isEqualTo(author.getEmail()); + assertThat(meta.changeId).isEqualTo("cid"); + assertThat(meta.patchSet).isEqualTo(1); + assertThat(meta.messageType).isEqualTo("comment"); + assertThat(meta.timestamp.getTime()).isEqualTo( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis()); + } + + @Test + public void parseMetadataFromHTML() { + // This tests if the metadata parser is able to parse metadata from the + // the HTML body of the message. + MailMessage.Builder b = MailMessage.builder(); + b.id(""); + b.dateReceived(new DateTime()); + b.subject(""); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("<p>" + + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "</p>"); + stringBuilder.append("<p>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + + "1" + "</p>"); + stringBuilder.append("<p>" + + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "</p>"); + stringBuilder.append("<p>" + toFooterWithDelimiter(MetadataName.TIMESTAMP) + + "Tue, 25 Oct 2016 02:11:35 -0700" + "</p>"); + b.htmlContent(stringBuilder.toString()); + + Address author = new Address("Diffy", "test@gerritcodereview.com"); + b.from(author); + + MailMetadata meta = MetadataParser.parse(b.build()); + assertThat(meta.author).isEqualTo(author.getEmail()); + assertThat(meta.changeId).isEqualTo("cid"); + assertThat(meta.patchSet).isEqualTo(1); + assertThat(meta.messageType).isEqualTo("comment"); + assertThat(meta.timestamp.getTime()).isEqualTo( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC).getMillis()); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java new file mode 100644 index 0000000..2e5b4c2 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/RawMailParserTest.java
@@ -0,0 +1,76 @@ +// 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.receive; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.server.mail.receive.data.AttachmentMessage; +import com.google.gerrit.server.mail.receive.data.Base64HeaderMessage; +import com.google.gerrit.server.mail.receive.data.HtmlMimeMessage; +import com.google.gerrit.server.mail.receive.data.NonUTF8Message; +import com.google.gerrit.server.mail.receive.data.QuotedPrintableHeaderMessage; +import com.google.gerrit.server.mail.receive.data.RawMailMessage; +import com.google.gerrit.server.mail.receive.data.SimpleTextMessage; +import com.google.gerrit.testutil.GerritBaseTests; + +import org.junit.Test; + +public class RawMailParserTest extends GerritBaseTests { + @Test + public void parseEmail() throws Exception { + RawMailMessage[] messages = new RawMailMessage[] { + new SimpleTextMessage(), + new Base64HeaderMessage(), + new QuotedPrintableHeaderMessage(), + new HtmlMimeMessage(), + new AttachmentMessage(), + new NonUTF8Message(), + }; + for (RawMailMessage rawMailMessage : messages) { + if (rawMailMessage.rawChars() != null) { + // Assert Character to Mail Parser + MailMessage parsedMailMessage = + RawMailParser.parse(rawMailMessage.rawChars()); + assertMail(parsedMailMessage, rawMailMessage.expectedMailMessage()); + } + if (rawMailMessage.raw() != null) { + // Assert String to Mail Parser + MailMessage parsedMailMessage = RawMailParser + .parse(rawMailMessage.raw()); + assertMail(parsedMailMessage, rawMailMessage.expectedMailMessage()); + } + } + } + + /** + * This method makes it easier to debug failing tests by checking each + * property individual instead of calling equals as it will immediately + * reveal the property that diverges between the two objects. + * @param have MailMessage retrieved from the parser + * @param want MailMessage that would be expected + */ + private void assertMail(MailMessage have, MailMessage want) { + assertThat(have.id()).isEqualTo(want.id()); + assertThat(have.to()).isEqualTo(want.to()); + assertThat(have.from()).isEqualTo(want.from()); + assertThat(have.cc()).isEqualTo(want.cc()); + assertThat(have.dateReceived().getMillis()) + .isEqualTo(want.dateReceived().getMillis()); + assertThat(have.additionalHeaders()).isEqualTo(want.additionalHeaders()); + assertThat(have.subject()).isEqualTo(want.subject()); + assertThat(have.textContent()).isEqualTo(want.textContent()); + assertThat(have.htmlContent()).isEqualTo(want.htmlContent()); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java new file mode 100644 index 0000000..7a55653 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/TextParserTest.java
@@ -0,0 +1,219 @@ +// 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.receive; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.reviewdb.client.Comment; + +import org.junit.Test; + +import java.util.List; + +public class TextParserTest extends AbstractParserTest { + private static final String quotedFooter = "" + + "> To view, visit https://gerrit-review.googlesource.com/123\n" + + "> To unsubscribe, visit https://gerrit-review.googlesource.com\n" + + "> \n" + + "> Gerrit-MessageType: comment\n" + + "> Gerrit-Change-Id: Ie1234021bf1e8d1425641af58fd648fc011db153\n" + + "> Gerrit-PatchSet: 1\n" + + "> Gerrit-Project: gerrit\n" + + "> Gerrit-Branch: master\n" + + "> Gerrit-Owner: Foo Bar <foo@bar.com>\n" + + "> Gerrit-HasComments: Yes"; + + @Test + public void simpleChangeMessage() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent("Looks good to me\n" + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(1); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + } + + @Test + public void simpleInlineComments() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent(newPlaintextBody("Looks good to me", + "I have a comment on this.", null, "Also have a comment here.", + null, null, null) + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertInlineComment("I have a comment on this.", parsedComments.get(1), + comments.get(1)); + assertInlineComment("Also have a comment here.", parsedComments.get(2), + comments.get(3)); + } + + @Test + public void simpleFileComment() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent(newPlaintextBody("Looks good to me", + null, null, "Also have a comment here.", + "This is a nice file", null, null) + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertFileComment("This is a nice file", parsedComments.get(1), + comments.get(1).key.filename); + assertInlineComment("Also have a comment here.", parsedComments.get(2), + comments.get(3)); + } + + @Test + public void noComments() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent(newPlaintextBody(null, null, null, null, null, null, null) + + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).isEmpty(); + } + + @Test + public void noChangeMessage() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent(newPlaintextBody(null, null, null, + "Also have a comment here.", "This is a nice file", null, null) + + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(2); + assertFileComment("This is a nice file", parsedComments.get(0), + comments.get(1).key.filename); + assertInlineComment("Also have a comment here.", parsedComments.get(1), + comments.get(3)); + } + + @Test + public void allCommentsGmail() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent((newPlaintextBody("Looks good to me", + null, null, "Also have a comment here.", + "This is a nice file", null, null) + quotedFooter) + .replace("> ", ">> ")); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertFileComment("This is a nice file", parsedComments.get(1), + comments.get(1).key.filename); + assertInlineComment("Also have a comment here.", parsedComments.get(2), + comments.get(3)); + } + + @Test + public void replyToFileComment() { + MailMessage.Builder b = newMailMessageBuilder(); + b.textContent(newPlaintextBody("Looks good to me", null, null, null, null, + null, "Comment in reply to file comment") + quotedFooter); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = + TextParser.parse(b.build(), comments, changeURL); + + assertThat(parsedComments).hasSize(2); + assertChangeMessage("Looks good to me", parsedComments.get(0)); + assertInlineComment("Comment in reply to file comment", + parsedComments.get(1), comments.get(0)); + } + + /** + * Create a plaintext message body with the specified comments. + * + * @param changeMessage + * @param c1 Comment in reply to first inline comment. + * @param c2 Comment in reply to second inline comment. + * @param c3 Comment in reply to third inline comment. + * @param f1 Comment on file one. + * @param f2 Comment on file two. + * @param fc1 Comment in reply to a comment of file 1. + * @return A string with all inline comments and the original quoted email. + */ + private static String newPlaintextBody(String changeMessage, String c1, + String c2, String c3, String f1, String f2, String fc1) { + return (changeMessage == null ? "" : changeMessage + "\n") + + "> Foo Bar has posted comments on this change. ( \n" + + "> " + changeURL +"/1 )\n" + + "> \n" + + "> Change subject: Test change\n" + + "> ...............................................................\n" + + "> \n" + + "> \n" + + "> Patch Set 1: Code-Review+1\n" + + "> \n" + + "> (3 comments)\n" + + "> \n" + + "> " + changeURL + "/1/gerrit-server/test.txt\n" + + "> File \n" + + "> gerrit-server/test.txt:\n" + + (f1 == null ? "" : f1 + "\n") + + "> \n" + + "> Patch Set #4:\n" + + "> " + changeURL + "/1/gerrit-server/test.txt\n" + + "> \n" + + "> Some comment" + + "> \n" + + (fc1 == null ? "" : fc1 + "\n") + + "> " + changeURL + "/1/gerrit-server/test.txt@2\n" + + "> PS1, Line 2: throw new Exception(\"Object has unsupported: \" +\n" + + "> : entry.getValue() +\n" + + "> : \" must be java.util.Date\");\n" + + "> Should entry.getKey() be included in this message?\n" + + "> \n" + + (c1 == null ? "" : c1 + "\n") + + "> \n" + + "> " + changeURL + "/1/gerrit-server/test.txt@3\n" + + "> PS1, Line 3: throw new Exception(\"Object has: \" +\n" + + "> : entry.getValue().getClass() +\n" + + "> : \" must be java.util.Date\");\n" + + "> same here\n" + + "> \n" + + (c2 == null ? "" : c2 + "\n") + + "> \n" + + "> " + changeURL + "/1/gerrit-server/readme.txt\n" + + "> File \n" + + "> gerrit-server/readme.txt:\n" + + (f2 == null ? "" : f2 + "\n") + + "> \n" + + "> " + changeURL + "/1/gerrit-server/readme.txt@3\n" + + "> PS1, Line 3: E\n" + + "> Should this be EEE like in other places?\n" + + (c3 == null ? "" : c3 + "\n"); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java new file mode 100644 index 0000000..390209a --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.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.mail.receive.data; + + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests that all mime parts that are neither text/plain, nor text/html are + * dropped. + */ +@Ignore +public class AttachmentMessage extends RawMailMessage { + private static String raw = "MIME-Version: 1.0\n" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "Message-ID: <CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w" + + "@mail.gmail.com>\n" + + "Subject: Test Subject\n" + + "From: Patrick Hiesel <hiesel@google.com>\n" + + "To: Patrick Hiesel <hiesel@google.com>\n" + + "Content-Type: multipart/mixed; boundary=001a114e019a56962d054062708f\n" + + "\n" + + "--001a114e019a56962d054062708f\n" + + "Content-Type: multipart/alternative; boundary=001a114e019a5696250540" + + "62708d\n" + + "\n" + + "--001a114e019a569625054062708d\n" + + "Content-Type: text/plain; charset=UTF-8\n" + + "\n" + + "Contains unwanted attachment" + + "\n" + + "--001a114e019a569625054062708d\n" + + "Content-Type: text/html; charset=UTF-8\n" + + "\n" + + "<div dir=\"ltr\">Contains unwanted attachment</div>" + + "\n" + + "--001a114e019a569625054062708d--\n" + + "--001a114e019a56962d054062708f\n" + + "Content-Type: text/plain; charset=US-ASCII; name=\"test.txt\"\n" + + "Content-Disposition: attachment; filename=\"test.txt\"\n" + + "Content-Transfer-Encoding: base64\n" + + "X-Attachment-Id: f_iv264bt50\n" + + "\n" + + "VEVTVAo=\n" + + "--001a114e019a56962d054062708f--"; + + @Override + public String raw() { + return raw; + } + + @Override + public int[] rawChars() { + return null; + } + + @Override + public MailMessage expectedMailMessage() { + System.out.println("\uD83D\uDE1B test"); + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w" + + "@mail.gmail.com>") + .from(new Address("Patrick Hiesel", "hiesel@google.com")) + .addTo(new Address("Patrick Hiesel", "hiesel@google.com")) + .textContent("Contains unwanted attachment") + .htmlContent("<div dir=\"ltr\">Contains unwanted attachment</div>") + .subject("Test Subject") + .addAdditionalHeader("MIME-Version: 1.0") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java new file mode 100644 index 0000000..5511e75 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/Base64HeaderMessage.java
@@ -0,0 +1,64 @@ +// 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.receive.data; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests parsing a Base64 encoded subject. + */ +@Ignore +public class Base64HeaderMessage extends RawMailMessage { + private static String textContent = "Some Text"; + private static String raw = "" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" + + "Subject: =?UTF-8?B?8J+YmyB0ZXN0?=\n" + + "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" + + "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" + + "To: ekempin <ekempin@google.com>\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" + + "\n" + textContent; + + @Override + public String raw() { + return raw; + } + + @Override + public int[] rawChars() { + return null; + } + + @Override + public MailMessage expectedMailMessage() { + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<001a114da7ae26e2eb053fe0c29c@google.com>") + .from(new Address("Jonathan Nieder (Gerrit)", + "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com")) + .addTo(new Address("ekempin","ekempin@google.com")) + .textContent(textContent) + .subject("\uD83D\uDE1B test") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java new file mode 100644 index 0000000..2ed096e --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.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.receive.data; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests a message containing mime/alternative (text + html) content. + */ +@Ignore +public class HtmlMimeMessage extends RawMailMessage { + private static String textContent = "Simple test"; + + // htmlContent is encoded in quoted-printable + private static String htmlContent = "<div dir=3D\"ltr\">Test <span style" + + "=3D\"background-color:rgb(255,255,0)\">Messa=\n" + + "ge</span> in <u>HTML=C2=A0</u><a href=3D\"https://en.wikipedia.org/" + + "wiki/%C3%=\n9Cmlaut_(band)\" class=3D\"gmail-mw-redirect\" title=3D\"" + + "=C3=9Cmlaut (band)\" st=\nyle=3D\"text-decoration:none;color:rgb(11," + + "0,128);background-image:none;backg=\nround-position:initial;background" + + "-size:initial;background-repeat:initial;ba=\nckground-origin:initial;" + + "background-clip:initial;font-family:sans-serif;font=\n" + + "-size:14px\">=C3=9C</a></div>"; + + private static String unencodedHtmlContent = "" + + "<div dir=\"ltr\">Test <span style=\"background-color:rgb(255,255,0)\">" + + "Message</span> in <u>HTML </u><a href=\"https://en.wikipedia.org/wiki/" + + "%C3%9Cmlaut_(band)\" class=\"gmail-mw-redirect\" title=\"Ümlaut " + + "(band)\" style=\"text-decoration:none;color:rgb(11,0,128);" + + "background-image:none;background-position:initial;background-size:" + + "initial;background-repeat:initial;background-origin:initial;background" + + "-clip:initial;font-family:sans-serif;font-size:14px\">Ü</a></div>"; + + private static String raw = "" + + "MIME-Version: 1.0\n" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "Message-ID: <001a114cd8be55b4ab053face5cd@google.com>\n" + + "Subject: Change in gerrit[master]: Implement receiver class structure " + + "and bindings\n" + + "From: \"ekempin (Gerrit)\" <noreply-gerritcodereview-qUgXfQecoDLHwp0Ml" + + "dAzig@google.com>\n" + + "To: Patrick Hiesel <hiesel@google.com>\n" + + "Cc: ekempin <ekempin@google.com>\n" + + "Content-Type: multipart/alternative; boundary=001a114cd8b" + + "e55b486053face5ca\n" + + "\n" + + "--001a114cd8be55b486053face5ca\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" + + "\n" + + textContent + + "\n" + + "--001a114cd8be55b486053face5ca\n" + + "Content-Type: text/html; charset=UTF-8\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + htmlContent + + "\n" + + "--001a114cd8be55b486053face5ca--"; + + @Override + public String raw() { + return raw; + } + + @Override + public int[] rawChars() { + return null; + } + + @Override + public MailMessage expectedMailMessage() { + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<001a114cd8be55b4ab053face5cd@google.com>") + .from(new Address("ekempin (Gerrit)", + "noreply-gerritcodereview-qUgXfQecoDLHwp0MldAzig@google.com")) + .addCc(new Address("ekempin","ekempin@google.com")) + .addTo(new Address("Patrick Hiesel","hiesel@google.com")) + .textContent(textContent) + .htmlContent(unencodedHtmlContent) + .subject("Change in gerrit[master]: Implement " + + "receiver class structure and bindings") + .addAdditionalHeader("MIME-Version: 1.0") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.java new file mode 100644 index 0000000..1472049 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/NonUTF8Message.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.mail.receive.data; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests that non-UTF8 encodings are handled correctly. + */ +@Ignore +public class NonUTF8Message extends RawMailMessage { + private static String textContent = "Some Text"; + private static String raw = "" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" + + "Subject: =?UTF-8?B?8J+YmyB0ZXN0?=\n" + + "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" + + "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" + + "To: ekempin <ekempin@google.com>\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" + + "\n" + textContent; + + @Override + public String raw() { + return null; + } + + @Override + public int[] rawChars() { + int[] arr = new int[raw.length()]; + int i = 0; + for (char c : raw.toCharArray()) { + arr[i++] = c; + } + return arr; + } + + @Override + public MailMessage expectedMailMessage() { + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<001a114da7ae26e2eb053fe0c29c@google.com>") + .from(new Address("Jonathan Nieder (Gerrit)", + "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com")) + .addTo(new Address("ekempin","ekempin@google.com")) + .textContent(textContent) + .subject("\uD83D\uDE1B test") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java new file mode 100644 index 0000000..f694447 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/QuotedPrintableHeaderMessage.java
@@ -0,0 +1,65 @@ +// 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.receive.data; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests parsing a quoted printable encoded subject + */ +@Ignore +public class QuotedPrintableHeaderMessage extends RawMailMessage { + private static String textContent = "Some Text"; + private static String raw = "" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" + + "Subject: =?UTF-8?Q?=C3=A2me vulgaire?=\n" + + "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-" + + "CtTy0igsBrnvL7dKoWEIEg@google.com>\n" + + "To: ekempin <ekempin@google.com>\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" + + "\n" + textContent; + + @Override + public String raw() { + return raw; + } + + @Override + public int[] rawChars() { + return null; + } + + @Override + public MailMessage expectedMailMessage() { + System.out.println("\uD83D\uDE1B test"); + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<001a114da7ae26e2eb053fe0c29c@google.com>") + .from(new Address("Jonathan Nieder (Gerrit)", + "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com")) + .addTo(new Address("ekempin","ekempin@google.com")) + .textContent(textContent) + .subject("âme vulgaire") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.java new file mode 100644 index 0000000..8afa8cc --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/RawMailMessage.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.server.mail.receive.data; + +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.junit.Ignore; + +/** + * Base class for all email parsing tests. + */ +@Ignore +public abstract class RawMailMessage { + // Raw content to feed the parser + public abstract String raw(); + public abstract int[] rawChars(); + // Parsed representation for asserting the expected parser output + public abstract MailMessage expectedMailMessage(); +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java new file mode 100644 index 0000000..179c514 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
@@ -0,0 +1,136 @@ +// 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.receive.data; + +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.receive.MailMessage; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Ignore; + +/** + * Tests parsing a simple text message with different headers. + */ +@Ignore +public class SimpleTextMessage extends RawMailMessage { + private static String textContent = "" + + "Jonathan Nieder has posted comments on this change. ( \n" + + "https://gerrit-review.googlesource.com/90018 )\n" + + "\n" + + "Change subject: (Re)enable voting buttons for merged changes\n" + + "...........................................................\n" + + "\n" + + "\n" + + "Patch Set 2:\n" + + "\n" + + "This is producing NPEs server-side and 500s for the client. \n" + + "when I try to load this change:\n" + + "\n" + + " Error in GET /changes/90018/detail?O=10004\n" + + " com.google.gwtorm.OrmException: java.lang.NullPointerException\n" + + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:303)\n" + + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:285)\n" + + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:263)\n" + + "\tat com.google.gerrit.change.GetChange.apply(GetChange.java:50)\n" + + "\tat com.google.gerrit.change.GetDetail.apply(GetDetail.java:51)\n" + + "\tat com.google.gerrit.change.GetDetail.apply(GetDetail.java:26)\n" + + "\tat \n" + + "com.google.gerrit.RestApiServlet.service(RestApiServlet.java:367)\n" + + "\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:717)\n" + + "[...]\n" + + " Caused by: java.lang.NullPointerException\n" + + "\tat \n" + + "com.google.gerrit.ChangeJson.setLabelScores(ChangeJson.java:670)\n" + + "\tat \n" + + "com.google.gerrit.ChangeJson.labelsFor(ChangeJson.java:845)\n" + + "\tat \n" + + "com.google.gerrit.change.ChangeJson.labelsFor(ChangeJson.java:598)\n" + + "\tat \n" + + "com.google.gerrit.change.ChangeJson.toChange(ChangeJson.java:499)\n" + + "\tat com.google.gerrit.change.ChangeJson.format(ChangeJson.java:294)\n" + + "\t... 105 more\n" + + "-- \n" + + "To view, visit https://gerrit-review.googlesource.com/90018\n" + + "To unsubscribe, visit https://gerrit-review.googlesource.com\n" + + "\n" + + "Gerrit-MessageType: comment\n" + + "Gerrit-Change-Id: Iba501e00bee77be3bd0ced72f88fd04ba0accaed\n" + + "Gerrit-PatchSet: 2\n" + + "Gerrit-Project: gerrit\n" + + "Gerrit-Branch: master\n" + + "Gerrit-Owner: ekempin <ekempin@google.com>\n" + + "Gerrit-Reviewer: Dave Borowitz <dborowitz@google.com>\n" + + "Gerrit-Reviewer: Edwin Kempin <ekempin@google.com>\n" + + "Gerrit-Reviewer: GerritForge CI <gerritforge@gmail.com>\n" + + "Gerrit-Reviewer: Jonathan Nieder <jrn@google.com>\n" + + "Gerrit-Reviewer: Patrick Hiesel <hiesel@google.com>\n" + + "Gerrit-Reviewer: ekempin <ekempin@google.com>\n" + + "Gerrit-HasComments: No"; + + private static String raw = "" + + "Authentication-Results: mx.google.com; dkim=pass header.i=" + + "@google.com;\n" + + "Date: Tue, 25 Oct 2016 02:11:35 -0700\n" + + "In-Reply-To: <gerrit.1477487889000.Iba501e00bee77be3bd0ced" + + "72f88fd04ba0accaed@gerrit-review.googlesource.com>\n" + + "References: <gerrit.1477487889000.Iba501e00bee77be3bd0ced72f8" + + "8fd04ba0accaed@gerrit-review.googlesource.com>\n" + + "Message-ID: <001a114da7ae26e2eb053fe0c29c@google.com>\n" + + "Subject: Change in gerrit[master]: (Re)enable voting buttons for " + + "merged changes\n" + + "From: \"Jonathan Nieder (Gerrit)\" <noreply-gerritcodereview-CtTy0" + + "igsBrnvL7dKoWEIEg@google.com>\n" + + "To: ekempin <ekempin@google.com>\n" + + "Cc: Dave Borowitz <dborowitz@google.com>, Jonathan Nieder " + + "<jrn@google.com>, Patrick Hiesel <hiesel@google.com>\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes\n" + + "\n" + textContent; + + @Override + public String raw() { + return raw; + } + + @Override + public int[] rawChars() { + return null; + } + + @Override + public MailMessage expectedMailMessage() { + MailMessage.Builder expect = MailMessage.builder(); + expect + .id("<001a114da7ae26e2eb053fe0c29c@google.com>") + .from(new Address("Jonathan Nieder (Gerrit)", + "noreply-gerritcodereview-CtTy0igsBrnvL7dKoWEIEg@google.com")) + .addTo(new Address("ekempin","ekempin@google.com")) + .addCc(new Address("Dave Borowitz", "dborowitz@google.com")) + .addCc(new Address("Jonathan Nieder", "jrn@google.com")) + .addCc(new Address("Patrick Hiesel", "hiesel@google.com")) + .textContent(textContent) + .subject("Change in gerrit[master]: (Re)enable voting" + + " buttons for merged changes") + .dateReceived( + new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC)) + .addAdditionalHeader("Authentication-Results: mx.google.com; " + + "dkim=pass header.i=@google.com;") + .addAdditionalHeader("In-Reply-To: <gerrit.1477487889000.Iba501e00bee" + + "77be3bd0ced72f88fd04ba0accaed@gerrit-review.googlesource.com>") + .addAdditionalHeader("References: <gerrit.1477487889000.Iba501e00bee" + + "77be3bd0ced72f88fd04ba0accaed@gerrit-review.googlesource.com>"); + return expect.build(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java new file mode 100644 index 0000000..ad06832 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/CommentFormatterTest.java
@@ -0,0 +1,454 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.LIST; +import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.PARAGRAPH; +import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.PRE_FORMATTED; +import static com.google.gerrit.server.mail.send.CommentFormatter.BlockType.QUOTE; + +import org.junit.Test; + +import java.util.List; + +public class CommentFormatterTest { + private void assertBlock(List<CommentFormatter.Block> list, int index, + CommentFormatter.BlockType type, String text) { + CommentFormatter.Block block = list.get(index); + assertThat(block.type).isEqualTo(type); + assertThat(block.text).isEqualTo(text); + assertThat(block.items).isNull(); + assertThat(block.quotedBlocks).isNull(); + } + + private void assertListBlock(List<CommentFormatter.Block> list, int index, + int itemIndex, String text) { + CommentFormatter.Block block = list.get(index); + assertThat(block.type).isEqualTo(LIST); + assertThat(block.items.get(itemIndex)).isEqualTo(text); + assertThat(block.text).isNull(); + assertThat(block.quotedBlocks).isNull(); + } + + private void assertQuoteBlock(List<CommentFormatter.Block> list, int index, + int size) { + CommentFormatter.Block block = list.get(index); + assertThat(block.type).isEqualTo(QUOTE); + assertThat(block.items).isNull(); + assertThat(block.text).isNull(); + assertThat(block.quotedBlocks).hasSize(size); + } + + @Test + public void parseNullAsEmpty() { + assertThat(CommentFormatter.parse(null)).isEmpty(); + } + + @Test + public void parseEmpty() { + assertThat(CommentFormatter.parse("")).isEmpty(); + } + + @Test + public void parseSimple() { + String comment = "Para1"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PARAGRAPH, comment); + } + + @Test + public void parseMultilinePara() { + String comment = "Para 1\nStill para 1"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PARAGRAPH, comment); + } + + @Test + public void parseParaBreak() { + String comment = "Para 1\n\nPara 2\n\nPara 3"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "Para 1"); + assertBlock(result, 1, PARAGRAPH, "Para 2"); + assertBlock(result, 2, PARAGRAPH, "Para 3"); + } + + @Test + public void parseQuote() { + String comment = "> Quote text"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertQuoteBlock(result, 0, 1); + assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, "Quote text"); + } + + @Test + public void parseExcludesEmpty() { + String comment = "Para 1\n\n\n\nPara 2"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "Para 1"); + assertBlock(result, 1, PARAGRAPH, "Para 2"); + } + + @Test + public void parseQuoteLeadSpace() { + String comment = " > Quote text"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertQuoteBlock(result, 0, 1); + assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, "Quote text"); + } + + @Test + public void parseMultiLineQuote() { + String comment = "> Quote line 1\n> Quote line 2\n > Quote line 3\n"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertQuoteBlock(result, 0, 1); + assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, + "Quote line 1\nQuote line 2\nQuote line 3\n"); + } + + @Test + public void parsePre() { + String comment = " Four space indent."; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PRE_FORMATTED, comment); + } + + @Test + public void parseOneSpacePre() { + String comment = " One space indent.\n Another line."; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PRE_FORMATTED, comment); + } + + @Test + public void parseTabPre() { + String comment = "\tOne tab indent.\n\tAnother line.\n Yet another!"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PRE_FORMATTED, comment); + } + + @Test + public void parseIntermediateLeadingWhitespacePre() { + String comment = "No indent.\n\tNonzero indent.\nNo indent again."; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertBlock(result, 0, PRE_FORMATTED, comment); + } + + @Test + public void parseStarList() { + String comment = "* Item 1\n* Item 2\n* Item 3"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertListBlock(result, 0, 0, "Item 1"); + assertListBlock(result, 0, 1, "Item 2"); + assertListBlock(result, 0, 2, "Item 3"); + } + + @Test + public void parseDashList() { + String comment = "- Item 1\n- Item 2\n- Item 3"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertListBlock(result, 0, 0, "Item 1"); + assertListBlock(result, 0, 1, "Item 2"); + assertListBlock(result, 0, 2, "Item 3"); + } + + @Test + public void parseMixedList() { + String comment = "- Item 1\n* Item 2\n- Item 3\n* Item 4"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertListBlock(result, 0, 0, "Item 1"); + assertListBlock(result, 0, 1, "Item 2"); + assertListBlock(result, 0, 2, "Item 3"); + assertListBlock(result, 0, 3, "Item 4"); + } + + @Test + public void parseMixedBlockTypes() { + String comment = "Paragraph\nacross\na\nfew\nlines." + + "\n\n" + + "> Quote\n> across\n> not many lines." + + "\n\n" + + "Another paragraph" + + "\n\n" + + "* Series\n* of\n* list\n* items" + + "\n\n" + + "Yet another paragraph" + + "\n\n" + + "\tPreformatted text." + + "\n\n" + + "Parting words."; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(7); + assertBlock(result, 0, PARAGRAPH, "Paragraph\nacross\na\nfew\nlines."); + assertQuoteBlock(result, 1, 1); + assertBlock(result.get(1).quotedBlocks, 0, PARAGRAPH, + "Quote\nacross\nnot many lines."); + assertBlock(result, 2, PARAGRAPH, "Another paragraph"); + assertListBlock(result, 3, 0, "Series"); + assertListBlock(result, 3, 1, "of"); + assertListBlock(result, 3, 2, "list"); + assertListBlock(result, 3, 3, "items"); + assertBlock(result, 4, PARAGRAPH, "Yet another paragraph"); + assertBlock(result, 5, PRE_FORMATTED, "\tPreformatted text."); + assertBlock(result, 6, PARAGRAPH, "Parting words."); + } + + @Test + public void bulletList1() { + String comment = "A\n\n* line 1\n* 2nd line"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "A"); + assertListBlock(result, 1, 0, "line 1"); + assertListBlock(result, 1, 1, "2nd line"); + } + + @Test + public void bulletList2() { + String comment = "A\n\n* line 1\n* 2nd line\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "A"); + assertListBlock(result, 1, 0, "line 1"); + assertListBlock(result, 1, 1, "2nd line"); + assertBlock(result, 2, PARAGRAPH, "B"); + } + + @Test + public void bulletList3() { + String comment = "* line 1\n* 2nd line\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertListBlock(result, 0, 0, "line 1"); + assertListBlock(result, 0, 1, "2nd line"); + assertBlock(result, 1, PARAGRAPH, "B"); + } + + @Test + public void bulletList4() { + String comment = "To see this bug, you have to:\n" // + + "* Be on IMAP or EAS (not on POP)\n"// + + "* Be very unlucky\n"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "To see this bug, you have to:"); + assertListBlock(result, 1, 0, "Be on IMAP or EAS (not on POP)"); + assertListBlock(result, 1, 1, "Be very unlucky"); + } + + @Test + public void bulletList5() { + String comment = "To see this bug,\n" // + + "you have to:\n" // + + "* Be on IMAP or EAS (not on POP)\n"// + + "* Be very unlucky\n"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "To see this bug, you have to:"); + assertListBlock(result, 1, 0, "Be on IMAP or EAS (not on POP)"); + assertListBlock(result, 1, 1, "Be very unlucky"); + } + + @Test + public void dashList1() { + String comment = "A\n\n- line 1\n- 2nd line"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "A"); + assertListBlock(result, 1, 0, "line 1"); + assertListBlock(result, 1, 1, "2nd line"); + } + + @Test + public void dashList2() { + String comment = "A\n\n- line 1\n- 2nd line\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "A"); + assertListBlock(result, 1, 0, "line 1"); + assertListBlock(result, 1, 1, "2nd line"); + assertBlock(result, 2, PARAGRAPH, "B"); + } + + @Test + public void dashList3() { + String comment = "- line 1\n- 2nd line\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertListBlock(result, 0, 0, "line 1"); + assertListBlock(result, 0, 1, "2nd line"); + assertBlock(result, 1, PARAGRAPH, "B"); + } + + @Test + public void preformat1() { + String comment = "A\n\n This is pre\n formatted"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PARAGRAPH, "A"); + assertBlock(result, 1, PRE_FORMATTED, " This is pre\n formatted"); + } + + @Test + public void preformat2() { + String comment = "A\n\n This is pre\n formatted\n\nbut this is not"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "A"); + assertBlock(result, 1, PRE_FORMATTED, " This is pre\n formatted"); + assertBlock(result, 2, PARAGRAPH, "but this is not"); + } + + @Test + public void preformat3() { + String comment = "A\n\n Q\n <R>\n S\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "A"); + assertBlock(result, 1, PRE_FORMATTED, " Q\n <R>\n S"); + assertBlock(result, 2, PARAGRAPH, "B"); + } + + @Test + public void preformat4() { + String comment = " Q\n <R>\n S\n\nB"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertBlock(result, 0, PRE_FORMATTED, " Q\n <R>\n S"); + assertBlock(result, 1, PARAGRAPH, "B"); + } + + @Test + public void quote1() { + String comment = "> I'm happy\n > with quotes!\n\nSee above."; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertQuoteBlock(result, 0, 1); + assertBlock(result.get(0).quotedBlocks, 0, PARAGRAPH, + "I'm happy\nwith quotes!"); + assertBlock(result, 1, PARAGRAPH, "See above."); + } + + @Test + public void quote2() { + String comment = "See this said:\n\n > a quoted\n > string block\n\nOK?"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(3); + assertBlock(result, 0, PARAGRAPH, "See this said:"); + assertQuoteBlock(result, 1, 1); + assertBlock(result.get(1).quotedBlocks, 0, PARAGRAPH, + "a quoted\nstring block"); + assertBlock(result, 2, PARAGRAPH, "OK?"); + } + + @Test + public void nestedQuotes1() { + String comment = " > > prior\n > \n > next\n"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(1); + assertQuoteBlock(result, 0, 2); + assertQuoteBlock(result.get(0).quotedBlocks, 0, 1); + assertBlock(result.get(0).quotedBlocks.get(0).quotedBlocks, 0, PARAGRAPH, + "prior"); + assertBlock(result.get(0).quotedBlocks, 1, PARAGRAPH, "next\n"); + } + + @Test + public void largeMixedQuote() { + String comment = + "> > Paragraph 1.\n" + + "> > \n" + + "> > > Paragraph 2.\n" + + "> > \n" + + "> > Paragraph 3.\n" + + "> > \n" + + "> > pre line 1;\n" + + "> > pre line 2;\n" + + "> > \n" + + "> > Paragraph 4.\n" + + "> > \n" + + "> > * List item 1.\n" + + "> > * List item 2.\n" + + "> > \n" + + "> > Paragraph 5.\n" + + "> \n" + + "> Paragraph 6.\n" + + "\n" + + "Paragraph 7.\n"; + List<CommentFormatter.Block> result = CommentFormatter.parse(comment); + + assertThat(result).hasSize(2); + assertQuoteBlock(result, 0, 2); + + assertQuoteBlock(result.get(0).quotedBlocks, 0, 7); + List<CommentFormatter.Block> bigQuote = + result.get(0).quotedBlocks.get(0).quotedBlocks; + assertBlock(bigQuote, 0, PARAGRAPH, "Paragraph 1."); + assertQuoteBlock(bigQuote, 1, 1); + assertBlock(bigQuote.get(1).quotedBlocks, 0, PARAGRAPH, "Paragraph 2."); + assertBlock(bigQuote, 2, PARAGRAPH, "Paragraph 3."); + assertBlock(bigQuote, 3, PRE_FORMATTED, " pre line 1;\n pre line 2;"); + assertBlock(bigQuote, 4, PARAGRAPH, "Paragraph 4."); + assertListBlock(bigQuote, 5, 0, "List item 1."); + assertListBlock(bigQuote, 5, 1, "List item 2."); + assertBlock(bigQuote, 6, PARAGRAPH, "Paragraph 5."); + assertBlock(result.get(0).quotedBlocks, 1, PARAGRAPH, "Paragraph 6."); + assertBlock(result, 1, PARAGRAPH, "Paragraph 7.\n"); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java new file mode 100644 index 0000000..9d05fc6 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -0,0 +1,397 @@ +// 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.server.mail.send; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import com.google.gerrit.common.TimeUtil; +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.AccountProjectWatch.NotifyType; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.mail.Address; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.PersonIdent; +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 { + private Config config; + private PersonIdent ident; + private AccountCache accountCache; + + @Before + public void setUp() throws Exception { + config = new Config(); + ident = new PersonIdent("NAME", "e@email", 0, 0); + accountCache = createStrictMock(AccountCache.class); + } + + private FromAddressGenerator create() { + return new FromAddressGeneratorProvider(config, "Anonymous Coward", ident, + accountCache).get(); + } + + private void setFrom(final String newFrom) { + config.setString("sendemail", null, "from", newFrom); + } + + private void setDomains(List<String> domains) { + config.setStringList("sendemail", null, "allowedDomain", domains); + } + + @Test + public void defaultIsMIXED() { + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); + } + + @Test + public void selectUSER() { + setFrom("USER"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); + + setFrom("user"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); + + setFrom("uSeR"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.UserGen.class); + } + + @Test + public void USER_FullyConfiguredUser() { + setFrom("USER"); + + 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.getName()).isEqualTo(name); + assertThat(r.getEmail()).isEqualTo(email); + verify(accountCache); + } + + @Test + public void USER_NoFullNameUser() { + setFrom("USER"); + + final String email = "a.u.thor@test.example.com"; + final Account.Id user = user(null, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isNull(); + assertThat(r.getEmail()).isEqualTo(email); + verify(accountCache); + } + + @Test + public void USER_NoPreferredEmailUser() { + setFrom("USER"); + + final String name = "A U. Thor"; + final Account.Id user = user(name, null); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(name + " (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void USER_NullUser() { + setFrom("USER"); + replay(accountCache); + final Address r = create().from(null); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(ident.getName()); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void USERAllowDomain() { + 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.getName()).isEqualTo(name); + assertThat(r.getEmail()).isEqualTo(email); + verify(accountCache); + } + + @Test + public void USERNoAllowDomain() { + 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.getName()).isEqualTo(name + " (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void USERAllowDomainTwice() { + 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.getName()).isEqualTo(name); + assertThat(r.getEmail()).isEqualTo(email); + verify(accountCache); + } + + @Test + public void USERAllowDomainTwiceReverse() { + 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.getName()).isEqualTo(name + " (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void USERAllowTwoDomains() { + 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.getName()).isEqualTo(name); + assertThat(r.getEmail()).isEqualTo(email); + verify(accountCache); + } + + @Test + public void selectSERVER() { + setFrom("SERVER"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); + + setFrom("server"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); + + setFrom("sErVeR"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class); + } + + @Test + public void SERVER_FullyConfiguredUser() { + setFrom("SERVER"); + + final String name = "A U. Thor"; + final String email = "a.u.thor@test.example.com"; + final Account.Id user = userNoLookup(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(ident.getName()); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void SERVER_NullUser() { + setFrom("SERVER"); + replay(accountCache); + final Address r = create().from(null); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(ident.getName()); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void selectMIXED() { + setFrom("MIXED"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); + + setFrom("mixed"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); + + setFrom("mIxEd"); + assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); + } + + @Test + public void MIXED_FullyConfiguredUser() { + setFrom("MIXED"); + + 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.getName()).isEqualTo(name + " (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void MIXED_NoFullNameUser() { + setFrom("MIXED"); + + final String email = "a.u.thor@test.example.com"; + final Account.Id user = user(null, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo("Anonymous Coward (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void MIXED_NoPreferredEmailUser() { + setFrom("MIXED"); + + final String name = "A U. Thor"; + final Account.Id user = user(name, null); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(name + " (Code Review)"); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void MIXED_NullUser() { + setFrom("MIXED"); + replay(accountCache); + final Address r = create().from(null); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(ident.getName()); + assertThat(r.getEmail()).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void CUSTOM_FullyConfiguredUser() { + setFrom("A ${user} B <my.server@email.address>"); + + 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.getName()).isEqualTo("A " + name + " B"); + assertThat(r.getEmail()).isEqualTo("my.server@email.address"); + verify(accountCache); + } + + @Test + public void CUSTOM_NoFullNameUser() { + setFrom("A ${user} B <my.server@email.address>"); + + final String email = "a.u.thor@test.example.com"; + final Account.Id user = user(null, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo("A Anonymous Coward B"); + assertThat(r.getEmail()).isEqualTo("my.server@email.address"); + verify(accountCache); + } + + @Test + public void CUSTOM_NullUser() { + setFrom("A ${user} B <my.server@email.address>"); + + replay(accountCache); + final Address r = create().from(null); + assertThat(r).isNotNull(); + assertThat(r.getName()).isEqualTo(ident.getName()); + assertThat(r.getEmail()).isEqualTo("my.server@email.address"); + verify(accountCache); + } + + private Account.Id user(final String name, final String email) { + final AccountState s = makeUser(name, email); + expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(s); + return s.getAccount().getId(); + } + + private Account.Id userNoLookup(final String name, final String email) { + final AccountState s = makeUser(name, email); + return s.getAccount().getId(); + } + + private AccountState makeUser(final String name, final String email) { + final Account.Id userId = new Account.Id(42); + final Account account = new Account(userId, TimeUtil.nowTs()); + account.setFullName(name); + account.setPreferredEmail(email); + return new AccountState(account, Collections.<AccountGroup.UUID> emptySet(), + Collections.<AccountExternalId> emptySet(), + new HashMap<ProjectWatchKey, Set<NotifyType>>()); + } +}
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..d827e6c 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,15 +52,14 @@ 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; import com.google.gerrit.testutil.TestChanges; import com.google.gerrit.testutil.TestNotesMigration; import com.google.gerrit.testutil.TestTimeUtil; -import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.StandardKeyEncoder; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; @@ -76,12 +73,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,18 +126,18 @@ 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 public void setUp() throws Exception { setTimeForTesting(); - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); serverIdent = new PersonIdent( "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ); @@ -143,9 +159,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 +170,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 +249,24 @@ 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, + boolean unresolved) { + Comment c = new Comment( + new Comment.Key(UUID, filename, psId.get()), + commenter.getAccountId(), + t, + side, + message, + serverId, + unresolved); + 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..f9c2c42 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,12 +39,12 @@ 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.GerritBaseTests; import com.google.gerrit.testutil.TestChanges; import com.google.gerrit.testutil.TestTimeUtil; -import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.protobuf.CodecFactory; import com.google.gwtorm.protobuf.ProtobufCodec; -import com.google.gwtorm.server.StandardKeyEncoder; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -59,11 +59,7 @@ import java.util.List; import java.util.TimeZone; -public class ChangeBundleTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } - +public class ChangeBundleTest extends GerritBaseTests { private static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class); private static final ProtobufCodec<ChangeMessage> CHANGE_MESSAGE_CODEC = @@ -327,6 +323,10 @@ throws Exception { Change c1 = TestChanges.newChange( new Project.NameKey("project"), new Account.Id(100)); + PatchSet ps = new PatchSet(c1.currentPatchSetId()); + ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); + ps.setUploader(accountId); + ps.setCreatedOn(TimeUtil.nowTs()); PatchSetApproval a = new PatchSetApproval( new PatchSetApproval.Key( c1.currentPatchSetId(), accountId, new LabelId("Code-Review")), @@ -337,16 +337,16 @@ c2.setLastUpdatedOn(a.getGranted()); // Both ReviewDb, exact match required. - ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(), + ChangeBundle b1 = new ChangeBundle(c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB); - ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(), + ChangeBundle b2 = new ChangeBundle(c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB); assertDiffs(b1, b2, "effective last updated time differs for Change.Id " + c1.getId() + ":" - + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}"); + + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:12.0}"); // NoteDb allows latest timestamp from all entities in bundle. - b2 = new ChangeBundle(c2, messages(), patchSets(), + b2 = new ChangeBundle(c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB); assertNoDiffs(b1, b2); } @@ -355,6 +355,10 @@ public void diffChangesIgnoresChangeTimestampIfAnyOtherEntitiesExist() { Change c1 = TestChanges.newChange( new Project.NameKey("project"), new Account.Id(100)); + PatchSet ps = new PatchSet(c1.currentPatchSetId()); + ps.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); + ps.setUploader(accountId); + ps.setCreatedOn(TimeUtil.nowTs()); PatchSetApproval a = new PatchSetApproval( new PatchSetApproval.Key( c1.currentPatchSetId(), accountId, new LabelId("Code-Review")), @@ -367,9 +371,9 @@ // ReviewDb has later lastUpdatedOn timestamp than NoteDb, allowed since // NoteDb matches the latest timestamp of a non-Change entity. - ChangeBundle b1 = new ChangeBundle(c2, messages(), patchSets(), + ChangeBundle b1 = new ChangeBundle(c2, messages(), patchSets(ps), approvals(a), comments(), reviewers(), REVIEW_DB); - ChangeBundle b2 = new ChangeBundle(c1, messages(), patchSets(), + ChangeBundle b2 = new ChangeBundle(c1, messages(), patchSets(ps), approvals(a), comments(), reviewers(), NOTE_DB); assertThat(b1.getChange().getLastUpdatedOn()) .isGreaterThan(b2.getChange().getLastUpdatedOn()); @@ -383,7 +387,7 @@ assertDiffs(b1, b2, "effective last updated time differs for Change.Id " + c1.getId() + " in NoteDb vs. ReviewDb:" - + " {2009-09-30 17:00:06.0} != {2009-09-30 17:00:12.0}"); + + " {2009-09-30 17:00:12.0} != {2009-09-30 17:00:18.0}"); } @Test @@ -766,39 +770,6 @@ } @Test - public void diffChangeMessagesIgnoresMessagesOnPatchSetGreaterThanCurrent() - throws Exception { - Change c = TestChanges.newChange(project, accountId); - - PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1)); - ps1.setRevision(new RevId("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); - ps1.setUploader(accountId); - ps1.setCreatedOn(TimeUtil.nowTs()); - PatchSet ps2 = new PatchSet(new PatchSet.Id(c.getId(), 2)); - ps2.setRevision(new RevId("badc0feebadc0feebadc0feebadc0feebadc0fee")); - ps2.setUploader(accountId); - ps2.setCreatedOn(TimeUtil.nowTs()); - - assertThat(c.currentPatchSetId()).isEqualTo(ps1.getId()); - - ChangeMessage cm1 = new ChangeMessage( - new ChangeMessage.Key(c.getId(), "uuid1"), - accountId, TimeUtil.nowTs(), ps1.getId()); - cm1.setMessage("a message"); - ChangeMessage cm2 = new ChangeMessage( - new ChangeMessage.Key(c.getId(), "uuid2"), - accountId, TimeUtil.nowTs(), ps2.getId()); - cm2.setMessage("other message"); - - ChangeBundle b1 = new ChangeBundle(c, messages(cm1, cm2), - patchSets(ps1, ps2), approvals(), comments(), reviewers(), REVIEW_DB); - ChangeBundle b2 = new ChangeBundle(c, messages(cm1), patchSets(ps1), - approvals(), comments(), reviewers(), NOTE_DB); - assertNoDiffs(b1, b2); - assertNoDiffs(b2, b1); - } - - @Test public void diffPatchSetIdSets() throws Exception { Change c = TestChanges.newChange(project, accountId); TestChanges.incrementPatchSet(c); @@ -915,7 +886,7 @@ } @Test - public void diffIgnoresPatchSetsGreaterThanCurrent() throws Exception { + public void diffPatchSetsGreaterThanCurrent() throws Exception { Change c = TestChanges.newChange(project, accountId); PatchSet ps1 = new PatchSet(new PatchSet.Id(c.getId(), 1)); @@ -928,6 +899,13 @@ ps2.setCreatedOn(TimeUtil.nowTs()); assertThat(ps2.getId().get()).isGreaterThan(c.currentPatchSetId().get()); + ChangeMessage cm1 = new ChangeMessage( + new ChangeMessage.Key(c.getId(), "uuid1"), + accountId, TimeUtil.nowTs(), c.currentPatchSetId()); + ChangeMessage cm2 = new ChangeMessage( + new ChangeMessage.Key(c.getId(), "uuid2"), + accountId, TimeUtil.nowTs(), c.currentPatchSetId()); + PatchSetApproval a1 = new PatchSetApproval( new PatchSetApproval.Key( ps1.getId(), accountId, new LabelId("Code-Review")), @@ -940,26 +918,44 @@ TimeUtil.nowTs()); // Both ReviewDb. - ChangeBundle b1 = new ChangeBundle(c, messages(), patchSets(ps1), + ChangeBundle b1 = new ChangeBundle(c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), REVIEW_DB); - ChangeBundle b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), - approvals(a1, a2), comments(), reviewers(), REVIEW_DB); - assertNoDiffs(b1, b2); + ChangeBundle b2 = new ChangeBundle(c, messages(cm1, cm2), + patchSets(ps1, ps2), approvals(a1, a2), comments(), reviewers(), + REVIEW_DB); + assertDiffs(b1, b2, + "ChangeMessage.Key sets differ: [] only in A; [" + cm2.getKey() + + "] only in B", + "PatchSet.Id sets differ:" + + " [] only in A; [" + ps2.getId() + "] only in B", + "PatchSetApproval.Key sets differ:" + + " [] only in A; [" + a2.getKey() + "] only in B"); // One NoteDb. - b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1), + b1 = new ChangeBundle(c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB); - b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2), + b2 = new ChangeBundle(c, messages(cm1, cm2), patchSets(ps1, ps2), approvals(a1, a2), comments(), reviewers(), REVIEW_DB); - assertNoDiffs(b1, b2); - assertNoDiffs(b2, b1); + assertDiffs(b1, b2, + "ChangeMessages differ for Change.Id " + c.getId() + "\n" + + "Only in B:\n " + cm2, + "PatchSet.Id sets differ:" + + " [] only in A; [" + ps2.getId() + "] only in B", + "PatchSetApproval.Key sets differ:" + + " [] only in A; [" + a2.getKey() + "] only in B"); // Both NoteDb. - b1 = new ChangeBundle(c, messages(), patchSets(ps1), approvals(a1), + b1 = new ChangeBundle(c, messages(cm1), patchSets(ps1), approvals(a1), comments(), reviewers(), NOTE_DB); - b2 = new ChangeBundle(c, messages(), patchSets(ps1, ps2), approvals(a1, a2), + b2 = new ChangeBundle(c, messages(cm1, cm2), patchSets(ps1, ps2), approvals(a1, a2), comments(), reviewers(), NOTE_DB); - assertNoDiffs(b1, b2); + assertDiffs(b1, b2, + "ChangeMessages differ for Change.Id " + c.getId() + "\n" + + "Only in B:\n " + cm2, + "PatchSet.Id sets differ:" + + " [] only in A; [" + ps2.getId() + "] only in B", + "PatchSetApproval.Key sets differ:" + + " [] only in A; [" + a2.getKey() + "] only in B"); } @Test @@ -1259,7 +1255,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..a4f3438 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
@@ -448,7 +448,28 @@ + "subject: This is a test change\n"); } + @Test + public void currentPatchSet() throws Exception { + assertParseSucceeds("Update change\n" + + "\n" + + "Patch-set: 1\n" + + "Current: true"); + assertParseSucceeds("Update change\n" + + "\n" + + "Patch-set: 1\n" + + "Current: tRUe"); + assertParseFails("Update change\n" + + "\n" + + "Patch-set: 1\n" + + "Current: false"); + assertParseFails("Update change\n" + + "\n" + + "Patch-set: 1\n" + + "Current: blah"); + } + 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 +517,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..03cb41b 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -19,12 +19,10 @@ import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; -import static com.google.gerrit.testutil.TestChanges.incrementPatchSet; 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.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -34,21 +32,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 +76,12 @@ @Inject private DraftCommentNotes.Factory draftNotesFactory; + @Inject + private ChangeNoteUtil noteUtil; + + @Inject + private @GerritServerId String serverId; + @Test public void tagChangeMessage() throws Exception { String tag = "jenkins"; @@ -92,23 +98,46 @@ } @Test + public void patchSetDescription() throws Exception { + String description = "descriptive"; + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.setPsDescription(description); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getCurrentPatchSet().getDescription()) + .isEqualTo(description); + + description = "new, now more descriptive!"; + update = newUpdate(c, changeOwner); + update.setPsDescription(description); + update.commit(); + + notes = newNotes(c); + assertThat(notes.getCurrentPatchSet().getDescription()) + .isEqualTo(description); + } + + @Test public void tagInlineCommenrts() throws Exception { String tag = "jenkins"; 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(), false)); 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); } @@ -131,10 +160,8 @@ ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals = notes.getApprovals(); - assertThat(approvals).hasSize(2); + assertThat(approvals).hasSize(1); assertThat(approvals.entries().asList().get(0).getValue().getTag()) - .isEqualTo(tag1); - assertThat(approvals.entries().asList().get(1).getValue().getTag()) .isEqualTo(tag2); } @@ -153,9 +180,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(), false)); update.setChangeMessage("coverage verification"); update.setTag(coverageTag); update.commit(); @@ -174,10 +202,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(); @@ -245,7 +272,7 @@ assertThat(psa2.getAccountId().get()).isEqualTo(1); assertThat(psa2.getLabel()).isEqualTo("Code-Review"); assertThat(psa2.getValue()).isEqualTo((short) +1); - assertThat(psa2.getGranted()).isEqualTo(truncate(after(c, 3000))); + assertThat(psa2.getGranted()).isEqualTo(truncate(after(c, 4000))); } @Test @@ -322,7 +349,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 +374,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 +401,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()) @@ -389,6 +418,81 @@ } @Test + public void approvalsPostSubmit() throws Exception { + Change c = newChange(); + RequestId submissionId = RequestId.forChange(c); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putApproval("Code-Review", (short) 1); + update.putApproval("Verified", (short) 1); + update.commit(); + + update = newUpdate(c, changeOwner); + update.merge(submissionId, ImmutableList.of( + submitRecord("NOT_READY", null, + submitLabel("Verified", "OK", changeOwner.getAccountId()), + submitLabel("Code-Review", "NEED", null)))); + update.commit(); + + update = newUpdate(c, changeOwner); + update.putApproval("Code-Review", (short) 2); + update.commit(); + + ChangeNotes notes = newNotes(c); + List<PatchSetApproval> approvals = + Lists.newArrayList(notes.getApprovals().values()); + assertThat(approvals).hasSize(2); + assertThat(approvals.get(0).getLabel()).isEqualTo("Verified"); + assertThat(approvals.get(0).getValue()).isEqualTo((short) 1); + assertThat(approvals.get(0).isPostSubmit()).isFalse(); + assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review"); + assertThat(approvals.get(1).getValue()).isEqualTo((short) 2); + assertThat(approvals.get(1).isPostSubmit()).isTrue(); + } + + @Test + public void approvalsDuringSubmit() throws Exception { + Change c = newChange(); + RequestId submissionId = RequestId.forChange(c); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putApproval("Code-Review", (short) 1); + update.putApproval("Verified", (short) 1); + update.commit(); + + Account.Id ownerId = changeOwner.getAccountId(); + Account.Id otherId = otherUser.getAccountId(); + update = newUpdate(c, otherUser); + update.merge(submissionId, ImmutableList.of( + submitRecord("NOT_READY", null, + submitLabel("Verified", "OK", ownerId), + submitLabel("Code-Review", "NEED", null)))); + update.putApproval("Other-Label", (short) 1); + update.putApprovalFor(ownerId, "Code-Review", (short) 2); + update.commit(); + + update = newUpdate(c, otherUser); + update.putApproval("Other-Label", (short) 2); + update.commit(); + + ChangeNotes notes = newNotes(c); + + List<PatchSetApproval> approvals = + Lists.newArrayList(notes.getApprovals().values()); + assertThat(approvals).hasSize(3); + assertThat(approvals.get(0).getAccountId()).isEqualTo(ownerId); + assertThat(approvals.get(0).getLabel()).isEqualTo("Verified"); + assertThat(approvals.get(0).getValue()).isEqualTo(1); + assertThat(approvals.get(0).isPostSubmit()).isFalse(); + assertThat(approvals.get(1).getAccountId()).isEqualTo(ownerId); + assertThat(approvals.get(1).getLabel()).isEqualTo("Code-Review"); + assertThat(approvals.get(1).getValue()).isEqualTo(2); + assertThat(approvals.get(1).isPostSubmit()).isFalse(); // During submit. + assertThat(approvals.get(2).getAccountId()).isEqualTo(otherId); + assertThat(approvals.get(2).getLabel()).isEqualTo("Other-Label"); + assertThat(approvals.get(2).getValue()).isEqualTo(2); + assertThat(approvals.get(2).isPostSubmit()).isTrue(); + } + + @Test public void multipleReviewers() throws Exception { Change c = newChange(); ChangeUpdate update = newUpdate(c, changeOwner); @@ -553,6 +657,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); @@ -724,10 +894,6 @@ assertThat(ts4).isGreaterThan(ts3); incrementPatchSet(c); - RevCommit commit = tr.commit().message("PS2").create(); - update = newUpdate(c, changeOwner); - update.setCommit(rw, commit); - update.commit(); Timestamp ts5 = newNotes(c).getChange().getLastUpdatedOn(); assertThat(ts5).isGreaterThan(ts4); @@ -834,11 +1000,7 @@ assertThat(ps1.getUploader()).isEqualTo(changeOwner.getAccountId()); // ps2 by other user - incrementPatchSet(c); - RevCommit commit = tr.commit().message("PS2").create(); - ChangeUpdate update = newUpdate(c, otherUser); - update.setCommit(rw, commit); - update.commit(); + RevCommit commit = incrementPatchSet(c, otherUser); notes = newNotes(c); PatchSet ps2 = notes.getCurrentPatchSet(); assertThat(ps2.getId()).isEqualTo(new PatchSet.Id(c.getId(), 2)); @@ -849,10 +1011,11 @@ assertThat(ps2.getRevision().get()).isNotEqualTo(ps1.getRevision()); assertThat(ps2.getRevision().get()).isEqualTo(commit.name()); assertThat(ps2.getUploader()).isEqualTo(otherUser.getAccountId()); - assertThat(ps2.getCreatedOn()).isEqualTo(update.getWhen()); + assertThat(ps2.getCreatedOn()) + .isEqualTo(notes.getChange().getLastUpdatedOn()); // comment on ps1, current patch set is still ps2 - update = newUpdate(c, changeOwner); + ChangeUpdate update = newUpdate(c, changeOwner); update.setPatchSetId(ps1.getId()); update.setChangeMessage("Comment on old patch set."); update.commit(); @@ -865,8 +1028,7 @@ Change c = newChange(); PatchSet.Id psId1 = c.currentPatchSetId(); - // ps2 - incrementPatchSet(c); + incrementCurrentPatchSetFieldOnly(c); PatchSet.Id psId2 = c.currentPatchSetId(); RevCommit commit = tr.commit().message("PS2").create(); ChangeUpdate update = newUpdate(c, changeOwner); @@ -874,9 +1036,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(), false)); update.commit(); ChangeNotes notes = newNotes(c); @@ -924,8 +1087,7 @@ assertThat(notes.getPatchSets().get(psId1).getGroups()) .containsExactly("a", "b").inOrder(); - // ps2 - incrementPatchSet(c); + incrementCurrentPatchSetFieldOnly(c); PatchSet.Id psId2 = c.currentPatchSetId(); update = newUpdate(c, changeOwner); update.setCommit(rw, tr.commit().message("PS2").create()); @@ -951,7 +1113,7 @@ // ps2 with push cert Change c = newChange(); PatchSet.Id psId1 = c.currentPatchSetId(); - incrementPatchSet(c); + incrementCurrentPatchSetFieldOnly(c); PatchSet.Id psId2 = c.currentPatchSetId(); ChangeUpdate update = newUpdate(c, changeOwner); update.setPatchSetId(psId2); @@ -960,7 +1122,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 +1135,34 @@ 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(), false)); 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" + + "Unresolved: false\n" + + "UUID: uuid1\n" + + "Bytes: 7\n" + + "Comment\n" + + "\n"); + } } @Test @@ -1046,11 +1216,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"); + (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234", false); update1.setPatchSetId(psId); - update1.putComment(comment1); + update1.putComment(Status.PUBLISHED, comment1); updateManager.add(update1); ChangeUpdate update2 = newUpdate(c, otherUser); @@ -1273,11 +1443,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()); + TimeUtil.nowTs(), "message", (short) 1, revId.get(), false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1293,11 +1463,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()); + TimeUtil.nowTs(), "message", (short) 1, revId.get(), false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1313,11 +1483,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()); + TimeUtil.nowTs(), "message", (short) 1, revId.get(), false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1333,11 +1503,11 @@ 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(), + false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1361,29 +1531,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", false); 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", false); 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", false); update.setPatchSetId(psId); - update.putComment(comment3); + update.putComment(Status.PUBLISHED, comment3); update.commit(); ChangeNotes notes = newNotes(c); @@ -1397,34 +1567,40 @@ 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" + + "Unresolved: false\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" + + "Unresolved: false\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" + + "Unresolved: false\n" + + "UUID: uuid3\n" + + "Bytes: 9\n" + + "comment 3\n" + + "\n"); + } } } @@ -1441,20 +1617,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"); + (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234", false); 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"); + (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234", false); update.setPatchSetId(psId); - update.putComment(comment2); + update.putComment(Status.PUBLISHED, comment2); update.commit(); ChangeNotes notes = newNotes(c); @@ -1468,25 +1644,97 @@ 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" + + "Unresolved: false\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" + + "Unresolved: false\n" + + "UUID: uuid2\n" + + "Bytes: 9\n" + + "comment 2\n" + + "\n"); + } + } + } + + @Test + public void patchLineCommentNotesResolvedChangesValue() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, otherUser); + String uuid1 = "uuid1"; + String uuid2 = "uuid2"; + String message1 = "comment 1"; + String message2 = "comment 2"; + CommentRange range1 = new CommentRange(1, 1, 2, 1); + Timestamp time1 = TimeUtil.nowTs(); + Timestamp time2 = TimeUtil.nowTs(); + PatchSet.Id psId = c.currentPatchSetId(); + + Comment comment1 = newComment(psId, "file1", + uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1, + (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234", false); + update.setPatchSetId(psId); + update.putComment(Status.PUBLISHED, comment1); + update.commit(); + + update = newUpdate(c, otherUser); + Comment comment2 = newComment(psId, "file1", + uuid2, range1, range1.getEndLine(), otherUser, uuid1, time2, message2, + (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234", true); + update.setPatchSetId(psId); + update.putComment(Status.PUBLISHED, comment2); + 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" + + "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" + + "Unresolved: false\n" + + "UUID: uuid1\n" + + "Bytes: 9\n" + + "comment 1\n" + + "\n" + + "1:1-2:1\n" + + ChangeNoteUtil.formatTime(serverIdent, time2) + "\n" + + "Author: Other Account <2@gerrit>\n" + + "Parent: uuid1\n" + + "Unresolved: true\n" + + "UUID: uuid2\n" + + "Bytes: 9\n" + + "comment 2\n" + + "\n"); + } } } @@ -1494,6 +1742,9 @@ public void patchLineCommentNotesFormatMultiplePatchSetsSameRevId() throws Exception { Change c = newChange(); + PatchSet.Id psId1 = c.currentPatchSetId(); + incrementPatchSet(c); + PatchSet.Id psId2 = c.currentPatchSetId(); String uuid1 = "uuid1"; String uuid2 = "uuid2"; String uuid3 = "uuid3"; @@ -1505,24 +1756,21 @@ Timestamp time = TimeUtil.nowTs(); RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234"); - 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(), false); + Comment comment2 = + newComment(psId1, "file1", uuid2, range2, range2.getEndLine(), + otherUser, null, time, message2, (short) 0, revId.get(), false); + Comment comment3 = + newComment(psId2, "file1", uuid3, range1, range1.getEndLine(), + otherUser, null, time, message3, (short) 0, revId.get(), false); 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 +1785,42 @@ 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" + + "Unresolved: false\n" + + "UUID: uuid1\n" + + "Bytes: 9\n" + + "comment 1\n" + + "\n" + + "2:1-3:1\n" + + timeStr + "\n" + + "Author: Other Account <2@gerrit>\n" + + "Unresolved: false\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" + + "Unresolved: false\n" + + "UUID: uuid3\n" + + "Bytes: 9\n" + + "comment 3\n" + + "\n"); + } + } assertThat(notes.getComments()).isEqualTo( ImmutableMultimap.of( revId, comment1, @@ -1576,6 +1829,60 @@ } @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(), false); + 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" + + "Unresolved: false\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 +1897,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", false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1609,22 +1916,25 @@ 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" + + "Unresolved: false\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 +1952,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, false); 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, - (short) 1, rev2); + Comment commentForPS = + newComment(psId, "filename", uuid2, range, range.getEndLine(), + otherUser, null, now, messageForPS, + (short) 1, rev2, false); update.setPatchSetId(psId); - update.putComment(commentForPS); + update.putComment(Status.PUBLISHED, commentForPS); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -1679,19 +1988,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, false); 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, false); update.setPatchSetId(psId); - update.putComment(comment2); + update.putComment(Status.PUBLISHED, comment2); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -1714,19 +2023,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); + side, rev, false); 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); + side, rev, false); update.setPatchSetId(psId); - update.putComment(comment2); + update.putComment(Status.PUBLISHED, comment2); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -1748,11 +2057,11 @@ 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, false); update.setPatchSetId(ps1); - update.putComment(comment1); + update.putComment(Status.PUBLISHED, comment1); update.commit(); incrementPatchSet(c); @@ -1760,11 +2069,11 @@ 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, false); update.setPatchSetId(ps2); - update.putComment(comment2); + update.putComment(Status.PUBLISHED, comment2); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -1785,11 +2094,11 @@ 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, false); update.setPatchSetId(ps1); - update.putComment(comment1); + update.putComment(Status.DRAFT, comment1); update.commit(); ChangeNotes notes = newNotes(c); @@ -1797,10 +2106,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 +2134,14 @@ // 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, false); + Comment comment2 = newComment(psId, filename, uuid2, range2, + range2.getEndLine(), otherUser, null, now, "other on ps1", + side, rev, false); + update.putComment(Status.DRAFT, comment1); + update.putComment(Status.DRAFT, comment2); update.commit(); ChangeNotes notes = newNotes(c); @@ -1846,8 +2154,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 +2181,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, false); + Comment psComment = + newComment(psId, filename, uuid2, range2, range2.getEndLine(), + otherUser, null, now, "comment on ps", (short) 1, rev2, false); - update.putComment(baseComment); - update.putComment(psComment); + update.putComment(Status.DRAFT, baseComment); + update.putComment(Status.DRAFT, psComment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1896,10 +2203,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 +2228,11 @@ 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, false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.DRAFT, comment); update.commit(); ChangeNotes notes = newNotes(c); @@ -1962,11 +2267,11 @@ 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, false); update.setPatchSetId(ps1); - update.putComment(comment1); + update.putComment(Status.DRAFT, comment1); update.commit(); incrementPatchSet(c); @@ -1974,11 +2279,11 @@ 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, false); update.setPatchSetId(ps2); - update.putComment(comment2); + update.putComment(Status.DRAFT, comment2); update.commit(); ChangeNotes notes = newNotes(c); @@ -2010,10 +2315,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, false); + update.putComment(Status.PUBLISHED, comment); update.commit(); assertThat(repo.exactRef(changeMetaRef(c.getId()))).isNotNull(); @@ -2033,10 +2337,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, false); + update.putComment(Status.DRAFT, draft); update.commit(); String draftRef = refsDraftComments(c.getId(), otherUser.getAccountId()); @@ -2044,10 +2348,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, false); + update.putComment(Status.PUBLISHED, pub); update.commit(); assertThat(exactRefAllUsers(draftRef)).isEqualTo(old); @@ -2063,11 +2366,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, false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -2084,11 +2386,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, false); update.setPatchSetId(psId); - update.putComment(comment); + update.putComment(Status.PUBLISHED, comment); update.commit(); assertThat(newNotes(c).getComments()).containsExactlyEntriesIn( @@ -2112,14 +2413,14 @@ 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, false); + Comment comment2 = newComment(ps2, filename, uuid, range, + range.getEndLine(), otherUser, null, now, "comment on ps2", + side, rev2, false); + update.putComment(Status.DRAFT, comment1); + update.putComment(Status.DRAFT, comment2); update.commit(); ChangeNotes notes = newNotes(c); @@ -2128,10 +2429,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 +2449,14 @@ 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(), false); + Comment comment2 = newComment(ps1, "file2", "uuid2", range, + range.getEndLine(), otherUser, null, now, "comment2", + side, rev1.get(), false); + update.putComment(Status.DRAFT, comment1); + update.putComment(Status.DRAFT, comment2); update.commit(); ChangeNotes notes = newNotes(c); @@ -2167,8 +2466,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 +2501,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(), false); + Comment comment2 = + newComment(ps1, "file2", "uuid2", range, range.getEndLine(), otherUser, + null, now, "another comment", side, rev1.get(), false); + update.putComment(Status.DRAFT, comment1); + update.putComment(Status.DRAFT, comment2); update.commit(); String refName = refsDraftComments(c.getId(), otherUserId); @@ -2218,8 +2516,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 +2526,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 +2538,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 +2547,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 +2561,18 @@ 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, false); + 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, false); + update2.putComment(Status.PUBLISHED, comment2); try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) { manager.add(update1); @@ -2288,10 +2581,90 @@ } 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()); + } + + @Test + public void ignoreEntitiesBeyondCurrentPatchSet() throws Exception { + Change c = newChange(); + ChangeNotes notes = newNotes(c); + int numMessages = notes.getChangeMessages().size(); + int numPatchSets = notes.getPatchSets().size(); + int numApprovals = notes.getApprovals().size(); + int numComments = notes.getComments().size(); + + ChangeUpdate update = newUpdate(c, changeOwner); + update.setPatchSetId( + new PatchSet.Id(c.getId(), c.currentPatchSetId().get() + 1)); + update.setChangeMessage("Should be ignored"); + update.putApproval("Code-Review", (short) 2); + CommentRange range = new CommentRange(1, 1, 2, 1); + Comment comment = newComment(update.getPatchSetId(), "filename", + "uuid", range, range.getEndLine(), changeOwner, null, + new Timestamp(update.getWhen().getTime()), "comment", (short) 1, + "abcd1234abcd1234abcd1234abcd1234abcd1234", false); + update.putComment(Status.PUBLISHED, comment); + update.commit(); + + notes = newNotes(c); + assertThat(notes.getChangeMessages()).hasSize(numMessages); + assertThat(notes.getPatchSets()).hasSize(numPatchSets); + assertThat(notes.getApprovals()).hasSize(numApprovals); + assertThat(notes.getComments()).hasSize(numComments); + } + + @Test + public void currentPatchSet() throws Exception { + Change c = newChange(); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1); + + incrementPatchSet(c); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2); + + ChangeUpdate update = newUpdate(c, changeOwner); + update.setPatchSetId(new PatchSet.Id(c.getId(), 1)); + update.setCurrentPatchSet(); + update.commit(); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1); + + incrementPatchSet(c); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(3); + + // Delete PS3, PS1 becomes current, as the most recent event explicitly set + // it to current. + update = newUpdate(c, changeOwner); + update.setPatchSetState(PatchSetState.DELETED); + update.commit(); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1); + + // Delete PS1, PS2 becomes current. + update = newUpdate(c, changeOwner); + update.setPatchSetId(new PatchSet.Id(c.getId(), 1)); + update.setPatchSetState(PatchSetState.DELETED); + update.commit(); + assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2); + } + + private boolean testJson() { + return noteUtil.getWriteJson(); } private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception { @@ -2322,4 +2695,24 @@ .isNotNull(); assertThat(cause.getMessage()).isEqualTo(expectedMsg); } + + private void incrementCurrentPatchSetFieldOnly(Change c) { + TestChanges.incrementPatchSet(c); + } + + private RevCommit incrementPatchSet(Change c) throws Exception { + return incrementPatchSet(c, userFactory.create(c.getOwner())); + } + + private RevCommit incrementPatchSet(Change c, IdentifiedUser user) + throws Exception { + incrementCurrentPatchSetFieldOnly(c); + RevCommit commit = tr.commit() + .message("PS" + c.currentPatchSetId().get()) + .create(); + ChangeUpdate update = newUpdate(c, user); + update.setCommit(rw, commit); + update.commit(); + return tr.parseBody(commit); + } }
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..1c1f653 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,43 @@ 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); + } + + @Test + public void currentPatchSet() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.setCurrentPatchSet(); + update.commit(); + + assertBodyEquals("Update patch set 1\n" + + "\n" + + "Patch-set: 1\n" + + "Current: true\n", + update.getResult()); + } + private RevCommit parseCommit(ObjectId id) throws Exception { if (id instanceof RevCommit) { return (RevCommit) id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java index 216f71b..b0bfe57 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -18,27 +18,25 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.server.notedb.NoteDbChangeState.applyDelta; import static com.google.gerrit.server.notedb.NoteDbChangeState.parse; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; import static org.eclipse.jgit.lib.ObjectId.zeroId; -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.Project; import com.google.gerrit.server.notedb.NoteDbChangeState.Delta; +import com.google.gerrit.testutil.GerritBaseTests; import com.google.gerrit.testutil.TestChanges; -import com.google.gwtorm.client.KeyUtil; -import com.google.gwtorm.server.StandardKeyEncoder; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; -/** Unit tests for {@link NoteDbChangeState}. */ -public class NoteDbChangeStateTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } +import java.util.Optional; +/** Unit tests for {@link NoteDbChangeState}. */ +public class NoteDbChangeStateTest extends GerritBaseTests { ObjectId SHA1 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); ObjectId SHA2 = @@ -47,30 +45,44 @@ ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"); @Test - public void parseWithoutDrafts() { + public void parseReviewDbWithoutDrafts() { NoteDbChangeState state = parse(new Change.Id(1), SHA1.name()); - + assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB); assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); assertThat(state.getChangeMetaId()).isEqualTo(SHA1); assertThat(state.getDraftIds()).isEmpty(); + assertThat(state.toString()).isEqualTo(SHA1.name()); + state = parse(new Change.Id(1), "R," + SHA1.name()); + assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB); + assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); + assertThat(state.getChangeMetaId()).isEqualTo(SHA1); + assertThat(state.getDraftIds()).isEmpty(); assertThat(state.toString()).isEqualTo(SHA1.name()); } @Test - public void parseWithDrafts() { - NoteDbChangeState state = parse( - new Change.Id(1), - SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name()); - + public void parseReviewDbWithDrafts() { + String str = SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name(); + String expected = + SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name(); + NoteDbChangeState state = parse(new Change.Id(1), str); + assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB); assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); assertThat(state.getChangeMetaId()).isEqualTo(SHA1); assertThat(state.getDraftIds()).containsExactly( new Account.Id(1001), SHA3, new Account.Id(2003), SHA2); + assertThat(state.toString()).isEqualTo(expected); - assertThat(state.toString()).isEqualTo( - SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name()); + state = parse(new Change.Id(1), "R," + str); + assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB); + assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); + assertThat(state.getChangeMetaId()).isEqualTo(SHA1); + assertThat(state.getDraftIds()).containsExactly( + new Account.Id(1001), SHA3, + new Account.Id(2003), SHA2); + assertThat(state.toString()).isEqualTo(expected); } @Test @@ -126,6 +138,27 @@ SHA3.name() + ",1001=" + SHA2.name()); } + @Test + public void parseNoteDbPrimary() { + NoteDbChangeState state = parse(new Change.Id(1), "N"); + assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); + assertThat(state.getRefState().isPresent()).isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void parseInvalidPrimaryStorage() { + parse(new Change.Id(1), "X"); + } + + @Test + public void applyDeltaToNoteDbPrimaryIsNoOp() { + Change c = newChange(); + c.setNoteDbState("N"); + applyDelta(c, Delta.create(c.getId(), metaId(SHA1), + drafts(new Account.Id(1001), SHA2))); + assertThat(c.getNoteDbState()).isEqualTo("N"); + } + private static Change newChange() { return TestChanges.newChange( new Project.NameKey("project"), new Account.Id(12345)); @@ -134,7 +167,7 @@ // Static factory methods to avoid type arguments when using as method args. private static Optional<ObjectId> noMetaId() { - return Optional.absent(); + return Optional.empty(); } private static Optional<ObjectId> metaId(ObjectId 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..1db59c5 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
@@ -0,0 +1,239 @@ +// 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 java.util.stream.Collectors.toList; +import static org.junit.Assert.fail; + +import com.google.common.collect.Collections2; +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.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +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(); + } + + @SuppressWarnings("deprecation") + @Override + public String toString() { + return "E{" + when.getSeconds() + '}'; + } + } + + 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()); + + for (List<Event> events : Collections2.permutations(events(e1, e2, e3))) { + assertSorted(events, events(e1, e2, e3)); + } + } + + @Test + public void topoSortOneDep() { + List<Event> es; + + // Input list is 0,1,2 + + // 0 depends on 1 => 1,0,2 + es = threeEventsOneDep(0, 1); + assertSorted(es, events(es, 1, 0, 2)); + + // 1 depends on 0 => 0,1,2 + es = threeEventsOneDep(1, 0); + assertSorted(es, events(es, 0, 1, 2)); + + // 0 depends on 2 => 1,2,0 + es = threeEventsOneDep(0, 2); + assertSorted(es, events(es, 1, 2, 0)); + + // 2 depends on 0 => 0,1,2 + es = threeEventsOneDep(2, 0); + assertSorted(es, events(es, 0, 1, 2)); + + // 1 depends on 2 => 0,2,1 + es = threeEventsOneDep(1, 2); + assertSorted(es, events(es, 0, 2, 1)); + + // 2 depends on 1 => 0,1,2 + es = threeEventsOneDep(2, 1); + assertSorted(es, events(es, 0, 1, 2)); + } + + private List<Event> threeEventsOneDep(int depFromIdx, int depOnIdx) { + List<Event> events = Lists.newArrayList( + new TestEvent(TimeUtil.nowTs()), + new TestEvent(TimeUtil.nowTs()), + new TestEvent(TimeUtil.nowTs())); + events.get(depFromIdx).addDep(events.get(depOnIdx)); + return events; + } + + @Test + public void lastEventDependsOnFirstEvent() { + List<Event> events = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + events.add(new TestEvent(TimeUtil.nowTs())); + } + events.get(events.size() - 1).addDep(events.get(0)); + assertSorted(events, events); + } + + @Test + public void firstEventDependsOnLastEvent() { + List<Event> events = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + events.add(new TestEvent(TimeUtil.nowTs())); + } + events.get(0).addDep(events.get(events.size() - 1)); + + List<Event> expected = new ArrayList<>(); + expected.addAll(events.subList(1, events.size())); + expected.add(events.get(0)); + assertSorted(events, expected); + } + + @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); + + assertSorted( + events(e1, e2, e3, e4), + events(e4, e3, e2, e1)); + } + + @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. + assertSorted( + events(e2, e3, e1, e4), + events(e3, e2, e4, e1)); + } + + @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. + assertSorted( + events(e4, e3, e2, e1), + events(e4, e1, e2, e3)); + } + + @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. + assertSorted( + events(e2, e1), + events(e1, e2)); + } + + @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 static List<Event> events(Event... es) { + return Lists.newArrayList(es); + } + + private static List<Event> events(List<Event> in, Integer... indexes) { + return Stream.of(indexes).map(in::get).collect(toList()); + } + + private static void assertSorted(List<Event> unsorted, List<Event> expected) { + List<Event> actual = new ArrayList<>(unsorted); + new EventSorter(actual).sort(); + assertThat(actual) + .named("sorted" + unsorted) + .isEqualTo(expected); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java index bff557c..2721b30 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
@@ -25,7 +25,7 @@ public class PatchListEntryTest { @Test - public void testEmpty1() { + public void empty1() { final String name = "empty-file"; final PatchListEntry e = PatchListEntry.empty(name); assertNull(e.getOldName());
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..ee1203d 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
@@ -57,6 +57,7 @@ import com.google.gerrit.server.config.AllUsersNameProvider; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; @@ -137,13 +138,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(); } @@ -241,6 +242,7 @@ @Inject private SchemaCreator schemaCreator; @Inject private InMemoryDatabase schemaFactory; @Inject private ThreadLocalRequestContext requestContext; + @Inject private Provider<InternalChangeQuery> queryProvider; @Before public void setUp() throws Exception { @@ -355,14 +357,14 @@ } @Test - public void testOwnerProject() { + public void ownerProject() { allow(local, OWNER, ADMIN, "refs/*"); assertAdminsAreOwnersAndDevsAreNot(); } @Test - public void testDenyOwnerProject() { + public void denyOwnerProject() { allow(local, OWNER, ADMIN, "refs/*"); deny(local, OWNER, DEVS, "refs/*"); @@ -370,7 +372,7 @@ } @Test - public void testBlockOwnerProject() { + public void blockOwnerProject() { allow(local, OWNER, ADMIN, "refs/*"); block(local, OWNER, DEVS, "refs/*"); @@ -378,7 +380,7 @@ } @Test - public void testBranchDelegation1() { + public void branchDelegation1() { allow(local, OWNER, ADMIN, "refs/*"); allow(local, OWNER, DEVS, "refs/heads/x/*"); @@ -395,7 +397,7 @@ } @Test - public void testBranchDelegation2() { + public void branchDelegation2() { allow(local, OWNER, ADMIN, "refs/*"); allow(local, OWNER, DEVS, "refs/heads/x/*"); allow(local, OWNER, fixers, "refs/heads/x/y/*"); @@ -424,7 +426,7 @@ } @Test - public void testInheritRead_SingleBranchDeniesUpload() { + public void inheritRead_SingleBranchDeniesUpload() { allow(parent, READ, REGISTERED_USERS, "refs/*"); allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/foobar"); @@ -438,7 +440,7 @@ } @Test - public void testBlockPushDrafts() { + public void blockPushDrafts() { allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*"); block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*"); @@ -448,7 +450,7 @@ } @Test - public void testBlockPushDraftsUnblockAdmin() { + public void blockPushDraftsUnblockAdmin() { block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*"); allow(parent, PUSH, ADMIN, "refs/drafts/*"); @@ -459,7 +461,7 @@ } @Test - public void testInheritRead_SingleBranchDoesNotOverrideInherited() { + public void inheritRead_SingleBranchDoesNotOverrideInherited() { allow(parent, READ, REGISTERED_USERS, "refs/*"); allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/foobar"); @@ -471,7 +473,7 @@ } @Test - public void testInheritDuplicateSections() throws Exception { + public void inheritDuplicateSections() throws Exception { allow(parent, READ, ADMIN, "refs/*"); allow(local, READ, DEVS, "refs/heads/*"); assertCanRead(user(local, "a", ADMIN)); @@ -484,7 +486,7 @@ } @Test - public void testInheritRead_OverrideWithDeny() { + public void inheritRead_OverrideWithDeny() { allow(parent, READ, REGISTERED_USERS, "refs/*"); deny(local, READ, REGISTERED_USERS, "refs/*"); @@ -492,7 +494,7 @@ } @Test - public void testInheritRead_AppendWithDenyOfRef() { + public void inheritRead_AppendWithDenyOfRef() { allow(parent, READ, REGISTERED_USERS, "refs/*"); deny(local, READ, REGISTERED_USERS, "refs/heads/*"); @@ -504,7 +506,7 @@ } @Test - public void testInheritRead_OverridesAndDeniesOfRef() { + public void inheritRead_OverridesAndDeniesOfRef() { allow(parent, READ, REGISTERED_USERS, "refs/*"); deny(local, READ, REGISTERED_USERS, "refs/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/*"); @@ -517,7 +519,7 @@ } @Test - public void testInheritSubmit_OverridesAndDeniesOfRef() { + public void inheritSubmit_OverridesAndDeniesOfRef() { allow(parent, SUBMIT, REGISTERED_USERS, "refs/*"); deny(local, SUBMIT, REGISTERED_USERS, "refs/*"); allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*"); @@ -529,7 +531,7 @@ } @Test - public void testCannotUploadToAnyRef() { + public void cannotUploadToAnyRef() { allow(parent, READ, REGISTERED_USERS, "refs/*"); allow(local, READ, DEVS, "refs/heads/*"); allow(local, PUSH, DEVS, "refs/for/refs/heads/*"); @@ -540,14 +542,14 @@ } @Test - public void testUsernamePatternCanUploadToAnyRef() { + public void usernamePatternCanUploadToAnyRef() { allow(local, PUSH, REGISTERED_USERS, "refs/heads/users/${username}/*"); ProjectControl u = user(local, "a-registered-user"); assertCanUpload(u); } @Test - public void testUsernamePatternNonRegex() { + public void usernamePatternNonRegex() { allow(local, READ, DEVS, "refs/sb/${username}/heads/*"); ProjectControl u = user(local, "u", DEVS); @@ -557,7 +559,7 @@ } @Test - public void testUsernamePatternWithRegex() { + public void usernamePatternWithRegex() { allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*"); ProjectControl u = user(local, "d.v", DEVS); @@ -567,7 +569,7 @@ } @Test - public void testUsernameEmailPatternWithRegex() { + public void usernameEmailPatternWithRegex() { allow(local, READ, DEVS, "^refs/sb/${username}/heads/.*"); ProjectControl u = user(local, "d.v@ger-rit.org", DEVS); @@ -577,7 +579,7 @@ } @Test - public void testSortWithRegex() { + public void sortWithRegex() { allow(local, READ, DEVS, "^refs/heads/.*"); allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*"); @@ -588,7 +590,7 @@ } @Test - public void testBlockRule_ParentBlocksChild() { + public void blockRule_ParentBlocksChild() { allow(local, PUSH, DEVS, "refs/tags/*"); block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*"); ProjectControl u = user(local, DEVS); @@ -596,7 +598,7 @@ } @Test - public void testBlockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() { + public void blockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() { allow(local, PUSH, DEVS, "refs/tags/*"); block(local, PUSH, ANONYMOUS_USERS, "refs/tags/*"); block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*"); @@ -606,7 +608,7 @@ } @Test - public void testBlockLabelRange_ParentBlocksChild() { + public void blockLabelRange_ParentBlocksChild() { allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); @@ -620,7 +622,7 @@ } @Test - public void testBlockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() { + public void blockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() { allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); block(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); block(parent, LABEL + "Code-Review", -2, +2, DEVS, @@ -637,7 +639,7 @@ } @Test - public void testInheritSubmit_AllowInChildDoesntAffectUnblockInParent() { + public void inheritSubmit_AllowInChildDoesntAffectUnblockInParent() { block(parent, SUBMIT, ANONYMOUS_USERS, "refs/heads/*"); allow(parent, SUBMIT, REGISTERED_USERS, "refs/heads/*"); allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*"); @@ -647,7 +649,7 @@ } @Test - public void testUnblockNoForce() { + public void unblockNoForce() { block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); allow(local, PUSH, DEVS, "refs/heads/*"); @@ -656,7 +658,7 @@ } @Test - public void testUnblockForce() { + public void unblockForce() { PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); r.setForce(true); allow(local, PUSH, DEVS, "refs/heads/*").setForce(true); @@ -666,7 +668,7 @@ } @Test - public void testUnblockForceWithAllowNoForce_NotPossible() { + public void unblockForceWithAllowNoForce_NotPossible() { PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); r.setForce(true); allow(local, PUSH, DEVS, "refs/heads/*"); @@ -676,7 +678,7 @@ } @Test - public void testUnblockMoreSpecificRef_Fails() { + public void unblockMoreSpecificRef_Fails() { block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); allow(local, PUSH, DEVS, "refs/heads/master"); @@ -685,7 +687,44 @@ } @Test - public void testUnblockLargerScope_Fails() { + public void unblockMoreSpecificRefInLocal_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 unblockMoreSpecificRefWithExclusiveFlag() { + 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 unblockMoreSpecificRefInLocalWithExclusiveFlag_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 unblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_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 unblockLargerScope_Fails() { block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master"); allow(local, PUSH, DEVS, "refs/heads/*"); @@ -694,7 +733,7 @@ } @Test - public void testUnblockInLocal_Fails() { + public void unblockInLocal_Fails() { block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*"); allow(local, PUSH, fixers, "refs/heads/*"); @@ -703,7 +742,7 @@ } @Test - public void testUnblockInParentBlockInLocal() { + public void unblockInParentBlockInLocal() { block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*"); allow(parent, PUSH, DEVS, "refs/heads/*"); block(local, PUSH, DEVS, "refs/heads/*"); @@ -713,7 +752,7 @@ } @Test - public void testUnblockVisibilityByRegisteredUsers() { + public void unblockVisibilityByRegisteredUsers() { block(local, READ, ANONYMOUS_USERS, "refs/heads/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/*"); @@ -724,7 +763,7 @@ } @Test - public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() { + public void unblockInLocalVisibilityByRegisteredUsers_Fails() { block(parent, READ, ANONYMOUS_USERS, "refs/heads/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/*"); @@ -735,7 +774,7 @@ } @Test - public void testUnblockForceEditTopicName() { + public void unblockForceEditTopicName() { block(local, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*"); allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true); @@ -746,7 +785,7 @@ } @Test - public void testUnblockInLocalForceEditTopicName_Fails() { + public void unblockInLocalForceEditTopicName_Fails() { block(parent, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*"); allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true); @@ -757,7 +796,7 @@ } @Test - public void testUnblockRange() { + public void unblockRange() { block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*"); allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); @@ -768,7 +807,7 @@ } @Test - public void testUnblockRangeOnMoreSpecificRef_Fails() { + public void unblockRangeOnMoreSpecificRef_Fails() { block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/*"); allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master"); @@ -779,7 +818,7 @@ } @Test - public void testUnblockRangeOnLargerScope_Fails() { + public void unblockRangeOnLargerScope_Fails() { block(local, LABEL + "Code-Review", -1, +1, ANONYMOUS_USERS, "refs/heads/master"); allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); @@ -790,7 +829,7 @@ } @Test - public void testUnblockInLocalRange_Fails() { + public void unblockInLocalRange_Fails() { block(parent, LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS, "refs/heads/*"); allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*"); @@ -803,7 +842,7 @@ } @Test - public void testUnblockRangeForChangeOwner() { + public void unblockRangeForChangeOwner() { allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*"); ProjectControl u = user(local, DEVS); @@ -814,7 +853,7 @@ } @Test - public void testUnblockRangeForNotChangeOwner() { + public void unblockRangeForNotChangeOwner() { allow(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*"); ProjectControl u = user(local, DEVS); @@ -825,7 +864,15 @@ } @Test - public void testValidateRefPatternsOK() throws Exception { + public void blockOwner() { + block(parent, OWNER, ANONYMOUS_USERS, "refs/*"); + allow(local, OWNER, DEVS, "refs/*"); + + assertThat(user(local, DEVS).isOwner()).isFalse(); + } + + @Test + public void validateRefPatternsOK() throws Exception { RefPattern.validate("refs/*"); RefPattern.validate("^refs/heads/*"); RefPattern.validate("^refs/tags/[0-9a-zA-Z-_.]+"); @@ -844,7 +891,7 @@ } @Test - public void testValidateRefPatternNoDanglingCharacter() throws Exception { + public void validateRefPatternNoDanglingCharacter() throws Exception { RefPattern.validate("^refs/heads/tmp/sdk/[0-9]{3,3}_R[1-9][A-Z][0-9]{3,3}"); } @@ -882,7 +929,7 @@ return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(), Collections.<AccountGroup.UUID> emptySet(), projectCache, - sectionSorter, null, changeControlFactory, null, null, + sectionSorter, null, changeControlFactory, null, queryProvider, null, canonicalWebUrl, new MockUser(name, memberOf), newProjectState(local)); }
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..d3f0bcb 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) @@ -121,6 +128,15 @@ return rule; } + public static PermissionRule remove(ProjectConfig project, + String permissionName, AccountGroup.UUID group, String ref) { + PermissionRule rule = newRule(project, group); + project.getAccessSection(ref, true) + .getPermission(permissionName, true) + .remove(rule); + return rule; + } + public static PermissionRule block(ProjectConfig project, String capabilityName, AccountGroup.UUID group) { PermissionRule rule = newRule(project, group); @@ -163,9 +179,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/AndPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java index 47df2db..01efa1d 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/AndPredicateTest.java
@@ -29,7 +29,7 @@ public class AndPredicateTest extends PredicateTest { @Test - public void testChildren() { + public void children() { final TestPredicate a = f("author", "alice"); final TestPredicate b = f("author", "bob"); final Predicate<String> n = and(a, b); @@ -39,7 +39,7 @@ } @Test - public void testChildrenUnmodifiable() { + public void childrenUnmodifiable() { final TestPredicate a = f("author", "alice"); final TestPredicate b = f("author", "bob"); final Predicate<String> n = and(a, b);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java index 8f16670..550bee5 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java
@@ -46,7 +46,7 @@ } @Test - public void testNameValue() { + public void nameValue() { final String name = "author"; final String value = "alice"; final OperatorPredicate<String> f = f(name, value);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java index 0256081..93c1bf4 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
@@ -30,7 +30,7 @@ public class NotPredicateTest extends PredicateTest { @Test - public void testNotNot() { + public void notNot() { final TestPredicate p = f("author", "bob"); final Predicate<String> n = not(p); assertTrue(n instanceof NotPredicate); @@ -39,7 +39,7 @@ } @Test - public void testChildren() { + public void children() { final TestPredicate p = f("author", "bob"); final Predicate<String> n = not(p); assertEquals(1, n.getChildCount()); @@ -47,7 +47,7 @@ } @Test - public void testChildrenUnmodifiable() { + public void childrenUnmodifiable() { final TestPredicate p = f("author", "bob"); final Predicate<String> n = not(p);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java index 5640d1b..27be48d 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/OrPredicateTest.java
@@ -29,7 +29,7 @@ public class OrPredicateTest extends PredicateTest { @Test - public void testChildren() { + public void children() { final TestPredicate a = f("author", "alice"); final TestPredicate b = f("author", "bob"); final Predicate<String> n = or(a, b); @@ -39,7 +39,7 @@ } @Test - public void testChildrenUnmodifiable() { + public void childrenUnmodifiable() { final TestPredicate a = f("author", "alice"); final TestPredicate b = f("author", "bob"); final Predicate<String> n = or(a, b);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java index e349273..efa1039 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java
@@ -20,7 +20,7 @@ public class QueryParserTest { @Test - public void testProjectBare() throws QueryParseException { + public void projectBare() throws QueryParseException { Tree r; r = parse("project:tools/gerrit");
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 83f83bb..037d1da 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
@@ -15,28 +15,33 @@ package com.google.gerrit.server.query.account; import static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toList; 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.Iterables; import com.google.gerrit.extensions.api.GerritApi; import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest; import com.google.gerrit.extensions.client.ListAccountsOption; +import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AuthRequest; -import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.schema.SchemaCreator; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gerrit.testutil.ConfigSuite; @@ -55,6 +60,7 @@ import org.junit.Test; import org.junit.rules.TestName; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -91,14 +97,20 @@ protected InMemoryDatabase schemaFactory; @Inject - protected InternalChangeQuery internalChangeQuery; - - @Inject protected SchemaCreator schemaCreator; @Inject protected ThreadLocalRequestContext requestContext; + @Inject + protected OneOffRequestContext oneOffRequestContext; + + @Inject + protected InternalAccountQuery internalAccountQuery; + + @Inject + protected AllProjectsName allProjects; + protected LifecycleManager lifecycle; protected ReviewDb db; protected AccountInfo currentUserInfo; @@ -271,6 +283,29 @@ } @Test + public void byWatchedProject() throws Exception { + Project.NameKey p = createProject(name("p")); + Project.NameKey p2 = createProject(name("p2")); + AccountInfo user1 = newAccountWithFullName("jdoe", "John Doe"); + AccountInfo user2 = newAccountWithFullName("jroe", "Jane Roe"); + AccountInfo user3 = newAccountWithFullName("user3", "Mr Selfish"); + + assertThat(internalAccountQuery.byWatchedProject(p)).isEmpty(); + + watch(user1, p, null); + assertAccounts(internalAccountQuery.byWatchedProject(p), user1); + + watch(user2, p, "keyword"); + assertAccounts(internalAccountQuery.byWatchedProject(p), user1, user2); + + watch(user3, p2, "keyword"); + watch(user3, allProjects, "keyword"); + assertAccounts(internalAccountQuery.byWatchedProject(p), user1, user2); + assertAccounts(internalAccountQuery.byWatchedProject(p2), user3); + assertAccounts(internalAccountQuery.byWatchedProject(allProjects), user3); + } + + @Test public void withLimit() throws Exception { String domain = name("test.com"); AccountInfo user1 = newAccountWithEmail("user1", "user1@" + domain); @@ -278,10 +313,10 @@ AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain); List<AccountInfo> result = assertQuery(domain, user1, user2, user3); - assertThat(result.get(result.size() - 1)._moreAccounts).isNull(); + assertThat(Iterables.getLast(result)._moreAccounts).isNull(); - result = assertQuery(newQuery(domain).withLimit(2), user1, user2); - assertThat(result.get(result.size() - 1)._moreAccounts).isTrue(); + result = assertQuery(newQuery(domain).withLimit(2), result.subList(0, 2)); + assertThat(Iterables.getLast(result)._moreAccounts).isTrue(); } @Test @@ -291,8 +326,8 @@ AccountInfo user2 = newAccountWithEmail("user2", "user2@" + domain); AccountInfo user3 = newAccountWithEmail("user3", "user3@" + domain); - assertQuery(domain, user1, user2, user3); - assertQuery(newQuery(domain).withStart(1), user2, user3); + List<AccountInfo> result = assertQuery(domain, user1, user2, user3); + assertQuery(newQuery(domain).withStart(1), result.subList(1, 3)); } @Test @@ -409,6 +444,24 @@ return gApi.accounts().id(id.get()).get(); } + protected Project.NameKey createProject(String name) throws RestApiException { + gApi.projects().create(name); + return new Project.NameKey(name); + } + + protected void watch(AccountInfo account, Project.NameKey project, + String filter) throws RestApiException { + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); + ProjectWatchInfo pwi = new ProjectWatchInfo(); + pwi.project = project.get(); + pwi.filter = filter; + pwi.notifyAbandonedChanges = true; + pwi.notifyNewChanges = true; + pwi.notifyAllComments = true; + projectsToWatch.add(pwi); + gApi.accounts().id(account._accountId).setWatchedProjects(projectsToWatch); + } + protected String quote(String s) { return "\"" + s + "\""; } @@ -426,18 +479,20 @@ private Account.Id createAccount(String username, String fullName, String email, boolean active) throws Exception { - Account.Id id = - accountManager.authenticate(AuthRequest.forUser(username)).getAccountId(); - if (email != null) { - accountManager.link(id, AuthRequest.forEmail(email)); + try (ManualRequestContext ctx = oneOffRequestContext.open()) { + Account.Id id = + accountManager.authenticate(AuthRequest.forUser(username)).getAccountId(); + if (email != null) { + accountManager.link(id, AuthRequest.forEmail(email)); + } + Account a = db.accounts().get(id); + a.setFullName(fullName); + a.setPreferredEmail(email); + a.setActive(active); + db.accounts().update(ImmutableList.of(a)); + accountCache.evict(id); + return id; } - Account a = db.accounts().get(id); - a.setFullName(fullName); - a.setPreferredEmail(email); - a.setActive(active); - db.accounts().update(ImmutableList.of(a)); - accountCache.evict(id); - return id; } private void addEmails(AccountInfo account, String... emails) @@ -458,8 +513,14 @@ return assertQuery(newQuery(query), accounts); } - protected List<AccountInfo> assertQuery(QueryRequest query, AccountInfo... accounts) - throws Exception { + protected List<AccountInfo> assertQuery(QueryRequest query, + AccountInfo... accounts) throws Exception { + return assertQuery(query, Arrays.asList(accounts)); + } + + + protected List<AccountInfo> assertQuery(QueryRequest query, + List<AccountInfo> accounts) throws Exception { List<AccountInfo> result = query.get(); Iterable<Integer> ids = ids(result); assertThat(ids).named(format(query, result, accounts)) @@ -467,12 +528,20 @@ return result; } - private String format(QueryRequest query, Iterable<AccountInfo> actualIds, + protected void assertAccounts(List<AccountState> accounts, AccountInfo... expectedAccounts) { + assertThat(accounts.stream().map(a -> a.getAccount().getId().get()) + .collect(toList())) + .containsExactlyElementsIn(Arrays.asList(expectedAccounts).stream() + .map(a -> a._accountId).collect(toList())); + } + + private String format(QueryRequest query, List<AccountInfo> actualIds, + List<AccountInfo> expectedAccounts) { StringBuilder b = new StringBuilder(); b.append("query '").append(query.getQuery()) .append("' with expected accounts "); - b.append(format(Arrays.asList(expectedAccounts))); + b.append(format(expectedAccounts)); b.append(" and result "); b.append(format(actualIds)); return b.toString(); @@ -496,22 +565,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 ids(Arrays.asList(accounts)); } - 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; - } - }); + protected static Iterable<Integer> ids(List<AccountInfo> accounts) { + return accounts.stream().map(a -> a._accountId).collect(toList()); } }
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..60fd2ef 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,8 @@ 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 static java.util.stream.Collectors.toList; -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,13 +30,16 @@ 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.ReviewInput.RobotCommentInput; import com.google.gerrit.extensions.api.changes.StarsInput; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.common.ChangeInfo; @@ -52,7 +54,9 @@ import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.Sequences; @@ -62,12 +66,18 @@ import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ChangeTriplet; import com.google.gerrit.server.change.PatchSetInserter; +import com.google.gerrit.server.edit.ChangeEditModifier; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.index.IndexConfig; +import com.google.gerrit.server.index.QueryOptions; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.server.index.change.ChangeIndexer; +import com.google.gerrit.server.index.change.IndexedChangeQuery; +import com.google.gerrit.server.index.change.StalenessChecker; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.NoteDbChangeState; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.RequestContext; @@ -87,6 +97,7 @@ import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.SystemReader; import org.junit.After; @@ -94,8 +105,10 @@ import org.junit.Ignore; import org.junit.Test; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -118,8 +131,10 @@ @Inject protected ChangeQueryBuilder queryBuilder; @Inject protected GerritApi gApi; @Inject protected IdentifiedUser.GenericFactory userFactory; + @Inject protected ChangeEditModifier changeEditModifier; @Inject protected ChangeIndexCollection indexes; @Inject protected ChangeIndexer indexer; + @Inject protected IndexConfig indexConfig; @Inject protected InMemoryDatabase schemaFactory; @Inject protected InMemoryRepositoryManager repoManager; @Inject protected InternalChangeQuery internalChangeQuery; @@ -189,7 +204,7 @@ @Before public void setTimeForTesting() { - resetTimeWithClockStep(1, MILLISECONDS); + resetTimeWithClockStep(1, SECONDS); } private void resetTimeWithClockStep(long clockStep, TimeUnit clockStepUnit) { @@ -244,7 +259,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 +358,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 @@ -370,6 +388,9 @@ assertQuery("owner:" + userId.get(), change1); assertQuery("owner:" + user2, change2); + + String nameEmail = user.asIdentifiedUser().getNameEmail(); + assertQuery("owner: \"" + nameEmail + "\"", change1); } @Test @@ -619,6 +640,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 +785,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)); } @@ -945,15 +988,22 @@ long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); - // Queried by AgePredicate constructor. + long startMs = TestTimeUtil.START.getMillis(); + Change change1 = + insert(repo, newChange(repo), null, new Timestamp(startMs)); + Change change2 = insert( + repo, newChange(repo), null, + new Timestamp(startMs + thirtyHoursInMs)); + + // Stop time so age queries use the same endpoint. TestTimeUtil.setClockStep(0, MILLISECONDS); - long now = TimeUtil.nowMs(); + TestTimeUtil.setClock(new Timestamp(startMs + 2 * thirtyHoursInMs)); + long nowMs = TimeUtil.nowMs(); + assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1)) .isEqualTo(thirtyHoursInMs); - assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs); - assertThat(TimeUtil.nowMs()).isEqualTo(now); + assertThat(nowMs - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs); + assertThat(TimeUtil.nowMs()).isEqualTo(nowMs); assertQuery("-age:1d"); assertQuery("-age:" + (30 * 60 - 1) + "m"); @@ -966,10 +1016,14 @@ @Test public void byBefore() throws Exception { - resetTimeWithClockStep(30, HOURS); + long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); + resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + long startMs = TestTimeUtil.START.getMillis(); + Change change1 = + insert(repo, newChange(repo), null, new Timestamp(startMs)); + Change change2 = insert( + repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs)); TestTimeUtil.setClockStep(0, MILLISECONDS); assertQuery("before:2009-09-29"); @@ -986,10 +1040,14 @@ @Test public void byAfter() throws Exception { - resetTimeWithClockStep(30, HOURS); + long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS); + resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS); TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo)); - Change change2 = insert(repo, newChange(repo)); + long startMs = TestTimeUtil.START.getMillis(); + Change change1 = + insert(repo, newChange(repo), null, new Timestamp(startMs)); + Change change2 = insert( + repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs)); TestTimeUtil.setClockStep(0, MILLISECONDS); assertQuery("after:2009-10-03"); @@ -1417,6 +1475,70 @@ } @Test + public void submitRecords() throws Exception { + Account.Id user1 = createAccount("user1"); + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo)); + Change change2 = insert(repo, newChange(repo)); + + gApi.changes() + .id(change1.getId().get()) + .current() + .review(ReviewInput.approve()); + requestContext.setContext(newRequestContext(user1)); + gApi.changes() + .id(change2.getId().get()) + .current() + .review(ReviewInput.recommend()); + requestContext.setContext(newRequestContext(user.getAccountId())); + + assertQuery("is:submittable", change1); + assertQuery("-is:submittable", change2); + assertQuery("submittable:ok", change1); + assertQuery("submittable:not_ready", change2); + + assertQuery("label:CodE-RevieW=ok", change1); + assertQuery("label:CodE-RevieW=ok,user=user", change1); + assertQuery("label:CodE-RevieW=ok,Administrators", change1); + assertQuery("label:CodE-RevieW=ok,group=Administrators", change1); + assertQuery("label:CodE-RevieW=ok,owner", change1); + assertQuery("label:CodE-RevieW=ok,user1"); + assertQuery("label:CodE-RevieW=need", change2); + // NEED records don't have associated users. + assertQuery("label:CodE-RevieW=need,user1"); + assertQuery("label:CodE-RevieW=need,user"); + } + + @Test + public void hasEdit() throws Exception { + Account.Id user1 = createAccount("user1"); + Account.Id user2 = createAccount("user2"); + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo)); + PatchSet ps1 = db.patchSets().get(change1.currentPatchSetId()); + Change change2 = insert(repo, newChange(repo)); + PatchSet ps2 = db.patchSets().get(change2.currentPatchSetId()); + + requestContext.setContext(newRequestContext(user1)); + assertQuery("has:edit"); + assertThat(changeEditModifier.createEdit(change1, ps1)) + .isEqualTo(RefUpdate.Result.NEW); + assertThat(changeEditModifier.createEdit(change2, ps2)) + .isEqualTo(RefUpdate.Result.NEW); + + requestContext.setContext(newRequestContext(user2)); + assertQuery("has:edit"); + assertThat(changeEditModifier.createEdit(change2, ps2)) + .isEqualTo(RefUpdate.Result.NEW); + + requestContext.setContext(newRequestContext(user1)); + assertQuery("has:edit", change2, change1); + + requestContext.setContext(newRequestContext(user2)); + assertQuery("has:edit", change2); + } + + @Test public void byCommitsOnBranchNotMerged() throws Exception { TestRepository<Repo> repo = createProject("repo"); int n = 10; @@ -1436,13 +1558,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) @@ -1505,6 +1622,123 @@ cd.currentApprovals(); } + @Test + public void reindexIfStale() throws Exception { + Account.Id user = createAccount("user"); + Project.NameKey project = new Project.NameKey("repo"); + TestRepository<Repo> repo = createProject(project.get()); + Change change = insert(repo, newChange(repo)); + PatchSet ps = db.patchSets().get(change.currentPatchSetId()); + + requestContext.setContext(newRequestContext(user)); + assertThat(changeEditModifier.createEdit(change, ps)) + .isEqualTo(RefUpdate.Result.NEW); + assertQuery("has:edit", change); + assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse(); + + // Delete edit ref behind index's back. + RefUpdate ru = repo.getRepository().updateRef( + RefNames.refsEdit(user, change.getId(), ps.getId())); + ru.setForceUpdate(true); + assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED); + + // Index is stale. + assertQuery("has:edit", change); + assertThat(indexer.reindexIfStale(project, change.getId()).get()).isTrue(); + assertQuery("has:edit"); + } + + @Test + public void refStateFields() throws Exception { + Account.Id user = createAccount("user"); + Project.NameKey project = new Project.NameKey("repo"); + TestRepository<Repo> repo = createProject(project.get()); + String path = "file"; + RevCommit commit = repo.parseBody( + repo.commit().message("one").add(path, "contents").create()); + Change change = insert(repo, newChangeForCommit(repo, commit)); + Change.Id id = change.getId(); + int c = id.get(); + PatchSet ps = db.patchSets().get(change.currentPatchSetId()); + requestContext.setContext(newRequestContext(user)); + + // Ensure one of each type of supported ref is present for the change. If + // any more refs are added, update this test to reflect them. + + // Edit + assertThat(changeEditModifier.createEdit(change, ps)) + .isEqualTo(RefUpdate.Result.NEW); + + // Star + gApi.accounts() + .self() + .starChange(change.getId().toString()); + + if (notesMigration.readChanges()) { + // Robot comment. + ReviewInput rin = new ReviewInput(); + RobotCommentInput rcin = new RobotCommentInput(); + rcin.robotId = "happyRobot"; + rcin.robotRunId = "1"; + rcin.line = 1; + rcin.message = "nit: trailing whitespace"; + rcin.path = path; + rin.robotComments = ImmutableMap.of(path, ImmutableList.of(rcin)); + gApi.changes().id(c).current().review(rin); + } + + // Draft. + DraftInput din = new DraftInput(); + din.path = path; + din.line = 1; + din.message = "draft"; + gApi.changes().id(c).current().createDraft(din); + + if (notesMigration.readChanges()) { + // Force NoteDb primary. + change = ReviewDbUtil.unwrapDb(db).changes().get(id); + change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + ReviewDbUtil.unwrapDb(db).changes().update(Collections.singleton(change)); + indexer.index(db, change); + } + + QueryOptions opts = IndexedChangeQuery.createOptions( + indexConfig, 0, 1, StalenessChecker.FIELDS); + ChangeData cd = indexes.getSearchIndex().get(id, opts).get(); + + String cs = RefNames.shard(c); + int u = user.get(); + String us = RefNames.shard(u); + + List<String> expectedStates = Lists.newArrayList( + "repo:refs/users/" + us + "/edit-" + c + "/1", + "All-Users:refs/starred-changes/" + cs + "/" + u); + if (notesMigration.readChanges()) { + expectedStates.add("repo:refs/changes/" + cs + "/meta"); + expectedStates.add("repo:refs/changes/" + cs + "/robot-comments"); + expectedStates.add("All-Users:refs/draft-comments/" + cs + "/" + u); + } + assertThat( + cd.getRefStates().stream() + .map(String::new) + // Omit SHA-1, we're just concerned with the project/ref names. + .map(s -> s.substring(0, s.lastIndexOf(':'))) + .collect(toList())) + .containsExactlyElementsIn(expectedStates); + + List<String> expectedPatterns = Lists.newArrayList( + "repo:refs/users/*/edit-" + c + "/*"); + expectedPatterns.add("All-Users:refs/starred-changes/" + cs + "/*"); + if (notesMigration.readChanges()) { + expectedPatterns.add("All-Users:refs/draft-comments/" + cs + "/*"); + } + assertThat( + cd.getRefStatePatterns().stream() + .map(String::new) + .collect(toList())) + .containsExactlyElementsIn(expectedPatterns); + } + protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception { return newChange(repo, null, null, null, null); @@ -1552,17 +1786,21 @@ } protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception { - return insert(repo, ins, null); + return insert(repo, ins, null, TimeUtil.nowTs()); } protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner) throws Exception { + return insert(repo, ins, owner, TimeUtil.nowTs()); + } + + protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, + @Nullable Account.Id owner, Timestamp createdOn) throws Exception { Project.NameKey project = new Project.NameKey( repo.getRepository().getDescription().getRepositoryName()); Account.Id ownerId = owner != null ? owner : userId; IdentifiedUser user = userFactory.create(ownerId); - try (BatchUpdate bu = - updateFactory.create(db, project, user, TimeUtil.nowTs())) { + try (BatchUpdate bu = updateFactory.create(db, project, user, createdOn)) { bu.insertChange(ins); bu.execute(); return ins.getChange(); @@ -1583,7 +1821,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 +1835,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 +1880,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 +1904,7 @@ .append("status=").append(c.status).append(", ") .append("lastUpdated=").append(c.updated.getTime()) .append("}"); - if (it.hasNext()) { + if (changeIds.hasNext()) { b.append(", "); } } @@ -1674,23 +1913,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/server/query/change/LuceneQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java index 038abda..70493e8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.query.change; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.testutil.InMemoryModule; import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo; @@ -52,4 +53,15 @@ assertQuery("message:one.two", change2); assertQuery("message:one two", change2); } + + @Test + public void byOwnerInvalidQuery() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo), userId); + String nameEmail = user.asIdentifiedUser().getNameEmail(); + + exception.expect(BadRequestException.class); + exception.expectMessage("Cannot create full-text query with value: \\"); + assertQuery("owner: \"" + nameEmail + "\"\\", change1); + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java index 5532108..0480c5e 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -27,7 +27,7 @@ public class RegexPathPredicateTest { @Test - public void testPrefixOnlyOptimization() throws OrmException { + public void prefixOnlyOptimization() throws OrmException { RegexPathPredicate p = predicate("^a/b/.*"); assertTrue(p.match(change("a/b/source.c"))); assertFalse(p.match(change("source.c"))); @@ -37,7 +37,7 @@ } @Test - public void testPrefixReducesSearchSpace() throws OrmException { + public void prefixReducesSearchSpace() throws OrmException { RegexPathPredicate p = predicate("^a/b/.*\\.[ch]"); assertTrue(p.match(change("a/b/source.c"))); assertFalse(p.match(change("a/b/source.res"))); @@ -47,7 +47,7 @@ } @Test - public void testFileExtension_Constant() throws OrmException { + public void fileExtension_Constant() throws OrmException { RegexPathPredicate p = predicate("^.*\\.res"); assertTrue(p.match(change("test.res"))); assertTrue(p.match(change("foo/bar/test.res"))); @@ -55,7 +55,7 @@ } @Test - public void testFileExtension_CharacterGroup() throws OrmException { + public void fileExtension_CharacterGroup() throws OrmException { RegexPathPredicate p = predicate("^.*\\.[ch]"); assertTrue(p.match(change("test.c"))); assertTrue(p.match(change("test.h"))); @@ -63,7 +63,7 @@ } @Test - public void testEndOfString() throws OrmException { + public void endOfString() throws OrmException { assertTrue(predicate("^a$").match(change("a"))); assertFalse(predicate("^a$").match(change("a$"))); @@ -72,7 +72,7 @@ } @Test - public void testExactMatch() throws OrmException { + public void exactMatch() throws OrmException { RegexPathPredicate p = predicate("^foo.c"); assertTrue(p.match(change("foo.c"))); assertFalse(p.match(change("foo.cc")));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java index cb0ab11..76bee6f 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java
@@ -32,7 +32,7 @@ } @Test - public void testGetUrl() throws Exception { + public void getUrl() throws Exception { config.setString("database", null, "instance", "3"); assertThat(hana.getUrl()).isEqualTo("jdbc:sap://my.host:30315"); @@ -41,7 +41,7 @@ } @Test - public void testGetIndexScript() throws Exception { + public void getIndexScript() throws Exception { assertThat(hana.getIndexScript()).isSameAs(ScriptRunner.NOOP); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java index cd6e825..ccd399f 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -14,10 +14,7 @@ package com.google.gerrit.server.schema; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -75,14 +72,14 @@ } @Test - public void testGetCauses_CreateSchema() throws OrmException, SQLException, + public void getCauses_CreateSchema() throws OrmException, SQLException, IOException { // Initially the schema should be empty. String[] types = {"TABLE", "VIEW"}; try (JdbcSchema d = (JdbcSchema) db.open(); ResultSet rs = d.getConnection().getMetaData() .getTables(null, null, null, types)) { - assertFalse(rs.next()); + assertThat(rs.next()).isFalse(); } // Create the schema using the current schema version. @@ -96,7 +93,8 @@ if (sitePath.getName().equals(".")) { sitePath = sitePath.getParentFile(); } - assertEquals(sitePath.getCanonicalPath(), db.getSystemConfig().sitePath); + assertThat(db.getSystemConfig().sitePath) + .isEqualTo(sitePath.getCanonicalPath()); } private LabelTypes getLabelTypes() throws Exception { @@ -110,32 +108,32 @@ } @Test - public void testCreateSchema_LabelTypes() throws Exception { + public void createSchema_LabelTypes() throws Exception { List<String> labels = new ArrayList<>(); for (LabelType label : getLabelTypes().getLabelTypes()) { labels.add(label.getName()); } - assertEquals(ImmutableList.of("Code-Review"), labels); + assertThat(labels).containsExactly("Code-Review"); } @Test - public void testCreateSchema_Label_CodeReview() throws Exception { + public void createSchema_Label_CodeReview() throws Exception { LabelType codeReview = getLabelTypes().byLabel("Code-Review"); - assertNotNull(codeReview); - assertEquals("Code-Review", codeReview.getName()); - assertEquals(0, codeReview.getDefaultValue()); - assertEquals("MaxWithBlock", codeReview.getFunctionName()); - assertTrue(codeReview.isCopyMinScore()); + assertThat(codeReview).isNotNull(); + assertThat(codeReview.getName()).isEqualTo("Code-Review"); + assertThat(codeReview.getDefaultValue()).isEqualTo(0); + assertThat(codeReview.getFunctionName()).isEqualTo("MaxWithBlock"); + assertThat(codeReview.isCopyMinScore()).isTrue(); assertValueRange(codeReview, 2, 1, 0, -1, -2); } private void assertValueRange(LabelType label, Integer... range) { - assertEquals(Arrays.asList(range), label.getValuesAsList()); - assertEquals(range[0], Integer.valueOf(label.getMax().getValue())); - assertEquals(range[range.length - 1], - Integer.valueOf(label.getMin().getValue())); + assertThat(label.getValuesAsList()) + .containsExactlyElementsIn(Arrays.asList(range)).inOrder(); + assertThat(label.getMax().getValue()).isEqualTo(range[0]); + assertThat(label.getMin().getValue()).isEqualTo(range[range.length - 1]); for (LabelValue v : label.getValues()) { - assertFalse(Strings.isNullOrEmpty(v.getText())); + assertThat(Strings.isNullOrEmpty(v.getText())).isFalse(); } } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java index a161405..6723386 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.schema; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.lifecycle.LifecycleManager; @@ -36,6 +36,7 @@ import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.StatementExecutor; import com.google.inject.Guice; +import com.google.inject.ProvisionException; import com.google.inject.TypeLiteral; import org.eclipse.jgit.lib.Config; @@ -71,7 +72,7 @@ } @Test - public void testUpdate() throws OrmException, FileNotFoundException, + public void update() throws OrmException, FileNotFoundException, IOException { db.create(); @@ -111,6 +112,21 @@ } }).getInstance(SchemaUpdater.class); + for (SchemaVersion s = u.getLatestSchemaVersion(); + s.getVersionNbr() > 1; s = s.getPrior()) { + try { + assertThat(s.getPrior().getVersionNbr()) + .named("schema %s has prior version %s. Not true that", + s.getVersionNbr(), s.getPrior().getVersionNbr()) + .isEqualTo(s.getVersionNbr() - 1); + } catch (ProvisionException e) { + // Ignored + // The oldest supported schema version doesn't have a prior schema + // version. + break; + } + } + u.update(new UpdateUI() { @Override public void message(String msg) { @@ -137,6 +153,7 @@ db.assertSchemaVersion(); final SystemConfig sc = db.getSystemConfig(); - assertEquals(paths.site_path.toAbsolutePath().toString(), sc.sitePath); + assertThat(sc.sitePath) + .isEqualTo(paths.site_path.toAbsolutePath().toString()); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java index c5d9151..0d3dfb8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -64,7 +64,7 @@ } @Test - public void testEmptyMessages() throws Exception { + public void emptyMessages() throws Exception { // Empty input must yield empty output so commit will abort. // Note we must consider different commit templates formats. // @@ -85,7 +85,7 @@ } @Test - public void testChangeIdAlreadySet() throws Exception { + public void changeIdAlreadySet() throws Exception { // If a Change-Id is already present in the footer, the hook must // not modify the message but instead must leave the identity alone. // @@ -107,7 +107,7 @@ } @Test - public void testTimeAltersId() throws Exception { + public void timeAltersId() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// @@ -127,7 +127,7 @@ } @Test - public void testFirstParentAltersId() throws Exception { + public void firstParentAltersId() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// @@ -141,7 +141,7 @@ } @Test - public void testDirCacheAltersId() throws Exception { + public void dirCacheAltersId() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// @@ -158,7 +158,7 @@ } @Test - public void testSingleLineMessages() throws Exception { + public void singleLineMessages() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// @@ -184,7 +184,7 @@ } @Test - public void testMultiLineMessagesWithoutFooter() throws Exception { + public void multiLineMessagesWithoutFooter() throws Exception { assertEquals("a\n" + // "\n" + // "b\n" + // @@ -210,7 +210,7 @@ } @Test - public void testSingleLineMessagesWithSignedOffBy() throws Exception { + public void singleLineMessagesWithSignedOffBy() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + // @@ -226,7 +226,7 @@ } @Test - public void testMultiLineMessagesWithSignedOffBy() throws Exception { + public void multiLineMessagesWithSignedOffBy() throws Exception { assertEquals("a\n" + // "\n" + // "b\nc\nd\ne\n" + // @@ -275,7 +275,7 @@ } @Test - public void testNoteInMiddle() throws Exception { + public void noteInMiddle() throws Exception { assertEquals("a\n" + // "\n" + // "NOTE: This\n" + // @@ -289,7 +289,7 @@ } @Test - public void testKernelStyleFooter() throws Exception { + public void kernelStyleFooter() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I1bd787f9e7590a2ac82b02c404c955ffb21877c4\n" + // @@ -306,7 +306,7 @@ } @Test - public void testChangeIdAfterBugOrIssue() throws Exception { + public void changeIdAfterBugOrIssue() throws Exception { assertEquals("a\n" + // "\n" + // "Bug: 42\n" + // @@ -329,7 +329,7 @@ } @Test - public void testCommitDashV() throws Exception { + public void commitDashV() throws Exception { assertEquals("a\n" + // "\n" + // "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + // @@ -347,7 +347,7 @@ } @Test - public void testWithEndingURL() throws Exception { + public void withEndingURL() throws Exception { assertEquals("a\n" + // "\n" + // "http://example.com/ fixes this\n" + // @@ -383,7 +383,7 @@ } @Test - public void testWithFalseTags() throws Exception { + public void withFalseTags() throws Exception { assertEquals("foo\n" + // "\n" + // "FakeLine:\n" + //
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java index 3be4f8a..2a61165 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -37,7 +37,7 @@ } @Test - public void testFormat() { + public void format() { assertEquals("0000000f", IdGenerator.format(0xf)); assertEquals("801234ab", IdGenerator.format(0x801234ab)); assertEquals("deadbeef", IdGenerator.format(0xdeadbeef));
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..875d43f 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,16 +14,16 @@ 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; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.mail.Address; -import com.google.gerrit.server.mail.EmailHeader; -import com.google.gerrit.server.mail.EmailSender; +import com.google.gerrit.server.mail.send.EmailHeader; +import com.google.gerrit.server.mail.send.EmailSender; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -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/GerritBaseTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java index 967e3f9..3edc9f4 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java
@@ -14,12 +14,19 @@ package com.google.gerrit.testutil; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.server.StandardKeyEncoder; + import org.junit.Ignore; import org.junit.Rule; import org.junit.rules.ExpectedException; @Ignore public abstract class GerritBaseTests { + static { + KeyUtil.setEncoderImpl(new StandardKeyEncoder()); + } + @Rule public ExpectedException exception = ExpectedException.none(); }
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/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java index f98d63f..ddc4196 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -37,12 +37,10 @@ public static class Description extends DfsRepositoryDescription { private final Project.NameKey name; - private String desc; private Description(Project.NameKey name) { super(name.get()); this.name = name; - desc = "In-memory repository " + name.get(); } public Project.NameKey getProject() { @@ -51,6 +49,8 @@ } public static class Repo extends InMemoryRepository { + private String description; + private Repo(Project.NameKey name) { super(new Description(name)); // TODO(dborowitz): Allow atomic transactions when this is supported: @@ -62,6 +62,16 @@ public Description getDescription() { return (Description) super.getDescription(); } + + @Override + public String getGitwebDescription() { + return description; + } + + @Override + public void setGitwebDescription(String d) { + description = d; + } } private Map<String, Repo> repos = new HashMap<>(); @@ -97,22 +107,6 @@ return ImmutableSortedSet.copyOf(names); } - @Override - public synchronized String getProjectDescription(Project.NameKey name) - throws RepositoryNotFoundException { - return get(name).getDescription().desc; - } - - @Override - public synchronized void setProjectDescription(Project.NameKey name, - String description) { - try { - get(name).getDescription().desc = description; - } catch (RepositoryNotFoundException e) { - // Ignore. - } - } - public synchronized void deleteRepository(Project.NameKey name) { repos.remove(normalize(name)); }
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-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java index 103fee3..8aadb92 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -14,10 +14,10 @@ package com.google.gerrit.testutil; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.common.base.Enums; -import com.google.common.base.Optional; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; public enum NoteDbMode { /** NoteDb is disabled. */ @@ -38,29 +38,18 @@ private static final String VAR = "GERRIT_NOTEDB"; public static NoteDbMode get() { - if (isEnvVarTrue("GERRIT_ENABLE_NOTEDB")) { - // TODO(dborowitz): Remove once GerritForge CI is migrated. - return READ_WRITE; - } String value = System.getenv(VAR); if (Strings.isNullOrEmpty(value)) { return OFF; } value = value.toUpperCase().replace("-", "_"); - Optional<NoteDbMode> mode = Enums.getIfPresent(NoteDbMode.class, value); - if (!mode.isPresent()) { - throw new IllegalArgumentException( - "Invalid value for " + VAR + ": " + System.getenv(VAR)); - } - return mode.get(); + NoteDbMode mode = Enums.getIfPresent(NoteDbMode.class, value).orNull(); + checkArgument(mode != null, + "Invalid value for %s: %s", VAR, System.getenv(VAR)); + return mode; } public static boolean readWrite() { return get() == READ_WRITE; } - - private static boolean isEnvVarTrue(String name) { - String value = Strings.nullToEmpty(System.getenv(name)).toLowerCase(); - return ImmutableList.of("yes", "y", "true", "1").contains(value); - } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java index 4c71c57..32f0af8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
@@ -22,11 +22,15 @@ import org.joda.time.DateTimeUtils.MillisProvider; import org.joda.time.DateTimeZone; +import java.sql.Timestamp; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** Static utility methods for dealing with dates and times in tests. */ public class TestTimeUtil { + public static final DateTime START = + new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4)); + private static Long clockStepMs; private static AtomicLong clockMs; @@ -41,9 +45,7 @@ public static synchronized void resetWithClockStep( long clockStep, TimeUnit clockStepUnit) { // Set an arbitrary start point so tests are more repeatable. - clockMs = new AtomicLong( - new DateTime(2009, 9, 30, 17, 0, 0, DateTimeZone.forOffsetHours(-4)) - .getMillis()); + clockMs = new AtomicLong(START.getMillis()); setClockStep(clockStep, clockStepUnit); } @@ -65,8 +67,31 @@ }); } + /** + * Set the clock to a specific timestamp. + * + * @param ts time to set + */ + public static synchronized void setClock(Timestamp ts) { + checkState(clockMs != null, "call resetWithClockStep first"); + clockMs.set(ts.getTime()); + } + + /** + * Increment the clock once by a given amount. + * + * @param clockStep amount to increment clock by. + * @param clockStepUnit time unit for {@code clockStep}. + */ + public static synchronized void incrementClock( + long clockStep, TimeUnit clockStepUnit) { + checkState(clockMs != null, "call resetWithClockStep first"); + clockMs.addAndGet(clockStepUnit.toMillis(clockStep)); + } + /** Reset the clock to use the actual system clock. */ public static synchronized void useSystemTime() { + clockMs = null; DateTimeUtils.setCurrentMillisSystem(); }
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/BUILD b/gerrit-sshd/BUILD index be49c73..2288e5d 100644 --- a/gerrit-sshd/BUILD +++ b/gerrit-sshd/BUILD
@@ -1,53 +1,53 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") -SRCS = glob(['src/main/java/**/*.java']) +SRCS = glob(["src/main/java/**/*.java"]) java_library( - name = 'sshd', - srcs = SRCS, - deps = [ - '//gerrit-extension-api:api', - '//gerrit-cache-h2:cache-h2', - '//gerrit-common:annotations', - '//gerrit-common:server', - '//gerrit-lucene:lucene', - '//gerrit-patch-jgit:server', - '//gerrit-reviewdb:server', - '//gerrit-server:server', - '//gerrit-util-cli:cli', - '//lib:args4j', - '//lib:gson', - '//lib:guava', - '//lib:gwtorm', - '//lib:jsch', - '//lib:servlet-api-3_1', - '//lib/auto:auto-value', - '//lib/bouncycastle:bcprov', - '//lib/commons:codec', - '//lib/dropwizard:dropwizard-core', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', # SSH should not depend on servlet - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.archive:jgit-archive', - '//lib/log:api', - '//lib/log:log4j', - '//lib/mina:core', - '//lib/mina:sshd', - ], - visibility = ['//visibility:public'], + name = "sshd", + srcs = SRCS, + visibility = ["//visibility:public"], + deps = [ + "//gerrit-cache-h2:cache-h2", + "//gerrit-common:annotations", + "//gerrit-common:server", + "//gerrit-extension-api:api", + "//gerrit-lucene:lucene", + "//gerrit-patch-jgit:server", + "//gerrit-reviewdb:server", + "//gerrit-server:server", + "//gerrit-util-cli:cli", + "//lib:args4j", + "//lib:gson", + "//lib:guava", + "//lib:gwtorm", + "//lib:jsch", + "//lib:servlet-api-3_1", + "//lib/auto:auto-value", + "//lib/bouncycastle:bcprov", + "//lib/commons:codec", + "//lib/dropwizard:dropwizard-core", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:guice-servlet", # SSH should not depend on servlet + "//lib/jgit/org.eclipse.jgit.archive:jgit-archive", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + "//lib/log:log4j", + "//lib/mina:core", + "//lib/mina:sshd", + ], ) junit_tests( - name = 'sshd_tests', - srcs = glob( - ['src/test/java/**/*.java'], - ), - deps = [ - ':sshd', - '//gerrit-extension-api:api', - '//gerrit-server:server', - '//lib:truth', - '//lib/mina:sshd', - ], + name = "sshd_tests", + srcs = glob( + ["src/test/java/**/*.java"], + ), + deps = [ + ":sshd", + "//gerrit-extension-api:api", + "//gerrit-server:server", + "//lib:truth", + "//lib/mina:sshd", + ], )
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java index fde3a66..4ddca0c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -97,7 +97,7 @@ try { cmd.destroy(); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new RuntimeException(e); } }
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/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java index db5f3aa..93a508c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -14,9 +14,8 @@ package com.google.gerrit.sshd; -import com.google.common.base.Function; -import com.google.common.base.Predicates; -import com.google.common.collect.FluentIterable; +import static java.util.stream.Collectors.toList; + import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -36,6 +35,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; public class ChangeArgumentParser { private final CurrentUser currentUser; @@ -95,32 +95,30 @@ changes.put(ctl.getId(), changesCollection.parse(ctl)); } - private List<ChangeControl> changeFromNotesFactory(String id, - final CurrentUser currentUser) throws OrmException, UnloggedFailure { - List<ChangeNotes> changes = - changeNotesFactory.create(db, parseId(id)); - return FluentIterable.from(changes) - .transform(new Function<ChangeNotes, ChangeControl>() { - @Override - public ChangeControl apply(ChangeNotes changeNote) { - return controlForChange(changeNote, currentUser); - } - }).filter(Predicates.notNull()).toList(); + private List<ChangeControl> changeFromNotesFactory(String id, CurrentUser currentUser) + throws OrmException, UnloggedFailure { + return changeNotesFactory.create(db, parseId(id)) + .stream() + .map(changeNote -> controlForChange(changeNote, currentUser)) + .filter(changeControl -> changeControl.isPresent()) + .map(changeControl -> changeControl.get()) + .collect(toList()); } private List<Change.Id> parseId(String id) throws UnloggedFailure { try { - return Arrays.asList(new Change.Id(Integer.parseInt(id))); + return Arrays.asList(new Change.Id(Integer.parseInt(id))); } catch (NumberFormatException e) { throw new UnloggedFailure(2, "Invalid change ID " + id, e); } } - private ChangeControl controlForChange(ChangeNotes change, CurrentUser user) { + private Optional<ChangeControl> controlForChange(ChangeNotes change, + CurrentUser user) { try { - return changeControlFactory.controlFor(change, user); + return Optional.of(changeControlFactory.controlFor(change, user)); } catch (NoSuchChangeException e) { - return null; + return Optional.empty(); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java index f2911dc..f3243c6 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -136,7 +136,7 @@ try { cmd.destroy(); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new RuntimeException(e); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java index 24bd8c2..c88a02c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -153,7 +153,7 @@ try { cmd.destroy(); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new RuntimeException(e); } }
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/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java index 85b1f32..f7d5c87 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -43,10 +43,8 @@ void addChange(String token) { try { changeArgumentParser.addChange(token, changes, null, false); - } catch (UnloggedFailure e) { - throw new IllegalArgumentException(e.getMessage(), e); - } catch (OrmException e) { - throw new IllegalArgumentException("database is down", e); + } catch (UnloggedFailure | OrmException e) { + writeError("warning", e.getMessage()); } }
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 79e74d7..893c8f2 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.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; @@ -115,55 +114,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-test-util/BUCK b/gerrit-test-util/BUCK new file mode 100644 index 0000000..b2f20a5 --- /dev/null +++ b/gerrit-test-util/BUCK
@@ -0,0 +1,9 @@ +java_library( + name = 'test_util', + srcs = glob(['src/main/java/**/*.java']), + visibility = ['PUBLIC'], + deps = [ + '//gerrit-extension-api:api', + '//lib:truth', + ], +)
diff --git a/gerrit-test-util/BUILD b/gerrit-test-util/BUILD new file mode 100644 index 0000000..55954ba --- /dev/null +++ b/gerrit-test-util/BUILD
@@ -0,0 +1,10 @@ +java_library( + name = "test_util", + testonly = 1, + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-extension-api:api", + "//lib:truth", + ], +)
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/client/RangeSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/client/RangeSubject.java new file mode 100644 index 0000000..310276e --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/client/RangeSubject.java
@@ -0,0 +1,75 @@ +// 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; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.IntegerSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; + +public class RangeSubject extends Subject<RangeSubject, Comment.Range> { + + private static final SubjectFactory<RangeSubject, Comment.Range> + RANGE_SUBJECT_FACTORY = + new SubjectFactory<RangeSubject, Comment.Range>() { + @Override + public RangeSubject getSubject(FailureStrategy failureStrategy, + Comment.Range range) { + return new RangeSubject(failureStrategy, range); + } + }; + + public static RangeSubject assertThat(Comment.Range range) { + return assertAbout(RANGE_SUBJECT_FACTORY) + .that(range); + } + + private RangeSubject(FailureStrategy failureStrategy, Comment.Range range) { + super(failureStrategy, range); + } + + public IntegerSubject startLine() { + return Truth.assertThat(actual().startLine).named("startLine"); + } + + public IntegerSubject startCharacter() { + return Truth.assertThat(actual().startCharacter).named("startCharacter"); + } + + public IntegerSubject endLine() { + return Truth.assertThat(actual().endLine).named("endLine"); + } + + public IntegerSubject endCharacter() { + return Truth.assertThat(actual().endCharacter).named("endCharacter"); + } + + public void isValid() { + isNotNull(); + if (!actual().isValid()) { + fail("is valid"); + } + } + + public void isInvalid() { + isNotNull(); + if (actual().isValid()) { + fail("is invalid"); + } + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfoSubject.java new file mode 100644 index 0000000..1f39326 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfoSubject.java
@@ -0,0 +1,63 @@ +// 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 static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.extensions.client.RangeSubject; + +public class FixReplacementInfoSubject + extends Subject<FixReplacementInfoSubject, FixReplacementInfo> { + + private static final SubjectFactory<FixReplacementInfoSubject, + FixReplacementInfo> FIX_REPLACEMENT_INFO_SUBJECT_FACTORY = + new SubjectFactory<FixReplacementInfoSubject, FixReplacementInfo>() { + @Override + public FixReplacementInfoSubject getSubject( + FailureStrategy failureStrategy, + FixReplacementInfo fixReplacementInfo) { + return new FixReplacementInfoSubject(failureStrategy, + fixReplacementInfo); + } + }; + + public static FixReplacementInfoSubject assertThat( + FixReplacementInfo fixReplacementInfo) { + return assertAbout(FIX_REPLACEMENT_INFO_SUBJECT_FACTORY) + .that(fixReplacementInfo); + } + + private FixReplacementInfoSubject(FailureStrategy failureStrategy, + FixReplacementInfo fixReplacementInfo) { + super(failureStrategy, fixReplacementInfo); + } + + public StringSubject path() { + return Truth.assertThat(actual().path).named("path"); + } + + public RangeSubject range() { + return RangeSubject.assertThat(actual().range).named("range"); + } + + public StringSubject replacement() { + return Truth.assertThat(actual().replacement).named("replacement"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfoSubject.java new file mode 100644 index 0000000..cc75505 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfoSubject.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.extensions.common; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.truth.ListSubject; + +public class FixSuggestionInfoSubject + extends Subject<FixSuggestionInfoSubject, FixSuggestionInfo> { + + private static final SubjectFactory<FixSuggestionInfoSubject, + FixSuggestionInfo> FIX_SUGGESTION_INFO_SUBJECT_FACTORY = + new SubjectFactory<FixSuggestionInfoSubject, FixSuggestionInfo>() { + @Override + public FixSuggestionInfoSubject getSubject( + FailureStrategy failureStrategy, + FixSuggestionInfo fixSuggestionInfo) { + return new FixSuggestionInfoSubject(failureStrategy, + fixSuggestionInfo); + } + }; + + public static FixSuggestionInfoSubject assertThat( + FixSuggestionInfo fixSuggestionInfo) { + return assertAbout(FIX_SUGGESTION_INFO_SUBJECT_FACTORY) + .that(fixSuggestionInfo); + } + + private FixSuggestionInfoSubject(FailureStrategy failureStrategy, + FixSuggestionInfo fixSuggestionInfo) { + super(failureStrategy, fixSuggestionInfo); + } + + public StringSubject fixId() { + return Truth.assertThat(actual().fixId).named("fixId"); + } + + public ListSubject<FixReplacementInfoSubject, + FixReplacementInfo> replacements() { + return ListSubject.assertThat(actual().replacements, + FixReplacementInfoSubject::assertThat).named("replacements"); + } + + public FixReplacementInfoSubject onlyReplacement() { + return replacements().onlyElement(); + } + + public StringSubject description() { + return Truth.assertThat(actual().description).named("description"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfoSubject.java new file mode 100644 index 0000000..6fb9b1b --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfoSubject.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.extensions.common; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.gerrit.truth.ListSubject; + +import java.util.List; + +public class RobotCommentInfoSubject + extends Subject<RobotCommentInfoSubject, RobotCommentInfo> { + + private static final SubjectFactory<RobotCommentInfoSubject, RobotCommentInfo> + ROBOT_COMMENT_INFO_SUBJECT_FACTORY = + new SubjectFactory<RobotCommentInfoSubject, RobotCommentInfo>() { + @Override + public RobotCommentInfoSubject getSubject( + FailureStrategy failureStrategy, + RobotCommentInfo robotCommentInfo) { + return new RobotCommentInfoSubject(failureStrategy, robotCommentInfo); + } + }; + + public static ListSubject<RobotCommentInfoSubject, + RobotCommentInfo> assertThatList( + List<RobotCommentInfo> robotCommentInfos) { + return ListSubject.assertThat(robotCommentInfos, + RobotCommentInfoSubject::assertThat).named("robotCommentInfos"); + } + + public static RobotCommentInfoSubject assertThat( + RobotCommentInfo robotCommentInfo) { + return assertAbout(ROBOT_COMMENT_INFO_SUBJECT_FACTORY) + .that(robotCommentInfo); + } + + private RobotCommentInfoSubject(FailureStrategy failureStrategy, + RobotCommentInfo robotCommentInfo) { + super(failureStrategy, robotCommentInfo); + } + + public ListSubject<FixSuggestionInfoSubject, + FixSuggestionInfo> fixSuggestions() { + return ListSubject.assertThat(actual().fixSuggestions, + FixSuggestionInfoSubject::assertThat).named("fixSuggestions"); + } + + public FixSuggestionInfoSubject onlyFixSuggestion() { + return fixSuggestions().onlyElement(); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/truth/ListSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/truth/ListSubject.java new file mode 100644 index 0000000..55f948e --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/truth/ListSubject.java
@@ -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. + +package com.google.gerrit.truth; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.IterableSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; + +import java.util.List; +import java.util.function.Function; + +public class ListSubject<S extends Subject<S, E>, E> extends IterableSubject { + + private final Function<E, S> elementAssertThatFunction; + + @SuppressWarnings("unchecked") + public static <S extends Subject<S, E>, E> ListSubject<S, E> assertThat( + List<E> list, Function<E, S> elementAssertThatFunction) { + // The ListSubjectFactory always returns ListSubjects. + // -> Casting is appropriate. + return (ListSubject<S, E>) assertAbout( + new ListSubjectFactory<>(elementAssertThatFunction)).that(list); + } + + private ListSubject(FailureStrategy failureStrategy, List<E> list, + Function<E, S> elementAssertThatFunction) { + super(failureStrategy, list); + this.elementAssertThatFunction = elementAssertThatFunction; + } + + public S element(int index) { + checkArgument(index >= 0, "index(%s) must be >= 0", index); + // The constructor only accepts lists. + // -> Casting is appropriate. + @SuppressWarnings("unchecked") + List<E> list = (List<E>) actual(); + isNotNull(); + if (index >= list.size()) { + fail("has an element at index " + index); + } + return elementAssertThatFunction.apply(list.get(index)); + } + + public S onlyElement() { + isNotNull(); + hasSize(1); + return element(0); + } + + @SuppressWarnings("unchecked") + @Override + public ListSubject<S, E> named(String s, Object... objects) { + // This object is returned which is of type ListSubject. + // -> Casting is appropriate. + return (ListSubject<S, E>) super.named(s, objects); + } + + private static class ListSubjectFactory<S extends Subject<S, T>, T> + extends SubjectFactory<IterableSubject, Iterable<?>> { + + private Function<T, S> elementAssertThatFunction; + + ListSubjectFactory(Function<T, S> elementAssertThatFunction) { + this.elementAssertThatFunction = elementAssertThatFunction; + } + + @SuppressWarnings("unchecked") + @Override + public ListSubject<S, T> getSubject(FailureStrategy failureStrategy, + Iterable<?> objects) { + // The constructor of ListSubject only accepts lists. + // -> Casting is appropriate. + return new ListSubject<>(failureStrategy, (List<T>) objects, + elementAssertThatFunction); + } + } +}
diff --git a/gerrit-util-cli/BUILD b/gerrit-util-cli/BUILD index f3be5f3..bb282f4 100644 --- a/gerrit-util-cli/BUILD +++ b/gerrit-util-cli/BUILD
@@ -1,13 +1,13 @@ java_library( - name = 'cli', - srcs = glob(['src/main/java/**/*.java']), - deps = [ - '//gerrit-common:annotations', - '//gerrit-common:server', - '//lib:args4j', - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - ], - visibility = ['//visibility:public'], + name = "cli", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = [ + "//gerrit-common:annotations", + "//gerrit-common:server", + "//lib:args4j", + "//lib:guava", + "//lib/guice", + "//lib/guice:guice-assistedinject", + ], )
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/BUILD b/gerrit-util-http/BUILD index 0e3ac0e..47cc62e 100644 --- a/gerrit-util-http/BUILD +++ b/gerrit-util-http/BUILD
@@ -1,39 +1,40 @@ -load('//tools/bzl:junit.bzl', 'junit_tests') +load("//tools/bzl:junit.bzl", "junit_tests") java_library( - name = 'http', - srcs = glob(['src/main/java/**/*.java']), - deps = ['//lib:servlet-api-3_1'], - visibility = ['//visibility:public'], + name = "http", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], + deps = ["//lib:servlet-api-3_1"], ) -TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java']) +TESTUTIL_SRCS = glob(["src/test/**/testutil/**/*.java"]) java_library( - name = 'testutil', - srcs = TESTUTIL_SRCS, - deps = [ - '//gerrit-extension-api:api', - '//lib:guava', - '//lib:servlet-api-3_1', - '//lib/httpcomponents:httpclient', - '//lib/jgit/org.eclipse.jgit:jgit', - ], - visibility = ['//visibility:public'], + name = "testutil", + testonly = 1, + srcs = TESTUTIL_SRCS, + visibility = ["//visibility:public"], + deps = [ + "//gerrit-extension-api:api", + "//lib:guava", + "//lib:servlet-api-3_1", + "//lib/httpcomponents:httpclient", + "//lib/jgit/org.eclipse.jgit:jgit", + ], ) junit_tests( - name = 'http_tests', - srcs = glob( - ['src/test/java/**/*.java'], - exclude = TESTUTIL_SRCS, - ), - deps = [ - ':http', - ':testutil', - '//lib:junit', - '//lib:servlet-api-3_1-without-neverlink', - '//lib:truth', - '//lib/easymock:easymock', - ], + name = "http_tests", + srcs = glob( + ["src/test/java/**/*.java"], + exclude = TESTUTIL_SRCS, + ), + deps = [ + ":http", + ":testutil", + "//lib:junit", + "//lib:servlet-api-3_1-without-neverlink", + "//lib:truth", + "//lib/easymock", + ], )
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..4eee495 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; @@ -26,12 +25,12 @@ import com.google.common.collect.Maps; import com.google.gerrit.extensions.restapi.Url; -import org.apache.http.client.utils.DateUtils; - import java.io.BufferedReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.Principal; +import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; @@ -56,6 +55,8 @@ /** Simple fake implementation of {@link HttpServletRequest}. */ public class FakeHttpServletRequest implements HttpServletRequest { public static final String SERVLET_PATH = "/b"; + public static final DateTimeFormatter rfcDateformatter = + DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss ZZZ"); private final Map<String, Object> attributes; private final ListMultimap<String, String> headers; @@ -143,18 +144,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 +159,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) { @@ -270,7 +265,8 @@ @Override public long getDateHeader(String name) { String v = getHeader(name); - return v != null ? DateUtils.parseDate(v).getTime() : 0; + return v == null ? 0 : + rfcDateformatter.parse(v, Instant::from).getEpochSecond(); } @Override
diff --git a/gerrit-util-ssl/BUILD b/gerrit-util-ssl/BUILD index 6333d45..ce53a26 100644 --- a/gerrit-util-ssl/BUILD +++ b/gerrit-util-ssl/BUILD
@@ -1,5 +1,5 @@ java_library( - name = 'ssl', - srcs = glob(['src/main/java/**/*.java']), - visibility = ['//visibility:public'], + name = "ssl", + srcs = glob(["src/main/java/**/*.java"]), + visibility = ["//visibility:public"], )
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..66a0a47 --- /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"]), + visibility = ["//visibility:public"], + 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", + "//lib/guice:guice-servlet", + "//lib/jgit/org.eclipse.jgit:jgit", + "//lib/log:api", + ], +) + +genrule2( + name = "webapp_assets", + srcs = glob(["src/main/webapp/**/*"]), + outs = ["webapp_assets.zip"], + cmd = "cd gerrit-war/src/main/webapp; zip -qr $$ROOT/$@ .", + visibility = ["//visibility:public"], +) + +java_import( + name = "log4j-config", + jars = [":log4j-config__jar"], + visibility = ["//visibility:public"], +) + +genrule2( + name = "log4j-config__jar", + srcs = ["src/main/resources/log4j.properties"], + outs = ["log4j-config.jar"], + cmd = "cd gerrit-war/src/main/resources && zip -9Dqr $$ROOT/$@ .", +) + +java_import( + name = "version", + jars = [":gen_version"], + visibility = ["//visibility:public"], +) + +genrule2( + name = "gen_version", + outs = ["gen_version.jar"], + 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"], +)
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml index 536f3f9..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.4</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..94168f4 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,7 @@ 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.LibModuleLoader; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -38,6 +40,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; @@ -51,7 +54,8 @@ import com.google.gerrit.server.index.IndexModule; import com.google.gerrit.server.index.IndexModule.IndexType; import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier; -import com.google.gerrit.server.mail.SmtpEmailSender; +import com.google.gerrit.server.mail.receive.MailReceiver; +import com.google.gerrit.server.mail.send.SmtpEmailSender; import com.google.gerrit.server.mime.MimeUtil2Module; import com.google.gerrit.server.notedb.ConfigNotesMigration; import com.google.gerrit.server.patch.DiffExecutorModule; @@ -308,6 +312,7 @@ modules.add(new SearchingChangeCacheImpl.Module()); modules.add(new InternalAccountDirectory.Module()); modules.add(new DefaultCacheFactory.Module()); + modules.add(cfgInjector.getInstance(MailReceiver.Module.class)); modules.add(new SmtpEmailSender.Module()); modules.add(new SignedTokenEmailTokenVerifier.Module()); modules.add(new PluginRestApiModule()); @@ -335,6 +340,7 @@ }); modules.add(new GarbageCollectionModule()); modules.add(new ChangeCleanupRunner.Module()); + modules.addAll(LibModuleLoader.loadModules(cfgInjector)); return cfgInjector.createChildInjector(modules); } @@ -342,6 +348,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 380a3ef..96365eb 100644 --- a/lib/BUCK +++ b/lib/BUCK
@@ -17,8 +17,10 @@ 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 = 'jsoup') define_license(name = 'MPL1.1') define_license(name = 'moment') define_license(name = 'OFL1.1') @@ -38,9 +40,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 +55,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', ) @@ -69,7 +71,7 @@ maven_jar( name = 'guava', id = 'com.google.guava:guava:' + GUAVA_VERSION, - sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9', + sha1 = GUAVA_BIN_SHA1, license = 'Apache2.0', ) @@ -90,7 +92,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( @@ -108,8 +114,8 @@ maven_jar( name = 'jsch', - id = 'com.jcraft:jsch:0.1.53', - sha1 = '658b682d5c817b27ae795637dfec047c63d29935', + id = 'com.jcraft:jsch:0.1.54', + sha1 = 'da3584329a263616e277e15462b387addd1b208d', license = 'jsch', ) @@ -217,8 +223,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'], ) @@ -241,8 +247,8 @@ maven_jar( name = 'truth', - id = 'com.google.truth:truth:0.28', - sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4', + id = 'com.google.truth:truth:0.30', + sha1 = '9d591b5a66eda81f0b88cf1c748ab8853d99b18b', license = 'DO_NOT_DISTRIBUTE', exported_deps = [ ':guava', @@ -273,3 +279,41 @@ 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', +) + +maven_jar( + name = 'errorprone', + id = 'com.google.errorprone:error_prone_ant:2.0.15', + sha1 = '607e3866e2ee25b74708c2898f84eac2f5452d2f', + license = 'Apache2.0', +)
diff --git a/lib/BUILD b/lib/BUILD index e89e63c..ca0fec3 100644 --- a/lib/BUILD +++ b/lib/BUILD
@@ -1,204 +1,289 @@ -java_library( - name = 'servlet-api-3_1', - neverlink = 1, - exports = ['@servlet_api_3_1//jar'], - visibility = ['//visibility:public'], +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-without-neverlink', - exports = ['@servlet_api_3_1//jar'], - visibility = ['//visibility:public'], + name = "servlet-api-3_1", + data = ["//lib:LICENSE-Apache2.0"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@servlet_api_3_1//jar"], ) java_library( - name = 'gwtjsonrpc', - exports = ['@gwtjsonrpc//jar'], - visibility = ['//visibility:public'], + name = "servlet-api-3_1-without-neverlink", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@servlet_api_3_1//jar"], ) java_library( - name = 'gwtjsonrpc_src', - exports = ['@gwtjsonrpc_src//jar'], - visibility = ['//visibility:public'], + name = "gwtjsonrpc", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@gwtjsonrpc//jar"], ) java_library( - name = 'gson', - exports = ['@gson//jar'], - visibility = ['//visibility:public'], + name = "gwtjsonrpc_src", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@gwtjsonrpc//jar:src"], ) java_library( - name = 'gwtorm_client', - exports = ['@gwtorm_client//jar'], - visibility = ['//visibility:public'], + name = "gson", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@gson//jar"], ) java_library( - name = 'gwtorm_client_src', - exports = ['@gwtorm_client_src//jar'], - visibility = ['//visibility:public'], + name = "gwtorm_client", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@gwtorm_client//jar"], ) java_library( - name = 'protobuf', - exports = ['@protobuf//jar'], - visibility = ['//visibility:public'], + name = "gwtorm_client_src", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@gwtorm_client//jar:src"], ) java_library( - name = 'gwtorm', - exports = [':gwtorm_client'], - runtime_deps = [':protobuf'], - visibility = ['//visibility:public'], + name = "protobuf", + data = ["//lib:LICENSE-protobuf"], + visibility = ["//visibility:public"], + exports = ["@protobuf//jar"], ) java_library( - name = 'guava', - exports = ['@guava//jar'], - visibility = ['//visibility:public'], + name = "gwtorm", + visibility = ["//visibility:public"], + exports = [":gwtorm_client"], + runtime_deps = [":protobuf"], ) java_library( - name = 'velocity', - exports = ['@velocity//jar'], - runtime_deps = [ - '//lib/commons:collections', - '//lib/commons:lang', - '//lib/commons:oro', - ], - visibility = ['//visibility:public'], + name = "guava", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@guava//jar"], ) java_library( - name = 'jsch', - exports = ['@jsch//jar'], - visibility = ['//visibility:public'], + name = "velocity", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@velocity//jar"], + runtime_deps = [ + "//lib/commons:collections", + "//lib/commons:lang", + "//lib/commons:oro", + ], ) java_library( - name = 'juniversalchardet', - exports = ['@juniversalchardet//jar'], - visibility = ['//visibility:public'], + name = "jsch", + data = ["//lib:LICENSE-jsch"], + visibility = ["//visibility:public"], + exports = ["@jsch//jar"], ) java_library( - name = 'args4j', - exports = ['@args4j//jar'], - visibility = ['//visibility:public'], + name = "juniversalchardet", + data = ["//lib:LICENSE-MPL1.1"], + visibility = ["//visibility:public"], + exports = ["@juniversalchardet//jar"], ) java_library( - name = 'automaton', - exports = ['@automaton//jar'], - visibility = ['//visibility:public'], + name = "args4j", + data = ["//lib:LICENSE-args4j"], + visibility = ["//visibility:public"], + exports = ["@args4j//jar"], ) java_library( - name = 'pegdown', - exports = ['@pegdown//jar'], - runtime_deps = [':grappa'], - visibility = ['//visibility:public'], + name = "automaton", + data = ["//lib:LICENSE-automaton"], + visibility = ["//visibility:public"], + exports = ["@automaton//jar"], ) java_library( - name = 'grappa', - exports = ['@grappa//jar'], - runtime_deps = [ - ':jitescript', - '//lib/ow2:ow2-asm', - '//lib/ow2:ow2-asm-analysis', - '//lib/ow2:ow2-asm-tree', - '//lib/ow2:ow2-asm-util', - ], - visibility = ['//visibility:public'], + name = "pegdown", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@pegdown//jar"], + runtime_deps = [":grappa"], ) java_library( - name = 'jitescript', - exports = ['@jitescript//jar'], - visibility = ['//visibility:public'], + name = "grappa", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@grappa//jar"], + runtime_deps = [ + ":jitescript", + "//lib/ow2:ow2-asm", + "//lib/ow2:ow2-asm-analysis", + "//lib/ow2:ow2-asm-tree", + "//lib/ow2:ow2-asm-util", + ], ) java_library( - name = 'tukaani-xz', - exports = ['@tukaani_xz//jar'], - visibility = ['//visibility:public'], + name = "jitescript", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jitescript//jar"], ) java_library( - name = 'mime-util', - exports = ['@mime_util//jar'], - visibility = ['//visibility:public'], + name = "tukaani-xz", + data = ["//lib:LICENSE-xz"], + visibility = ["//visibility:public"], + exports = ["@tukaani_xz//jar"], ) java_library( - name = 'guava-retrying', - exports = ['@guava_retrying//jar'], - runtime_deps = [':jsr305'], - visibility = ['//visibility:public'], + name = "mime-util", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@mime_util//jar"], ) java_library( - name = 'jsr305', - exports = ['@jsr305//jar'], + name = "guava-retrying", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@guava_retrying//jar"], + runtime_deps = [":jsr305"], ) java_library( - name = 'blame-cache', - exports = ['@blame_cache//jar'], - visibility = ['//visibility:public'], + name = "jsr305", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jsr305//jar"], ) java_library( - name = 'h2', - exports = ['@h2//jar'], - visibility = ['//visibility:public'], -) - - -java_library( - name = 'jimfs', - exports = ['@jimfs//jar'], - runtime_deps = [':guava'], - visibility = ['//visibility:public'], + name = "blame-cache", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@blame_cache//jar"], ) java_library( - name = 'junit', - exports = [ - '@junit//jar', - ':hamcrest-core', - ], - runtime_deps = [':hamcrest-core'], - visibility = ['//visibility:public'], + name = "h2", + data = ["//lib:LICENSE-h2"], + visibility = ["//visibility:public"], + exports = ["@h2//jar"], ) java_library( - name = 'hamcrest-core', - exports = ['@hamcrest_core//jar'], - visibility = ['//visibility:public'], + name = "jimfs", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@jimfs//jar"], + runtime_deps = [":guava"], ) java_library( - name = 'truth', - exports = [ - '@truth//jar', - ':guava', - ':junit', - ], - visibility = ['//visibility:public'], + name = "junit", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":hamcrest-core", + "@junit//jar", + ], + runtime_deps = [":hamcrest-core"], ) java_library( - name = 'javassist', - exports = ['@javassist//jar'], - visibility = ['//visibility:public'], + name = "hamcrest-core", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@hamcrest_core//jar"], ) java_library( - name = 'derby', - exports = ['@derby//jar'], - visibility = ['//visibility:public'], + name = "truth", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":guava", + ":junit", + "@truth//jar", + ], +) + +java_library( + name = "javassist", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@javassist//jar"], +) + +java_library( + name = "derby", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@derby//jar"], +) + +java_library( + name = "soy", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@soy//jar"], + runtime_deps = [ + ":args4j", + ":gson", + ":guava", + ":icu4j", + ":jsr305", + ":protobuf", + "//lib/guice", + "//lib/guice:guice-assistedinject", + "//lib/guice:javax-inject", + "//lib/guice:multibindings", + "//lib/ow2:ow2-asm", + "//lib/ow2:ow2-asm-analysis", + "//lib/ow2:ow2-asm-commons", + "//lib/ow2:ow2-asm-util", + ], +) + +java_library( + name = "icu4j", + data = ["//lib:LICENSE-icu4j"], + visibility = ["//visibility:public"], + exports = ["@icu4j//jar"], +) + +java_library( + name = "postgresql", + data = ["//lib:LICENSE-postgresql"], + visibility = ["//visibility:public"], + exports = ["@postgresql//jar"], +) + +java_library( + name = "commons-io", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_io//jar"], )
diff --git a/lib/GUAVA_VERSION b/lib/GUAVA_VERSION index f889e2b..b5f47b3 100644 --- a/lib/GUAVA_VERSION +++ b/lib/GUAVA_VERSION
@@ -1,2 +1 @@ -GUAVA_VERSION = '19.0' -GUAVA_DOC_URL = 'https://google.github.io/guava/releases/' + GUAVA_VERSION + '/api/docs/' +include_defs('//lib/guava.bzl')
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION index b7f7c84..a6ed50a 100644 --- a/lib/JGIT_VERSION +++ b/lib/JGIT_VERSION
@@ -1,6 +1,5 @@ +include_defs('//lib/jgit/jgit.bzl') include_defs('//lib/maven.defs') -REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL. -VERS = '4.5.0.201609210915-r' -DOC_VERS = VERS # Set to VERS unless using a snapshot -JGIT_DOC_URL="http://download.eclipse.org/jgit/site/" + DOC_VERS + "/apidocs" +REPO = GERRIT +#REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
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/LICENSE-jsoup b/lib/LICENSE-jsoup new file mode 100644 index 0000000..9e15540 --- /dev/null +++ b/lib/LICENSE-jsoup
@@ -0,0 +1,21 @@ +The MIT License + +© 2009-2016, Jonathan Hedley <jonathan@hedley.net> + +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, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.
diff --git a/lib/antlr/BUILD b/lib/antlr/BUILD index ede7665..6afe7b8 100644 --- a/lib/antlr/BUILD +++ b/lib/antlr/BUILD
@@ -1,31 +1,33 @@ - [java_library( - name = n, - exports = ['@%s//jar' % n], + name = n, + data = ["//lib:LICENSE-antlr"], + exports = ["@%s//jar" % n], ) for n in [ - 'antlr27', - 'stringtemplate', + "antlr27", + "stringtemplate", ]] java_library( - name = 'java_runtime', - exports = ['@java_runtime//jar'], - visibility = ['//visibility:public'], + name = "java_runtime", + data = ["//lib:LICENSE-antlr"], + visibility = ["//visibility:public"], + exports = ["@java_runtime//jar"], ) java_binary( - name = 'antlr-tool', - main_class = 'org.antlr.Tool', - runtime_deps = [':tool'], - visibility = ['//gerrit-antlr:__pkg__'], + name = "antlr-tool", + main_class = "org.antlr.Tool", + visibility = ["//gerrit-antlr:__pkg__"], + runtime_deps = [":tool"], ) java_library( - name = 'tool', - exports = ['@org_antlr//jar'], - runtime_deps = [ - ':antlr27', - ':java_runtime', - ':stringtemplate', - ], + name = "tool", + data = ["//lib:LICENSE-antlr"], + exports = ["@org_antlr//jar"], + runtime_deps = [ + ":antlr27", + ":java_runtime", + ":stringtemplate", + ], )
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..c7567d9 --- /dev/null +++ b/lib/asciidoctor/BUILD
@@ -0,0 +1,54 @@ +java_binary( + name = "asciidoc", + main_class = "AsciiDoctor", + visibility = ["//visibility:public"], + runtime_deps = [":asciidoc_lib"], +) + +java_library( + name = "asciidoc_lib", + srcs = ["java/AsciiDoctor.java"], + visibility = ["//visibility:public"], + deps = [ + ":asciidoctor", + "//lib:args4j", + "//lib:guava", + "//lib/log:api", + "//lib/log:nop", + ], +) + +java_binary( + name = "doc_indexer", + main_class = "DocIndexer", + visibility = ["//visibility:public"], + runtime_deps = [":doc_indexer_lib"], +) + +java_library( + name = "doc_indexer_lib", + srcs = ["java/DocIndexer.java"], + visibility = ["//visibility:public"], + deps = [ + ":asciidoc_lib", + "//gerrit-server:constants", + "//lib:args4j", + "//lib:guava", + "//lib/lucene:lucene-analyzers-common", + "//lib/lucene:lucene-core-and-backward-codecs", + ], +) + +java_library( + name = "asciidoctor", + data = ["//lib:LICENSE-asciidoctor"], + visibility = ["//visibility:public"], + exports = ["@asciidoctor//jar"], + runtime_deps = [":jruby"], +) + +java_library( + name = "jruby", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + exports = ["@jruby//jar"], +)
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java index 8e18feb1..1871b0c 100644 --- a/lib/asciidoctor/java/AsciiDoctor.java +++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -25,11 +25,14 @@ 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 +44,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 +64,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 +99,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 +157,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 +216,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/BUCK b/lib/auto/BUCK index 6197e34..c186f87 100644 --- a/lib/auto/BUCK +++ b/lib/auto/BUCK
@@ -2,8 +2,8 @@ maven_jar( name = 'auto-value', - id = 'com.google.auto.value:auto-value:1.3-rc1', - sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179', + id = 'com.google.auto.value:auto-value:1.4-rc1', + sha1 = '9347939002003a7a3c3af48271fc2c18734528a4', license = 'Apache2.0', visibility = ['PUBLIC'], )
diff --git a/lib/auto/BUILD b/lib/auto/BUILD index e07c36d..569398e 100644 --- a/lib/auto/BUILD +++ b/lib/auto/BUILD
@@ -1,21 +1,22 @@ java_plugin( - name = 'auto-annotation-plugin', - processor_class = 'com.google.auto.value.processor.AutoAnnotationProcessor', - deps = ['@auto_value//jar'], + name = "auto-annotation-plugin", + processor_class = "com.google.auto.value.processor.AutoAnnotationProcessor", + deps = ["@auto_value//jar"], ) java_plugin( - name = 'auto-value-plugin', - processor_class = 'com.google.auto.value.processor.AutoValueProcessor', - deps = ['@auto_value//jar'], + name = "auto-value-plugin", + processor_class = "com.google.auto.value.processor.AutoValueProcessor", + deps = ["@auto_value//jar"], ) java_library( - name = 'auto-value', - exported_plugins = [ - ':auto-annotation-plugin', - ':auto-value-plugin', - ], - exports = ['@auto_value//jar'], - visibility = ['//visibility:public'], + name = "auto-value", + data = ["//lib:LICENSE-Apache2.0"], + exported_plugins = [ + ":auto-annotation-plugin", + ":auto-value-plugin", + ], + visibility = ["//visibility:public"], + exports = ["@auto_value//jar"], )
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK index 68fa006..be8b2f7 100644 --- a/lib/bouncycastle/BUCK +++ b/lib/bouncycastle/BUCK
@@ -2,19 +2,19 @@ # This version must match the version that also appears in # gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config -VERSION = '1.52' +VERSION = '1.55' maven_jar( name = 'bcprov', id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION, - sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269', + sha1 = '935f2e57a00ec2c489cbd2ad830d4a399708f979', license = 'DO_NOT_DISTRIBUTE', #'bouncycastle' ) maven_jar( name = 'bcpg', id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION, - sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858', + sha1 = '54ce841795ecdf10f24e50c48d4fdec59c691699', license = 'DO_NOT_DISTRIBUTE', #'bouncycastle' deps = [':bcprov'], ) @@ -22,7 +22,7 @@ maven_jar( name = 'bcpkix', id = 'org.bouncycastle:bcpkix-jdk15on:' + VERSION, - sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504', + sha1 = '6392d8cba22b722c6570d660ca0b3921ff1bae4f', license = 'DO_NOT_DISTRIBUTE', #'bouncycastle' deps = [':bcprov'], )
diff --git a/lib/bouncycastle/BUILD b/lib/bouncycastle/BUILD index 49c54ba..4ec7fa0 100644 --- a/lib/bouncycastle/BUILD +++ b/lib/bouncycastle/BUILD
@@ -1,38 +1,44 @@ java_library( - name = 'bcprov', - neverlink = 1, - exports = ['@bcprov//jar'], - visibility = ['//visibility:public'], + name = "bcprov", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@bcprov//jar"], ) java_library( - name = 'bcprov-without-neverlink', - exports = ['@bcprov//jar'], - visibility = ['//visibility:public'], + name = "bcprov-without-neverlink", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@bcprov//jar"], ) java_library( - name = 'bcpg', - neverlink = 1, - exports = ['@bcpg//jar'], - visibility = ['//visibility:public'], + name = "bcpg", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@bcpg//jar"], ) java_library( - name = 'bcpg-without-neverlink', - exports = ['@bcpg//jar'], - visibility = ['//visibility:public'], + name = "bcpg-without-neverlink", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@bcpg//jar"], ) java_library( - name = 'bcpkix', - neverlink = 1, - exports = ['@bcpkix//jar'], - visibility = ['//visibility:public'], + name = "bcpkix", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@bcpkix//jar"], ) java_library( - name = 'bcpkix-without-neverlink', - exports = ['@bcpkix//jar'], - visibility = ['//visibility:public'], + name = "bcpkix-without-neverlink", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@bcpkix//jar"], )
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK index a0e0e9a..eb5adb8 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,13 +17,12 @@ maven_jar( name = 'codemirror-original', id = 'org.webjars.npm:codemirror:' + VERSION, - sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85', + sha1 = 'e9ab382c6be240d55f112051bba3f6c637b798ce', attach_source = False, license = 'codemirror-original', visibility = [], ) -DIFF_MATCH_PATCH_VERSION = '20121119-1' DIFF_MATCH_PATCH_TOP = ('META-INF/resources/webjars/google-diff-match-patch/%s' % DIFF_MATCH_PATCH_VERSION) @@ -95,12 +94,12 @@ # Merge Addon bundled with diff-match-patch genrule( - name = 'addon_merge%s' % suffix, + name = 'addon_merge_with_diff_match_patch%s' % suffix, cmd = ';'.join([ "echo '/** @license' >$OUT", 'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top), "echo '*/\n' >>$OUT", - "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' >> $OUT", + "echo '// The google-diff-match-patch library is from https://repo1.maven.org/maven2/org/webjars/google-diff-match-patch/%s/google-diff-match-patch-%s.jar\n' >> $OUT" % (DIFF_MATCH_PATCH_VERSION, DIFF_MATCH_PATCH_VERSION), "echo '/** @license' >>$OUT", 'cat $(location //lib:LICENSE-Apache2.0) >>$OUT', "echo '*/' >>$OUT", @@ -109,7 +108,7 @@ 'unzip -p $(location :%s) %s/addon/merge/merge.js >>$OUT' % (archive, top) ] ), - out = 'addon_merge%s.js' % suffix, + out = 'addon_merge_with_diff_match_patch%s.js' % suffix, ) # Jar packaging @@ -124,7 +123,7 @@ for n in CM_MODES] + ['cp $(location :theme_%s%s) net/codemirror/theme/%s.css' % (n, suffix, n) for n in CM_THEMES] - + ['cp $(location :addon_merge%s) net/codemirror/addon/merge_bundled.js' % suffix] + + ['cp $(location :addon_merge_with_diff_match_patch%s) net/codemirror/addon/merge_bundled.js' % suffix] + ['zip -qr $OUT net/codemirror/{addon,lib,mode,theme}']), out = 'codemirror%s.jar' % suffix, )
diff --git a/lib/codemirror/BUILD b/lib/codemirror/BUILD new file mode 100644 index 0000000..56ed174 --- /dev/null +++ b/lib/codemirror/BUILD
@@ -0,0 +1,3 @@ +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..9c68299 --- /dev/null +++ b/lib/codemirror/cm.bzl
@@ -0,0 +1,351 @@ +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 = [archive], + outs = ['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 = [archive], + outs = ['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 = [archive], + outs = ['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 = [archive], + outs = ['theme_%s%s.css' % (n, suffix)], + ) + + # Merge Addon bundled with diff-match-patch + genrule2( + name = 'addon_merge_with_diff_match_patch%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://repo1.maven.org/maven2/org/webjars/google-diff-match-patch/%s/google-diff-match-patch-%s.jar\n' >> $@" % (DIFF_MATCH_PATCH_VERSION, DIFF_MATCH_PATCH_VERSION), + "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', + archive, + "//lib:LICENSE-Apache2.0", + ], + outs = ['addon_merge_with_diff_match_patch%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_with_diff_match_patch%s) net/codemirror/addon/merge_bundled.js' % suffix] + + ['zip -qr $$ROOT/$@ net/codemirror/{addon,lib,mode,theme}']), + tools = [ + ':addon_merge_with_diff_match_patch%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 + ], + outs = ['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..a6e873a 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', @@ -209,3 +209,5 @@ 'yaml', 'z80', ] + +DIFF_MATCH_PATCH_VERSION = "20121119-1"
diff --git a/lib/commons/BUCK b/lib/commons/BUCK index 7c27477..5c2e9b2 100644 --- a/lib/commons/BUCK +++ b/lib/commons/BUCK
@@ -19,8 +19,8 @@ maven_jar( name = 'compress', - id = 'org.apache.commons:commons-compress:1.7', - sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d', + id = 'org.apache.commons:commons-compress:1.12', + sha1 = '84caa68576e345eb5e7ae61a0e5a9229eb100d7b', license = 'Apache2.0', exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], ) @@ -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..cc4de55 100644 --- a/lib/commons/BUILD +++ b/lib/commons/BUILD
@@ -1,54 +1,71 @@ +package(default_visibility = ["//visibility:public"]) + java_library( - name = 'codec', - exports = ['@commons_codec//jar'], - visibility = ['//visibility:public'], + name = "codec", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_codec//jar"], ) java_library( - name = 'collections', - exports = ['@commons_collections//jar'], - visibility = ['//visibility:public'], + name = "collections", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_collections//jar"], ) java_library( - name = 'compress', - exports = ['@commons_compress//jar'], - visibility = ['//visibility:public'], + name = "compress", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_compress//jar"], ) java_library( - name = 'lang', - exports = ['@commons_lang//jar'], - visibility = ['//visibility:public'], + name = "lang", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_lang//jar"], ) java_library( - name = 'net', - exports = ['@commons_net//jar'], - visibility = ['//visibility:public'], + name = "lang3", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@commons_lang3//jar"], ) java_library( - name = 'dbcp', - exports = ['@commons_dbcp//jar'], - runtime_deps = [':pool'], - visibility = ['//visibility:public'], + name = "net", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_net//jar"], ) java_library( - name = 'pool', - exports = ['@commons_pool//jar'], - visibility = ['//visibility:public'], + name = "dbcp", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_dbcp//jar"], + runtime_deps = [":pool"], ) java_library( - name = 'oro', - exports = ['@commons_oro//jar'], - visibility = ['//visibility:public'], + name = "pool", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_pool//jar"], ) java_library( - name = 'validator', - exports = ['@commons_validator//jar'], - visibility = ['//visibility:public'], + name = "oro", + data = ["//lib:LICENSE-Apache1.1"], + visibility = ["//visibility:public"], + exports = ["@commons_oro//jar"], +) + +java_library( + name = "validator", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@commons_validator//jar"], )
diff --git a/lib/dropwizard/BUILD b/lib/dropwizard/BUILD index 9d4a8d3..dd14699 100644 --- a/lib/dropwizard/BUILD +++ b/lib/dropwizard/BUILD
@@ -1,5 +1,6 @@ java_library( - name = 'dropwizard-core', - exports = ['@dropwizard_core//jar'], - visibility = ['//visibility:public'], + name = "dropwizard-core", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@dropwizard_core//jar"], )
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..b579ec5 100644 --- a/lib/easymock/BUILD +++ b/lib/easymock/BUILD
@@ -1,22 +1,24 @@ java_library( - name = 'easymock', - exports = ['@easymock//jar'], - runtime_deps = [ - ':cglib-2_2', - ':objenesis', - ], - visibility = ['//visibility:public'], + name = "easymock", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@easymock//jar"], + runtime_deps = [ + ":cglib-3_2", + ":objenesis", + ], ) java_library( - name = 'cglib-2_2', - exports = ['@cglib_2_2//jar'], - visibility = ['//visibility:public'], + name = "cglib-3_2", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@cglib_3_2//jar"], ) java_library( - name = 'objenesis', - exports = ['@objenesis//jar'], - visibility = ['//visibility:public'], + name = "objenesis", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@objenesis//jar"], ) -
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..8dc4bce --- /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 = [ + ":compress-lzf", + ":hppc", + ":jna", + ":jsr166e", + ":netty", + ":t-digest", + "//lib/jackson:jackson-core", + "//lib/jackson:jackson-dataformat-cbor", + "//lib/jackson:jackson-dataformat-smile", + "//lib/joda:joda-time", + "//lib/lucene:lucene-codecs", + "//lib/lucene:lucene-highlighter", + "//lib/lucene:lucene-join", + "//lib/lucene:lucene-memory", + "//lib/lucene:lucene-queries", + "//lib/lucene:lucene-sandbox", + "//lib/lucene:lucene-spatial", + "//lib/lucene:lucene-suggest", + ], +) + +# Java REST client for Elasticsearch. +VERSION = "0.1.7" + +java_library( + name = "jest-common", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jest_common//jar"], +) + +java_library( + name = "jest", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jest//jar"], + 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", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//lib/elasticsearch:__pkg__"], + exports = ["@compress_lzf//jar"], +) + +java_library( + name = "hppc", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//lib/elasticsearch:__pkg__"], + exports = ["@hppc//jar"], +) + +java_library( + name = "jsr166e", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//lib/elasticsearch:__pkg__"], + exports = ["@jsr166e//jar"], +) + +java_library( + name = "netty", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//lib/elasticsearch:__pkg__"], + exports = ["@netty//jar"], +) + +java_library( + name = "t-digest", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//lib/elasticsearch:__pkg__"], + exports = ["@t_digest//jar"], +) + +java_library( + name = "jna", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jna//jar"], +)
diff --git a/lib/fonts/BUCK b/lib/fonts/BUCK index c5b78eb..7b64cf2 100644 --- a/lib/fonts/BUCK +++ b/lib/fonts/BUCK
@@ -11,20 +11,3 @@ license = 'OFL1.1', 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 -genrule( - name = 'opensans', - cmd = 'zip -rq $OUT .', - srcs = [ - 'OpenSans-Bold.woff', - 'OpenSans-Bold.woff2', - 'OpenSans-Regular.woff', - 'OpenSans-Regular.woff2' - ], - out = 'opensans.zip', - license = 'Apache2.0', - visibility = ['PUBLIC'], -)
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD new file mode 100644 index 0000000..fb5ea84 --- /dev/null +++ b/lib/fonts/BUILD
@@ -0,0 +1,13 @@ +load("//tools/bzl:genrule2.bzl", "genrule2") + +# Source Code Pro. Version 2.010 Roman / 1.030 Italics +# https://github.com/adobe-fonts/source-code-pro/releases/tag/2.010R-ro%2F1.030R-it +filegroup( + name = "sourcecodepro", + srcs = [ + "SourceCodePro-Regular.woff", + "SourceCodePro-Regular.woff2", + ], + data = ["//lib:LICENSE-OFL1.1"], + visibility = ["//visibility:public"], +)
diff --git a/lib/fonts/OpenSans-Bold.woff b/lib/fonts/OpenSans-Bold.woff deleted file mode 100644 index 74c4086..0000000 --- a/lib/fonts/OpenSans-Bold.woff +++ /dev/null Binary files differ
diff --git a/lib/fonts/OpenSans-Bold.woff2 b/lib/fonts/OpenSans-Bold.woff2 deleted file mode 100644 index 44d6c26..0000000 --- a/lib/fonts/OpenSans-Bold.woff2 +++ /dev/null Binary files differ
diff --git a/lib/fonts/OpenSans-Regular.woff b/lib/fonts/OpenSans-Regular.woff deleted file mode 100644 index 882f7c9..0000000 --- a/lib/fonts/OpenSans-Regular.woff +++ /dev/null Binary files differ
diff --git a/lib/fonts/OpenSans-Regular.woff2 b/lib/fonts/OpenSans-Regular.woff2 deleted file mode 100644 index 52217ee..0000000 --- a/lib/fonts/OpenSans-Regular.woff2 +++ /dev/null Binary files differ
diff --git a/lib/greenmail/BUCK b/lib/greenmail/BUCK new file mode 100644 index 0000000..9a49c4d --- /dev/null +++ b/lib/greenmail/BUCK
@@ -0,0 +1,21 @@ +include_defs('//lib/maven.defs') + +VERSION = '1.5.2' + +java_library( + name = 'greenmail', + exported_deps = [ + ':greenmail_library', + ], + visibility = ['PUBLIC'], +) + +maven_jar( + name = 'greenmail_library', + id = 'com.icegreen:greenmail:' + VERSION, + sha1 = '6b4862a09f8642da58c109117b24ccc19a4a6d39', + license = 'Apache2.0', + exclude_java_sources = True, + visibility = ['PUBLIC'], +) +
diff --git a/lib/greenmail/BUILD b/lib/greenmail/BUILD new file mode 100644 index 0000000..55eb9f3 --- /dev/null +++ b/lib/greenmail/BUILD
@@ -0,0 +1,8 @@ +package(default_visibility = ["//visibility:public"]) + +java_library( + name = "greenmail", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@greenmail//jar"], +)
diff --git a/lib/guava.bzl b/lib/guava.bzl new file mode 100644 index 0000000..7ba9079f --- /dev/null +++ b/lib/guava.bzl
@@ -0,0 +1,5 @@ +GUAVA_VERSION = "20.0" + +GUAVA_BIN_SHA1 = "89507701249388e1ed5ddcf8c41f4ce1be7831ef" + +GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
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..6d7bf91 100644 --- a/lib/guice/BUILD +++ b/lib/guice/BUILD
@@ -1,39 +1,54 @@ java_library( - name = 'guice', - exports = [ - ':guice_library', - ':javax-inject', - ], - visibility = ['//visibility:public'], + name = "guice", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = [ + ":guice_library", + ":javax-inject", + ":multibindings", + ], ) java_library( - name = 'guice_library', - exports = ['@guice_library//jar'], - runtime_deps = ['aopalliance'], - visibility = ['//visibility:public'], + name = "guice_library", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@guice_library//jar"], + runtime_deps = ["aopalliance"], ) java_library( - name = 'guice-assistedinject', - exports = ['@guice_assistedinject//jar'], - runtime_deps = [':guice'], - visibility = ['//visibility:public'], + name = "guice-assistedinject", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@guice_assistedinject//jar"], + runtime_deps = [":guice"], ) java_library( - name = 'guice-servlet', - exports = ['@guice_servlet//jar'], - runtime_deps = [':guice'], - visibility = ['//visibility:public'], + name = "guice-servlet", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@guice_servlet//jar"], + runtime_deps = [":guice"], ) java_library( - name = 'aopalliance', - exports = ['@aopalliance//jar'], + name = "aopalliance", + data = ["//lib:LICENSE-PublicDomain"], + exports = ["@aopalliance//jar"], ) java_library( - name = 'javax-inject', - exports = ['@javax_inject//jar'], + name = "javax-inject", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@javax_inject//jar"], +) + +java_library( + name = "multibindings", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@multibindings//jar"], )
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK index ebd58f1..6e8b8d1 100644 --- a/lib/gwt/BUCK +++ b/lib/gwt/BUCK
@@ -1,23 +1,21 @@ include_defs('//lib/maven.defs') -VERSION = '2.7.0' +VERSION = '2.8.0' maven_jar( name = 'user', id = 'com.google.gwt:gwt-user:' + VERSION, - sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b', + sha1 = '518579870499e15531f454f35dca0772d7fa31f7', license = 'Apache2.0', attach_source = False, - exclude = ['javax/servlet/*'], ) maven_jar( name = 'dev', id = 'com.google.gwt:gwt-dev:' + VERSION, - sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982', + sha1 = 'f160a61272c5ebe805cd2d3d3256ed3ecf14893f', license = 'Apache2.0', attach_source = False, - exclude = ['org/eclipse/jetty/*'], ) maven_jar( @@ -29,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..487e05b 100644 --- a/lib/gwt/BUILD +++ b/lib/gwt/BUILD
@@ -1,9 +1,45 @@ [java_library( - name = n, - exports = ['@%s//jar' % n.replace("-", "_")], - visibility = ["//visibility:public"], + name = n, + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@%s//jar" % n.replace("-", "_")], ) for n in [ - 'javax-validation', - 'dev', - 'user', + "ant", + "colt", + "dev", + "javax-validation", + "jsinterop-annotations", + "tapestry", + "user", + "w3c-css-sac", ]] + +java_library( + name = "user-neverlink", + data = ["//lib:LICENSE-Apache2.0"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@user//jar"], +) + +java_library( + name = "dev-neverlink", + data = ["//lib:LICENSE-Apache2.0"], + neverlink = 1, + visibility = ["//visibility:public"], + exports = ["@dev//jar"], +) + +java_library( + name = "javax-validation_src", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@javax_validation//jar:src"], +) + +java_library( + name = "jsinterop-annotations_src", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jsinterop_annotations//jar:src"], +)
diff --git a/lib/highlightjs/BUILD b/lib/highlightjs/BUILD new file mode 100644 index 0000000..b10bc55 --- /dev/null +++ b/lib/highlightjs/BUILD
@@ -0,0 +1,3 @@ +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..2b2cc6f 100644 --- a/lib/httpcomponents/BUILD +++ b/lib/httpcomponents/BUILD
@@ -1,29 +1,53 @@ +package(default_visibility = ["//visibility:public"]) + java_library( - name = 'fluent-hc', - exports = ['@fluent_hc//jar'], - runtime_deps = [':httpclient'], - visibility = ['//visibility:public'], + name = "fluent-hc", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@fluent_hc//jar"], + runtime_deps = [":httpclient"], ) java_library( - name = 'httpclient', - exports = ['@httpclient//jar'], - runtime_deps = [ - '//lib/commons:codec', - ':httpcore', - '//lib/log:jcl-over-slf4j', - ], - visibility = ['//visibility:public'], + name = "httpclient", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@httpclient//jar"], + runtime_deps = [ + ":httpcore", + "//lib/commons:codec", + "//lib/log:jcl-over-slf4j", + ], ) java_library( - name = 'httpcore', - exports = ['@httpcore//jar'], - visibility = ['//visibility:public'], + name = "httpcore", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@httpcore//jar"], ) java_library( - name = 'httpmime', - exports = ['@httpmime//jar'], - visibility = ['//visibility:public'], + name = "httpmime", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@httpmime//jar"], +) + +java_library( + name = "httpasyncclient", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@httpasyncclient//jar"], +) + +java_library( + name = "httpcore-nio", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@httpcore_nio//jar"], +) + +java_library( + name = "httpcore-niossl", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@httpcore_niossl//jar"], )
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..4847371 --- /dev/null +++ b/lib/jackson/BUILD
@@ -0,0 +1,21 @@ +package(default_visibility = ["//visibility:public"]) + +VERSION = "2.6.6" + +java_library( + name = "jackson-core", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jackson_core//jar"], +) + +java_library( + name = "jackson-dataformat-smile", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jackson_dataformat_smile//jar"], +) + +java_library( + name = "jackson-dataformat-cbor", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jackson_dataformat_cbor//jar"], +)
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..c6ba9c8 100644 --- a/lib/jetty/BUILD +++ b/lib/jetty/BUILD
@@ -1,67 +1,76 @@ java_library( - name = 'servlet', - exports = ['@jetty_servlet//jar'], - runtime_deps = [':security'], - visibility = ['//visibility:public'], + name = "servlet", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jetty_servlet//jar"], + runtime_deps = [":security"], ) java_library( - name = 'security', - exports = ['@jetty_security//jar'], - runtime_deps = [':server'], - visibility = ['//visibility:public'], + name = "security", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jetty_security//jar"], + runtime_deps = [":server"], ) java_library( - name = 'servlets', - exports = ['@jetty_servlets//jar'], - visibility = ['//visibility:public'], + name = "servlets", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jetty_servlets//jar"], ) java_library( - name = 'server', - exports = [ - '@jetty_server//jar', - ':continuation', - ':http', - ], - visibility = ['//visibility:public'], + name = "server", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = [ + ":continuation", + ":http", + "@jetty_server//jar", + ], ) java_library( - name = 'jmx', - exports = [ - '@jetty_jmx//jar', - ':continuation', - ':http', - ], - visibility = ['//visibility:public'], + name = "jmx", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = [ + ":continuation", + ":http", + "@jetty_jmx//jar", + ], ) java_library( - name = 'continuation', - exports = ['@jetty_continuation//jar'], - visibility = ['//visibility:public'], + name = "continuation", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jetty_continuation//jar"], ) java_library( - name = 'http', - exports = [ - '@jetty_http//jar', - ':io', - ], - visibility = ['//visibility:public'], + name = "http", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = [ + ":io", + "@jetty_http//jar", + ], ) java_library( - name = 'io', - exports = [ - '@jetty_io//jar', - ':util', - ], + name = "io", + data = ["//lib:LICENSE-Apache2.0"], + exports = [ + ":util", + "@jetty_io//jar", + ], ) java_library( - name = 'util', - exports = ['@jetty_util//jar'], + name = "util", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@jetty_util//jar"], )
diff --git a/lib/jgit/BUILD b/lib/jgit/BUILD new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/jgit/BUILD
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl new file mode 100644 index 0000000..11bcec5 --- /dev/null +++ b/lib/jgit/jgit.bzl
@@ -0,0 +1,6 @@ +JGIT_VERS = "4.5.0.201609210915-r.115-g81f9c1843" + +DOC_VERS = "4.5.0.201609210915-r" + +#DOC_VERS = JGIT_VERS # Set to VERS unless using a snapshot +JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + DOC_VERS + "/apidocs"
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK index 7c967b3..c0272af 100644 --- a/lib/jgit/org.eclipse.jgit.archive/BUCK +++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -3,8 +3,8 @@ maven_jar( name = 'jgit-archive', - id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS, - sha1 = '2db2e7666672a31fa41b7e1dadcba51df6d30954', + id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS, + sha1 = '4a5d058915400c1ef497bfeeb5e87d235213e273', license = 'jgit', repository = REPO, deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUILD b/lib/jgit/org.eclipse.jgit.archive/BUILD index 8fa94f2..d4e0a8c 100644 --- a/lib/jgit/org.eclipse.jgit.archive/BUILD +++ b/lib/jgit/org.eclipse.jgit.archive/BUILD
@@ -1,6 +1,7 @@ java_library( - name = 'jgit-archive', - exports = ['@jgit_archive//jar'], - runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'], - visibility = ['//visibility:public'], + name = "jgit-archive", + data = ["//lib:LICENSE-jgit"], + visibility = ["//visibility:public"], + exports = ["@jgit_archive//jar"], + runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"], )
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK index 06865cb..29e7e27 100644 --- a/lib/jgit/org.eclipse.jgit.http.server/BUCK +++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -3,8 +3,8 @@ maven_jar( name = 'jgit-servlet', - id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS, - sha1 = '6e36638888918d9941dddec7e2abe1f162cc74d9', + id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + JGIT_VERS, + sha1 = '927990025d2970995dbb58f03763eeb776fec8fd', license = 'jgit', repository = REPO, deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUILD b/lib/jgit/org.eclipse.jgit.http.server/BUILD index 6a442cc..c448c4b 100644 --- a/lib/jgit/org.eclipse.jgit.http.server/BUILD +++ b/lib/jgit/org.eclipse.jgit.http.server/BUILD
@@ -1,6 +1,7 @@ java_library( - name = 'jgit-servlet', - exports = ['@jgit_servlet//jar'], - runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'], - visibility = ['//visibility:public'], + name = "jgit-servlet", + data = ["//lib:LICENSE-jgit"], + visibility = ["//visibility:public"], + exports = ["@jgit_servlet//jar"], + runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"], )
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK index 77b637a..255b47c 100644 --- a/lib/jgit/org.eclipse.jgit.junit/BUCK +++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -3,8 +3,8 @@ maven_jar( name = 'junit', - id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS, - sha1 = 'e8fb1d81f588c3174a9730bdecdbde9faa04140a', + id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS, + sha1 = '8e3cb9b1f632fdfea76b04c286a2c0d8d260ebce', license = 'DO_NOT_DISTRIBUTE', repository = REPO, unsign = True,
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUILD b/lib/jgit/org.eclipse.jgit.junit/BUILD index d00b82c9..2c8966a 100644 --- a/lib/jgit/org.eclipse.jgit.junit/BUILD +++ b/lib/jgit/org.eclipse.jgit.junit/BUILD
@@ -1,6 +1,7 @@ java_library( - name = 'junit', - exports = ['@jgit_junit//jar'], - runtime_deps = ['//lib/jgit/org.eclipse.jgit:jgit'], - visibility = ['//visibility:public'], + name = "junit", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@jgit_junit//jar"], + runtime_deps = ["//lib/jgit/org.eclipse.jgit:jgit"], )
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK index 458703c..4f5da75 100644 --- a/lib/jgit/org.eclipse.jgit/BUCK +++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -3,13 +3,13 @@ maven_jar( name = 'jgit', - id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS, - bin_sha1 = '3e3d0b73dcf4ad649f37758ea8502d92f3d299de', - src_sha1 = 'fc352952db91a4046e4b832145eb2dc8afce8db1', + id = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS, + bin_sha1 = '34315f71bb9becf6ff75947a9c43c415b929ec21', + src_sha1 = '8320c18472870904eb7fb860af353fea818d07e4', license = 'jgit', repository = REPO, unsign = True, - deps = [':ewah'], + deps = [':javaewah'], exclude = [ 'META-INF/eclipse.inf', 'about.html', @@ -18,8 +18,8 @@ ) maven_jar( - name = 'ewah', - id = 'com.googlecode.javaewah:JavaEWAH:0.7.9', - sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a', + name = 'javaewah', + id = 'com.googlecode.javaewah:JavaEWAH:1.1.6', + sha1 = '94ad16d728b374d65bd897625f3fbb3da223a2b6', license = 'Apache2.0', )
diff --git a/lib/jgit/org.eclipse.jgit/BUILD b/lib/jgit/org.eclipse.jgit/BUILD index a1f9cad..33de929 100644 --- a/lib/jgit/org.eclipse.jgit/BUILD +++ b/lib/jgit/org.eclipse.jgit/BUILD
@@ -1,12 +1,14 @@ java_library( - name = 'jgit', - exports = ['@jgit//jar'], - runtime_deps = [':ewah'], - visibility = ['//visibility:public'], + name = "jgit", + data = ["//lib:LICENSE-jgit"], + visibility = ["//visibility:public"], + exports = ["@jgit//jar"], + runtime_deps = [":javaewah"], ) java_library( - name = 'ewah', - exports = ['@ewah//jar'], - visibility = ['//visibility:public'], + name = "javaewah", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@javaewah//jar"], )
diff --git a/lib/joda/BUILD b/lib/joda/BUILD index a673bf5..e1a1924 100644 --- a/lib/joda/BUILD +++ b/lib/joda/BUILD
@@ -1,11 +1,13 @@ java_library( - name = 'joda-time', - exports = ['@joda_time//jar'], - runtime_deps = ['joda-convert'], - visibility = ['//visibility:public'], + name = "joda-time", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@joda_time//jar"], + runtime_deps = ["joda-convert"], ) java_library( - name = 'joda-convert', - exports = ['@joda_convert//jar'], + name = "joda-convert", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@joda_convert//jar"], )
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..9528151 --- /dev/null +++ b/lib/js/BUILD
@@ -0,0 +1,35 @@ +package(default_visibility = ["//visibility:public"]) + +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", +) + +filegroup( + name = "highlightjs_files", + srcs = ["//lib/highlightjs:highlight.min.js"], + data = ["//lib:LICENSE-highlightjs"], +)
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl new file mode 100644 index 0000000..aaa8b81 --- /dev/null +++ b/lib/js/bower_archives.bzl
@@ -0,0 +1,109 @@ +# 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 = "accessibility-developer-tools", + package = "accessibility-developer-tools", + version = "2.11.0", + sha1 = "792cb24b649dafb316e7e536f8ae65d0d7b52bab") + bower_archive( + name = "async", + package = "async", + version = "1.5.2", + sha1 = "1ec975d3b3834646a7e3d4b7e68118b90ed72508") + bower_archive( + name = "chai", + package = "chai", + version = "3.5.0", + sha1 = "849ad3ee7c77506548b7b5db603a4e150b9431aa") + 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 = "lodash", + package = "lodash", + version = "3.10.1", + sha1 = "2f207a8293c4c554bf6cf071241f7a00dc513d3a") + bower_archive( + name = "mocha", + package = "mocha", + version = "2.5.3", + sha1 = "22ef0d1f43ba5e2241369c501ac648f00c0440c0") + bower_archive( + name = "neon-animation", + package = "neon-animation", + version = "1.2.4", + sha1 = "e8ccbb930c4b7ff470b1450baa901618888a7fd3") + bower_archive( + name = "sinon-chai", + package = "sinon-chai", + version = "2.8.0", + sha1 = "0464b5d944fdf8116bb23e0b02ecfbac945b3517") + bower_archive( + name = "sinonjs", + package = "sinonjs", + version = "1.17.1", + sha1 = "a26a6aab7358807de52ba738770f6ac709afd240") + bower_archive( + name = "stacky", + package = "stacky", + version = "1.3.2", + sha1 = "d6c07a0112ab2e9677fe085933744466a89232fb") + 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..bb665ff --- /dev/null +++ b/lib/js/bower_components.bzl
@@ -0,0 +1,222 @@ +# 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 = "accessibility-developer-tools", + license = "//lib:LICENSE-Apache2.0", + ) + bower_component( + name = "async", + license = "//lib:LICENSE-polymer", + ) + bower_component( + name = "chai", + license = "//lib:LICENSE-polymer", + ) + 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-test-helpers", + 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 = "lodash", + license = "//lib:LICENSE-polymer", + ) + bower_component( + name = "mocha", + license = "//lib:LICENSE-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 = "sinon-chai", + license = "//lib:LICENSE-polymer", + ) + bower_component( + name = "sinonjs", + license = "//lib:LICENSE-polymer", + ) + bower_component( + name = "stacky", + license = "//lib:LICENSE-polymer", + ) + bower_component( + name = "test-fixture", + license = "//lib:LICENSE-polymer", + seed = True, + ) + bower_component( + name = "web-animations-js", + license = "//lib:LICENSE-Apache2.0", + ) + bower_component( + name = "web-component-tester", + license = "//lib:LICENSE-polymer", + deps = [ + ":accessibility-developer-tools", + ":async", + ":chai", + ":lodash", + ":mocha", + ":sinon-chai", + ":sinonjs", + ":stacky", + ":test-fixture", + ], + seed = True, + ) + bower_component( + name = "webcomponentsjs", + license = "//lib:LICENSE-polymer", + )
diff --git a/lib/jsoup/BUCK b/lib/jsoup/BUCK new file mode 100644 index 0000000..8d8dab0 --- /dev/null +++ b/lib/jsoup/BUCK
@@ -0,0 +1,20 @@ +include_defs('//lib/maven.defs') + +VERSION = '1.9.2' + +java_library( + name = 'jsoup', + exported_deps = [ + ':jsoup_library', + ], + visibility = ['PUBLIC'], +) + +maven_jar( + name = 'jsoup_library', + id = 'org.jsoup:jsoup:' + VERSION, + sha1 = '5e3bda828a80c7a21dfbe2308d1755759c2fd7b4', + license = 'jsoup', + exclude_java_sources = True, + visibility = ['PUBLIC'], +)
diff --git a/lib/jsoup/BUILD b/lib/jsoup/BUILD new file mode 100644 index 0000000..3142dac --- /dev/null +++ b/lib/jsoup/BUILD
@@ -0,0 +1,6 @@ +java_library( + name = "jsoup", + data = ["//lib:LICENSE-jsoup"], + visibility = ["//visibility:public"], + exports = ["@jsoup//jar"], +)
diff --git a/lib/log/BUILD b/lib/log/BUILD index ac92ab6..af83d19 100644 --- a/lib/log/BUILD +++ b/lib/log/BUILD
@@ -1,47 +1,54 @@ java_library( - name = 'api', - exports = ['@log_api//jar'], - visibility = ['//visibility:public'], + name = "api", + data = ["//lib:LICENSE-slf4j"], + visibility = ["//visibility:public"], + exports = ["@log_api//jar"], ) java_library( - name = 'nop', - exports = ['@log_nop//jar'], - runtime_deps = [':api'], - visibility = ['//visibility:public'], + name = "nop", + data = ["//lib:LICENSE-slf4j"], + visibility = ["//visibility:public"], + exports = ["@log_nop//jar"], + runtime_deps = [":api"], ) java_library( - name = 'impl_log4j', - exports = ['@impl_log4j//jar'], - runtime_deps = [':log4j'], - visibility = ['//visibility:public'], + name = "impl_log4j", + data = ["//lib:LICENSE-slf4j"], + visibility = ["//visibility:public"], + exports = ["@impl_log4j//jar"], + runtime_deps = [":log4j"], ) java_library( - name = 'jcl-over-slf4j', - exports = ['@jcl_over_slf4j//jar'], - visibility = ['//visibility:public'], + name = "jcl-over-slf4j", + data = ["//lib:LICENSE-slf4j"], + visibility = ["//visibility:public"], + exports = ["@jcl_over_slf4j//jar"], ) java_library( - name = 'log4j', - exports = ['@log4j//jar'], - visibility = ['//visibility:public'], + name = "log4j", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@log4j//jar"], ) java_library( - name = 'jsonevent-layout', - exports = ['@jsonevent_layout//jar'], - runtime_deps = [ - ':json-smart', - '//lib/commons:lang' - ], - visibility = ['//visibility:public'], + name = "jsonevent-layout", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@jsonevent_layout//jar"], + runtime_deps = [ + ":json-smart", + "//lib/commons:lang", + ], ) java_library( - name = 'json-smart', - exports = ['@json_smart//jar'], - visibility = ['//visibility:public'], + name = "json-smart", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@json_smart//jar"], )
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..bbf43a6 100644 --- a/lib/lucene/BUILD +++ b/lib/lucene/BUILD
@@ -1,33 +1,95 @@ -load('//tools/bzl:maven.bzl', 'merge_maven_jars') +package(default_visibility = ["//visibility:public"]) + +load("//tools/bzl:maven.bzl", "merge_maven_jars") # core and backward-codecs both provide # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged. merge_maven_jars( - name = 'lucene-core-and-backward-codecs', - srcs = [ - '@backward_codecs//jar', - '@lucene_core//jar', - ], - visibility = ['//visibility:public'], + name = "lucene-core-and-backward-codecs", + srcs = [ + "@backward_codecs//jar", + "@lucene_core//jar", + ], + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], ) java_library( - name = 'lucene-analyzers-common', - exports = ['@lucene_analyzers_common//jar'], - runtime_deps = [':lucene-core-and-backward-codecs'], - visibility = ['//visibility:public'], + name = "lucene-analyzers-common", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@lucene_analyzers_common//jar"], + runtime_deps = [":lucene-core-and-backward-codecs"], ) java_library( - name = 'lucene-misc', - exports = ['@lucene_misc//jar'], - runtime_deps = [':lucene-core-and-backward-codecs'], - visibility = ['//visibility:public'], + name = "lucene-codecs", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@lucene_codecs//jar"], ) java_library( - name = 'lucene-queryparser', - exports = ['@lucene_queryparser//jar'], - runtime_deps = [':lucene-core-and-backward-codecs'], - visibility = ['//visibility:public'], + name = "lucene-core", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@lucene_core//jar"], +) + +java_library( + name = "lucene-misc", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@lucene_misc//jar"], + runtime_deps = [":lucene-core-and-backward-codecs"], +) + +java_library( + name = "lucene-queryparser", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@lucene_queryparser//jar"], + runtime_deps = [":lucene-core-and-backward-codecs"], +) + +java_library( + name = "lucene-highlighter", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_highlighter//jar"], +) + +java_library( + name = "lucene-join", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_join//jar"], +) + +java_library( + name = "lucene-memory", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_memory//jar"], +) + +java_library( + name = "lucene-sandbox", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_sandbox//jar"], +) + +java_library( + name = "lucene-spatial", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_spatial//jar"], +) + +java_library( + name = "lucene-suggest", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_suggest//jar"], +) + +java_library( + name = "lucene-queries", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@lucene_queries//jar"], )
diff --git a/lib/mail/BUCK b/lib/mail/BUCK new file mode 100644 index 0000000..07c78d8 --- /dev/null +++ b/lib/mail/BUCK
@@ -0,0 +1,21 @@ +include_defs('//lib/maven.defs') + +VERSION = '1.5.6' + +java_library( + name = 'mail', + exported_deps = [ + ':mail_library', + ], + visibility = ['PUBLIC'], +) + +maven_jar( + name = 'mail_library', + id = 'com.sun.mail:javax.mail:' + VERSION, + sha1 = 'ab5daef2f881c42c8e280cbe918ec4d7fdfd7efe', + license = 'DO_NOT_DISTRIBUTE', + exclude_java_sources = True, + visibility = ['PUBLIC'], +) +
diff --git a/lib/mail/BUILD b/lib/mail/BUILD new file mode 100644 index 0000000..eca2b6b --- /dev/null +++ b/lib/mail/BUILD
@@ -0,0 +1,6 @@ +java_library( + name = "mail", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = ["@mail//jar"], +)
diff --git a/lib/mime4j/BUCK b/lib/mime4j/BUCK new file mode 100644 index 0000000..13fc42a --- /dev/null +++ b/lib/mime4j/BUCK
@@ -0,0 +1,35 @@ +include_defs('//lib/maven.defs') + +VERSION = '0.8.0' + +java_library( + name = 'core', + exported_deps = [ + ':core_library', + ], + visibility = ['PUBLIC'], +) + +maven_jar( + name = 'core_library', + id = 'org.apache.james:apache-mime4j-core:' + VERSION, + sha1 = 'd54f45fca44a2f210569656b4ca3574b42911c95', + license = 'Apache2.0', + visibility = ['PUBLIC'], +) + +java_library( + name = 'dom', + exported_deps = [ + ':dom_library', + ], + visibility = ['PUBLIC'], +) + +maven_jar( + name = 'dom_library', + id = 'org.apache.james:apache-mime4j-dom:' + VERSION, + sha1 = '6720c93d14225c3e12c4a69768a0370c80e376a3', + license = 'Apache2.0', + visibility = ['PUBLIC'], +)
diff --git a/lib/mime4j/BUILD b/lib/mime4j/BUILD new file mode 100644 index 0000000..e7b85ef --- /dev/null +++ b/lib/mime4j/BUILD
@@ -0,0 +1,13 @@ +java_library( + name = "core", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@mime4j_core//jar"], +) + +java_library( + name = "dom", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@mime4j_dom//jar"], +)
diff --git a/lib/mina/BUILD b/lib/mina/BUILD index 52468a4..a30b3d2 100644 --- a/lib/mina/BUILD +++ b/lib/mina/BUILD
@@ -1,12 +1,14 @@ java_library( - name = 'sshd', - exports = ['@sshd//jar'], - visibility = ['//visibility:public'], - runtime_deps = [':core'], + name = "sshd", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@sshd//jar"], + runtime_deps = [":core"], ) java_library( - name = 'core', - exports = ['@mina_core//jar'], - visibility = ['//visibility:public'], + name = "core", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@mina_core//jar"], )
diff --git a/lib/openid/BUILD b/lib/openid/BUILD index 7d97a86..2b36fbb 100644 --- a/lib/openid/BUILD +++ b/lib/openid/BUILD
@@ -1,23 +1,26 @@ java_library( - name = 'consumer', - exports = ['@openid_consumer//jar'], - runtime_deps = [ - ':nekohtml', - ':xerces', - '//lib/httpcomponents:httpclient', - '//lib/log:jcl-over-slf4j', - '//lib/guice:guice', - ], - visibility = ['//visibility:public'], + name = "consumer", + data = ["//lib:LICENSE-Apache2.0"], + visibility = ["//visibility:public"], + exports = ["@openid_consumer//jar"], + runtime_deps = [ + ":nekohtml", + ":xerces", + "//lib/guice", + "//lib/httpcomponents:httpclient", + "//lib/log:jcl-over-slf4j", + ], ) java_library( - name = 'nekohtml', - exports = ['@nekohtml//jar'], - runtime_deps = [':xerces'], + name = "nekohtml", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@nekohtml//jar"], + runtime_deps = [":xerces"], ) java_library( - name = 'xerces', - exports = ['@xerces//jar'], + name = "xerces", + data = ["//lib:LICENSE-Apache2.0"], + exports = ["@xerces//jar"], )
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..aebca49 100644 --- a/lib/ow2/BUILD +++ b/lib/ow2/BUILD
@@ -1,30 +1,35 @@ java_library( - name = 'ow2-asm', - exports = ['@ow2_asm//jar'], - visibility = ["//visibility:public"], + name = "ow2-asm", + data = ["//lib:LICENSE-ow2"], + visibility = ["//visibility:public"], + exports = ["@ow2_asm//jar"], ) java_library( - name = 'ow2-asm-analysis', - exports = ['@ow2_asm_analysis//jar'], - visibility = ["//visibility:public"], + name = "ow2-asm-analysis", + data = ["//lib:LICENSE-ow2"], + visibility = ["//visibility:public"], + exports = ["@ow2_asm_analysis//jar"], ) java_library( - name = 'ow2-asm-commons', - exports = ['@ow2_asm_commons//jar'], - runtime_deps = [':ow2-asm-tree'], - visibility = ["//visibility:public"], + name = "ow2-asm-commons", + data = ["//lib:LICENSE-ow2"], + visibility = ["//visibility:public"], + exports = ["@ow2_asm_commons//jar"], + runtime_deps = [":ow2-asm-tree"], ) java_library( - name = 'ow2-asm-tree', - exports = ['@ow2_asm_tree//jar'], - visibility = ["//visibility:public"], + name = "ow2-asm-tree", + data = ["//lib:LICENSE-ow2"], + visibility = ["//visibility:public"], + exports = ["@ow2_asm_tree//jar"], ) java_library( - name = 'ow2-asm-util', - exports = ['@ow2_asm_util//jar'], - visibility = ["//visibility:public"], + name = "ow2-asm-util", + data = ["//lib:LICENSE-ow2"], + visibility = ["//visibility:public"], + exports = ["@ow2_asm_util//jar"], )
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..d0cab9b 100644 --- a/lib/powermock/BUILD +++ b/lib/powermock/BUILD
@@ -1,60 +1,66 @@ java_library( - name = 'powermock-module-junit4', - exports = [ - '@powermock_module_junit4//jar', - ':powermock-module-junit4-common', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "powermock-module-junit4", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":powermock-module-junit4-common", + "//lib:junit", + "@powermock_module_junit4//jar", + ], ) java_library( - name = 'powermock-module-junit4-common', - exports = [ - '@powermock_module_junit4_common//jar', - ':powermock-reflect', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "powermock-module-junit4-common", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":powermock-reflect", + "//lib:junit", + "@powermock_module_junit4_common//jar", + ], ) java_library( - name = 'powermock-reflect', - exports = [ - '@powermock_reflect//jar', - '//lib:junit', - '//lib/easymock:objenesis', - ], - visibility = ['//visibility:public'], + name = "powermock-reflect", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + "//lib:junit", + "//lib/easymock:objenesis", + "@powermock_reflect//jar", + ], ) java_library( - name = 'powermock-api-easymock', - exports = [ - '@powermock_api_easymock//jar', - ':powermock-api-support', - '//lib/easymock:easymock', - ], - visibility = ['//visibility:public'], + name = "powermock-api-easymock", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":powermock-api-support", + "//lib/easymock", + "@powermock_api_easymock//jar", + ], ) java_library( - name = 'powermock-api-support', - exports = [ - '@powermock_api_support//jar', - ':powermock-core', - ':powermock-reflect', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "powermock-api-support", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":powermock-core", + ":powermock-reflect", + "//lib:junit", + "@powermock_api_support//jar", + ], ) java_library( - name = 'powermock-core', - exports = [ - ':powermock-reflect', - '//lib:javassist', - '//lib:junit', - ], - visibility = ['//visibility:public'], + name = "powermock-core", + data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"], + visibility = ["//visibility:public"], + exports = [ + ":powermock-reflect", + "//lib:javassist", + "//lib:junit", + ], )
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..875f135 100644 --- a/lib/prolog/BUILD +++ b/lib/prolog/BUILD
@@ -1,47 +1,51 @@ java_library( - name = 'runtime', - exports = ['@prolog_runtime//jar'], - visibility = ['//visibility:public'], + name = "runtime", + data = ["//lib:LICENSE-prologcafe"], + visibility = ["//visibility:public"], + exports = ["@prolog_runtime//jar"], ) java_library( - name = 'compiler', - exports = ['@prolog_compiler//jar'], - runtime_deps = [ - ':io', - ':runtime', - ], - visibility = ['//visibility:public'], + name = "compiler", + data = ["//lib:LICENSE-prologcafe"], + visibility = ["//visibility:public"], + exports = ["@prolog_compiler//jar"], + runtime_deps = [ + ":io", + ":runtime", + ], ) java_library( - name = 'io', - exports = ['@prolog_io//jar'], + name = "io", + data = ["//lib:LICENSE-prologcafe"], + exports = ["@prolog_io//jar"], ) java_library( - name = 'cafeteria', - exports = ['@cafeteria//jar'], - runtime_deps = [ - 'io', - 'runtime', - ], - visibility = ['//visibility:public'], + name = "cafeteria", + data = ["//lib:LICENSE-prologcafe"], + visibility = ["//visibility:public"], + exports = ["@cafeteria//jar"], + runtime_deps = [ + "io", + "runtime", + ], ) java_binary( - name = 'compiler_bin', - main_class = 'BuckPrologCompiler', - runtime_deps = [':compiler_lib'], - visibility = ['//visibility:public'], + name = "compiler_bin", + main_class = "BuckPrologCompiler", + visibility = ["//visibility:public"], + runtime_deps = [":compiler_lib"], ) java_library( - name = 'compiler_lib', - srcs = ['java/BuckPrologCompiler.java'], - deps = [ - ':compiler', - ':runtime', - ], - visibility = ['//visibility:public'], + name = "compiler_lib", + srcs = ["java/BuckPrologCompiler.java"], + visibility = ["//visibility:public"], + deps = [ + ":compiler", + ":runtime", + ], )
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl index 3afb031..cae85ad 100644 --- a/lib/prolog/prolog.bzl +++ b/lib/prolog/prolog.bzl
@@ -12,25 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -load('//tools/bzl:genrule2.bzl', 'genrule2') +load("//tools/bzl:genrule2.bzl", "genrule2") def prolog_cafe_library( name, srcs, deps = [], - visibility = []): + **kwargs): genrule2( name = name + '__pl2j', cmd = '$(location //lib/prolog:compiler_bin) ' + - '$$TMP $@ ' + + '$$(dirname $@) $@ ' + '$(SRCS)', srcs = srcs, tools = ['//lib/prolog:compiler_bin'], - out = name + '.srcjar', + outs = [ name + '.srcjar' ], ) native.java_library( name = name, srcs = [':' + name + '__pl2j'], deps = ['//lib/prolog:runtime'] + deps, - visibility = visibility, + **kwargs )
diff --git a/plugins/BUILD b/plugins/BUILD new file mode 100644 index 0000000..86788d7 --- /dev/null +++ b/plugins/BUILD
@@ -0,0 +1,14 @@ +load("//tools/bzl:genrule2.bzl", "genrule2") +load("//tools/bzl:plugins.bzl", "CORE_PLUGINS") + +genrule2( + name = "core", + srcs = ["//plugins/%s:%s.jar" % (n, n) for n in CORE_PLUGINS], + outs = ["core.zip"], + 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/$@ .", + visibility = ["//visibility:public"], +)
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator index 9b163e1..1c9b04f 160000 --- a/plugins/commit-message-length-validator +++ b/plugins/commit-message-length-validator
@@ -1 +1 @@ -Subproject commit 9b163e113de9f3a49219a02d388f7f46ea2559d3 +Subproject commit 1c9b04feb0818412187f9fb9a67dca51027f0b33
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index 536beda..2b78594 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin
@@ -1 +1 @@ -Subproject commit 536beda3ab4f6f8d8d8c5be1e3cf0e6b4e9b10d5 +Subproject commit 2b785946fdfff06954772d31e611f82fd04f3687
diff --git a/plugins/download-commands b/plugins/download-commands index 5615076..29b2516 160000 --- a/plugins/download-commands +++ b/plugins/download-commands
@@ -1 +1 @@ -Subproject commit 5615076bcf114723d1744f7d8944f0df72dbbf2b +Subproject commit 29b2516f2caef184e0d4441f88344d2a69a938ba
diff --git a/plugins/hooks b/plugins/hooks index fa5d24b..e5b687e 160000 --- a/plugins/hooks +++ b/plugins/hooks
@@ -1 +1 @@ -Subproject commit fa5d24bbf8c90423a63ac309f42745f7e80e8ddc +Subproject commit e5b687e9fc1c1f67de83873ed26b295f52fe77d9
diff --git a/plugins/replication b/plugins/replication index 974dda6..0ed3b13 160000 --- a/plugins/replication +++ b/plugins/replication
@@ -1 +1 @@ -Subproject commit 974dda67f9c4282d6d94af4b2c71e08e26534ab1 +Subproject commit 0ed3b13df0b4f88c67ece722e56e554f8f38e83a
diff --git a/plugins/reviewnotes b/plugins/reviewnotes index 3f3d572..a4586ed 160000 --- a/plugins/reviewnotes +++ b/plugins/reviewnotes
@@ -1 +1 @@ -Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c +Subproject commit a4586ed9cf38ffbbe1f890abe3bc6ac4a873a3ed
diff --git a/plugins/singleusergroup b/plugins/singleusergroup index 3ca1167..7a9b878 160000 --- a/plugins/singleusergroup +++ b/plugins/singleusergroup
@@ -1 +1 @@ -Subproject commit 3ca1167edda713f4bfdcecd9c0e2626797d7027f +Subproject commit 7a9b8781cf0850815969f861f5f57178957f55c8
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK index 80f9f29..b5dee99 100644 --- a/polygerrit-ui/BUCK +++ b/polygerrit-ui/BUCK
@@ -6,6 +6,7 @@ '//lib/js:es6-promise', '//lib/js:fetch', '//lib/js:highlightjs', + '//lib/js:iron-a11y-keys-behavior', '//lib/js:iron-autogrow-textarea', '//lib/js:iron-dropdown', '//lib/js:iron-input', @@ -22,8 +23,9 @@ name = 'fonts', cmd = ' && '.join([ 'cd $TMP', - 'for file in $SRCS; do unzip -q $file; done', - 'zip -q $OUT *', + 'mkdir fonts', + 'for file in $SRCS; do echo `pwd` > /tmp/log.log; unzip -qd fonts/ $file; done', + 'zip -qr $OUT fonts', ]), srcs = [ '//lib/fonts:sourcecodepro',
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD new file mode 100644 index 0000000..1f11cde --- /dev/null +++ b/polygerrit-ui/BUILD
@@ -0,0 +1,43 @@ +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.bower_components", + deps = [ + "//lib/js:es6-promise", + "//lib/js:fetch", + # TODO(hanwen): this is inserted separately in the UI zip. Do we need this here? + "//lib/js:highlightjs", + "//lib/js:iron-a11y-keys-behavior", + "//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", + srcs = [ + "//lib/fonts:sourcecodepro", + ], + outs = ["fonts.zip"], + cmd = " && ".join([ + "mkdir -p $$TMP/fonts", + "cp $(SRCS) $$TMP/fonts/", + "cd $$TMP", + "find fonts/ -exec touch -t 198001010000 '{}' ';'", + "zip -qr $$ROOT/$@ fonts", + ]), + output_to_bindir = 1, + visibility = ["//visibility:public"], +)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md index 383fb50..28d028b 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). @@ -101,6 +111,14 @@ Then visit http://localhost:8081/elements/foo/bar_test.html +## Running tests (bazel) + +Run + +```sh +WCT_ARGS='--some-flag' sh polygerrit-ui/app/run_test.sh +``` + ## Style guide We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK index d03acf2..5d4f06d 100644 --- a/polygerrit-ui/app/BUCK +++ b/polygerrit-ui/app/BUCK
@@ -10,6 +10,7 @@ ['**'], excludes = [ 'BUCK', + 'BUILD', 'index.html', 'test/**', ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS) @@ -42,7 +43,7 @@ 'cd $TMP/polygerrit_ui', 'mkdir -p {fonts,elements}', ' && '.join(JS_LIBS_MKDIR_CMDS), - 'unzip -qd fonts $(location //polygerrit-ui:fonts)', + 'unzip -qd . $(location //polygerrit-ui:fonts)', 'unzip -qd elements $(location :gr-app)', 'cp -rp $SRCDIR/* .', ' && '.join(JS_LIBS_UNZIP_CMDS),
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD new file mode 100644 index 0000000..94f9bc8 --- /dev/null +++ b/polygerrit-ui/app/BUILD
@@ -0,0 +1,123 @@ +package( + default_visibility = ["//visibility:public"], +) + +load("//tools/bzl:genrule2.bzl", "genrule2") +load( + "//tools/bzl:js.bzl", + "bower_component_bundle", + "vulcanize", + "bower_component", + "js_component", +) + +vulcanize( + name = "gr-app", + srcs = glob( + [ + "**/*.html", + "**/*.js", + ], + exclude = [ + "bower_components/**", + "index.html", + "test/**", + "**/*_test.html", + ], + ), + app = "elements/gr-app.html", + deps = ["//polygerrit-ui:polygerrit_components.bower_components"], +) + +filegroup( + name = "top_sources", + srcs = [ + "favicon.ico", + "index.html", + ], +) + +filegroup( + name = "css_sources", + srcs = glob(["styles/**/*.css"]), +) + +genrule2( + name = "polygerrit_ui", + srcs = [ + "//lib/fonts:sourcecodepro", + "//lib/js:highlightjs_files", + ":top_sources", + ":css_sources", + ":gr-app", + # we extract from the zip, but depend on the component for license checking. + "@webcomponentsjs//:zipfile", + "//lib/js:webcomponentsjs", + ], + outs = ["polygerrit_ui.zip"], + cmd = " && ".join([ + "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}", + "cp $(locations :gr-app) $$TMP/polygerrit_ui/elements/", + "cp $(locations //lib/fonts:sourcecodepro) $$TMP/polygerrit_ui/fonts/", + "for f in $(locations :top_sources); do cp $$f $$TMP/polygerrit_ui/; done", + "for f in $(locations :css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done", + "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done", + "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js", + "cd $$TMP", + "find . -exec touch -t 198001010000 '{}' ';'", + "zip -qr $$ROOT/$@ *", + ]), +) + +bower_component_bundle( + name = "test_components", + testonly = 1, + deps = [ + "//lib/js:iron-test-helpers", + "//lib/js:test-fixture", + "//lib/js:web-component-tester", + "//polygerrit-ui:polygerrit_components.bower_components", + ], +) + +filegroup( + name = "pg_code", + srcs = glob( + [ + "**/*.html", + "**/*.js", + ], + exclude = [ + "bower_components/**", + ], + ), +) + +genrule2( + name = "pg_code_zip", + srcs = [":pg_code"], + outs = ["pg_code.zip"], + cmd = " && ".join([ + ("tar -hcf- $(locations :pg_code) |" + + " tar --strip-components=2 -C $$TMP/ -xf-"), + "cd $$TMP", + "find . -exec touch -t 198001010000 '{}' ';'", + "zip -rq $$ROOT/$@ *", + ]), +) + +sh_test( + name = "wct_test", + size = "large", + srcs = ["wct_test.sh"], + data = [ + "test/index.html", + ":pg_code.zip", + ":test_components.zip", + ], + # Should not run sandboxed. + tags = [ + "local", + "manual", + ], +)
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html new file mode 100644 index 0000000..acf3a62 --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -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. +--> +<script> +(function(window) { + 'use strict'; + + /** @polymerBehavior Gerrit.PatchSetBehavior */ + var PatchSetBehavior = { + /** + * Given an object of revisions, get a particular revision based on patch + * num. + * + * @param {Object} revisions The object of revisions given by the API + * @param {number|string} patchNum The number index of the revision + * @return {Object} The correspondent revision obj from {revisions} + */ + getRevisionByPatchNum: function(revisions, patchNum) { + patchNum = parseInt(patchNum, 10); + for (var rev in revisions) { + if (revisions.hasOwnProperty(rev) && + revisions[rev]._number === patchNum) { + return revisions[rev]; + } + } + }, + }; + + window.Gerrit = window.Gerrit || {}; + window.Gerrit.PatchSetBehavior = PatchSetBehavior; +})(window); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html new file mode 100644 index 0000000..7ff9371 --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -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. +--> +<!-- Polymer included for the html import polyfill. --> +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../bower_components/web-component-tester/browser.js"></script> +<title>gr-patch-set-behavior</title> + +<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-patch-set-behavior.html"> + +<script> + suite('gr-path-list-behavior tests', function() { + test('getRevisionByPatchNum', function() { + var get = Gerrit.PatchSetBehavior.getRevisionByPatchNum; + var revisions = [ + {_number: 0}, + {_number: 1}, + {_number: 2}, + ]; + assert.deepEqual(get(revisions, '1'), revisions[1]); + assert.deepEqual(get(revisions, 2), revisions[2]); + assert.equal(get(revisions, '3'), undefined); + }); + }); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html 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..adf0bf1 --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -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. +--> +<!-- Polymer included for the html import polyfill. --> +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../bower_components/web-component-tester/browser.js"></script> +<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-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js index 3702c84..c910d8f 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -22,6 +22,12 @@ properties: { hasTooltip: Boolean, + _isTouchDevice: { + type: Boolean, + value: function() { + return 'ontouchstart' in document.documentElement; + }, + }, _tooltip: Element, _titleText: String, }, @@ -29,10 +35,10 @@ attached: function() { if (!this.hasTooltip) { return; } - this.addEventListener('mouseover', this._handleShowTooltip.bind(this)); - this.addEventListener('mouseout', this._handleHideTooltip.bind(this)); - this.addEventListener('focusin', this._handleShowTooltip.bind(this)); - this.addEventListener('focusout', this._handleHideTooltip.bind(this)); + this.addEventListener('mouseenter', this._handleShowTooltip.bind(this)); + this.addEventListener('mouseleave', this._handleHideTooltip.bind(this)); + this.addEventListener('tap', this._handleHideTooltip.bind(this)); + this.listen(window, 'scroll', '_handleWindowScroll'); }, @@ -41,6 +47,8 @@ }, _handleShowTooltip: function(e) { + if (this._isTouchDevice) { return; } + if (!this.hasAttribute('title') || this.getAttribute('title') === '' || this._tooltip) { @@ -66,9 +74,11 @@ }, _handleHideTooltip: function(e) { + if (this._isTouchDevice) { return; } if (!this.hasAttribute('title') || - this._titleText == null || - this === document.activeElement) { return; } + this._titleText == null) { + return; + } this.setAttribute('title', this._titleText); if (this._tooltip && this._tooltip.parentNode) {
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/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html deleted file mode 100644 index 17acac8..0000000 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior.html +++ /dev/null
@@ -1,68 +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. ---> -<link rel="import" href="../bower_components/polymer/polymer.html"> -<script> -(function(window) { - 'use strict'; - - /** @polymerBehavior Gerrit.KeyboardShortcutBehavior */ - var KeyboardShortcutBehavior = { - enabled: true, - - properties: { - keyEventTarget: { - type: Object, - value: function() { return this; }, - }, - - _boundKeyHandler: { - type: Function, - readonly: true, - value: function() { return this._handleKey.bind(this); }, - }, - }, - - attached: function() { - this.keyEventTarget.addEventListener('keydown', this._boundKeyHandler); - }, - - detached: function() { - this.keyEventTarget.removeEventListener('keydown', this._boundKeyHandler); - }, - - shouldSupressKeyboardShortcut: function(e) { - if (!KeyboardShortcutBehavior.enabled) { return true; } - var getModifierState = e.getModifierState ? - e.getModifierState.bind(e) : - function() { return false; }; - var target = e.detail ? e.detail.keyboardEvent : e.target; - return getModifierState('Control') || - getModifierState('Alt') || - getModifierState('Meta') || - getModifierState('Fn') || - target.tagName == 'INPUT' || - target.tagName == 'TEXTAREA' || - target.tagName == 'SELECT' || - target.tagName == 'BUTTON' || - target.tagName == 'A' || - target.tagName == 'GR-BUTTON'; - }, - }; - - window.Gerrit = window.Gerrit || {}; - window.Gerrit.KeyboardShortcutBehavior = KeyboardShortcutBehavior; -})(window); -</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html new file mode 100644 index 0000000..3d99cec --- /dev/null +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -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. +--> +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> + +<script> +(function(window) { + 'use strict'; + + var getKeyboardEvent = function(e) { + return Polymer.dom(e.detail ? e.detail.keyboardEvent : e); + }; + + var KeyboardShortcutBehaviorImpl = { + modifierPressed: function(e) { + e = getKeyboardEvent(e); + // When e is a keyboardEvent, e.event is not null. + if (e.event) { e = e.event; } + return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; + }, + + shouldSuppressKeyboardShortcut: function(e) { + e = getKeyboardEvent(e); + if (e.path[0].tagName === 'INPUT' || e.path[0].tagName === 'TEXTAREA') { + return true; + } + for (var i = 0; i < e.path.length; i++) { + if (e.path[i].tagName === 'GR-OVERLAY') { return true; } + } + return false; + }, + }; + + window.Gerrit = window.Gerrit || {}; + /** @polymerBehavior Gerrit.KeyboardShortcutBehavior */ + window.Gerrit.KeyboardShortcutBehavior = [ + Polymer.IronA11yKeysBehavior, + KeyboardShortcutBehaviorImpl, + ]; +})(window); +</script>
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html new file mode 100644 index 0000000..a72eb75 --- /dev/null +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -0,0 +1,131 @@ +<!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>keyboard-shortcut-behavior</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="keyboard-shortcut-behavior.html"> + +<test-fixture id="basic"> + <template> + <test-element></test-element> + </template> +</test-fixture> + +<test-fixture id="within-overlay"> + <template> + <gr-overlay> + <test-element></test-element> + </gr-overlay> + </template> +</test-fixture> + +<script> + suite('keyboard-shortcut-behavior tests', function() { + var element; + var overlay; + var sandbox; + + suiteSetup(function() { + // Define a Polymer element that uses this behavior. + Polymer({ + is: 'test-element', + behaviors: [Gerrit.KeyboardShortcutBehavior], + keyBindings: { + 'k': '_handleKey' + }, + _handleKey: function() {}, + }); + }); + + setup(function() { + element = fixture('basic'); + overlay = fixture('within-overlay'); + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('doesn’t block kb shortcuts for non-whitelisted els', function(done) { + var divEl = document.createElement('div'); + element.appendChild(divEl); + element._handleKey = function(e) { + assert.isFalse(element.shouldSuppressKeyboardShortcut(e)); + done(); + }; + MockInteractions.keyDownOn(divEl, 75, null, 'k'); + }); + + test('blocks kb shortcuts for input els', function(done) { + var inputEl = document.createElement('input'); + element.appendChild(inputEl); + element._handleKey = function(e) { + assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); + done(); + }; + MockInteractions.keyDownOn(inputEl, 75, null, 'k'); + }); + + test('blocks kb shortcuts for textarea els', function(done) { + var textareaEl = document.createElement('textarea'); + element.appendChild(textareaEl); + element._handleKey = function(e) { + assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); + done(); + }; + MockInteractions.keyDownOn(textareaEl, 75, null, 'k'); + }); + + test('blocks kb shortcuts for anything in a gr-overlay', function(done) { + var divEl = document.createElement('div'); + var element = overlay.querySelector('test-element'); + element.appendChild(divEl); + element._handleKey = function(e) { + assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); + done(); + }; + MockInteractions.keyDownOn(divEl, 75, null, 'k'); + }); + + test('modifierPressed returns accurate values', function() { + var spy = sandbox.spy(element, 'modifierPressed'); + element._handleKey = function(e) { + element.modifierPressed(e); + }; + MockInteractions.keyDownOn(element, 75, 'shift', 'k'); + assert.isTrue(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'ctrl', 'k'); + assert.isTrue(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'meta', 'k'); + assert.isTrue(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'alt', 'k'); + assert.isTrue(spy.lastCall.returnValue); + }); + }); +</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..b66c70b 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
@@ -35,6 +35,10 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + getLoggedIn: function() { return Promise.resolve(false); }, + }); element = fixture('basic'); }); @@ -120,12 +124,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..944e963 --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -0,0 +1,54 @@ +<!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() { + stub('gr-rest-api-interface', { + getLoggedIn: function() { return Promise.resolve(false); }, + }); + 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-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html index bab2014..bd81a48 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -15,7 +15,7 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior.html"> <link rel="import" href="../../../styles/gr-change-list-styles.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js index 4e17253..3f9de03 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -78,6 +78,12 @@ Gerrit.RESTClientBehavior, ], + keyBindings: { + 'j': '_handleJKey', + 'k': '_handleKKey', + 'o enter': '_handleEnterKey', + }, + attached: function() { this._loadPreferences(); }, @@ -149,31 +155,40 @@ account._account_id != change.owner._account_id; }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - if (this.groups == null) { return; } + _getAggregateGroupsLen: function(groups) { + groups = groups || []; var len = 0; this.groups.forEach(function(group) { len += group.length; }); - switch (e.keyCode) { - case 74: // 'j' - e.preventDefault(); - if (this.selectedIndex == len - 1) { return; } - this.selectedIndex += 1; - break; - case 75: // 'k' - e.preventDefault(); - if (this.selectedIndex == 0) { return; } - this.selectedIndex -= 1; - break; - case 79: // 'o' - case 13: // 'enter' - e.preventDefault(); - page.show(this._changeURLForIndex(this.selectedIndex)); - break; - } + return len; + }, + + _handleJKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + var len = this._getAggregateGroupsLen(this.groups); + if (this.selectedIndex === len - 1) { return; } + this.selectedIndex += 1; + }, + + _handleKKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + if (this.selectedIndex === 0) { return; } + this.selectedIndex -= 1; + }, + + _handleEnterKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + page.show(this._changeURLForIndex(this.selectedIndex)); }, _changeURLForIndex: function(index) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html index aa77b77..33b4279 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -21,7 +21,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> <script src="../../../bower_components/page/page.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-list.html"> @@ -132,25 +131,25 @@ flush(function() { assert.isTrue(elementItems[0].selected); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); var showStub = sinon.stub(page, 'show'); assert.equal(element.selectedIndex, 2); - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); assert(showStub.lastCall.calledWithExactly('/c/2/'), 'Should navigate to /c/2/'); - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); assert(showStub.lastCall.calledWithExactly('/c/1/'), 'Should navigate to /c/1/'); - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); assert.equal(element.selectedIndex, 0); showStub.restore();
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html index 98f2b18..43714f2 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -42,7 +42,9 @@ account="[[account]]" class$="[[_computeChipClass(account)]]" data-account-id$="[[account._account_id]]" - removable="[[_computeRemovable(account)]]"> + removable="[[_computeRemovable(account)]]" + on-keydown="_handleChipKeydown" + tabindex$="[[index]]"> </gr-account-chip> </template> <gr-account-entry @@ -52,7 +54,8 @@ change="[[change]]" filter="[[filter]]" placeholder="[[placeholder]]" - on-add="_handleAdd"> + on-add="_handleAdd" + on-input-keydown="_handleInputKeydown"> </gr-account-entry> </template> <script src="gr-account-list.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js index 87d7116..3b17756 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -37,6 +37,10 @@ 'remove': '_handleRemove', }, + get accountChips() { + return Polymer.dom(this.root).querySelectorAll('gr-account-chip'); + }, + get focusStart() { return this.$.entry.focusStart; }, @@ -86,6 +90,12 @@ _handleRemove: function(e) { var toRemove = e.detail.account; + this._removeAccount(toRemove); + this.$.entry.focus(); + }, + + _removeAccount: function(toRemove) { + if (!toRemove || !this._computeRemovable(toRemove)) { return; } for (var i = 0; i < this.accounts.length; i++) { var matches; var account = this.accounts[i]; @@ -96,16 +106,70 @@ } if (matches) { this.splice('accounts', i, 1); - this.$.entry.focus(); return; } } - console.warn('received remove event for missing account', - e.detail.account); + console.warn('received remove event for missing account', toRemove); + }, + + _handleInputKeydown: function(e) { + var input = e.detail.input; + if (input.selectionStart !== input.selectionEnd || + input.selectionStart !== 0) { + return; + } + switch (e.detail.keyCode) { + case 8: // Backspace + this._removeAccount(this.accounts[this.accounts.length - 1]); + break; + case 37: // Left arrow + var chips = this.accountChips; + if (chips[chips.length - 1]) { + chips[chips.length - 1].focus(); + } + break; + } + }, + + _handleChipKeydown: function(e) { + var chip = e.target; + var chips = this.accountChips; + var index = chips.indexOf(chip); + switch (e.keyCode) { + case 8: // Backspace + case 13: // Enter + case 32: // Spacebar + case 46: // Delete + this._removeAccount(chip.account); + // Splice from this array to avoid inconsistent ordering of + // event handling. + chips.splice(index, 1); + if (index < chips.length) { + chips[index].focus(); + } else if (index > 0) { + chips[index - 1].focus(); + } else { + this.$.entry.focus(); + } + break; + case 37: // Left arrow + if (index > 0) { + chip.blur(); + chips[index - 1].focus(); + } + break; + case 39: // Right arrow + chip.blur(); + if (index < chips.length - 1) { + chips[index + 1].focus(); + } else { + this.$.entry.focus(); + } + break; + } }, additions: function() { - var result = []; return this.accounts.filter(function(account) { return account._pendingAdd; }).map(function(account) { @@ -115,7 +179,6 @@ return {account: account}; } }); - return result; }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html index bb55d08..1bd12b4 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-account-list.html"> @@ -49,6 +48,7 @@ var existingReviewer1; var existingReviewer2; + var sandbox; var element; function getChips() { @@ -56,17 +56,19 @@ } setup(function() { + sandbox = sinon.sandbox.create(); existingReviewer1 = makeAccount(); existingReviewer2 = makeAccount(); + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); element = fixture('basic'); element.accounts = [existingReviewer1, existingReviewer2]; + }); - stub('gr-rest-api-interface', { - getConfig: function() { - return Promise.resolve({}); - }, - }); + teardown(function() { + sandbox.restore(); }); test('account entry only appears when editable', function() { @@ -78,7 +80,7 @@ test('addition and removal of account/group chips', function() { flushAsynchronousOperations(); - + sandbox.stub(element, '_computeRemovable').returns(true); // Existing accounts are listed. var chips = getChips(); assert.equal(chips.length, 2); @@ -232,5 +234,72 @@ }, ]); }); + + test('removeAccount fails if account is not removable', function() { + element.readonly = true; + var acct = makeAccount(); + element.accounts = [acct]; + element._removeAccount(acct); + assert.equal(element.accounts.length, 1); + }); + + suite('keyboard interactions', function() { + + test('backspace at text input start removes last account', function() { + var input = element.$.entry.$.input; + sandbox.stub(element.$.entry, '_getReviewerSuggestions'); + sandbox.stub(input, '_updateSuggestions'); + sandbox.stub(element, '_computeRemovable').returns(true); + // Next line is a workaround for Firefix not moving cursor + // on input field update + assert.equal(input.$.input.selectionStart, 0); + input.text = 'test'; + MockInteractions.focus(input.$.input); + flushAsynchronousOperations(); + assert.equal(element.accounts.length, 2); + MockInteractions.pressAndReleaseKeyOn(input.$.input, 8); // Backspace + assert.equal(element.accounts.length, 2); + input.text = ''; + MockInteractions.pressAndReleaseKeyOn(input.$.input, 8); // Backspace + assert.equal(element.accounts.length, 1); + }); + + test('arrow key navigation', function() { + var input = element.$.entry.$.input; + input.text = ''; + element.accounts = [makeAccount(), makeAccount()]; + MockInteractions.focus(input.$.input); + flushAsynchronousOperations(); + var chips = element.accountChips; + var chipsOneSpy = sandbox.spy(chips[1], 'focus'); + MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left + assert.isTrue(chipsOneSpy.called); + var chipsZeroSpy = sandbox.spy(chips[0], 'focus'); + MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left + assert.isTrue(chipsZeroSpy.called); + MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left + assert.isTrue(chipsZeroSpy.calledOnce); + MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right + assert.isTrue(chipsOneSpy.calledTwice); + }); + + test('delete', function(done) { + element.accounts = [makeAccount(), makeAccount()]; + flush(function() { + var chips = element.accountChips; + var focusSpy = sandbox.spy(element.accountChips[1], 'focus'); + var removeSpy = sandbox.spy(element, '_removeAccount'); + MockInteractions.pressAndReleaseKeyOn( + element.accountChips[0], 8); // Backspace + assert.isTrue(focusSpy.called); + assert.isTrue(removeSpy.calledOnce); + + MockInteractions.pressAndReleaseKeyOn( + element.accountChips[1], 46); // Delete + assert.isTrue(removeSpy.calledTwice); + done(); + }); + }); + }); }); </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..9c591db 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,9 +64,9 @@ </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]]" + hidden$="[[_computeActionHidden(action.__key, _hiddenChangeActions.*)]]" primary$="[[action.__primary]]" hidden$="[[!action.enabled]]" data-action-key$="[[action.__key]]" @@ -72,10 +76,10 @@ on-tap="_handleActionTap"></gr-button> </template> </section> - <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]"> - <div class="groupLabel">Revision</div> + <section hidden$="[[!_actionCount(revisionActions.*, _additionalActions.*)]]"> <template is="dom-repeat" items="[[_revisionActionValues]]" as="action"> <gr-button title$="[[action.title]]" + hidden$="[[_computeActionHidden(action.__key, _hiddenRevisionActions.*)]]" primary$="[[action.__primary]]" disabled$="[[!action.enabled]]" data-action-key$="[[action.__key]]" @@ -94,13 +98,14 @@ hidden></gr-confirm-rebase-dialog> <gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" - commit-info="[[commitInfo]]" + change-status="[[changeStatus]]" + commit-message="[[commitMessage]]" + commit-num="[[commitNum]]" 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> @@ -109,6 +114,19 @@ on-confirm="_handleAbandonDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden></gr-confirm-abandon-dialog> + <gr-confirm-dialog + id="confirmDeleteDialog" + class="confirmDialog" + confirm-label="Delete" + on-cancel="_handleConfirmDialogCancel" + on-confirm="_handleDeleteConfirm"> + <div class="header"> + Delete Change + </div> + <div class="main"> + Do you really want to delete the change? + </div> + </gr-confirm-dialog> </gr-overlay> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js index 3445f4e..f6c86ec 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
@@ -14,6 +14,35 @@ (function() { 'use strict'; + /** + * @enum {number} + */ + var LabelStatus = { + /** + * This label provides what is necessary for submission. + */ + OK: 'OK', + /** + * This label prevents the change from being submitted. + */ + REJECT: 'REJECT', + /** + * The label may be set, but it's neither necessary for submission + * nor does it block submission if set. + */ + MAY: 'MAY', + /** + * The label is required for submission, but has not been satisfied. + */ + NEED: 'NEED', + /** + * The label is required for submission, but is impossible to complete. + * The likely cause is access has not been granted correctly by the + * project owner or site administrator. + */ + IMPOSSIBLE: 'IMPOSSIBLE', + }; + // TODO(davido): Add the rest of the change actions. var ChangeActions = { ABANDON: 'abandon', @@ -49,6 +78,15 @@ var ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; + var QUICK_APPROVE_ACTION = { + __key: 'review', + __type: 'change', + enabled: true, + key: 'review', + label: 'Quick Approve', + method: 'POST', + }; + Polymer({ is: 'gr-change-actions', @@ -74,31 +112,44 @@ }, }, changeNum: String, + changeStatus: String, + commitNum: String, patchNum: String, - commitInfo: Object, + commitMessage: { + type: String, + value: '', + }, + revisionActions: { + type: Object, + value: function() { return {}; }, + }, _loading: { type: Boolean, value: true, }, - _revisionActions: { - type: Object, - value: function() { return {}; }, - }, _revisionActionValues: { type: Array, - computed: '_computeRevisionActionValues(_revisionActions.*, ' + + computed: '_computeRevisionActionValues(revisionActions.*, ' + 'primaryActionKeys.*, _additionalActions.*)', }, _changeActionValues: { type: Array, computed: '_computeChangeActionValues(actions.*, ' + - 'primaryActionKeys.*, _additionalActions.*)', + 'primaryActionKeys.*, _additionalActions.*, change)', }, _additionalActions: { type: Array, value: function() { return []; }, }, + _hiddenChangeActions: { + type: Array, + value: function() { return []; }, + }, + _hiddenRevisionActions: { + type: Array, + value: function() { return []; }, + }, }, ActionType: ActionType, @@ -110,7 +161,7 @@ ], observers: [ - '_actionsChanged(actions.*, _revisionActions.*, _additionalActions.*)', + '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', ], ready: function() { @@ -126,7 +177,7 @@ return this._getRevisionActions().then(function(revisionActions) { if (!revisionActions) { return; } - this._revisionActions = revisionActions; + this.revisionActions = revisionActions; this._loading = false; }.bind(this)).catch(function(err) { alert('Couldn’t load revision actions. Check the console ' + @@ -166,6 +217,24 @@ ], value); }, + setActionHidden: function(type, key, hidden) { + var path; + if (type === ActionType.CHANGE) { + path = '_hiddenChangeActions'; + } else if (type === ActionType.REVISION) { + path = '_hiddenRevisionActions'; + } else { + throw Error('Invalid action type given: ' + type); + } + + var idx = this.get(path).indexOf(key); + if (hidden && idx === -1) { + this.push(path, key); + } else if (!hidden && idx !== -1) { + this.splice(path, idx, 1); + } + }, + _indexOfActionButtonWithKey: function(key) { for (var i = 0; i < this._additionalActions.length; i++) { if (this._additionalActions[i].__key === key) { @@ -208,13 +277,95 @@ _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'); + primariesChangeRecord, additionalActionsChangeRecord, change) { + var actions = this._getActionValues( + actionsChangeRecord, primariesChangeRecord, + additionalActionsChangeRecord, ActionType.CHANGE, change); + var quickApprove = this._getQuickApproveAction(); + if (quickApprove) { + actions.unshift(quickApprove); + } + return actions; + }, + + _getLabelStatus: function(label) { + if (label.approved) { + return LabelStatus.OK; + } else if (label.rejected) { + return LabelStatus.REJECT; + } else if (label.optional) { + return LabelStatus.OPTIONAL; + } else { + return LabelStatus.NEED; + } + }, + + /** + * Get highest score for last missing permitted label for current change. + * Returns null if no labels permitted or more than one label missing. + * + * @return {{label: string, score: string}} + */ + _getTopMissingApproval: function() { + if (!this.change || + !this.change.labels || + !this.change.permitted_labels) { + return null; + } + var result; + for (var label in this.change.labels) { + if (!(label in this.change.permitted_labels)) { + continue; + } + if (this.change.permitted_labels[label].length === 0) { + continue; + } + var status = this._getLabelStatus(this.change.labels[label]); + if (status === LabelStatus.NEED) { + if (result) { + // More than one label is missing, so it's unclear which to quick + // approve, return null; + return null; + } + result = label; + } else if (status === LabelStatus.REJECT || + status === LabelStatus.IMPOSSIBLE) { + return null; + } + } + if (result) { + var score = this.change.permitted_labels[result].slice(-1)[0]; + var maxScore = + Object.keys(this.change.labels[result].values).slice(-1)[0]; + if (score === maxScore) { + // Allow quick approve only for maximal score. + return { + label: result, + score: score, + }; + } + } + return null; + }, + + _getQuickApproveAction: function() { + var approval = this._getTopMissingApproval(); + if (!approval) { + return null; + } + var action = Object.assign({}, QUICK_APPROVE_ACTION); + action.label = approval.label + approval.score; + var review = { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: {}, + }; + review.labels[approval.label] = approval.score; + action.payload = review; + return action; }, _getActionValues: function(actionsChangeRecord, primariesChangeRecord, @@ -231,6 +382,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 +414,37 @@ }, _canSubmitChange: function() { - return this.$.jsAPI.canSubmitChange(); + return this.$.jsAPI.canSubmitChange(this.change, + this._getRevision(this.change, this.patchNum)); + }, + + _computeActionHidden: function(key, hiddenActionsChangeRecord) { + var hiddenActions = + (hiddenActionsChangeRecord && hiddenActionsChangeRecord.base) || []; + return hiddenActions.indexOf(key) !== -1; + }, + + _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) { @@ -273,12 +458,18 @@ var type = el.getAttribute('data-action-type'); if (type === ActionType.REVISION) { this._handleRevisionAction(key); + } else if (key === ChangeActions.DELETE) { + this._showActionDialog(this.$.confirmDeleteDialog); } 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 if (key === QUICK_APPROVE_ACTION.key) { + var action = this._changeActionValues.find(function(o) { + return o.key === key; + }); + this._fireAction( + this._prependSlash(key), action, true, action.payload); } else { this._fireAction(this._prependSlash(key), this.actions[key], false); } @@ -290,16 +481,17 @@ this._showActionDialog(this.$.confirmRebase); break; case RevisionActions.CHERRYPICK: + this.$.confirmCherrypick.branch = ''; this._showActionDialog(this.$.confirmCherrypick); break; case RevisionActions.SUBMIT: if (!this._canSubmitChange()) { return; } - /* falls through */ // required by JSHint + /* falls through */ // required by JSHint default: this._fireAction(this._prependSlash(key), - this._revisionActions[key], true); + this.revisionActions[key], true); } }, @@ -308,6 +500,10 @@ }, _handleConfirmDialogCancel: function() { + this._hideAllDialogs(); + }, + + _hideAllDialogs: function() { var dialogEls = Polymer.dom(this.root).querySelectorAll('.confirmDialog'); for (var i = 0; i < dialogEls.length; i++) { @@ -331,8 +527,8 @@ payload.base = el.base; } this.$.overlay.close(); - el.hidden = false; - this._fireAction('/rebase', this._revisionActions.rebase, true, payload); + el.hidden = true; + this._fireAction('/rebase', this.revisionActions.rebase, true, payload); }, _handleCherrypickConfirm: function() { @@ -347,10 +543,10 @@ return; } this.$.overlay.close(); - el.hidden = false; + el.hidden = true; this._fireAction( '/cherrypick', - this._revisionActions.cherrypick, + this.revisionActions.cherrypick, true, { destination: el.branch, @@ -362,7 +558,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,11 +566,15 @@ _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}); }, + _handleDeleteConfirm: function() { + this._fireAction('/', this.actions[ChangeActions.DELETE], false); + }, + _setLoadingOnButtonWithKey: function(key) { var buttonEl = this.$$('[data-action-key="' + key + '"]'); buttonEl.setAttribute('loading', true); @@ -393,14 +593,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); + /* falls 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..b41567c 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) { @@ -77,21 +84,126 @@ }); element = fixture('basic'); + element.change = {}; 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('hide revision action', function(done) { + flush(function() { + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + assert.throws(element.setActionHidden.bind(element, 'invalid type')); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenRevisionActions, 1); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenRevisionActions, 1); + flush(function() { + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.isTrue(buttonEl.hasAttribute('hidden')); + + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, false); + flush(function() { + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + done(); + }); + }); + }); + }); + + test('hide change action', function(done) { + flush(function() { + var buttonEl = element.$$('[data-action-key="/"]'); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + assert.throws(element.setActionHidden.bind(element, 'invalid type')); + element.setActionHidden(element.ActionType.CHANGE, + element.ChangeActions.DELETE, true); + assert.lengthOf(element._hiddenChangeActions, 1); + element.setActionHidden(element.ActionType.CHANGE, + element.ChangeActions.DELETE, true); + assert.lengthOf(element._hiddenChangeActions, 1); + flush(function() { + var buttonEl = element.$$('[data-action-key="/"]'); + assert.isOk(buttonEl); + assert.isTrue(buttonEl.hasAttribute('hidden')); + + element.setActionHidden(element.ActionType.CHANGE, + element.RevisionActions.DELETE, false); + flush(function() { + var buttonEl = element.$$('[data-action-key="/"]'); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + 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 +241,7 @@ __key: 'rebase', __type: 'revision', __primary: false, + enabled: true, label: 'Rebase', method: 'POST', title: 'Rebase onto tip of branch or parent change', @@ -154,6 +267,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 +301,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', @@ -188,9 +321,16 @@ element._handleCherrypickConfirm(); assert.equal(fireActionStub.callCount, 0); // Still needs a message. - element.$.confirmCherrypick.message = 'foo message'; + // Add attributes that are used to determine the message. + element.$.confirmCherrypick.commitMessage = 'foo message'; + element.$.confirmCherrypick.changeStatus = 'OPEN'; + element.$.confirmCherrypick.commitNum = '123'; + element._handleCherrypickConfirm(); + assert.equal(element.$.confirmCherrypick.$.messageInput.value, + 'foo message'); + assert.deepEqual(fireActionStub.lastCall.args, [ '/cherrypick', action, true, { destination: 'master', @@ -198,6 +338,16 @@ } ]); }); + + test('branch name cleared when re-open cherrypick', function() { + var cherryPickButton = + element.$$('gr-button[data-action-key="cherrypick"]'); + var emptyBranchName = ''; + element.$.confirmCherrypick.branch = 'master'; + + MockInteractions.tap(cherryPickButton); + assert.equal(element.$.confirmCherrypick.branch, emptyBranchName); + }); }); test('custom actions', function(done) { @@ -241,6 +391,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 +413,9 @@ }); test('works', function() { + element.change = { + current_revision: 'abc1234', + }; var populateRevertMsgStub = sinon.stub( element.$.confirmRevertDialog, 'populateRevertMessage', function() { return 'original msg'; }); @@ -286,5 +442,221 @@ populateRevertMsgStub.restore(); }); }); + + suite('delete change', function() { + var fireActionStub; + var deleteAction; + + var tapDeleteAction = function() { + var deleteButton = element.$$('gr-button[data-action-key=\'/\']'); + MockInteractions.tap(deleteButton); + flushAsynchronousOperations(); + }; + + setup(function() { + fireActionStub = sinon.stub(element, '_fireAction'); + element.change = { + current_revision: 'abc1234', + }; + deleteAction = { + method: 'DELETE', + label: 'Delete Change', + title: 'Delete change X_X', + enabled: true, + }; + element.actions = { + '/': deleteAction, + }; + }); + + teardown(function() { + fireActionStub.restore(); + }); + + test('does not delete on action', function() { + tapDeleteAction(); + assert.isFalse(fireActionStub.called); + }); + + test('shows confirm dialog', function() { + tapDeleteAction(); + assert.isFalse(element.$$('#confirmDeleteDialog').hidden); + MockInteractions.tap( + element.$$('#confirmDeleteDialog').$$('gr-button[primary]')); + flushAsynchronousOperations(); + assert.isTrue(fireActionStub.calledWith('/', deleteAction, false)); + }); + + test('hides delete confirm on cancel', function() { + tapDeleteAction(); + MockInteractions.tap( + element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])')); + flushAsynchronousOperations(); + assert.isTrue(element.$$('#confirmDeleteDialog').hidden); + assert.isFalse(fireActionStub.called); + }); + }); + + suite('quick approve', function() { + setup(function() { + element.change = { + current_revision: 'abc1234', + }; + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + values: { + '-1': '', + ' 0': '', + '+1': '', + }, + }, + }, + permitted_labels: { + foo: ['-1', ' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + }); + + test('added when can approve', function() { + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.isNotNull(approveButton); + }); + + test('is first in list of actions', function() { + var approveButton = element.$$('gr-button'); + assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); + }); + + test('not added when already approved', function() { + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + approved: {}, + values: {}, + }, + }, + permitted_labels: { + foo: [' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('not added when label not permitted', function() { + element.change = { + current_revision: 'abc1234', + labels: { + foo: {values: {}}, + }, + permitted_labels: { + bar: [], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('approves when taped', function() { + var fireActionStub = sinon.stub(element, '_fireAction'); + MockInteractions.tap( + element.$$('gr-button[data-action-key=\'review\']')); + flushAsynchronousOperations(); + assert.isTrue(fireActionStub.called); + assert.isTrue(fireActionStub.calledWith('/review')); + var payload = fireActionStub.lastCall.args[3]; + assert.deepEqual(payload.labels, {foo: '+1'}); + fireActionStub.restore(); + }); + + test('not added when multiple labels are required', function() { + element.change = { + current_revision: 'abc1234', + labels: { + foo: {values: {}}, + bar: {values: {}}, + }, + permitted_labels: { + foo: [' 0', '+1'], + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('button label for missing approval', function() { + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + values: { + ' 0': '', + '+1': '', + }, + }, + bar: {approved: {}, values: {}}, + }, + permitted_labels: { + foo: [' 0', '+1'], + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); + }); + + test('no quick approve if score is not maximal for a label', function() { + element.change = { + current_revision: 'abc1234', + labels: { + bar: { + value: 1, + values: { + ' 0': '', + '+1': '', + '+2': '', + }, + }, + }, + permitted_labels: { + bar: [' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('approving label with a non-max score', function() { + element.change = { + current_revision: 'abc1234', + labels: { + bar: { + value: 1, + values: { + ' 0': '', + '+1': '', + '+2': '', + }, + }, + }, + permitted_labels: { + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + assert.equal(approveButton.getAttribute('data-label'), 'bar+2'); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html index 8b51312..ce17b3a 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,10 +16,11 @@ <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"> +<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html"> @@ -36,18 +37,30 @@ .title { color: #666; font-weight: bold; + white-space: nowrap; + } + gr-account-link { + max-width: 20ch; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + 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; @@ -55,6 +68,9 @@ .notApproved { background-color: #ffd4d4; } + .labelStatus { + max-width: 9em; + } @media screen and (max-width: 50em), screen and (min-width: 75em) { :host { display: table; @@ -128,25 +144,22 @@ <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 - value="{{change.topic}}" - placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" - read-only="[[_topicReadOnly]]" - on-changed="_handleTopicChanged"></gr-editable-label> + <template is="dom-if" if="[[change.topic]]"> + <gr-linked-chip + text="[[change.topic]]" + href="[[_computeTopicHref(change.topic)]]" + removable + on-remove="_handleTopicRemoved"></gr-linked-chip> + </template> + <template is="dom-if" if="[[!change.topic]]"> + <gr-editable-label + value="{{change.topic}}" + placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" + read-only="[[_topicReadOnly]]" + on-changed="_handleTopicChanged"></gr-editable-label> + </template> </span> </section> <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden> @@ -159,7 +172,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,13 +182,27 @@ 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> </span> </section> </template> + <template is="dom-if" if="[[_showLabelStatus]]"> + <section> + <span class="title">Label Status</span> + <span class="value labelStatus"> + [[_computeSubmitStatus(change.labels)]] + </span> + </section> + </template> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-change-metadata.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js index af19703..b56324a 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)', @@ -46,44 +37,16 @@ type: Boolean, computed: '_computeShowReviewersByState(serverConfig)', }, + _showLabelStatus: { + type: Boolean, + computed: '_computeShowLabelStatus(change)', + }, }, behaviors: [ 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 +59,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 +92,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 +106,81 @@ _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)); + }, + + _computeShowLabelStatus: function(change) { + var isNewChange = change.status === this.ChangeStatus.NEW; + var hasLabels = Object.keys(change.labels).length > 0; + return isNewChange && hasLabels; + }, + + _computeSubmitStatus: function(labels) { + var missingLabels = []; + var output = ''; + for (var label in labels) { + var obj = labels[label]; + if (!obj.optional && !obj.approved) { + missingLabels.push(label); + } + } + if (missingLabels.length) { + output += 'Needs '; + output += missingLabels.join(' and '); + output += missingLabels.length > 1 ? ' labels' : ' label'; + } else { + output = 'Ready to submit'; + } + return output; + }, + + _computeTopicHref: function(topic) { + return '/q/topic:' + encodeURIComponent(encodeURIComponent(topic)) + + '+(status:open OR status:merged)'; + }, + + _handleTopicRemoved: function() { + this.set(['change', 'topic'], ''); + this.$.restAPI.setChangeTopic(this.change.change_id, null); + }, }); })();
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..d354fd7 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> @@ -38,6 +36,7 @@ setup(function() { stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, getLoggedIn: function() { return Promise.resolve(false); }, }); @@ -68,79 +67,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 +78,110 @@ element.serverConfig = {note_db_enabled: true}; assert.isTrue(hasCc()); }); + + test('computes submit status', function() { + var labels = {}; + assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); + labels = {test: {}}; + assert.equal(element._computeSubmitStatus(labels), 'Needs test label'); + labels.test.approved = true; + assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); + labels.test.approved = false; + labels.test.optional = true; + assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); + labels.test.optional = false; + labels.test2 = {}; + assert.equal(element._computeSubmitStatus(labels), + 'Needs test and test2 labels'); + }); + + 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, + values: [], + }, + }, + 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')); + }); + + test('clicking x on topic chip removes topic', function() { + var topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic'); + flushAsynchronousOperations(); + var remove = element.$$('gr-linked-chip').$.remove; + MockInteractions.tap(remove); + assert.equal(element.change.topic, ''); + assert.isTrue(topicStub.called); + }); + }); }); </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..50b889c 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
@@ -15,13 +15,16 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior.html"> <link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html"> +<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html"> <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> @@ -29,6 +32,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 +49,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,70 +71,62 @@ 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; } .changeStatus { - 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; + font-size: .95em; } - gr-change-actions { - margin-top: 1em; + /* Prevent plugin text from overflowing. */ + #change_plugins { + word-break: break-word; } .commitMessage { font-family: var(--monospace-font-family); - flex: 0 0 72ch; + flex: 1 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; + min-width: 0; + } .commitAndRelated { align-content: flex-start; display: flex; @@ -144,14 +138,36 @@ 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; + } + .patchSetSelect { + max-width: 8em; + } + gr-editable-label.descriptionLabel { + max-width: 15em; } @media screen and (max-width: 50em) { - .headerContainer { - height: 5.15em; - } .header { align-items: flex-start; flex-direction: column; @@ -163,30 +179,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; + width: 100vw; } - .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,93 +210,142 @@ margin-top: .25em; max-width: none; } + .commitActions { + flex-direction: column; + } .commitMessage { flex: initial; margin-right: 0; } + .scrollable { + @apply(--layout-scroll); + } } </style> - <div class="container loading" hidden$="{{!_loading}}">Loading...</div> + <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 class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span><!-- + -->: + [[_change.subject]] + </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]]" + revision-actions="[[_currentRevisionActions]]" + change-num="[[_changeNum]]" + change-status="[[_change.status]]" + commit-num="[[_commitInfo.commit]]" + 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 + is="gr-select" + id="patchSetSelect" + bind-value="{{_selectedPatchSet}}" + class="patchSetSelect" + on-change="_handlePatchChange"> + <template is="dom-repeat" items="[[_allPatchSets]]" + as="patchNum"> + <option value$="[[patchNum.num]]" label> + [[patchNum.num]] + / + [[_computeLatestPatchNum(_allPatchSets)]] + [[patchNum.desc]] + </option> + </template> + </select> + <span class="descriptionContainer"> + / + <gr-editable-label + id="descriptionLabel" + class="descriptionLabel" + value="[[_computePatchSetDescription(_change, _selectedPatchSet)]]" + placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" + read-only="[[_descriptionReadOnly]]" + on-changed="_handleDescriptionChanged"></gr-editable-label> + </span> + <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]]" + project-config="[[_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 +357,7 @@ </div> <gr-overlay id="downloadOverlay" with-backdrop> <gr-download-dialog + id="downloadDialog" change="[[_change]]" logged-in="[[_loggedIn]]" patch-num="[[_patchRange.patchNum]]" @@ -312,16 +365,17 @@ on-close="_handleDownloadDialogClose"></gr-download-dialog> </gr-overlay> <gr-overlay id="replyOverlay" + class="scrollable" + 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]]" - labels="[[_change.labels]]" + patch-num="[[_computeLatestPatchNum(_allPatchSets)]]" permitted-labels="[[_change.permitted_labels]]" diff-drafts="[[_diffDrafts]]" server-config="[[serverConfig]]" + project-config="[[_projectConfig]]" on-send="_handleReplySent" on-cancel="_handleReplyCancel" on-autogrow="_handleReplyAutogrow"
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..510da1d 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,18 +42,24 @@ notify: true, value: function() { return {}; }, }, + backPage: String, serverConfig: Object, keyEventTarget: { type: Object, value: function() { return document.body; }, }, + _account: { + type: Object, + value: {}, + }, _comments: Object, _change: { type: Object, observer: '_changeChanged', }, _commitInfo: Object, + _files: Object, _changeNum: String, _diffDrafts: { type: Object, @@ -66,30 +72,46 @@ _hideEditCommitMessage: { type: Boolean, computed: '_computeHideEditCommitMessage(_loggedIn, ' + - '_editingCommitMessage, _change.*, _patchRange.patchNum)', + '_editingCommitMessage, _change)', }, - _patchRange: Object, + _latestCommitMessage: { + type: String, + value: '', + }, + _patchRange: { + type: Object, + observer: '_updateSelected', + }, + _currentRevisionActions: Object, _allPatchSets: { type: Array, - computed: '_computeAllPatchSets(_change)', + computed: '_computeAllPatchSets(_change, _change.revisions.*)', }, _loggedIn: { type: Boolean, value: false, }, _loading: Boolean, - _headerContainerEl: Object, - _headerEl: Object, _projectConfig: Object, _replyButtonLabel: { type: String, value: 'Reply', computed: '_computeReplyButtonLabel(_diffDrafts.*)', }, + _selectedPatchSet: String, + _initialLoadComplete: { + type: Boolean, + value: false, + }, + _descriptionReadOnly: { + type: Boolean, + computed: '_computeDescriptionReadOnly(_loggedIn, _change, _account)', + }, }, behaviors: [ Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, Gerrit.RESTClientBehavior, ], @@ -98,13 +120,21 @@ '_paramsAndChangeChanged(params, _change)', ], - ready: function() { - this._headerEl = this.$$('.header'); + keyBindings: { + 'shift+r': '_handleCapitalRKey', + 'a': '_handleAKey', + 'd': '_handleDKey', + 'u': '_handleUKey', }, attached: function() { this._getLoggedIn().then(function(loggedIn) { this._loggedIn = loggedIn; + if (loggedIn) { + this.$.restAPI.getAccount().then(function(acct) { + this._account = acct; + }.bind(this)); + } }.bind(this)); this.addEventListener('comment-save', this._handleCommentSave.bind(this)); @@ -114,34 +144,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) { @@ -152,12 +159,14 @@ _handleCommitMessageSave: function(e) { var message = e.detail.content; + this.$.jsAPI.handleCommitMessage(this._change, message); + this.$.commitMessageEditor.disabled = true; this._saveCommitMessage(message).then(function(resp) { this.$.commitMessageEditor.disabled = false; if (!resp.ok) { return; } - this.set('_commitInfo.message', message); + this._latestCommitMessage = message; this._editingCommitMessage = false; this._reloadWindow(); }.bind(this)).catch(function(err) { @@ -182,16 +191,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 +268,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 +278,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 +293,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 +326,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 && + (value.patchNum !== undefined && value.basePatchNum !== undefined) && + (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 && history.state.scrollTop) { + document.documentElement.scrollTop = + document.body.scrollTop = history.state.scrollTop; + } else { + this._maybeScrollToMessage(); + } + }, 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 +427,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 +470,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); }, @@ -401,10 +488,31 @@ this._patchRange.patchNum || this._computeLatestPatchNum(this._allPatchSets)); + this._updateSelected(); + var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; 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; }, @@ -412,40 +520,39 @@ _computeChangeStatus: function(change, patchNum) { var statusString; if (change.status === this.ChangeStatus.NEW) { - var rev = this._getRevisionNumber(change, patchNum); + var rev = this.getRevisionByPatchNum(change.revisions, patchNum); if (rev && rev.draft === true) { statusString = 'Draft'; } } else { statusString = this.changeStatusString(change); } - return statusString ? '(' + statusString + ')' : ''; + return statusString ? ' (' + statusString + ')' : ''; }, _computeLatestPatchNum: function(allPatchSets) { - return allPatchSets[allPatchSets.length - 1]; + return allPatchSets[allPatchSets.length - 1].num; + }, + + _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) { - patchNums.push(change.revisions[rev]._number); - } - return patchNums.sort(function(a, b) { - return a - b; - }); - }, - - _getRevisionNumber: function(change, patchNum) { - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - return change.revisions[rev]; + for (var commit in change.revisions) { + if (change.revisions.hasOwnProperty(commit)) { + patchNums.push({ + num: change.revisions[commit]._number, + desc: change.revisions[commit].description, + }); } } - }, - - _computePatchIndexIsSelected: function(index, patchNum) { - return this._allPatchSets[index] == patchNum; + return patchNums.sort(function(a, b) { return a.num - b.num; }); }, _computeLabelNames: function(labels) { @@ -477,11 +584,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,25 +597,75 @@ return label; }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } + _switchToMostRecentPatchNum: function() { + this._reload().then(function() { + var patchNum = this._computeLatestPatchNum(this._allPatchSets); + if (patchNum !== this._patchRange.patchNum) { + this._changePatchNum(patchNum); + } + }.bind(this)); + }, - switch (e.keyCode) { - case 65: // 'a' - if (this._loggedIn && !e.shiftKey) { - e.preventDefault(); - this._openReplyDialog(); + _handleAKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e) || + !this._loggedIn) { return; } + + e.preventDefault(); + this._openReplyDialog(); + }, + + _handleDKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.downloadOverlay.open(); + }, + + _handleCapitalRKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._switchToMostRecentPatchNum(); + }, + + _handleUKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._determinePageBack(); + }, + + _determinePageBack: function() { + // Default backPage to '/' if user came to change view page + // via an email link, etc. + page.show(this.backPage || '/'); + }, + + _handleLabelRemoved: function(splices, path) { + for (var i = 0; i < splices.length; i++) { + var splice = splices[i]; + for (var j = 0; j < splice.removed.length; j++) { + var removed = splice.removed[j]; + var changePath = path.split('.'); + var labelPath = changePath.splice(0, changePath.length - 2); + var labelDict = this.get(labelPath); + if (labelDict.approved && + labelDict.approved._account_id === removed._account_id) { + this._reload(); + return; } - break; - case 85: // 'u' - e.preventDefault(); - page.show('/'); - break; + } } }, _labelsChanged: function(changeRecord) { if (!changeRecord) { return; } + if (changeRecord.value.indexSplices) { + this._handleLabelRemoved(changeRecord.value.indexSplices, + changeRecord.path); + } this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { change: this._change, }); @@ -527,7 +679,7 @@ }, _handleReloadChange: function() { - page.show(this.changePath(this._changeNum)); + this._reload(); }, _handleGetChangeDetailError: function(response) { @@ -561,7 +713,25 @@ if (!change.reviewer_updates) { change.reviewer_updates = null; } + var latestRevisionSha = this._getLatestRevisionSHA(change); + var currentRevision = change.revisions[latestRevisionSha]; + if (currentRevision.commit && currentRevision.commit.message) { + this._latestCommitMessage = currentRevision.commit.message; + } else { + this._latestCommitMessage = null; + } + this._change = change; + if (!this._patchRange || !this._patchRange.patchNum || + this._patchRange.patchNum === currentRevision._number) { + // CommitInfo.commit is optional, and may need patching. + if (!currentRevision.commit.commit) { + currentRevision.commit.commit = latestRevisionSha; + } + this._commitInfo = currentRevision.commit; + this._currentRevisionActions = currentRevision.actions; + // TODO: Fetch and process files. + } }.bind(this)); }, @@ -572,6 +742,33 @@ }.bind(this)); }, + _getLatestCommitMessage: function() { + return this.$.restAPI.getChangeCommitInfo(this._changeNum, + this._computeLatestPatchNum(this._allPatchSets)).then( + function(commitInfo) { + this._latestCommitMessage = commitInfo.message; + }.bind(this)); + }, + + _getLatestRevisionSHA: function(change) { + if (change.current_revision) { + return change.current_revision; + } + // current_revision may not be present in the case where the latest rev is + // a draft and the user doesn’t have permission to view that rev. + var latestRev = null; + var latestPatchNum = -1; + for (var rev in change.revisions) { + if (!change.revisions.hasOwnProperty(rev)) { continue; } + + if (change.revisions[rev]._number > latestPatchNum) { + latestRev = rev; + latestPatchNum = change.revisions[rev]._number; + } + } + return latestRev; + }, + _getCommitInfo: function() { return this.$.restAPI.getChangeCommitInfo( this._changeNum, this._patchRange.patchNum).then( @@ -600,36 +797,83 @@ var detailCompletes = this._getChangeDetail().then(function() { this._loading = false; + this._getProjectConfig(); }.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 detailCompletes; - }).then(reloadDetailDependentResources); + return Promise.all([ + this._reloadPatchNumDependentResources(), + detailCompletes, + ]).then(function() { + return this.$.actions.reload(); + }.bind(this)); } else { // The patch number is reliant on the change detail request. - return detailCompletes.then(reloadPatchNumDependentResources).then( - reloadDetailDependentResources); + return detailCompletes.then(function() { + this.$.fileList.reload(); + if (!this._latestCommitMessage) { + this._getLatestCommitMessage(); + } + }.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(), + ]); + }, + + _updateSelected: function() { + this._selectedPatchSet = this._patchRange.patchNum; + }, + + _computePatchSetDescription: function(change, patchNum) { + var rev = this.getRevisionByPatchNum(change.revisions, patchNum); + return (rev && rev.description) ? rev.description : ''; + }, + + _computeDescriptionPlaceholder: function(readOnly) { + return (readOnly ? 'No' : 'Add a') + ' patch set description'; + }, + + _handleDescriptionChanged: function(e) { + var desc = e.detail.trim(); + var rev = this.getRevisionByPatchNum(this._change.revisions, + this._selectedPatchSet); + var sha = this._getPatchsetHash(this._change.revisions, rev); + this.$.restAPI.setDescription(this._changeNum, + this._selectedPatchSet, desc) + .then(function(res) { + if (res.ok) { + this.set(['_change', 'revisions', sha, 'description'], desc); + } + }.bind(this)); + }, + + + /** + * @param {Object} revisions The revisions object keyed by revision hashes + * @param {Object} patchSet A revision already fetched from {revisions} + * @return {string} the SHA hash corresponding to the revision. + */ + _getPatchsetHash: function(revisions, patchSet) { + for (var rev in revisions) { + if (revisions.hasOwnProperty(rev) && + revisions[rev] === patchSet) { + return rev; + } + } + }, + + _computeDescriptionReadOnly: function(loggedIn, change, account) { + return !(loggedIn && (account._account_id === change.owner._account_id)); + }, }); })();
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..4a035b2 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
@@ -21,7 +21,6 @@ <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> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-view.html"> @@ -35,52 +34,206 @@ <script> suite('gr-change-view tests', function() { var element; + var sandbox; + var TEST_SCROLL_TOP_PX = 100; setup(function() { + sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, getAccount: function() { return Promise.resolve(null); }, }); element = fixture('basic'); }); - test('keyboard shortcuts', function() { - var showStub = sinon.stub(page, 'show'); - - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U' - assert(showStub.lastCall.calledWithExactly('/'), - 'Should navigate to /'); - showStub.restore(); - - 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); // 'A' - assert.isTrue(overlayEl.opened); - overlayEl.close(); - assert.isFalse(overlayEl.opened); + teardown(function() { + sandbox.restore(); }); - test('reply button is highlighted when there are drafts', function() { + suite('keyboard shortcuts', function() { + test('U should navigate to / if no backPage set', function() { + var showStub = sandbox.stub(page, 'show'); + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert(showStub.lastCall.calledWithExactly('/')); + }); + + test('U should navigate to backPage if set', function() { + element.backPage = '/dashboard/self'; + var showStub = sandbox.stub(page, 'show'); + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert(showStub.lastCall.calledWithExactly('/dashboard/self')); + }); + + test('A should toggle overlay', function() { + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + var overlayEl = element.$.replyOverlay; + assert.isFalse(overlayEl.opened); + element._loggedIn = true; + + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); + assert.isFalse(overlayEl.opened); + + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + assert.isTrue(overlayEl.opened); + overlayEl.close(); + assert.isFalse(overlayEl.opened); + }); + + 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: {}, + }; + + sandbox.stub(element.$.actions, 'reload'); + sandbox.stub(element.$.restAPI, '_getChangeDetail', function() { + // Mock change obj. + return Promise.resolve({ + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1, commit: {}}, + rev13: {_number: 13}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + }); + }); + + var showStub = sandbox.stub(page, 'show', function(arg) { + assert.equal(arg, '/c/42/13'); + done(); + }); + + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + }); + + test('d should open download overlay', function() { + var stub = sandbox.stub(element.$.downloadOverlay, 'open'); + MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd'); + assert.isTrue(stub.called); + }); + }); + + test('_computeDescriptionReadOnly', function() { + assert.equal(element._computeDescriptionReadOnly(false, + {owner: {_account_id: 1}}, {_account_id: 1}), true); + assert.equal(element._computeDescriptionReadOnly(true, + {owner: {_account_id: 0}}, {_account_id: 1}), true); + assert.equal(element._computeDescriptionReadOnly(true, + {owner: {_account_id: 1}}, {_account_id: 1}), false); + }); + + test('_computeDescriptionPlaceholder', function() { + assert.equal(element._computeDescriptionPlaceholder(true), + 'No patch set description'); + assert.equal(element._computeDescriptionPlaceholder(false), + 'Add a patch set description'); + }); + + test('_handleDescriptionChanged', function() { + var putDescStub = sandbox.stub(element.$.restAPI, 'setDescription') + .returns(Promise.resolve({ok: true})); + sandbox.stub(element, '_computeDescriptionReadOnly'); + + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._selectedPatchNum = '1'; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + owner: {_account_id: 1}, + }; + element._account = {_account_id: 1}; + element._loggedIn = true; + + flushAsynchronousOperations(); + var label = element.$.descriptionLabel; + assert.equal(label.value, 'test'); + label.editing = true; + label._inputText = 'test2'; + label._save(); + flushAsynchronousOperations(); + assert.isTrue(putDescStub.called); + assert.equal(putDescStub.args[0][2], 'test2'); + }); + + test('_reload is called when an approved label is removed', function() { + var vote = {_account_id: 1, name: 'bojack', value: 1}; + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + status: 'NEW', + labels: { + test: { + all: [vote], + default_value: 0, + values: [], + approved: {}, + }, + }, + }; + flushAsynchronousOperations(); + var reloadStub = sandbox.stub(element, '_reload'); + element.splice('_change.labels.test.all', 0, 1); + assert.isFalse(reloadStub.called); + element._change.labels.test.all.push(vote); + element._change.labels.test.all.push(vote); + element._change.labels.test.approved = vote; + flushAsynchronousOperations(); + element.splice('_change.labels.test.all', 0, 2); + assert.isTrue(reloadStub.called); + assert.isTrue(reloadStub.calledOnce); + }); + + test('reply button has updated count when there are drafts', function() { var replyButton = element.$$('gr-button.reply'); assert.ok(replyButton); - assert.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 +273,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 +321,25 @@ 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')); + var select = element.$$('.patchInfo-header #patchSetSelect').bindValue; + assert.notEqual(select, 1); + assert.equal(select, 2); + assert.notEqual(select, 3); assert.equal(optionEls[3].value, 13); - var showStub = sinon.stub(page, 'show'); + var showStub = sandbox.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'), @@ -165,7 +349,6 @@ } else if (numEvents == 2) { assert(showStub.lastCall.calledWithExactly('/c/42'), 'Should navigate to /c/42'); - showStub.restore(); done(); } }); @@ -191,20 +374,20 @@ 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.notEqual( + element.$$('.patchInfo-header #patchSetSelect').bindValue, 1); + assert.equal( + element.$$('.patchInfo-header #patchSetSelect').bindValue, 2); + assert.notEqual( + element.$$('.patchInfo-header #patchSetSelect').bindValue, 3); assert.equal(optionEls[3].value, 13); - var showStub = sinon.stub(page, 'show'); + var showStub = sandbox.stub(page, 'show'); var numEvents = 0; selectEl.addEventListener('change', function(e) { @@ -217,7 +400,6 @@ } else if (numEvents == 2) { assert(showStub.lastCall.calledWithExactly('/c/42/3'), 'Should navigate to /c/42/3'); - showStub.restore(); done(); } }); @@ -225,6 +407,43 @@ element.fire('change', {}, {node: selectEl}); }); + test('don’t reload entire page when patchRange changes', function() { + var reloadStub = sandbox.stub(element, '_reload', + function() { return Promise.resolve(); }); + var reloadPatchDependentStub = sandbox.stub(element, + '_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); + + }); + + test('reload entire page when patchRange doesnt change', function() { + var reloadStub = sandbox.stub(element, '_reload', + function() { return Promise.resolve(); }); + + var value = { + view: 'gr-change-view', + }; + element._paramsChanged(value); + assert.isTrue(reloadStub.calledOnce); + element._initialLoadComplete = true; + element._paramsChanged(value); + assert.isTrue(reloadStub.calledTwice); + }); + test('change status new', function() { element._changeNum = '1'; element._patchRange = { @@ -260,7 +479,7 @@ labels: {}, }; var status = element._computeChangeStatus(element._change, '1'); - assert.equal(status, '(Draft)'); + assert.equal(status, ' (Draft)'); }); test('revision status draft', function() { @@ -283,43 +502,75 @@ labels: {}, }; var status = element._computeChangeStatus(element._change, '2'); - assert.equal(status, '(Draft)'); + assert.equal(status, ' (Draft)'); + }); + + test('get latest revision', function() { + var change = { + revisions: { + rev1: {_number: 1}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + }; + assert.equal(element._getLatestRevisionSHA(change), 'rev3'); + change = { + revisions: { + rev1: {_number: 1}, + }, + }; + assert.equal(element._getLatestRevisionSHA(change), 'rev1'); }); 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() { - sinon.stub(element, '_changeChanged'); - sinon.stub(element.$.restAPI, 'getChangeDetail', function(num) { - return Promise.resolve({id: '123456789', labels: {}}); + test('topic is coalesced to null', function(done) { + sandbox.stub(element, '_changeChanged'); + sandbox.stub(element.$.restAPI, 'getChangeDetail', function() { + return Promise.resolve({ + id: '123456789', + labels: {}, + current_revision: 'foo', + revisions: {foo: {commit: {}}}, + }); }); element._getChangeDetail().then(function() { assert.isNull(element._change.topic); + done(); + }); + }); + + test('commit sha is populated from getChangeDetail', function(done) { + sandbox.stub(element, '_changeChanged'); + sandbox.stub(element.$.restAPI, 'getChangeDetail', function() { + return Promise.resolve({ + id: '123456789', + labels: {}, + current_revision: 'foo', + revisions: {foo: {commit: {}}}, + }); + }); + + element._getChangeDetail().then(function() { + assert.equal('foo', element._commitInfo.commit); + done(); }); }); test('reply dialog focus can be controlled', function() { var FocusTarget = element.$.replyDialog.FocusTarget; - var openSpy = sinon.spy(element, '_openReplyDialog'); + var openSpy = sandbox.spy(element, '_openReplyDialog'); var e = {detail: {}}; element._handleShowReplyDialog(e); @@ -331,5 +582,145 @@ 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 allPatchSets = [{num: 1}, {num: 2}, {num: 4}]; + assert.equal(element._computePatchInfoClass('1', allPatchSets), + 'patchInfo--oldPatchSet'); + assert.equal(element._computePatchInfoClass('2', allPatchSets), + 'patchInfo--oldPatchSet'); + assert.equal(element._computePatchInfoClass('4', allPatchSets), ''); + }); + + test('getUrlParameter functionality', function() { + var locationStub = sandbox.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')); + + }); + + test('revert dialog opened with revert param', function(done) { + sandbox.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 = sandbox.stub(element, '_getUrlParameter', + function(param) { + assert.equal(param, 'revert'); + return param; + }); + + var revertDialogStub = sandbox.stub(element.$.actions, 'showRevertDialog', + done); + + element._maybeShowRevertDialog(); + }); + + suite('scroll related tests', function() { + test('document scrolling calls function to set scroll height', + function(done) { + var originalHeight = document.body.scrollHeight; + var scrollStub = sandbox.stub(element, '_handleScroll', + function() { + assert.isTrue(scrollStub.called); + document.body.style.height = + originalHeight + 'px'; + scrollStub.restore(); + done(); + }); + document.body.style.height = '10000px'; + document.body.scrollTop = TEST_SCROLL_TOP_PX; + element._handleScroll(); + }); + + test('history is loaded correctly', function() { + history.replaceState( + { + scrollTop: 100, + path: location.pathname, + }, + location.pathname); + + var reloadStub = sandbox.stub(element, '_reload', + function() { + // When element is reloaded, ensure that the history + // state has the scrollTop set earlier. This will then + // be reset. + assert.isTrue(history.state.scrollTop == 100); + return Promise.resolve({}); + }); + + // simulate reloading component, which is done when route + // changes to match a regex of change view type. + element._paramsChanged({view: 'gr-change-view'}); + }); + }); + + suite('reply dialog tests', function() { + setup(function() { + sandbox.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: sandbox.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..40dffcc 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,14 +14,16 @@ 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-formatted-text/gr-formatted-text.html"> <dom-module id="gr-comment-list"> <template> <style> :host { display: block; - font-family: var(--monospace-font-family); + word-wrap: break-word; } .file { border-top: 1px solid #ddd; @@ -35,12 +37,12 @@ } .lineNum { margin-right: .35em; - min-width: 7em; + min-width: 5em; + text-align: right; } .message { flex: 1; - white-space: pre-wrap; - word-wrap: break-word; + max-width: 80ch; } </style> <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file"> @@ -60,7 +62,9 @@ File comment: </span> </a> - <div class="message">[[comment.message]]</div> + <gr-formatted-text class="message" + content="[[comment.message]]" + config="[[projectConfig.commentlinks]]"></gr-formatted-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.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html index 7366d74..481b124 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -28,6 +28,11 @@ opacity: .5; pointer-events: none; } + .main { + display: flex; + flex-direction: column; + width: 100%; + } label { cursor: pointer; display: block; @@ -54,6 +59,7 @@ <iron-autogrow-textarea id="messageInput" class="message" + autocomplete="on" placeholder="<Insert reasoning here>" bind-value="{{message}}"></iron-autogrow-textarea> </div>
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.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html index b21575b..ebc6533 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -35,6 +35,11 @@ iron-autogrow-textarea { padding: 0; } + .main { + display: flex; + flex-direction: column; + width: 100%; + } .main label, .main input[type="text"] { display: block; @@ -66,6 +71,9 @@ <iron-autogrow-textarea id="messageInput" class="message" + autocomplete="on" + rows="4" + max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-confirm-dialog>
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..e6f60ad 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
@@ -31,17 +31,22 @@ properties: { branch: String, - message: String, - commitInfo: { - type: Object, - readOnly: true, - observer: '_commitInfoChanged', + changeStatus: String, + commitMessage: String, + commitNum: String, + message: { + type: String, + computed: '_computeMessage(changeStatus, commitNum, commitMessage)', }, }, - _commitInfoChanged: function(commitInfo) { - // Pre-populate cherry-pick message for editing from commit info. - this.message = commitInfo.message; + _computeMessage: function(changeStatus, commitNum, commitMessage) { + var newMessage = commitMessage; + + if (changeStatus === 'MERGED') { + newMessage += '(cherry picked from commit ' + commitNum + ')'; + } + return newMessage; }, _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html new file mode 100644 index 0000000..edf7d7a --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -0,0 +1,61 @@ +<!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-confirm-cherrypick-dialog</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-confirm-cherrypick-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-confirm-cherrypick-dialog></gr-confirm-cherrypick-dialog> + </template> +</test-fixture> + +<script> + suite('gr-confirm-cherrypick-dialog tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('with merged change', function() { + element.changeStatus = 'MERGED'; + element.commitMessage = 'message\n'; + element.commitNum = '123'; + element.branch = 'master'; + flushAsynchronousOperations(); + var expectedMessage = 'message\n(cherry picked from commit 123)'; + assert.equal(element._message, expectedMessage); + }); + + test('with unmerged change', function() { + element.changeStatus = 'OPEN'; + element.commitMessage = 'message\n'; + element.commitNum = '123'; + element.branch = 'master'; + flushAsynchronousOperations(); + var expectedMessage = 'message\n'; + assert.equal(element._message, expectedMessage); + }); + }); +</script>
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..a38811f8 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
@@ -56,6 +56,8 @@ <iron-autogrow-textarea id="messageInput" class="message" + autocomplete="on" + max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-confirm-dialog>
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..485c8f9 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
@@ -30,6 +30,7 @@ loggedIn: { type: Boolean, value: false, + observer: '_loggedInChanged', }, _schemes: { @@ -49,8 +50,20 @@ Gerrit.RESTClientBehavior, ], - attached: function() { - if (!this.loggedIn) { return; } + focus: function() { + this.$.download.focus(); + }, + + getFocusStops: function() { + var links = this.$$('#archives').querySelectorAll('a'); + return { + start: this.$.closeButton, + end: links[links.length - 1], + }; + }, + + _loggedInChanged: function(loggedIn) { + if (!loggedIn) { return; } this.$.restAPI.getPreferences().then(function(prefs) { if (prefs.download_scheme) { this._selectedScheme = prefs.download_scheme;
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..fdeecd7 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')); @@ -155,6 +163,21 @@ }); }); + test('loads scheme from preferences w/o initial login', function(done) { + stub('gr-rest-api-interface', { + getPreferences: function() { + return Promise.resolve({download_scheme: 'repo'}); + }, + }); + + element.loggedIn = true; + + assert.isTrue(element.$.restAPI.getPreferences.called); + element.$.restAPI.getPreferences.lastCall.returnValue.then(function() { + assert.equal(element._selectedScheme, 'repo'); + done(); + }); + }); }); suite('gr-download-dialog tests', function() {
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..cf66b49 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,12 +14,17 @@ limitations under the License. --> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.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-cursor-manager/gr-cursor-manager.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"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> <dom-module id="gr-file-list"> <template> @@ -38,7 +43,13 @@ margin-bottom: .5em; } .rightControls { + display: flex; + flex-wrap: wrap; font-weight: normal; + justify-content: flex-end; + } + .separator { + margin: 0 .25em; } .reviewed, .status { @@ -54,7 +65,7 @@ .row:not(.header):hover { background-color: #f5fafd; } - .row[selected] { + .row.selected { background-color: #ebf5fb; } .path { @@ -87,7 +98,8 @@ .invisible { visibility: hidden; } - .row:not(.header) .stats { + .row:not(.header) .stats, + .total-stats { font-family: var(--monospace-font-family); } .added { @@ -100,13 +112,47 @@ 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; + } + .warning { + color: #666; + } + 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; margin: .25em 0 1em; } + .patchSetSelect { + max-width: 8em; + } + .truncatedFileName { + display: none; + } + .expanded .fullFileName { + white-space: normal; + } @media screen and (max-width: 50em) { - .row[selected] { + .row.selected { background-color: transparent; } .stats { @@ -119,57 +165,118 @@ .comments { min-width: initial; } + .expanded .fullFileName, + .truncatedFileName { + display: block; + } + .expanded .truncatedFileName, + .fullFileName { + display: none; + } } </style> <header> <div>Files</div> <div class="rightControls"> - <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button> - / - <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button> - / + <template is="dom-if" + if="[[_fileListActionsVisible(_numFilesShown, _maxFilesForBulkActions)]]"> + <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button> + <span class="separator">/</span> + <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button> + </template> + <template is="dom-if" + if="[[!_fileListActionsVisible(_numFilesShown, _maxFilesForBulkActions)]]"> + <div class="warning"> + Bulk file list actions disabled for large amounts of files + </div> + </template> + <span class="separator">/</span> + <select + id="modeSelect" + is="gr-select" + bind-value="{{diffViewMode}}"> + <option value="SIDE_BY_SIDE">Side By Side</option> + <option value="UNIFIED_DIFF">Unified</option> + </select> + <span class="separator">/</span> <label> Diff against - <select on-change="_handlePatchChange"> + <select id="patchChange" bind-value="{{_diffAgainst}}" is="gr-select" + class="patchSetSelect" on-change="_handlePatchChange"> <option value="PARENT">Base</option> - <template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum"> - <option - value$="[[patchNum]]" - selected$="[[_computePatchSetSelected(patchNum, patchRange.basePatchNum)]]" - disabled$="[[_computePatchSetDisabled(patchNum, patchRange.patchNum)]]">[[patchNum]]</option> + <template + is="dom-repeat" + items="[[_computePatchSets(revisions.*, patchRange.*)]]" + as="patchNum"> + <option value$="[[patchNum.num]]" + disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum)]]"> + [[patchNum.num]] + [[patchNum.desc]] + </option> </template> </select> </label> </div> </header> - <template is="dom-repeat" items="[[_files]]" as="file"> - <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]"> + <template is="dom-repeat" + items="[[_shownFiles]]" + as="file" + initial-count="[[_fileListIncrement]]"> + <div class="file-row row"> <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> <input type="checkbox" checked$="[[_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)]] </div> - <a class="path" href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"> - <div title$="[[_computeFileDisplayName(file.__path)]]"> + <a class$="[[_computePathClass(file.__expanded)]]" + href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"> + <div title$="[[_computeFileDisplayName(file.__path)]]" + class="fullFileName"> [[_computeFileDisplayName(file.__path)]] </div> + <div title$="[[_computeFileDisplayName(file.__path)]]" + class="truncatedFileName"> + [[_computeTruncatedFileDisplayName(file.__path)]] + </div> <div class="oldPath" hidden$="[[!file.old_path]]" hidden title$="[[file.old_path]]"> [[file.old_path]] </div> </a> <div class="comments"> - <span class="drafts">[[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]</span> + <span class="drafts"> + [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]] + </span> [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]] </div> <div class$="[[_computeClass('stats', file.__path)]]"> - <span class="added">+[[file.lines_inserted]]</span> - <span class="removed">-[[file.lines_deleted]]</span> + <span class="added" hidden$=[[file.binary]]> + +[[file.lines_inserted]] + </span> + <span class="removed" hidden$=[[file.binary]]> + -[[file.lines_deleted]] + </span> + <span class$="[[_computeBinaryClass(file.size_delta)]]" + hidden$=[[!file.binary]]> + [[_formatBytes(file.size_delta)]] + [[_formatPercentage(file.size, file.size_delta)]] + </span> + </div> + <div class="show-hide"> + <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 +284,49 @@ path="[[file.__path]]" prefs="[[_diffPrefs]]" project-config="[[projectConfig]]" - view-mode="[[_userPrefs.diff_view]]"></gr-diff> + view-mode="[[diffViewMode]]"></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> + <div class="row totalChanges"> + <div class="total-stats" hidden$="[[_hideBinaryChangeTotals]]"> + <span class="added"> + [[_formatBytes(_patchChange.size_delta_inserted)]] + [[_formatPercentage(_patchChange.total_size, + _patchChange.size_delta_inserted)]] + </span> + <span class="removed"> + [[_formatBytes(_patchChange.size_delta_deleted)]] + [[_formatPercentage(_patchChange.total_size, + _patchChange.size_delta_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="diffCursor"></gr-diff-cursor> + <gr-cursor-manager + id="fileCursor" + scroll-behavior="keep-visible" + cursor-target-class="selected"></gr-cursor-manager> </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..1bb998f 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
@@ -20,14 +20,16 @@ is: 'gr-file-list', properties: { - patchRange: Object, + patchRange: { + type: Object, + observer: '_updateSelected', + }, patchNum: String, changeNum: String, comments: Object, drafts: Object, revisions: Object, projectConfig: Object, - topMargin: Number, selectedIndex: { type: Number, notify: true, @@ -37,10 +39,14 @@ value: function() { return document.body; }, }, change: Object, - + diffViewMode: { + type: String, + notify: true, + }, _files: { type: Array, observer: '_filesChanged', + value: function() { return []; }, }, _loggedIn: { type: Boolean, @@ -50,23 +56,72 @@ type: Array, value: function() { return []; }, }, + _diffAgainst: String, _diffPrefs: Object, _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)', + }, + _hideBinaryChangeTotals: { + type: Boolean, + computed: '_shouldHideBinaryChangeTotals(_patchChange)', + }, + _shownFiles: { + type: Array, + computed: '_computeFilesShown(_numFilesShown, _files.*)', + }, + // Caps the number of files that can be shown and have the 'show diffs' / + // 'hide diffs' buttons still be functional. + _maxFilesForBulkActions: { + type: Number, + readOnly: true, + value: 225, + }, }, behaviors: [ Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.URLEncodingBehavior, ], + keyBindings: { + 'shift+left': '_handleShiftLeftKey', + 'shift+right': '_handleShiftRightKey', + 'i': '_handleIKey', + 'shift+i': '_handleCapitalIKey', + 'down j': '_handleDownKey', + 'up k': '_handleUpKey', + 'c': '_handleCKey', + '[': '_handleLeftBracketKey', + ']': '_handleRightBracketKey', + 'o enter': '_handleEnterKey', + 'n': '_handleNKey', + 'p': '_handlePKey', + 'shift+a': '_handleCapitalAKey', + }, + reload: function() { if (!this.changeNum || !this.patchRange.patchNum) { return Promise.resolve(); } - this._collapseAllDiffs(); - var promises = []; var _this = this; @@ -90,6 +145,9 @@ promises.push(this._getPreferences().then(function(prefs) { this._userPrefs = prefs; + if (!this.diffViewMode) { + this.set('diffViewMode', prefs.default_diff_view); + } }.bind(this))); }, @@ -97,6 +155,31 @@ 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; + var total_size = (obj.size && obj.binary) ? obj.size : 0; + var size_delta_inserted = + obj.binary && obj.size_delta > 0 ? obj.size_delta : 0; + var size_delta_deleted = + obj.binary && obj.size_delta < 0 ? obj.size_delta : 0; + + return { + inserted: acc.inserted + inserted, + deleted: acc.deleted + deleted, + size_delta_inserted: acc.size_delta_inserted + size_delta_inserted, + size_delta_deleted: acc.size_delta_deleted + size_delta_deleted, + total_size: acc.total_size + total_size, + }; + }, {inserted: 0, deleted: 0, size_delta_inserted: 0, + size_delta_deleted: 0, total_size: 0}); + }, + _getDiffPreferences: function() { return this.$.restAPI.getDiffPreferences(); }, @@ -105,26 +188,34 @@ return this.$.restAPI.getPreferences(); }, - _computePatchSets: function(revisions) { + _computePatchSets: function(revisionRecord) { + var revisions = revisionRecord.base; var patchNums = []; for (var commit in revisions) { - patchNums.push(revisions[commit]._number); + if (revisions.hasOwnProperty(commit)) { + patchNums.push({ + num: revisions[commit]._number, + desc: revisions[commit].description, + }); + } } - return patchNums.sort(function(a, b) { return a - b; }); + return patchNums.sort(function(a, b) { return a.num - b.num; }); }, _computePatchSetDisabled: function(patchNum, currentPatchNum) { return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10); }, - _computePatchSetSelected: function(patchNum, basePatchNum) { - 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,26 +225,26 @@ } }, + /** + * 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(); - }); - if (e && e.target) { - e.target.blur(); + for (var i = 0; i < this._shownFiles.length; i++) { + this.set(['_shownFiles', i, '__expanded'], true); + this.set(['_files', i, '__expanded'], true); } }, _collapseAllDiffs: function(e) { this._showInlineDiffs = false; - this._forEachDiff(function(diff) { - diff.hidden = true; - }); - this.$.cursor.handleDiffUpdate(); - if (e && e.target) { - e.target.blur(); + for (var i = 0; i < this._shownFiles.length; i++) { + this.set(['_shownFiles', i, '__expanded'], false); + this.set(['_files', i, '__expanded'], false); } + this.$.diffCursor.handleDiffUpdate(); }, _computeCommentsString: function(comments, patchNum, path) { @@ -212,108 +303,151 @@ _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; } + _handleShiftLeftKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (!this._showInlineDiffs) { return; } - switch (e.keyCode) { - case 37: // left - if (e.shiftKey && this._showInlineDiffs) { - e.preventDefault(); - this.$.cursor.moveLeft(); - } - break; - case 39: // right - if (e.shiftKey && this._showInlineDiffs) { - e.preventDefault(); - this.$.cursor.moveRight(); - } - break; - case 73: // 'i' - if (!e.shiftKey) { return; } - e.preventDefault(); - this._toggleInlineDiffs(); - break; - case 40: // down - case 74: // 'j' - e.preventDefault(); - if (this._showInlineDiffs) { - this.$.cursor.moveDown(); - } else { - this.selectedIndex = - Math.min(this._files.length - 1, this.selectedIndex + 1); - this._scrollToSelectedFile(); - } - break; - case 38: // up - case 75: // 'k' - e.preventDefault(); - if (this._showInlineDiffs) { - this.$.cursor.moveUp(); - } else { - this.selectedIndex = Math.max(0, this.selectedIndex - 1); - this._scrollToSelectedFile(); - } - break; - case 67: // 'c' - var isRangeSelected = this.diffs.some(function(diff) { - return diff.isRangeSelected(); - }, this); - if (this._showInlineDiffs && !isRangeSelected) { - e.preventDefault(); - this._addDraftAtTarget(); - } - break; - case 219: // '[' - e.preventDefault(); - this._openSelectedFile(this._files.length - 1); - break; - case 221: // ']' - e.preventDefault(); - this._openSelectedFile(0); - break; - case 13: // <enter> - case 79: // 'o' - e.preventDefault(); - if (this._showInlineDiffs) { - this._openCursorFile(); - } else { - this._openSelectedFile(); - } - break; - case 78: // 'n' - if (this._showInlineDiffs) { - e.preventDefault(); - if (e.shiftKey) { - this.$.cursor.moveToNextCommentThread(); - } else { - this.$.cursor.moveToNextChunk(); - } - } - break; - case 80: // 'p' - if (this._showInlineDiffs) { - e.preventDefault(); - if (e.shiftKey) { - this.$.cursor.moveToPreviousCommentThread(); - } else { - this.$.cursor.moveToPreviousChunk(); - } - } - break; - case 65: // 'a' - if (e.shiftKey) { // Hide left diff. - e.preventDefault(); - this._forEachDiff(function(diff) { - diff.toggleLeftDiff(); - }); - } - break; + e.preventDefault(); + this.$.diffCursor.moveLeft(); + }, + + _handleShiftRightKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (!this._showInlineDiffs) { return; } + + e.preventDefault(); + this.$.diffCursor.moveRight(); + }, + + _handleIKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e) || + this.$.fileCursor.index === -1) { return; } + + e.preventDefault(); + var expanded = this._files[this.$.fileCursor.index].__expanded; + // Until Polymer 2.0, manual management of reflection between _files + // and _shownFiles is necessary. + this.set(['_shownFiles', this.$.fileCursor.index, '__expanded'], + !expanded); + this.set(['_files', this.$.fileCursor.index, '__expanded'], !expanded); + }, + + _handleCapitalIKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._toggleInlineDiffs(); + }, + + _handleDownKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + e.preventDefault(); + if (this._showInlineDiffs) { + this.$.diffCursor.moveDown(); + } else { + this.$.fileCursor.next(); + this.selectedIndex = this.$.fileCursor.index; } }, + _handleUpKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + if (this._showInlineDiffs) { + this.$.diffCursor.moveUp(); + } else { + this.$.fileCursor.previous(); + this.selectedIndex = this.$.fileCursor.index; + } + }, + + _handleCKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + var isRangeSelected = this.diffs.some(function(diff) { + return diff.isRangeSelected(); + }, this); + if (this._showInlineDiffs && !isRangeSelected) { + e.preventDefault(); + this._addDraftAtTarget(); + } + }, + + _handleLeftBracketKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._openSelectedFile(this._files.length - 1); + }, + + _handleRightBracketKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._openSelectedFile(0); + }, + + _handleEnterKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + if (this._showInlineDiffs) { + this._openCursorFile(); + } else { + this._openSelectedFile(); + } + }, + + _handleNKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + if (!this._showInlineDiffs) { return; } + + e.preventDefault(); + if (e.shiftKey) { + this.$.diffCursor.moveToNextCommentThread(); + } else { + this.$.diffCursor.moveToNextChunk(); + } + }, + + _handlePKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + if (!this._showInlineDiffs) { return; } + + e.preventDefault(); + if (e.shiftKey) { + this.$.diffCursor.moveToPreviousCommentThread(); + } else { + this.$.diffCursor.moveToPreviousChunk(); + } + }, + + _handleCapitalAKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._forEachDiff(function(diff) { + diff.toggleLeftDiff(); + }); + }, + _toggleInlineDiffs: function() { if (this._showInlineDiffs) { this._collapseAllDiffs(); @@ -323,45 +457,34 @@ }, _openCursorFile: function() { - var diff = this.$.cursor.getTargetDiffElement(); + var diff = this.$.diffCursor.getTargetDiffElement(); page.show(this._computeDiffURL(diff.changeNum, diff.patchRange, diff.path)); }, _openSelectedFile: function(opt_index) { if (opt_index != null) { - this.selectedIndex = opt_index; + this.$.fileCursor.setCursorAtIndex(opt_index); } page.show(this._computeDiffURL(this.changeNum, this.patchRange, - this._files[this.selectedIndex].__path)); + this._files[this.$.fileCursor.index].__path)); }, _addDraftAtTarget: function() { - var diff = this.$.cursor.getTargetDiffElement(); - var target = this.$.cursor.getTargetLineElement(); + var diff = this.$.diffCursor.getTargetDiffElement(); + var target = this.$.diffCursor.getTargetLineElement(); if (diff && target) { diff.addDraftAtLine(target); } }, - _scrollToSelectedFile: function() { - var el = this.$$('.row[selected]'); - var top = 0; - for (var node = el; node; node = node.offsetParent) { - top += node.offsetTop; - } - - // Don't scroll if it's already in view. - if (top > window.pageYOffset + this.topMargin && - top < window.pageYOffset + window.innerHeight - el.clientHeight) { - return; - } - - 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; + _shouldHideBinaryChangeTotals: function(_patchChange) { + return _patchChange.size_delta_inserted === 0 && + _patchChange.size_delta_deleted === 0; }, _computeFileStatus: function(status) { @@ -369,12 +492,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) { @@ -387,6 +506,36 @@ return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path; }, + _computeTruncatedFileDisplayName: function(path) { + return path === COMMIT_MESSAGE_PATH ? + 'Commit message' : util.truncatePath(path); + }, + + _formatBytes: function(bytes) { + if (bytes == 0) return '+/-0 B'; + var bits = 1024; + var decimals = 1; + var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + var exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits)); + var prepend = bytes > 0 ? '+' : ''; + return prepend + parseFloat((bytes / Math.pow(bits, exponent)) + .toFixed(decimals)) + ' ' + sizes[exponent]; + }, + + _formatPercentage: function(size, delta) { + var oldSize = size - delta; + + if (oldSize === 0) { return ''; } + + var percentage = Math.round(Math.abs(delta * 100 / oldSize)); + return '(' + (delta > 0 ? '+' : '-') + percentage + '%)'; + }, + + _computeBinaryClass: function(delta) { + if (delta === 0) { return; } + return delta >= 0 ? 'added' : 'removed'; + }, + _computeClass: function(baseClass, path) { var classes = [baseClass]; if (path === COMMIT_MESSAGE_PATH) { @@ -395,14 +544,67 @@ return classes.join(' '); }, + _computePathClass: function(expanded) { + return expanded ? 'path expanded' : 'path'; + }, + + _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'); // Overwrite the cursor's list of diffs: - this.$.cursor.splice.apply(this.$.cursor, - ['diffs', 0, this.$.cursor.diffs.length].concat(diffElements)); + this.$.diffCursor.splice.apply(this.$.diffCursor, + ['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements)); + + var files = Polymer.dom(this.root).querySelectorAll('.file-row'); + this.$.fileCursor.stops = files; + this.$.fileCursor.setCursorAtIndex(this.selectedIndex); }.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; + }, + + _updateSelected: function(patchRange) { + this._diffAgainst = patchRange.basePatchNum; + }, + + _fileListActionsVisible: function(numFilesShown, maxFilesForBulkActions) { + return numFilesShown <= maxFilesForBulkActions; + }, + + _computePatchSetDescription: function(revisions, patchNum) { + var rev = this.getRevisionByPatchNum(revisions, patchNum); + return (rev && rev.description) ? rev.description : ''; + }, }); })();
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..04070b5 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,39 @@ </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(''); } + }); + stub('gr-diff', { + reload: 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 +80,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 +100,264 @@ }); }); - 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, + size_delta: 10, + size: 100, + }, + { + __path: 'myfile.txt', + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + ]; + assert.deepEqual(element._patchChange, { + inserted: 2, + deleted: 2, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, + }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + + // 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, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, + }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + + // 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, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, + }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + + // 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, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, + }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); }); - test('keyboard shortcuts', function() { - var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs'); - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I' - assert.isTrue(toggleInlineDiffsStub.calledOnce); - toggleInlineDiffsStub.restore(); - + test('binary only files', function() { element._files = [ - {__path: '/COMMIT_MSG'}, - {__path: 'file_added_in_rev2.txt'}, - {__path: 'myfile.txt'}, + {__path: '/COMMIT_MSG', lines_inserted: 9}, + {__path: 'file_binary', binary: true, size_delta: 10, size: 100}, + {__path: 'file_binary', binary: true, size_delta: -5, size: 120}, ]; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', + assert.deepEqual(element._patchChange, { + inserted: 0, + deleted: 0, + size_delta_inserted: 10, + size_delta_deleted: -5, + total_size: 220, + }); + assert.isFalse(element._hideBinaryChangeTotals); + assert.isTrue(element._hideChangeTotals); + }); + + test('binary and regular files', function() { + element._files = [ + {__path: '/COMMIT_MSG', lines_inserted: 9}, + {__path: 'file_binary', binary: true, size_delta: 10, size: 100}, + {__path: 'file_binary', binary: true, size_delta: -5, size: 120}, + {__path: 'myfile.txt', lines_deleted: 5, size_delta: -10, size: 100}, + {__path: 'myfile2.txt', lines_inserted: 10}, + ]; + assert.deepEqual(element._patchChange, { + inserted: 10, + deleted: 5, + size_delta_inserted: 10, + size_delta_deleted: -5, + total_size: 220, + }); + assert.isFalse(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + }); + + test('_formatBytes function', function() { + var table = { + 64: '+64 B', + 1023: '+1023 B', + 1024: '+1 KiB', + 4096: '+4 KiB', + 1073741824: '+1 GiB', + '-64': '-64 B', + '-1023': '-1023 B', + '-1024': '-1 KiB', + '-4096': '-4 KiB', + '-1073741824': '-1 GiB', + 0: '+/-0 B', }; - element.selectedIndex = 0; - 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' + for (var bytes in table) { + if (table.hasOwnProperty(bytes)) { + assert.equal(element._formatBytes(bytes), table[bytes]); + } + } + }); - 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'); + test('_formatPercentage function', function() { + var table = [ + { size: 100, + delta: 100, + display: '', + }, + { size: 195060, + delta: 64, + display: '(+0%)', + }, + { size: 195060, + delta: -64, + display: '(-0%)', + }, + { size: 394892, + delta: -7128, + display: '(-2%)', + }, + { size: 90, + delta: -10, + display: '(-10%)', + }, + { size: 110, + delta: 10, + display: '(+10%)', + }, + ]; - 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'); + table.forEach(function(item) { + assert.equal(element._formatPercentage( + item.size, item.delta), item.display); + }); + }); - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - assert.equal(element.selectedIndex, 0); + 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.$.fileCursor.setCursorAtIndex(0); + }); - showStub.restore(); + 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(); + }); + + test('keyboard shortcuts', function() { + flushAsynchronousOperations(); + + var items = Polymer.dom(element.root).querySelectorAll('.file-row'); + element.$.fileCursor.stops = items; + element.$.fileCursor.setCursorAtIndex(0); + assert.equal(items.length, 3); + assert.isTrue(items[0].classList.contains('selected')); + assert.isFalse(items[1].classList.contains('selected')); + assert.isFalse(items[2].classList.contains('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert.equal(element.$.fileCursor.index, 1); + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + + var showStub = sandbox.stub(page, 'show'); + assert.equal(element.$.fileCursor.index, 2); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'), + 'Should navigate to /c/42/2/myfile.txt'); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.$.fileCursor.index, 1); + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o'); + assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'), + 'Should navigate to /c/42/2/file_added_in_rev2.txt'); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.$.fileCursor.index, 0); + assert.equal(element.selectedIndex, 0); + }); + + test('i key shows/hides selected inline diff', function() { + flushAsynchronousOperations(); + element.$.fileCursor.stops = element.diffs; + element.$.fileCursor.setCursorAtIndex(0); + MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + assert.isFalse(element.diffs[0].hasAttribute('hidden')); + MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + assert.isTrue(element.diffs[0].hasAttribute('hidden')); + element.$.fileCursor.setCursorAtIndex(1); + MockInteractions.pressAndReleaseKeyOn(element, 73, null, '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 +412,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'; @@ -197,20 +422,23 @@ basePatchNum: 'PARENT', patchNum: '2', }; - element.selectedIndex = 0; + element.$.fileCursor.setCursorAtIndex(0); 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); @@ -222,13 +450,24 @@ }); test('patch set from revisions', function() { + var expected = [ + {num: 1, desc: 'test'}, + {num: 2, desc: 'test'}, + {num: 3, desc: 'test'}, + {num: 4, desc: 'test'}, + ]; var patchNums = element._computePatchSets({ - rev3: {_number: 3}, - rev1: {_number: 1}, - rev4: {_number: 4}, - rev2: {_number: 2}, + base: { + rev3: {_number: 3, description: 'test'}, + rev1: {_number: 1, description: 'test'}, + rev4: {_number: 4, description: 'test'}, + rev2: {_number: 2, description: 'test'}, + } }); - assert.deepEqual(patchNums, [1, 2, 3, 4]); + assert.equal(patchNums.length, expected.length); + for (var i = 0; i < expected.length; i++) { + assert.deepEqual(patchNums[i], expected[i]); + } }); test('patch range string', function() { @@ -241,7 +480,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 +492,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 +506,123 @@ 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.$.fileCursor.setCursorAtIndex(0); + flushAsynchronousOperations(); + var fileRows = + Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); + var showHideCheck = fileRows[0].querySelector( + 'input.show-hide[type="checkbox"]'); + assert.isTrue(showHideCheck.checked); + MockInteractions.tap(showHideCheck); + assert.isFalse(element.diffs[0].hidden); + }); + + 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.$.fileCursor.setCursorAtIndex(0); + flushAsynchronousOperations(); + var diffDisplay = element.diffs[0]; + element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; + assert.equal(element.diffViewMode, 'SIDE_BY_SIDE'); + assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE'); + element.set('diffViewMode', 'UNIFIED_DIFF'); + assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF'); + }); + + 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({default_diff_view: 'UNIFIED'}); + flushAsynchronousOperations(); + assert.equal(select.value, 'SIDE_BY_SIDE'); + document.getElementById('blank').restore(); + }); + + test('show/hide diffs disabled for large amounts of files', function(done) { + element._files = []; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + var computeSpy = sandbox.spy(element, '_fileListActionsVisible'); + element.$.fileCursor.setCursorAtIndex(0); + element._numFilesShown = 1; + flush(function() { + assert.isTrue(computeSpy.lastCall.returnValue); + var arr = []; + _.times(element._maxFilesForBulkActions + 1, function() { + arr.push({__path: 'myfile.txt', __expanded: false}); + }); + element._files = arr; + element._numFilesShown = arr.length; + assert.isFalse(computeSpy.lastCall.returnValue); + done(); + }); + }); + + test('expanded attribute not set on path when not expanded', function() { + element._files = [ + {__path: '/COMMIT_MSG', __expanded: false}, + ]; + assert.isNotOk(element.$$('.expanded')); + }); + + test('expanded attribute set on path when expanded', function() { + element._files = [ + {__path: '/COMMIT_MSG', __expanded: true}, + ]; + flushAsynchronousOperations(); + assert.isOk(element.$$('.expanded')); + }); }); </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..a3cfa93 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -18,7 +18,7 @@ <link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> +<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-comment-list/gr-comment-list.html"> @@ -39,10 +39,10 @@ left: var(--default-horizontal-margin); } .collapsed .contentContainer { + align-items: baseline; color: #777; + display: flex; white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; } .showAvatar.expanded .contentContainer { margin-left: calc(var(--default-horizontal-margin) + 2.5em); @@ -50,12 +50,15 @@ } .showAvatar.collapsed .contentContainer { margin-left: calc(var(--default-horizontal-margin) + 1.75em); - padding: .75em 2em .75em 0; } .hideAvatar.collapsed .contentContainer, .hideAvatar.expanded .contentContainer { margin-left: 0; - padding: .75em 2em .75em 0; + } + .showAvatar.collapsed .contentContainer, + .hideAvatar.collapsed .contentContainer, + .hideAvatar.expanded .contentContainer { + padding: .75em 0; } .collapsed gr-avatar { top: .5em; @@ -70,8 +73,13 @@ .name { font-weight: bold; } - .content { - font-family: var(--monospace-font-family); + .message { + max-width: 80ch; + } + .collapsed .message { + max-width: none; + overflow: hidden; + text-overflow: ellipsis; } .collapsed .name, .collapsed .content, @@ -80,11 +88,27 @@ display: inline; } .collapsed gr-comment-list, - .collapsed .replyContainer { + .collapsed .replyContainer, + .collapsed .hideOnCollapsed, + .hideOnOpen { display: none; } + .collapsed .hideOnOpen { + display: block; + } + .collapsed .content { + flex: 1; + margin-right: .25em; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + } + .collapsed .date { + position: static; + } .collapsed .name { color: var(--default-text-color); + margin-right: .4em; } .expanded .name { cursor: pointer; @@ -105,26 +129,28 @@ <div class="name" on-tap="_handleNameTap">[[author.name]]</div> <template is="dom-if" if="[[message.message]]"> <div class="content"> - <gr-linked-text - class="message" - pre="[[expanded]]" + <div class="message hideOnOpen">[[message.message]]</div> + <gr-formatted-text + class="message hideOnCollapsed" content="[[message.message]]" - disabled="[[!expanded]]" - config="[[projectConfig.commentlinks]]"></gr-linked-text> + config="[[projectConfig.commentlinks]]"></gr-formatted-text> <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> </a> </template> <template is="dom-if" if="[[message.reviewer]]"> - set reviewer status for - <gr-account-chip account="[[message.reviewer]]"> - </gr-account-chip> - to [[message.state]]. + <div class="content"> + set reviewer status for + <gr-account-chip account="[[message.reviewer]]"> + </gr-account-chip> + to [[message.state]]. + </div> <gr-date-formatter class="date" date-str="[[message.updated]]"> </gr-date-formatter> </template>
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..244f7d3 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -50,21 +50,41 @@ value: true, reflectToAttribute: true, }, + hideAutomated: { + type: Boolean, + value: false, + }, + hidden: { + type: Boolean, + computed: '_computeIsHidden(hideAutomated, isAutomated)', + reflectToAttribute: true, + }, + isAutomated: { + type: Boolean, + computed: '_computeIsAutomated(message)', + }, showAvatar: { type: Boolean, computed: '_computeShowAvatar(author, config)', }, 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 +95,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) { @@ -94,6 +114,15 @@ this.expanded = false; }, + _computeIsAutomated: function(message) { + return !!(message.reviewer || + (message.tag && message.tag.indexOf('autogenerated') === 0)); + }, + + _computeIsHidden: function(hideAutomated, isAutomated) { + return hideAutomated && isAutomated; + }, + _computeClass: function(expanded, showAvatar) { var classes = []; classes.push(expanded ? 'expanded' : 'collapsed');
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..b8c12af 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-message.html"> @@ -36,6 +35,9 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getLoggedIn: function() { return Promise.resolve(false); }, + }); element = fixture('basic'); }); @@ -85,5 +87,55 @@ assert.equal(0, content.textContent.trim().indexOf(updatedBy.name)); }); + test('autogenerated prefix hiding', function() { + element.message = { + tag: 'autogenerated:gerrit:test', + updated: '2016-01-12 20:24:49.448000000', + }; + + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); + + element.hideAutomated = true; + + assert.isTrue(element.hidden); + }); + + test('reviewer message treated as autogenerated', function() { + element.message = { + tag: 'autogenerated:gerrit:test', + updated: '2016-01-12 20:24:49.448000000', + reviewer: {}, + }; + + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); + + element.hideAutomated = true; + + assert.isTrue(element.hidden); + }); + + test('tag that is not autogenerated prefix does not hide', function() { + element.message = { + tag: 'something', + updated: '2016-01-12 20:24:49.448000000', + }; + + assert.isFalse(element.isAutomated); + assert.isFalse(element.hidden); + + element.hideAutomated = true; + + assert.isFalse(element.hidden); + }); + + 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.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html index 3ae6b44..7c1759d 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -43,9 +43,21 @@ </style> <div class="header"> <h3>Messages</h3> - <gr-button link on-tap="_handleExpandCollapseTap"> - [[_computeExpandCollapseMessage(_expanded)]] - </gr-button> + <div> + <gr-button id="collapse-messages" link + on-tap="_handleExpandCollapseTap"> + [[_computeExpandCollapseMessage(_expanded)]] + </gr-button> + <span + id="automatedMessageToggleContainer" + hidden$="[[!_hasAutomatedMessages(messages)]]"> + / + <gr-button id="automatedMessageToggle" link + on-tap="_handleAutomatedMessageToggleTap"> + [[_computeAutomatedToggleText(_hideAutomated)]] + </gr-button> + </span> + </div> </div> <template is="dom-repeat" @@ -55,6 +67,7 @@ change-num="[[changeNum]]" message="[[message]]" comments="[[_computeCommentsForMessage(comments, message)]]" + hide-automated="[[_hideAutomated]]" project-config="[[projectConfig]]" show-reply-button="[[showReplyButtons]]" on-scroll-to="_handleScrollTo"
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..dc98527 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, @@ -39,6 +38,10 @@ type: Boolean, value: false, }, + _hideAutomated: { + type: Boolean, + value: false, + }, }, scrollToMessage: function(messageID) { @@ -52,7 +55,7 @@ offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } - window.scrollTo(0, top - this.topMargin); + window.scrollTo(0, top); this._highlightEl(el); }, @@ -112,32 +115,69 @@ } }, + _handleAutomatedMessageToggleTap: function(e) { + e.preventDefault(); + this._hideAutomated = !this._hideAutomated; + }, + _handleScrollTo: function(e) { this.scrollToMessage(e.detail.message.id); }, + _hasAutomatedMessages: function(messages) { + for (var i = 0; messages && i < messages.length; i++) { + if (messages[i].reviewer || (messages[i].tag && + messages[i].tag.indexOf('autogenerated') === 0)) { + return true; + } + } + return false; + }, + _computeExpandCollapseMessage: function(expanded) { return expanded ? 'Collapse all' : 'Expand all'; }, + _computeAutomatedToggleText: function(hideAutomated) { + return hideAutomated ? 'Show automated' : 'Hide automated'; + }, + + /** + * Computes message author's file comments for change's message. + * Method uses this.messages to find next message and relies on messages + * to be sorted by date field descending. + * @param {!Object} comments Hash of arrays of comments, filename as key. + * @param {!Object} message + * @return {!Object} Hash of arrays of comments, filename as key. + */ _computeCommentsForMessage: function(comments, message) { if (message._index === undefined || !comments || !this.messages) { return []; } - var index = message._index; var messages = this.messages || []; - var msgComments = {}; - var mDate = util.parseDate(message.date); + var index = message._index; + var authorId = message.author._account_id; + var mDate = util.parseDate(message.date).getTime(); + // NB: Messages array has oldest messages first. var nextMDate; - if (index < messages.length - 1) { - nextMDate = util.parseDate(messages[index + 1].date); + if (index > 0) { + for (var i = index - 1; i >= 0; i--) { + if (messages[i].author._account_id === authorId) { + nextMDate = util.parseDate(messages[i].date).getTime(); + break; + } + } } + var msgComments = {}; for (var file in comments) { var fileComments = comments[file]; for (var i = 0; i < fileComments.length; i++) { - var cDate = util.parseDate(fileComments[i].updated); - if (cDate >= mDate) { - if (nextMDate && cDate >= nextMDate) { + if (fileComments[i].author._account_id !== authorId) { + continue; + } + var cDate = util.parseDate(fileComments[i].updated).getTime(); + if (cDate <= mDate) { + if (nextMDate && cDate <= nextMDate) { continue; } msgComments[file] = msgComments[file] || [];
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html index 3cda480..51aceba 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-messages-list.html"> @@ -34,44 +33,32 @@ <script> suite('gr-messages-list tests', function() { var element; + var messages; + + var randomMessage = function(opt_params) { + var params = opt_params || {}; + var author1 = { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }; + return { + id: params.id || Math.random().toString(), + date: params.date || '2016-01-12 20:28:33.038000', + message: params.message || Math.random().toString(), + _revision_number: params._revision_number || 1, + author: params.author || author1, + }; + }; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + getLoggedIn: function() { return Promise.resolve(false); }, + }); element = fixture('basic'); - element.messages = [ - { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1 - }, - { - id: '47c43261_9593e420', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:28:33.038000000', - message: 'Patch Set 1:\n\n(1 comment)', - _revision_number: 1 - }, - { - id: '87b2aaf4_f73260c5', - author: { - _account_id: 1143760, - name: 'Mark Mentovai', - email: 'mark@chromium.org', - }, - date: '2016-01-12 21:17:07.554000000', - message: 'Patch Set 1:\n\n(3 comments)', - _revision_number: 1 - } - ]; + messages = _.times(3, randomMessage); + element.messages = messages; flushAsynchronousOperations(); }); @@ -91,7 +78,7 @@ assert.isTrue(allMessageEls[i].expanded); } - MockInteractions.tap(element.$$('.header gr-button')); + MockInteractions.tap(element.$$('#collapse-messages')); allMessageEls = Polymer.dom(element.root).querySelectorAll('gr-message'); for (var i = 0; i < allMessageEls.length; i++) { @@ -99,6 +86,11 @@ } }); + test('hide messages does not appear when no automated messages', + function() { + assert.isOk(element.$$('#automatedMessageToggleContainer[hidden]')); + }); + test('scroll to message', function() { var allMessageEls = Polymer.dom(element.root).querySelectorAll('gr-message'); @@ -116,7 +108,7 @@ 'expected gr-message ' + i + ' to not be expanded'); } - var messageID = '47c43261_9593e420'; + var messageID = messages[1].id; element.scrollToMessage(messageID); assert.isTrue( element.$$('[data-message-id="' + messageID + '"]').expanded); @@ -127,5 +119,174 @@ scrollToStub.restore(); highlightStub.restore(); }); + + test('messages', function() { + var author = { + _account_id: 42, + name: 'Marvin the Paranoid Android', + email: 'marvin@sirius.org', + }; + var comments = { + file1: [ + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: '6505d749_f0bec0aa', + line: 62, + id: '6505d749_10ed44b2', + patch_set: 2, + author: { + email: 'some@email.com', + _account_id: 123, + }, + }, + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: 'c5912363_6b820105', + line: 42, + id: '450a935e_0f1c05db', + patch_set: 2, + author: author, + }, + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: '6505d749_f0bec0aa', + line: 62, + id: '6505d749_10ed44b2', + patch_set: 2, + author: author, + }, + ], + file2: [ + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: 'c5912363_4b7d450a', + line: 132, + id: '450a935e_4f260d25', + patch_set: 2, + author: author, + }, + ] + }; + var messages = [].concat( + randomMessage(), + { + _index: 5, + _revision_number: 4, + message: 'Uploaded patch set 4.', + date: '2016-09-28 13:36:33.000000000', + author: author, + id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', + }, + { + _index: 6, + _revision_number: 4, + message: 'Patch Set 4:\n\n(6 comments)', + date: '2016-09-28 13:36:33.000000000', + author: author, + id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5', + } + ); + element.comments = comments; + element.messages = messages; + var isAuthor = function(author, message) { + return message.author._account_id === author._account_id; + }; + var isMarvin = isAuthor.bind(null, author); + flushAsynchronousOperations(); + var messageElements = + Polymer.dom(element.root).querySelectorAll('gr-message'); + assert.equal(messageElements.length, messages.length); + assert.deepEqual(messageElements[1].message, messages[1]); + assert.deepEqual(messageElements[2].message, messages[2]); + assert.deepEqual(messageElements[1].comments.file1, + comments.file1.filter(isMarvin)); + assert.deepEqual(messageElements[1].comments.file2, + comments.file2.filter(isMarvin)); + assert.deepEqual(messageElements[2].comments, {}); + }); + }); + + suite('gr-messages-list automate tests', function() { + var element; + var messages; + + var randomMessage = function(opt_params) { + var params = opt_params || {}; + var author1 = { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }; + return { + id: params.id || Math.random().toString(), + date: params.date || '2016-01-12 20:28:33.038000', + message: params.message || Math.random().toString(), + _revision_number: params._revision_number || 1, + author: params.author || author1, + tag: 'autogenerated:gerrit:replace', + }; + }; + + var randomMessageReviewer = { + reviewer: {}, + }; + + setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + getLoggedIn: function() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + messages = _.times(2, randomMessage); + messages.push(randomMessageReviewer); + element.messages = messages; + flushAsynchronousOperations(); + }); + + test('hide autogenerated button is not hidden', function() { + assert.isNotOk(element.$$('#automatedMessageToggle[hidden]')); + }); + + test('autogenerated messages are not hidden initially', function() { + var allHiddenMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); + + //There are no hidden messages. + assert.isFalse(!!allHiddenMessageEls.length); + }); + + test('autogenerated messages are hidden after clicking hide button', + function() { + var allHiddenMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); + + element._hideAutomated = false; + MockInteractions.tap(element.$$('#automatedMessageToggle')); + allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + allHiddenMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); + + // Autogenerated messages are now hidden. + assert.equal(allHiddenMessageEls.length, allMessageEls.length); + }); + + test('autogenerated messages are not hidden after clicking show button', + function() { + var allHiddenMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); + + element._hideAutomated = true; + MockInteractions.tap(element.$$('#automatedMessageToggle')); + allHiddenMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); + + //Autogenerated messages are now hidden. + assert.isFalse(!!allHiddenMessageEls.length); + }); }); </script>
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..c066b17 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
@@ -60,15 +60,19 @@ this._getSubmittedTogether().then(function(response) { this._submittedTogether = response; }.bind(this)), - this._getConflicts().then(function(response) { - this._conflicts = response; - }.bind(this)), this._getCherryPicks().then(function(response) { this._cherryPicks = response; }.bind(this)), ]; - return this._getServerConfig().then(function(config) { + // Get conflicts if change is open and is mergeable. + if (this.changeIsOpen(this.change.status) && this.change.mergeable) { + promises.push(this._getConflicts().then(function(response) { + this._conflicts = response; + }.bind(this))); + } + + 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 +81,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-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html index f7864ce..21903d2 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -33,9 +33,15 @@ <script> suite('gr-related-changes-list tests', function() { var element; + var sandbox; setup(function() { element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); }); test('connected revisions', function() { @@ -223,5 +229,64 @@ assert.equal(element._computeChangeContainerClass( change1, change2).indexOf('thisChange'), -1); }); + + suite('get conflicts tests', function() { + var element; + var conflictsStub; + + setup(function() { + element = fixture('basic'); + + sandbox.stub(element, '_getRelatedChanges', + function() { return Promise.resolve(); }); + sandbox.stub(element, '_getSubmittedTogether', + function() { return Promise.resolve(); }); + sandbox.stub(element, '_getCherryPicks', + function() { return Promise.resolve(); }); + conflictsStub = sandbox.stub(element, '_getConflicts', + function() { return Promise.resolve(); }); + }); + + test('request conflicts if open and mergeable', function() { + element.patchNum = 7; + element.change = { + status: 'NEW', + mergeable: true, + }; + element.reload(); + assert.isTrue(conflictsStub.called); + }); + + test('does not request conflicts if closed and mergeable', function() { + element.patchNum = 7; + element.change = { + status: 'MERGED', + mergeable: true, + }; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + + test('does not request conflicts if open and not mergeable', function() { + element.patchNum = 7; + element.change = { + status: 'NEW', + mergeable: false, + }; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + + test('does not request conflicts if closed and not mergeable', + function() { + element.patchNum = 7; + element.change = { + status: 'MERGED', + mergeable: false, + }; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html index cec1e90..5832f7f 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; } @@ -141,6 +138,14 @@ .action:visited { color: #00e; } + @media screen and (max-width: 50em) { + :host { + max-height: none; + } + .container { + max-height: none; + } + } </style> <div class="container"> <section class="peopleContainer"> @@ -182,11 +187,11 @@ <div class="reviewerConfirmation"> Group <span class="groupName"> - {{_reviewerPendingConfirmation.group.name}} + [[_pendingConfirmationDetails.group.name]] </span> has <span class="groupSize"> - {{_reviewerPendingConfirmation.count}} + [[_pendingConfirmationDetails.count]] </span> members. <br> @@ -202,6 +207,7 @@ <iron-autogrow-textarea id="textarea" class="message" + autocomplete="on" placeholder="Say something..." disabled="{{disabled}}" rows="4" @@ -211,28 +217,20 @@ </iron-autogrow-textarea> </section> <section class="labelsContainer"> - <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]"> - <template is="dom-repeat" - items="[[_computeLabelArray(permittedLabels)]]" as="label"> - <div class="labelContainer"> - <span class="labelName">[[label]]</span> - <iron-selector data-label$="[[label]]" - selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]"> - <template is="dom-repeat" - items="[[_computePermittedLabelValues(permittedLabels, label)]]" - as="value"> - <gr-button has-tooltip data-value$="[[value]]" - title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button> - </template> - </iron-selector> - </div> - </template> - </template> - <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]"> - <span class="labelsNotShown"> - Labels are not shown because this is not the most recent patch set. - <a href$="/c/[[change._number]]">Go to the latest patch set.</a> - </span> + <template is="dom-repeat" + items="[[_labels]]" as="label"> + <div class="labelContainer"> + <span class="labelName">[[label.name]]</span> + <iron-selector data-label$="[[label.name]]" + selected="[[_computeIndexOfLabelValue(change.labels, permittedLabels, label)]]"> + <template is="dom-repeat" + items="[[_computePermittedLabelValues(permittedLabels, label.name)]]" + as="value"> + <gr-button has-tooltip data-value$="[[value]]" + title$="[[_computeLabelValueTitle(change.labels, label.name, value)]]">[[value]]</gr-button> + </template> + </iron-selector> + </div> </template> </section> <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]"> @@ -240,6 +238,7 @@ <gr-comment-list comments="[[diffDrafts]]" change-num="[[change._number]]" + project-config="[[projectConfig]]" patch-num="[[patchNum]]"></gr-comment-list> </section> <section class="actionsContainer">
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..1cad8e5 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -48,7 +48,6 @@ properties: { change: Object, patchNum: String, - revisions: Object, disabled: { type: Boolean, value: false, @@ -59,6 +58,10 @@ value: '', observer: '_draftChanged', }, + quote: { + type: String, + value: '', + }, diffDrafts: Object, filterReviewerSuggestion: { type: Function, @@ -66,9 +69,9 @@ return this._filterReviewerSuggestion.bind(this); }, }, - labels: Object, permittedLabels: Object, serverConfig: Object, + projectConfig: Object, _account: Object, _ccs: Array, @@ -76,7 +79,12 @@ type: Object, observer: '_reviewerPendingConfirmationUpdated', }, + _labels: { + type: Array, + computed: '_computeLabels(change.labels.*, _account)', + }, _owner: Object, + _pendingConfirmationDetails: Object, _reviewers: Array, _reviewerPendingConfirmation: { type: Object, @@ -96,7 +104,7 @@ attached: function() { this._getAccount().then(function(account) { - this._account = account; + this._account = account || {}; }.bind(this)); }, @@ -153,12 +161,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.change.labels, label, + this._account); + if (prevVal !== null) { + prevVal = parseInt(prevVal, 10); + } + if (selectedVal !== prevVal) { + obj.labels[label] = selectedVal; + } } if (this.draft != null) { obj.message = this.draft; @@ -259,16 +276,6 @@ }.bind(this)); }, - _computeShowLabels: function(patchNum, revisions) { - var num = parseInt(patchNum, 10); - for (var rev in revisions) { - if (revisions[rev]._number > num) { - return false; - } - } - return true; - }, - _computeHideDraftList: function(drafts) { return Object.keys(drafts || {}).length == 0; }, @@ -287,31 +294,36 @@ return labels[label] && labels[label].values[value]; }, - _computeLabelArray: function(labelsObj) { - return Object.keys(labelsObj).sort(); + _computeLabels: function(labelRecord) { + var labelsObj = labelRecord.base; + if (!labelsObj) { return []; } + return Object.keys(labelsObj).sort().map(function(key) { + return { + name: key, + value: this._getVoteForAccount(labelsObj, key, this._account), + }; + }.bind(this)); }, - _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; + }, - var len = permittedLabels[labelName] != null ? - permittedLabels[labelName].length : 0; + _computeIndexOfLabelValue: function(labels, permittedLabels, label) { + if (!labels[label.name]) { return null; } + var labelValue = label.value; + var len = permittedLabels[label.name] != null ? + permittedLabels[label.name].length : 0; for (var i = 0; i < len; i++) { - var val = parseInt(permittedLabels[labelName][i], 10); + var val = parseInt(permittedLabels[label.name][i], 10); if (val == labelValue) { return i; } @@ -408,6 +420,8 @@ if (reviewer === null) { this.$.reviewerConfirmationOverlay.close(); } else { + this._pendingConfirmationDetails = + this._ccPendingConfirmation || this._reviewerPendingConfirmation; this.$.reviewerConfirmationOverlay.open(); } },
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..23a6c86 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-reply-dialog.html"> @@ -49,43 +48,46 @@ patchNum = 1; stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, getAccount: function() { return Promise.resolve({}); }, }); element = fixture('basic'); - element.change = { _number: changeNum }; - element.patchNum = patchNum; - element.labels = { - Verified: { - values: { - '-1': 'Fails', - ' 0': 'No score', - '+1': 'Verified' + element.change = { + _number: changeNum, + labels: { + Verified: { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified', + }, + default_value: 0, }, - default_value: 0 + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + default_value: 0, + }, }, - 'Code-Review': { - values: { - '-2': 'Do not submit', - '-1': 'I would prefer that you didn\'t submit this', - ' 0': 'No score', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved' - }, - default_value: 0 - } }; + element.patchNum = patchNum; element.permittedLabels = { 'Code-Review': [ '-1', ' 0', - '+1' + '+1', ], Verified: [ '-1', ' 0', - '+1' - ] + '+1', + ], }; element.serverConfig = {}; @@ -102,69 +104,73 @@ sandbox.restore(); }); + test('changes in label score are reflected in the DOM', function() { + element._account = {_account_id: 1}; + element.set(['change', 'labels', 'Verified', 'all'], + [{_account_id: 1, value: -1}]); + flushAsynchronousOperations(); + var selector = element.$$('iron-selector[data-label="Verified"]'); + assert.equal(selector.selected, 0); // Index 0, value -1 + element.set(['change', 'labels', 'Verified', 'all'], + [{_account_id: 1, value: 1}]); + flushAsynchronousOperations(); + assert.equal(selector.selected, 2); // Index 2, value 1 + }); + test('cancel event', function(done) { element.addEventListener('cancel', function() { done(); }); MockInteractions.tap(element.$$('.cancel')); }); - test('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 = ''; // Async tick is needed because iron-selector content is distributed and // distributed content requires an observer to be set up. + // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. flush(function() { - for (var label in element.permittedLabels) { - assert.ok(element.$$('iron-selector[data-label="' + label + '"]'), - label); - } - element.draft = 'I wholeheartedly disapprove'; - MockInteractions.tap(element.$$( - 'iron-selector[data-label="Code-Review"] > ' + - 'gr-button[data-value="-1"]')); - MockInteractions.tap(element.$$( - 'iron-selector[data-label="Verified"] > ' + - 'gr-button[data-value="-1"]')); - - var saveReviewStub = sinon.stub(element, '_saveReview', - function(review) { - assert.deepEqual(review, { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: { - 'Code-Review': -1, - 'Verified': -1 - }, - message: 'I wholeheartedly disapprove', - reviewers: [], - }); - return Promise.resolve({ok: true}); - }); - - element.addEventListener('send', function() { - assert.isFalse(element.disabled, - 'Element should be enabled when done sending reply.'); - assert.equal(element.draft.length, 0); - saveReviewStub.restore(); - showLabelsStub.restore(); - done(); - }); - - // This is needed on non-Blink engines most likely due to the ways in - // which the dom-repeat elements are stamped. flush(function() { - MockInteractions.tap(element.$$('.send')); - assert.isTrue(element.disabled); + for (var label in element.permittedLabels) { + assert.ok(element.$$('iron-selector[data-label="' + label + '"]'), + label); + } + element.draft = 'I wholeheartedly disapprove'; + MockInteractions.tap(element.$$( + 'iron-selector[data-label="Code-Review"] > ' + + 'gr-button[data-value="-1"]')); + MockInteractions.tap(element.$$( + 'iron-selector[data-label="Verified"] > ' + + 'gr-button[data-value="-1"]')); + + var saveReviewStub = sinon.stub(element, '_saveReview', + function(review) { + assert.deepEqual(review, { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: { + 'Code-Review': -1, + 'Verified': -1, + }, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + return Promise.resolve({ok: true}); + }); + + element.addEventListener('send', function() { + assert.isFalse(element.disabled, + 'Element should be enabled when done sending reply.'); + assert.equal(element.draft.length, 0); + saveReviewStub.restore(); + done(); + }); + + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(function() { + MockInteractions.tap(element.$$('.send')); + assert.isTrue(element.disabled); + }); }); }); }); @@ -188,12 +194,14 @@ }); } - test('reviewer confirmation', function(done) { + function testConfirmationDialog(done, cc) { var yesButton = element.$$('.reviewerConfirmationButtons gr-button:first-child'); var noButton = element.$$('.reviewerConfirmationButtons gr-button:last-child'); + element.serverConfig = {note_db_enabled: true}; + element._ccPendingConfirmation = null; element._reviewerPendingConfirmation = null; flushAsynchronousOperations(); assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); @@ -203,15 +211,37 @@ var group = { id: 'id', name: 'name', - count: 10, }; - element._reviewerPendingConfirmation = { - group: group, - }; + if (cc) { + element._ccPendingConfirmation = { + group: group, + count: 10, + }; + } else { + element._reviewerPendingConfirmation = { + group: group, + count: 10, + }; + } + flushAsynchronousOperations(); + + if (cc) { + assert.deepEqual( + element._ccPendingConfirmation, + element._pendingConfirmationDetails); + } else { + assert.deepEqual( + element._reviewerPendingConfirmation, + element._pendingConfirmationDetails); + } observer.then(function() { assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); observer = overlayObserver('closed'); + var expected = 'Group name has 10 members'; + assert.notEqual( + element.$.reviewerConfirmationOverlay.innerText.indexOf(expected), + -1); MockInteractions.tap(noButton); // close the overlay return observer; }).then(function() { @@ -220,30 +250,41 @@ // We should be focused on account entry input. assert.equal(getActiveElement().id, 'input'); - // No reviewer should have been added. - assert.deepEqual(element.$.reviewers.additions(), []); + // No reviewer/CC should have been added. + assert.equal(element.$$('#ccs').additions().length, 0); + assert.equal(element.$.reviewers.additions().length, 0); // Reopen confirmation dialog. observer = overlayObserver('opened'); - element._reviewerPendingConfirmation = { - group: group, - }; + if (cc) { + element._ccPendingConfirmation = { + group: group, + count: 10, + }; + } else { + element._reviewerPendingConfirmation = { + group: group, + count: 10, + }; + } return observer; }).then(function() { assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); observer = overlayObserver('closed'); - MockInteractions.tap(yesButton); // confirm the group + MockInteractions.tap(yesButton); // Confirm the group. return observer; }).then(function() { assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); + var additions = cc ? + element.$$('#ccs').additions() : + element.$.reviewers.additions(); assert.deepEqual( - element.$.reviewers.additions(), + additions, [ { group: { id: 'id', name: 'name', - count: 10, confirmed: true, _group: true, _pendingAdd: true, @@ -254,6 +295,14 @@ // We should be focused on account entry input. assert.equal(getActiveElement().id, 'input'); }).then(done); + }; + + test('cc confirmation', function(done) { + testConfirmationDialog(done, true); + }); + + test('reviewer confirmation', function(done) { + testConfirmationDialog(done, false); }); test('_getStorageLocation', function() { @@ -312,7 +361,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 +441,74 @@ 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')); + }); + }); + + test('do not display tooltips on touch devices', function() { + element._account = {_account_id: 1}; + element.set(['change', 'labels', 'Verified', 'all'], + [{_account_id: 1, value: -1}]); + element.labels = { + Verified: { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified' + }, + default_value: 0 + }, + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved' + }, + default_value: 0 + } + }; + + flushAsynchronousOperations(); + + var verifiedBtn = element.$$( + 'iron-selector[data-label="Verified"] > ' + + 'gr-button[data-value="-1"]'); + + // On touch devices, tooltips should not be shown. + verifiedBtn._isTouchDevice = true; + verifiedBtn._handleShowTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + + // On other devices, tooltips should be shown. + verifiedBtn._isTouchDevice = false; + verifiedBtn._handleShowTooltip(); + assert.isOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html index 6c6125c..d2ee15e 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-reviewer-list.html"> @@ -40,6 +39,7 @@ element = fixture('basic'); sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, removeChangeReviewer: function() { return Promise.resolve({ok: true}); },
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html index 1d31c12..f35cc49 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -15,23 +15,13 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <dom-module id="gr-account-dropdown"> <template> <style> - :host { - display: inline-block; - } - .dropdown-trigger { - text-decoration: none; - } - .dropdown-content { - background-color: #fff; - box-shadow: 0 1px 5px rgba(0, 0, 0, .3); - } button { background: none; border: none; @@ -43,51 +33,15 @@ width: 2em; vertical-align: middle; } - ul { - list-style: none; - } - ul .accountName { - font-weight: bold; - } - li .accountInfo, - li a { - display: block; - padding: .85em 1em; - } - li a:link, - li a:visited { - color: #00e; - text-decoration: none; - } - li a:hover { - background-color: #6B82D6; - color: #fff; - } </style> - <gr-button link class="dropdown-trigger" id="trigger" - on-tap="_showDropdownTapHandler"> - <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span> - <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden - image-size="56"></gr-avatar> - </gr-button> - <iron-dropdown id="dropdown" - vertical-align="top" - vertical-offset="25" + <gr-dropdown items=[[links]] top-content=[[topContent]] horizontal-align="right"> - <div class="dropdown-content"> - <ul> - <li> - <div class="accountInfo"> - <div class="accountName">[[account.name]]</div> - <div>[[account.email]]</div> - </div> - </li> - <li><a href$="[[_computeRelativeURL('/settings')]]">Settings</a></li> - <li><a href$="[[_computeRelativeURL('/switch-account')]]">Switch account</a></li> - <li><a href$="[[_computeRelativeURL('/logout')]]">Sign out</a></li> - </ul> - </div> - </iron-dropdown> + <gr-button link class="dropdown-trigger" id="trigger"> + <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span> + <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden + image-size="56"></gr-avatar> + </gr-button> + </gr-dropdown> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-account-dropdown.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js index ad944dc..4011135 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -20,26 +20,31 @@ properties: { account: Object, _hasAvatars: Boolean, + links: { + type: Array, + value: [ + {name: 'Settings', url: '/settings'}, + {name: 'Switch account', url: '/switch-account'}, + {name: 'Sign out', url: '/logout'}, + ], + }, + topContent: { + type: Array, + computed: '_getTopContent(account)', + }, }, attached: function() { this.$.restAPI.getConfig().then(function(cfg) { this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); }.bind(this)); - - this.listen(this.$.dropdown, 'tap', '_handleDropdownTap'); }, - _handleDropdownTap: function(e) { - this.$.dropdown.close(); - }, - - _showDropdownTapHandler: function(e) { - this.$.dropdown.open(); - }, - - _computeRelativeURL: function(path) { - return '//' + window.location.host + path; + _getTopContent: function(account) { + return [ + {text: account.name, bold: true}, + {text: account.email}, + ]; }, }); })();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html index 3ae3b14..ec9141f 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -35,14 +35,16 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); element = fixture('basic'); }); - test('tap on trigger opens menu', function() { - assert.isFalse(element.$.dropdown.opened); - MockInteractions.tap(element.$.trigger); - assert.isTrue(element.$.dropdown.opened); + test('account information', function() { + element.account = {name: 'John Doe', email: 'john@doe.com'}; + assert.deepEqual(element.topContent, + [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); }); - }); </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..479f389 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'); }, + _shouldSuppressError: 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._shouldSuppressError(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..7a7e761 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
@@ -98,13 +98,6 @@ <td><span class="key">u</span></td> <td>Up to change list</td> </tr> - <tr> - <td> - <span class="key modifier">Shift</span> - <span class="key">i</span> - </td> - <td>Show/hide inline diffs</td> - </tr> </tbody> <!-- Diff View --> <tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden> @@ -155,7 +148,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 +170,17 @@ <td>Show selected file</td> </tr> <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">i</span> + </td> + <td>Show/hide all inline diffs</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> @@ -206,6 +214,17 @@ <td>Go to previous comment thread</td> </tr> <tr> + <td><span class="key">e</span></td> + <td>Expand all comment threads</td> + </tr> + <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">e</span> + </td> + <td>Collapse all comment threads</td> + </tr> + <tr> <td> <span class="key modifier">Shift</span> <span class="key">←</span> @@ -269,6 +288,17 @@ <td>Show previous comment thread</td> </tr> <tr> + <td><span class="key">e</span></td> + <td>Expand all comment threads</td> + </tr> + <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">e</span> + </td> + <td>Collapse all comment threads</td> + </tr> + <tr> <td> <span class="key modifier">Shift</span> <span class="key">←</span> @@ -297,7 +327,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..c3d9b16 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
@@ -17,8 +17,8 @@ <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-account-dropdown/gr-account-dropdown.html"> +<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../gr-search-bar/gr-search-bar.html"> <dom-module id="gr-main-header"> @@ -42,40 +42,15 @@ ul { list-style: none; } - .links { - margin-left: 1em; - } - .links ul { - display: none; - } .links > li { cursor: default; display: inline-block; margin-left: 1em; - padding: .4em 0; + padding: 0; position: relative; } - .links li:hover ul { - background-color: #fff; - box-shadow: 0 1px 1px rgba(0, 0, 0, .3); - display: block; - left: -.75em; - position: absolute; - top: 2em; - z-index: 1000; - } - .links li ul li a:link, - .links li ul li a:visited { - color: #00e; - display: block; - padding: .5em .75em; - text-decoration: none; - white-space: nowrap; - } - .links li ul li:hover a { - background-color: var(--selection-background-color); - } .linksTitle { + color: black; display: inline-block; padding-right: 1em; position: relative; @@ -87,13 +62,15 @@ 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 { border-top-color: #666; } .rightItems { + align-items: center; display: flex; flex: 1; justify-content: flex-end; @@ -116,6 +93,13 @@ overflow: hidden; text-overflow: ellipsis; } + .dropdown-trigger { + text-decoration: none; + } + .dropdown-content { + background-color: #fff; + box-shadow: 0 1px 5px rgba(0, 0, 0, .3); + } @media screen and (max-width: 50em) { .bigTitle { font-size: 14px; @@ -134,14 +118,13 @@ <ul class="links"> <template is="dom-repeat" items="[[_links]]" as="linkGroup"> <li> - <span class="linksTitle"> + <gr-dropdown + items = [[linkGroup.links]] + horizontal-align="left"> + <span class="linksTitle" id="[[linkGroup.title]]"> [[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> + </gr-dropdown> </li> </template> </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js index 6fc3cc1..ebeb9af 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -36,7 +36,7 @@ is: 'gr-main-header', hostAttributes: { - role: 'banner' + role: 'banner', }, properties: { @@ -79,6 +79,10 @@ this.unlisten(window, 'location-change', '_handleLocationChange'); }, + reload: function() { + this._loadAccount(); + }, + _handleLocationChange: function(e) { this._loginURL = '/login/' + encodeURIComponent( window.location.pathname +
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html index 0b40d87..aef338b 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -35,6 +35,9 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); stub('gr-main-header', { _loadAccount: function() {}, });
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..9c0e902 --- /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.indexOf('/q/') === 0) { + 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..85b1119 --- /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 = sandbox.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); + sandbox.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..74344a7 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(params.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; }); @@ -146,6 +181,12 @@ }); }); + page(/^\/register(\/.*)?/, function(ctx) { + app.params = {justRegistered: true}; + var path = ctx.params[0] || '/'; + page.show(path); + }); + page.start(); }); })();
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..7a63810 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="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + <dom-module id="gr-search-bar"> <template> @@ -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..e9fdbd1 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,17 +78,26 @@ 'tr', ]; + var MAX_AUTOCOMPLETE_RESULTS = 10; + + var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; + Polymer({ is: 'gr-search-bar', behaviors: [ Gerrit.KeyboardShortcutBehavior, + Gerrit.URLEncodingBehavior, ], listeners: { 'searchButton.tap': '_preventDefaultAndNavigateToInputVal', }, + keyBindings: { + '/': '_handleForwardSlashKey', + }, + properties: { value: { type: String, @@ -117,55 +126,183 @@ 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))); - }, - - // TODO(kaspern): Flesh this out better. - _makeSuggestion: function(str) { - return { - name: str, - value: str, - }; - }, - - // TODO(kaspern): Expand support for more complicated autocomplete features. - _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)); - }, - - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - switch (e.keyCode) { - case 191: // '/' or '?' with shift key. - // TODO(andybons): Localization using e.key/keypress event. - if (e.shiftKey) { break; } - e.preventDefault(); - var s = this.$.searchInput; - s.focus(); - s.setSelectionRange(0, s.value.length); - break; + 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)); + } + }, + + /** + * 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 + '>"'; + }); + }); + }, + + /** + * 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) { + // 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, + }; + }); + }); + }, + + _handleForwardSlashKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.searchInput.focus(); + this.$.searchInput.selectAll(); }, }); })();
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..621511f 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,33 +69,134 @@ assert.notEqual(getActiveElement(), element.$.searchButton); done(); }); - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13); + element.value = 'test'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); }); test('search query should be double-escaped', function() { var showStub = sinon.stub(page, 'show'); element.$.searchInput.text = 'fate/stay'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13); + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); assert.equal(showStub.lastCall.args[0], '/q/fate%252Fstay'); 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, + null, 'enter'); + 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, + null, 'enter'); + assert.isFalse(showSpy.called); + }); + + test('keyboard shortcuts', function() { + var focusSpy = sinon.spy(element.$.searchInput, 'focus'); + var selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll'); + MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); + assert.isTrue(focusSpy.called); + assert.isTrue(selectAllSpy.called); + }); + + suite('_getSearchSuggestions', + function() { + setup(function() { + sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() { + return Promise.resolve([ + { + 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) { + element._getSearchSuggestions('owner:fr').then(function(s) { + assert.equal(s[0].value, 'owner:"fred <fred@goog.co>"'); + done(); + }); + }); + + test('Autocompletes groups', function(done) { + element._getSearchSuggestions('ownerin:pol').then(function(s) { + assert.equal(s[0].value, 'ownerin:Polygerrit'); + done(); + }); + }); + + test('Autocompletes projects', function(done) { + element._getSearchSuggestions('project:pol').then(function(s) { + assert.equal(s[0].value, 'project:Polygerrit'); + done(); + }); + }); + + test('Autocompletes simple searches', function(done) { + element._getSearchSuggestions('is:o').then(function(s) { + assert.equal(s[0].name, 'is:open'); + assert.equal(s[0].value, 'is:open'); + assert.equal(s[1].name, 'is:owner'); + assert.equal(s[1].value, 'is:owner'); + done(); + }); + }); + + test('Does not autocomplete with no match', function(done) { + element._getSearchSuggestions('asdasdasdasd').then(function(s) { + assert.equal(s.length, 0); + done(); + }); + }); + + test('Autocomplete doesnt override exact matches to input', + function(done) { + element._getSearchSuggestions('ownerin:gerrit').then(function(s) { + assert.equal(s[0].value, 'ownerin:gerrit'); + done(); + }); + }); + + test('Autocomplete respects spaces', function(done) { + element._getSearchSuggestions('is:ope').then(function(s) { + assert.equal(s[0].name, 'is:open'); + assert.equal(s[0].value, 'is:open'); + element._getSearchSuggestions('is:ope ').then(function(s) { + assert.equal(s.length, 0); + done(); + }); + }); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js index 1cb8cc7..23c5036 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -34,6 +34,29 @@ return sectionEl; }; + GrDiffBuilderSideBySide.prototype.addColumns = function(outputEl, fontSize) { + var width = fontSize * 4; + var colgroup = document.createElement('colgroup'); + + // Add left-side line number. + var col = document.createElement('col'); + col.setAttribute('width', width); + colgroup.appendChild(col); + + // Add left-side content. + colgroup.appendChild(document.createElement('col')); + + // Add right-side line number. + col = document.createElement('col'); + col.setAttribute('width', width); + colgroup.appendChild(col); + + // Add right-side content. + colgroup.appendChild(document.createElement('col')); + + outputEl.appendChild(colgroup); + }; + GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine, rightLine) { var row = this._createElement('tr'); @@ -69,7 +92,6 @@ GrDiffBuilderSideBySide.prototype._getNextContentOnSide = function( content, side) { var tr = content.parentElement.parentElement; - var content; while (tr = tr.nextSibling) { content = tr.querySelector( 'td.content .contentText[data-side="' + side + '"]');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js index 960bf46..d2c543d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -33,6 +33,26 @@ return sectionEl; }; + GrDiffBuilderUnified.prototype.addColumns = function(outputEl, fontSize) { + var width = fontSize * 4; + var colgroup = document.createElement('colgroup'); + + // Add left-side line number. + var col = document.createElement('col'); + col.setAttribute('width', width); + colgroup.appendChild(col); + + // Add right-side line number. + col = document.createElement('col'); + col.setAttribute('width', width); + colgroup.appendChild(col); + + // Add the content. + colgroup.appendChild(document.createElement('col')); + + outputEl.appendChild(colgroup); + }; + GrDiffBuilderUnified.prototype._createRow = function(section, line) { var row = this._createElement('tr', line.type); var lineEl = this._createLineEl(line, line.beforeNumber, @@ -62,7 +82,6 @@ GrDiffBuilderUnified.prototype._getNextContentOnSide = function( content, side) { var tr = content.parentElement.parentElement; - var content; while (tr = tr.nextSibling) { if (tr.classList.contains('both') || ( (side === 'left' && tr.classList.contains('remove')) ||
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html index ec19a2d..6066c26 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,22 @@ 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; + + var TRAILING_WHITESPACE_PATTERN = /\s+$/; + Polymer({ is: 'gr-diff-builder', /** + * Fired when the diff begins rendering. + * + * @event render-start + */ + + /** * Fired when the diff is rendered. * * @event render @@ -74,6 +89,7 @@ _builder: Object, _groups: Array, _layers: Array, + _showTabs: Boolean, }, get diffElement() { @@ -87,8 +103,10 @@ attached: function() { // Setup annotation layers. this._layers = [ + this._createTrailingWhitespaceLayer(), this.$.syntaxLayer, this._createIntralineLayer(), + this._createTabIndicatorLayer(), this.$.rangeLayer, ]; @@ -99,6 +117,8 @@ render: function(comments, prefs) { this.$.syntaxLayer.enabled = prefs.syntax_highlighting; + this._showTabs = !!prefs.show_tabs; + this._showTrailingWhitespace = !!prefs.show_whitespace_errors; // Stop the processor (if it's running). this.$.processor.cancel(); @@ -110,18 +130,27 @@ this.$.processor.keyLocations = this._getCommentLocations(comments); this._clearDiffContent(); + this._builder.addColumns(this.diffElement, prefs.font_size); - 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)); @@ -298,8 +327,6 @@ _createIntralineLayer: function() { return { - addListener: function() {}, - // Take a DIV.contentText element and a line object with intraline // differences to highlight and apply them to the element as // annotations. @@ -325,6 +352,53 @@ }; }, + _createTabIndicatorLayer: function() { + var show = function() { return this._showTabs; }.bind(this); + return { + 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++; + } + }, + }; + }, + + _createTrailingWhitespaceLayer: function() { + var show = function() { + return this._showTrailingWhitespace; + }.bind(this); + + return { + annotate: function(el, line) { + if (!show()) { return; } + + var match = line.text.match(TRAILING_WHITESPACE_PATTERN); + if (match) { + // Normalize string positions in case there is unicode before or + // within the match. + var index = GrAnnotation.getStringLength( + line.text.substr(0, match.index)); + var length = GrAnnotation.getStringLength(match[0]); + GrAnnotation.annotateElement(el, index, length, + 'style-scope gr-diff trailing-whitespace'); + } + }, + }; + }, + /** * In pages with large diffs, creating the first comment thread can be * slow because nested Polymer elements (particularly @@ -343,6 +417,18 @@ Polymer.dom.flush(); parent.removeChild(thread); }, + + /** + * @return {Boolean} whether any of the lines in _groups are longer + * than SYNTAX_MAX_LINE_LENGTH. + */ + _anyLineTooLong: function() { + return this._groups.reduce(function(acc, group) { + return acc || group.lines.reduce(function(acc, line) { + 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..497cae4 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
@@ -29,7 +29,9 @@ this.layers = layers || []; this.layers.forEach(function(layer) { - layer.addListener(this._handleLayerUpdate.bind(this)); + if (layer.addListener) { + layer.addListener(this._handleLayerUpdate.bind(this)); + } }.bind(this)); } @@ -65,7 +67,20 @@ var PARTIAL_CONTEXT_AMOUNT = 10; - GrDiffBuilder.prototype.buildSectionElement = function(group) { + /** + * Abstract method + * @param {string} outputEl + * @param {number} fontSize + */ + GrDiffBuilder.prototype.addColumns = function() { + throw Error('Subclasses must implement addColumns'); + }; + + /** + * Abstract method + * @param {Object} group + */ + GrDiffBuilder.prototype.buildSectionElement = function() { throw Error('Subclasses must implement buildGroupElement'); }; @@ -163,8 +178,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 +355,9 @@ side, this._comments.meta.projectConfig); threadEl.comments = comments; + if (opt_side) { + threadEl.setAttribute('data-side', opt_side); + } return threadEl; }; @@ -363,15 +381,16 @@ 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'); } td.classList.add(line.type); - var text = line.text; var html = util.escapeHTML(text); html = this._addTabWrappers(html, this._prefs.tab_size); - if (this._textLength(text, this._prefs.tab_size) > + if (!this._prefs.line_wrapping && + this._textLength(text, this._prefs.tab_size) > this._prefs.line_length) { html = this._addNewlines(text, html); } @@ -493,7 +512,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 +522,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 +530,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..80f48ff 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
@@ -57,6 +57,9 @@ var builder; setup(function() { + stub('gr-rest-api-interface', { + getLoggedIn: function() { return Promise.resolve(false); }, + }); var prefs = { line_length: 10, show_tabs: true, @@ -121,6 +124,36 @@ '6789'); }); + test('_addNewlines not called if line_wrapping is true', function(done) { + builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; + var text = (new Array(52)).join('a'); + + var line = {text: text, highlights: []}; + var newLineStub = sinon.stub(builder, '_addNewlines'); + builder._createTextEl(line); + flush(function() { + assert.isFalse(newLineStub.called); + newLineStub.restore(); + done(); + }); + }); + + test('_addNewlines called if line_wrapping is true and meets other ' + + 'conditions', function(done) { + builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50}; + var text = (new Array(52)).join('a'); + + var line = {text: text, highlights: []}; + var newLineStub = sinon.stub(builder, '_addNewlines'); + builder._createTextEl(line); + + flush(function() { + assert.isTrue(newLineStub.called); + newLineStub.restore(); + done(); + }); + }); + test('text length with tabs and unicode', function() { assert.equal(builder._textLength('12345', 4), 5); assert.equal(builder._textLength('\t\t12', 4), 10); @@ -414,16 +447,224 @@ }); }); + 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('trailing whitespace', function() { + var sandbox; + var element; + var layer; + + setup(function() { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element._showTrailingWhitespace = true; + layer = element._createTrailingWhitespaceLayer(); + }); + + 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 trailing whitespace', function() { + var str = 'lorem ipsum blah blah'; + 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 trailing spaces', function() { + var str = 'lorem ipsum '; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('annotates trailing tabs', function() { + var str = 'lorem ipsum\t\t\t'; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('annotates mixed trailing whitespace', function() { + var str = 'lorem ipsum\t \t'; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('unicode preceding trailing whitespace', function() { + var str = '💢\t'; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 1); + assert.equal(annotateElementStub.lastCall.args[2], 1); + }); + + test('does not annotate when disabled', function() { + element._showTrailingWhitespace = false; + var str = 'lorem upsum\t \t '; + 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); + }); + }); + suite('rendering', function() { var content; var outputEl; + var sandbox; setup(function(done) { + sandbox = sinon.sandbox.create(); var prefs = { line_length: 10, show_tabs: true, tab_size: 4, - context: -1 + context: -1, + syntax_highlighting: true, }; content = [ { @@ -437,14 +678,16 @@ ] }, ]; + stub('gr-reporting', { + time: sandbox.stub(), + timeEnd: sandbox.stub(), + }); element = fixture('basic'); outputEl = element.queryEffectiveChildren('#diffTable'); - element.addEventListener('render', function() { - done(); - }); - sinon.stub(element, '_getDiffBuilder', function() { + sandbox.stub(element, '_getDiffBuilder', function() { var builder = new GrDiffBuilder( {content: content}, {left: [], right: []}, prefs, outputEl); + sandbox.stub(builder, 'addColumns'); builder.buildSectionElement = function(group) { var section = document.createElement('stub'); section.textContent = group.lines.reduce(function(acc, line) { @@ -455,7 +698,23 @@ return builder; }); element.diff = {content: content}; - element.render({left: [], right: []}, prefs); + element.render({left: [], right: []}, prefs).then(done); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('reporting', function(done) { + var timeStub = element.$.reporting.time; + var timeEndStub = element.$.reporting.timeEnd; + assert.isTrue(timeStub.calledWithExactly('Diff Total Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Content Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render')); + 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() { @@ -467,6 +726,11 @@ assert.equal(section.innerHTML, prevInnerHTML); }); + test('addColumns is called', function(done) { + element.render({left: [], right: []}, {}).then(done); + assert.isTrue(element._builder.addColumns.called); + }); + test('getSectionsByLineRange one line', function() { var section = outputEl.querySelector('stub:nth-of-type(2)'); var sections = element._builder.getSectionsByLineRange(1, 1, 'left'); @@ -484,6 +748,36 @@ assert.strictEqual(sections[0], section[0]); assert.strictEqual(sections[1], section[1]); }); + + test('render-start and render are fired', function(done) { + var fireStub = sinon.stub(element, 'fire'); + element.render({left: [], right: []}, {}).then(function() { + assert.isTrue(fireStub.calledWithExactly('render-start')); + assert.isTrue(fireStub.calledWithExactly('render')); + done(); + }); + }); + + test('rendering normal-sized diff does not disable syntax', function() { + assert.isTrue(element.$.syntaxLayer.enabled); + }); + + test('rendering large diff disables syntax', function(done) { + // Before it renders, set the first diff line to 500 '*' characters. + element.diff.content[0].a = [new Array(501).join('*')]; + element.addEventListener('render', function() { + assert.isFalse(element.$.syntaxLayer.enabled); + done(); + }); + var prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + syntax_highlighting: true, + }; + element.render({left: [], right: []}, prefs); + }); }); suite('mock-diff', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html index 25237b5..0f00e18 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -22,8 +22,8 @@ <template> <style> :host { - border: 1px solid #ddd; - border-right: none; + background-color: #ffd; + border: 1px solid #bbb; display: block; white-space: normal; } @@ -37,9 +37,11 @@ draft="[[comment.__draft]]" show-actions="[[_showActions]]" project-config="[[projectConfig]]" - on-reply="_handleCommentReply" on-comment-discard="_handleCommentDiscard" - on-done="_handleCommentDone"></gr-diff-comment> + on-create-ack-comment="_handleCommentAck" + on-create-done-comment="_handleCommentDone" + on-create-fix-comment="_handleCommentFix" + on-create-reply-comment="_handleCommentReply"></gr-diff-comment> </template> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js index 305c36a..00719b2 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -14,6 +14,8 @@ (function() { 'use strict'; + var NEWLINE_PATTERN = /\n/g; + Polymer({ is: 'gr-diff-comment-thread', @@ -29,6 +31,10 @@ type: Array, value: function() { return []; }, }, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, patchNum: String, path: String, projectConfig: Object, @@ -41,6 +47,10 @@ _orderedComments: Array, }, + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + listeners: { 'comment-update': '_handleCommentUpdate', }, @@ -49,6 +59,10 @@ '_commentsChanged(comments.splices)', ], + keyBindings: { + 'e shift+e': '_handleEKey', + }, + attached: function() { this._getLoggedIn().then(function(loggedIn) { this._showActions = loggedIn; @@ -80,44 +94,52 @@ this._orderedComments = this._sortedComments(this.comments); }, - _sortedComments: function(comments) { - comments.sort(function(c1, c2) { - var c1Date = c1.__date || util.parseDate(c1.updated); - var c2Date = c2.__date || util.parseDate(c2.updated); - return c1Date - c2Date; - }); + _handleEKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } - var commentIDToReplies = {}; - var topLevelComments = []; - for (var i = 0; i < comments.length; i++) { - var c = comments[i]; - if (c.in_reply_to) { - if (commentIDToReplies[c.in_reply_to] == null) { - commentIDToReplies[c.in_reply_to] = []; - } - commentIDToReplies[c.in_reply_to].push(c); - } else { - topLevelComments.push(c); - } + // Don’t preventDefault in this case because it will render the event + // useless for other handlers (other gr-diff-comment-thread elements). + if (e.detail.keyboardEvent.shiftKey) { + this._expandCollapseComments(true); + } else { + if (this.modifierPressed(e)) { return; } + this._expandCollapseComments(false); } - var results = []; - for (var i = 0; i < topLevelComments.length; i++) { - this._visitComment(topLevelComments[i], commentIDToReplies, results); - } - for (var missingCommentId in commentIDToReplies) { - results = results.concat(commentIDToReplies[missingCommentId]); - } - return results; }, - _visitComment: function(parent, commentIDToReplies, results) { - results.push(parent); + _expandCollapseComments: function(actionIsCollapse) { + var comments = + Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); + comments.forEach(function(comment) { + comment.collapsed = actionIsCollapse; + }); + }, - var replies = commentIDToReplies[parent.id]; - delete commentIDToReplies[parent.id]; - if (!replies) { return; } - for (var i = 0; i < replies.length; i++) { - this._visitComment(replies[i], commentIDToReplies, results); + _sortedComments: function(comments) { + return comments.slice().sort(function(c1, c2) { + var c1Date = c1.__date || util.parseDate(c1.updated); + var c2Date = c2.__date || util.parseDate(c2.updated); + var dateCompare = c1Date - c2Date; + // If same date, fall back to sorting by id. + return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); + }); + }, + + _createReplyComment: function(parent, content, opt_isEditing) { + var reply = this._newReply(parent.id, parent.line, content); + + if (opt_isEditing) { + reply.__editing = true; + } + + this.push('comments', reply); + + if (!opt_isEditing) { + // Allow the reply to render in the dom-repeat. + this.async(function() { + var commentEl = this._commentElWithDraftID(reply.__draftID); + commentEl.save(); + }, 1); } }, @@ -126,24 +148,27 @@ var quoteStr; if (e.detail.quote) { var msg = comment.message; - var quoteStr = msg.split('\n').map( - function(line) { return ' > ' + line; }).join('\n') + '\n\n'; + quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; } - var reply = this._newReply(comment.id, comment.line, quoteStr); - reply.__editing = true; - this.push('comments', reply); + this._createReplyComment(comment, quoteStr, true); + }, + + _handleCommentAck: function(e) { + var comment = e.detail.comment; + this._createReplyComment(comment, 'Ack'); }, _handleCommentDone: function(e) { var comment = e.detail.comment; - var reply = this._newReply(comment.id, comment.line, 'Done'); - this.push('comments', reply); + this._createReplyComment(comment, 'Done'); + }, - // Allow the reply to render in the dom-repeat. - this.async(function() { - var commentEl = this._commentElWithDraftID(reply.__draftID); - commentEl.save(); - }.bind(this), 1); + _handleCommentFix: function(e) { + var comment = e.detail.comment; + var msg = comment.message; + var quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; + var response = quoteStr + 'Please Fix'; + this._createReplyComment(comment, response); }, _commentElWithDraftID: function(id) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html index 641dc0f..29e63aa 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -54,33 +54,28 @@ message: 'i like you, too', in_reply_to: 'sallys_confession', updated: '2015-12-25 15:00:20.396000000', - }, - { + }, { id: 'sallys_confession', message: 'i like you, jack', updated: '2015-12-24 15:00:20.396000000', - }, - { + }, { id: 'sally_to_dr_finklestein', message: 'i’m running away', updated: '2015-10-31 09:00:20.396000000', - }, - { + }, { id: 'sallys_defiance', in_reply_to: 'sally_to_dr_finklestein', message: 'i will poison you so i can get away', updated: '2015-10-31 15:00:20.396000000', - }, - { + }, { id: 'dr_finklesteins_response', in_reply_to: 'sally_to_dr_finklestein', message: 'no i will pull a thread and your arm will fall off', - updated: '2015-10-31 11:00:20.396000000' - }, - { + updated: '2015-10-31 11:00:20.396000000', + }, { id: 'sallys_mission', message: 'i have to find santa', - updated: '2015-12-24 21:00:20.396000000' + updated: '2015-12-24 15:00:20.396000000', } ]; var results = element._sortedComments(comments); @@ -89,34 +84,29 @@ id: 'sally_to_dr_finklestein', message: 'i’m running away', updated: '2015-10-31 09:00:20.396000000', - }, - { + }, { id: 'dr_finklesteins_response', in_reply_to: 'sally_to_dr_finklestein', message: 'no i will pull a thread and your arm will fall off', - updated: '2015-10-31 11:00:20.396000000' - }, - { + updated: '2015-10-31 11:00:20.396000000', + }, { id: 'sallys_defiance', in_reply_to: 'sally_to_dr_finklestein', message: 'i will poison you so i can get away', updated: '2015-10-31 15:00:20.396000000', - }, - { + }, { id: 'sallys_confession', message: 'i like you, jack', updated: '2015-12-24 15:00:20.396000000', - }, - { + }, { + id: 'sallys_mission', + message: 'i have to find santa', + updated: '2015-12-24 15:00:20.396000000', + }, { id: 'jacks_reply', message: 'i like you, too', in_reply_to: 'sallys_confession', updated: '2015-12-25 15:00:20.396000000', - }, - { - id: 'sallys_mission', - message: 'i have to find santa', - updated: '2015-12-24 21:00:20.396000000' } ]); }); @@ -138,7 +128,7 @@ line: 5, in_reply_to: 'baf0414d_60047215', updated: '2015-12-21 02:01:10.850000000', - message: 'Done' + message: 'Done', })); }, }); @@ -162,7 +152,7 @@ test('reply', function(done) { var commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - commentEl.addEventListener('reply', function() { + commentEl.addEventListener('create-reply-comment', function() { var drafts = element._orderedComments.filter(function(c) { return c.__draft == true; }); @@ -171,22 +161,70 @@ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); done(); }); - commentEl.fire('reply', {comment: commentEl.comment}, {bubbles: false}); + commentEl.fire('create-reply-comment', {comment: commentEl.comment}, + {bubbles: false}); }); test('quote reply', function(done) { var commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - commentEl.addEventListener('reply', function() { + commentEl.addEventListener('create-reply-comment', function() { var drafts = element._orderedComments.filter(function(c) { return c.__draft == true; }); assert.equal(drafts.length, 1); - assert.equal(drafts[0].message, ' > is this a crossover episode!?\n\n'); + assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n'); assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); done(); }); - commentEl.fire('reply', {comment: commentEl.comment, quote: true}, + commentEl.fire('create-reply-comment', {comment: commentEl.comment, + quote: true}, {bubbles: false}); + }); + + test('quote reply multiline', function(done) { + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?\nIt might be!', + updated: '2015-12-08 19:48:33.843000000', + }]; + flushAsynchronousOperations(); + + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('create-reply-comment', function() { + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, + '> is this a crossover episode!?\n> It might be!\n\n'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('create-reply-comment', {comment: commentEl.comment, + quote: true}, {bubbles: false}); + }); + + test('ack', function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('create-ack-comment', function() { + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, 'Ack'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('create-ack-comment', {comment: commentEl.comment}, {bubbles: false}); }); @@ -195,7 +233,7 @@ element.patchNum = '1'; var commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - commentEl.addEventListener('done', function() { + commentEl.addEventListener('create-done-comment', function() { var drafts = element._orderedComments.filter(function(c) { return c.__draft == true; }); @@ -204,7 +242,27 @@ assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); done(); }); - commentEl.fire('done', {comment: commentEl.comment}, {bubbles: false}); + commentEl.fire('create-done-comment', {comment: commentEl.comment}, + {bubbles: false}); + }); + + test('please fix', function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + var commentEl = element.$$('gr-diff-comment'); + assert.ok(commentEl); + commentEl.addEventListener('create-fix-comment', function() { + var drafts = element._orderedComments.filter(function(c) { + return c.__draft == true; + }); + assert.equal(drafts.length, 1); + assert.equal( + drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + done(); + }); + commentEl.fire('create-fix-comment', {comment: commentEl.comment}, + {bubbles: false}); }); test('discard', function(done) { @@ -247,20 +305,17 @@ message: 'i like you, too', in_reply_to: 'sallys_confession', updated: '2015-12-25 15:00:20.396000000', - }, - { + }, { id: 'sallys_confession', in_reply_to: 'nonexistent_comment', message: 'i like you, jack', updated: '2015-12-24 15:00:20.396000000', - }, - { + }, { id: 'sally_to_dr_finklestein', in_reply_to: 'nonexistent_comment', message: 'i’m running away', updated: '2015-10-31 09:00:20.396000000', - }, - { + }, { id: 'sallys_defiance', message: 'i will poison you so i can get away', updated: '2015-10-31 15:00:20.396000000', @@ -268,5 +323,37 @@ element.comments = comments; assert.equal(4, element._orderedComments.length); }); + + test('keyboard shortcuts', function() { + var comments = [ + { + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', + }, { + id: 'sallys_confession', + in_reply_to: 'nonexistent_comment', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, { + id: 'sally_to_dr_finklestein', + in_reply_to: 'nonexistent_comment', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, { + id: 'sallys_defiance', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }]; + element.comments = comments; + var expandCollapseStub = sinon.stub(element, '_expandCollapseComments'); + MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e'); + assert.isTrue(expandCollapseStub.lastCall.calledWith(false)); + + MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e'); + assert.isTrue(expandCollapseStub.lastCall.calledWith(true)); + expandCollapseStub.restore(); + }); }); </script>
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..2bde4e7 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
@@ -18,7 +18,7 @@ <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> +<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> <link rel="import" href="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> @@ -26,33 +26,41 @@ <template> <style> :host { - background-color: #ffd; display: block; + font-family: var(--font-family); + padding: .7em .7em; --iron-autogrow-textarea: { padding: 2px; }; } - :host([disabled]) { + :host[disabled] { pointer-events: none; } - :host([disabled]) .container { + :host[disabled] .container { opacity: .5; } - .header, - .message, - .actions { - padding: .5em .7em; + :host[is-robot-comment] { + background-color: #cfe8fc; } .header { + cursor: pointer; display: flex; - padding-bottom: 0; font-family: 'Open Sans', sans-serif; + margin: 0.7em 0; + padding-bottom: 0; } - .headerLeft { + .container.collapsed .header { + margin: 0; + } + .headerMiddle { + color: #666; flex: 1; + overflow: hidden; } .authorName, .draftLabel { + display: block; + float: left; font-weight: bold; } .draftLabel { @@ -62,6 +70,7 @@ .date { justify-content: flex-end; margin-left: 5px; + white-space: nowrap; } a.date:link, a.date:visited { @@ -87,11 +96,12 @@ .danger .action { margin-right: 0; } - .container:not(.draft) .actions :not(.reply):not(.quote):not(.done) { + .container:not(.draft) .actions .hideOnPublished { display: none; } .draft .reply, .draft .quote, + .draft .ack, .draft .done { display: none; } @@ -105,6 +115,7 @@ .editing .message, .editing .reply, .editing .quote, + .editing .ack, .editing .done, .editing .edit { display: none; @@ -113,43 +124,124 @@ background-color: #fff; display: block; } + .show-hide { + margin-left: .4em; + } + .robotId { + color: #808080; + margin-bottom: .8em; + margin-top: -.4em; + } + .runIdInformation { + margin-bottom: .5em; + } + .robotRun { + margin-left: .5em; + } + .robotRunLink { + margin-left: .5em; + } + 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-formatted-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$="[[collapsed]]" + on-change="_handleToggleCollapsed"> + [[_computeShowHideText(collapsed)]] + </label> + </div> </div> + <template is="dom-if" if="[[comment.robot_id]]"> + <div class="robotId" hidden$="[[collapsed]]""> + [[comment.robot_id]] + </div> + </template> <iron-autogrow-textarea id="editTextarea" class="editMessage" + autocomplete="on" disabled="{{disabled}}" rows="4" bind-value="{{_messageText}}" on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea> - <gr-linked-text class="message" - pre + <gr-formatted-text class="message" content="[[comment.message]]" - config="[[projectConfig.commentlinks]]"></gr-linked-text> - <div class="actions" hidden$="[[!showActions]]"> + collapsed="[[collapsed]]" + config="[[projectConfig.commentlinks]]"></gr-formatted-text> + <div hidden$="[[!comment.robot_run_id]]"> + <div class="runIdInformation" hidden$="[[collapsed]]"> + Run ID: + <a class="robotRunLink" href$="[[comment.url]]"> + <span class="robotRun">[[comment.robot_run_id]]</span> + </a> + </div> + </div> + <div class="actions humanActions" hidden$="[[!_showHumanActions]]"> <gr-button class="action reply" on-tap="_handleReply">Reply</gr-button> <gr-button class="action quote" on-tap="_handleQuote">Quote</gr-button> - <gr-button class="action done" on-tap="_handleDone">Done</gr-button> - <gr-button class="action edit" on-tap="_handleEdit">Edit</gr-button> - <gr-button class="action save" on-tap="_handleSave" + <gr-button class="action ack" on-tap="_handleAck">Ack</gr-button> + <gr-button class="action done" on-tap="_handleDone"> + Done</gr-button> + <gr-button class="action edit hideOnPublished" on-tap="_handleEdit"> + Edit</gr-button> + <gr-button class="action save hideOnPublished" on-tap="_handleSave" disabled$="[[_computeSaveDisabled(_messageText)]]">Save</gr-button> - <gr-button class="action cancel" on-tap="_handleCancel" hidden>Cancel</gr-button> + <gr-button class="action cancel hideOnPublished" + on-tap="_handleCancel" hidden>Cancel</gr-button> <div class="danger"> - <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button> + <gr-button class="action discard hideOnPublished" + on-tap="_handleDiscard">Discard</gr-button> </div> </div> + <div class="actions robotActions" hidden$="[[!_showRobotActions]]"> + <gr-button class="action fix" on-tap="_handleFix"> + Please Fix + </gr-button> + </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage>
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..d974fe5 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
@@ -22,13 +22,25 @@ /** * Fired when the Reply action is triggered. * - * @event reply + * @event create-reply-comment + */ + + /** + * Fired when the Ack action is triggered. + * + * @event create-ack-comment */ /** * Fired when the Done action is triggered. * - * @event done + * @event create-done-comment + */ + + /** + * Fired when the create fix comment action is triggered. + * + * @event create-fix-comment */ /** @@ -64,6 +76,11 @@ notify: true, observer: '_commentChanged', }, + isRobotComment: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, disabled: { type: Boolean, value: false, @@ -79,8 +96,16 @@ value: false, observer: '_editingChanged', }, + hasChildren: Boolean, patchNum: String, showActions: Boolean, + _showHumanActions: Boolean, + _showRobotActions: Boolean, + collapsed: { + type: Boolean, + value: true, + observer: '_toggleCollapseClass', + }, projectConfig: Object, _xhrPromise: Object, // Used for testing. @@ -94,22 +119,38 @@ observers: [ '_commentMessageChanged(comment.message)', '_loadLocalDraft(changeNum, patchNum, comment)', + '_isRobotComment(comment)', + '_calculateActionstoShow(showActions, isRobotComment)', ], + attached: function() { + if (this.editing) { + this.collapsed = false; + } + }, + detached: function() { this.cancelDebouncer('fire-update'); }, + _computeShowHideText: function(collapsed) { + return collapsed ? 'â—€' : 'â–¼'; + }, + + _calculateActionstoShow: function(showActions, isRobotComment) { + this._showHumanActions = showActions && !isRobotComment; + this._showRobotActions = showActions && isRobotComment; + }, + + _isRobotComment: function(comment) { + this.isRobotComment = !!comment.robot_id; + }, + save: function() { this.comment.message = this._messageText; this.disabled = true; - this.$.storage.eraseDraftComment({ - changeNum: this.changeNum, - patchNum: this.patchNum, - path: this.comment.path, - line: this.comment.line, - }); + this._eraseDraftComment(); this._xhrPromise = this._saveDraft(this.comment).then(function(response) { this.disabled = false; @@ -134,6 +175,15 @@ }.bind(this)); }, + _eraseDraftComment: function() { + this.$.storage.eraseDraftComment({ + changeNum: this.changeNum, + patchNum: this.patchNum, + path: this.comment.path, + line: this.comment.line, + }); + }, + _commentChanged: function(comment) { this.editing = !!comment.__editing; if (this.editing) { // It's a new draft/reply, notify. @@ -198,8 +248,29 @@ }, _handleTextareaKeydown: function(e) { - if (e.keyCode == 27) { // 'esc' - this._handleCancel(e); + switch (e.keyCode) { + case 27: // 'esc' + if (this._messageText.length === 0) { + this._handleCancel(e); + } + break; + case 83: // 's' + if (e.ctrlKey) { + this._handleSave(e); + } + break; + } + }, + + _handleToggleCollapsed: function() { + this.collapsed = !this.collapsed; + }, + + _toggleCollapseClass: function(collapsed) { + if (collapsed) { + this.$.container.classList.add('collapsed'); + } else { + this.$.container.classList.remove('collapsed'); } }, @@ -243,34 +314,48 @@ }, _handleReply: function(e) { - this._preventDefaultAndBlur(e); - this.fire('reply', this._getEventPayload(), {bubbles: false}); + e.preventDefault(); + this.fire('create-reply-comment', this._getEventPayload(), + {bubbles: false}); }, _handleQuote: function(e) { - this._preventDefaultAndBlur(e); - this.fire( - 'reply', this._getEventPayload({quote: true}), {bubbles: false}); + e.preventDefault(); + this.fire('create-reply-comment', this._getEventPayload({quote: true}), + {bubbles: false}); + }, + + _handleFix: function(e) { + e.preventDefault(); + this.fire('create-fix-comment', this._getEventPayload({quote: true}), + {bubbles: false}); + }, + + _handleAck: function(e) { + e.preventDefault(); + this.fire('create-ack-comment', this._getEventPayload(), + {bubbles: false}); }, _handleDone: function(e) { - this._preventDefaultAndBlur(e); - this.fire('done', this._getEventPayload(), {bubbles: false}); + e.preventDefault(); + this.fire('create-done-comment', this._getEventPayload(), + {bubbles: false}); }, _handleEdit: function(e) { - this._preventDefaultAndBlur(e); + e.preventDefault(); this._messageText = this.comment.message; this.editing = true; }, _handleSave: function(e) { - this._preventDefaultAndBlur(e); + e.preventDefault(); this.save(); }, _handleCancel: function(e) { - this._preventDefaultAndBlur(e); + e.preventDefault(); if (this.comment.message == null || this.comment.message.length == 0) { this._fireDiscard(); return; @@ -285,12 +370,14 @@ }, _handleDiscard: function(e) { - this._preventDefaultAndBlur(e); + e.preventDefault(); if (!this.comment.__draft) { throw Error('Cannot discard a non-draft comment.'); } this.editing = false; this.disabled = true; + this._eraseDraftComment(); + if (!this.comment.id) { this.disabled = false; this._fireDiscard(); @@ -309,11 +396,6 @@ }.bind(this)); }, - _preventDefaultAndBlur: function(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.blur(); - }, - _saveDraft: function(draft) { return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft); },
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..6793144 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,8 +39,15 @@ </test-fixture> <script> + + function isVisible(el) { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') !== 'none'; + } + suite('gr-diff-comment tests', function() { var element; + var sandbox; setup(function() { stub('gr-rest-api-interface', { getAccount: function() { return Promise.resolve(null); }, @@ -56,10 +63,43 @@ message: 'is this a crossover episode!?', updated: '2015-12-08 19:48:33.843000000', }; + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('collapsible comments', function() { + // When a comment (not draft) is loaded, it should be collapsed + assert.isTrue(element.collapsed); + assert.isFalse(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.$$('.actions')), + 'actions are not visible'); + assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + '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.isFalse(element.collapsed); + assert.isTrue(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-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) { + element.addEventListener('create-reply-comment', function(e) { assert.ok(e.detail.comment); done(); }); @@ -67,7 +107,7 @@ }); test('proper event fires on quote', function(done) { - element.addEventListener('reply', function(e) { + element.addEventListener('create-reply-comment', function(e) { assert.ok(e.detail.comment); assert.isTrue(e.detail.quote); done(); @@ -75,13 +115,27 @@ MockInteractions.tap(element.$$('.quote')); }); + test('proper event fires on ack', function(done) { + element.addEventListener('create-ack-comment', function(e) { + done(); + }); + MockInteractions.tap(element.$$('.ack')); + }); + test('proper event fires on done', function(done) { - element.addEventListener('done', function(e) { + element.addEventListener('create-done-comment', function(e) { done(); }); MockInteractions.tap(element.$$('.done')); }); + test('proper event fires on fix', function(done) { + element.addEventListener('create-fix-comment', function(e) { + done(); + }); + MockInteractions.tap(element.$$('.fix')); + }); + test('clicking on date link does not trigger nav', function() { var showStub = sinon.stub(page, 'show'); var dateEl = element.$$('.date'); @@ -92,10 +146,50 @@ 'Should navigate to ' + dest + ' without triggering nav'); showStub.restore(); }); + + test('comment expand and collapse', function() { + element.collapsed = true; + assert.isFalse(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.$$('.actions')), + 'actions are not visible'); + assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + 'textarea is not visible'); + assert.isTrue(isVisible(element.$$('.collapsedContent')), + 'header middle content is visible'); + + element.collapsed = false; + assert.isFalse(element.collapsed); + assert.isTrue(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-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'); + }); + + test('esc does not close comment unless text is empty', function(done) { + element.editing = true; + element._messageText = 'test'; + var textarea = element.$.editTextarea; + var closeSpy = sandbox.spy(element, '_handleCancel'); + + flush(function() { + MockInteractions.pressAndReleaseKeyOn(textarea, 27); // esc + assert.isFalse(closeSpy.called); + element._messageText = ''; + MockInteractions.pressAndReleaseKeyOn(textarea, 27); // esc + assert.isTrue(closeSpy.called); + done(); + }); + }); }); suite('gr-diff-comment draft tests', function() { var element; + var sandbox; setup(function() { stub('gr-rest-api-interface', { @@ -133,18 +227,21 @@ path: '/path/to/file', line: 5, }; + sandbox = sinon.sandbox.create(); }); - function isVisible(el) { - assert.ok(el); - return getComputedStyle(el).getPropertyValue('display') != 'none'; - } + teardown(function() { + sandbox.restore(); + }); test('button visibility states', function() { element.showActions = false; - assert.isTrue(element.$$('.actions').hasAttribute('hidden')); + assert.isTrue(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); + element.showActions = true; - assert.isFalse(element.$$('.actions').hasAttribute('hidden')); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); element.draft = true; assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible'); @@ -153,7 +250,10 @@ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible'); assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible'); assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible'); + assert.isFalse(isVisible(element.$$('.ack')), 'ack is not visible'); assert.isFalse(isVisible(element.$$('.done')), 'done is not visible'); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); element.editing = true; assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible'); @@ -162,7 +262,10 @@ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is visible'); assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible'); assert.isFalse(isVisible(element.$$('.quote')), 'quote is not visible'); + assert.isFalse(isVisible(element.$$('.ack')), 'ack is not visible'); assert.isFalse(isVisible(element.$$('.done')), 'done is not visible'); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); element.draft = false; element.editing = false; @@ -173,12 +276,89 @@ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible'); assert.isTrue(isVisible(element.$$('.reply')), 'reply is visible'); assert.isTrue(isVisible(element.$$('.quote')), 'quote is visible'); + assert.isTrue(isVisible(element.$$('.ack')), 'ack is visible'); assert.isTrue(isVisible(element.$$('.done')), 'done is visible'); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); element.comment.id = 'foo'; element.draft = true; element.editing = true; assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible'); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); + + element.isRobotComment = true; + element.draft = true; + assert.isTrue(element.$$('.humanActions').hasAttribute('hidden')); + assert.isFalse(element.$$('.robotActions').hasAttribute('hidden')); + + // It is not expected to see Robot comment drafts, but if they appear, + // they will behave the same as non-drafts. + element.draft = false; + assert.isTrue(element.$$('.humanActions').hasAttribute('hidden')); + assert.isFalse(element.$$('.robotActions').hasAttribute('hidden')); + }); + + test('collapsible drafts', function() { + assert.isTrue(element.collapsed); + assert.isFalse(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.$$('.actions')), + 'actions are not visible'); + assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + 'textarea is not visible'); + assert.isTrue(isVisible(element.$$('.collapsedContent')), + 'header middle content is visible'); + + MockInteractions.tap(element.$.header); + assert.isFalse(element.collapsed); + assert.isTrue(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-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(element.collapsed); + assert.isFalse(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-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.isTrue(element.collapsed); + assert.isFalse(isVisible(element.$$('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.$$('.actions')), + 'actions are not visible'); + assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + '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-formatted-text')), + 'gr-formatted-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) { @@ -187,6 +367,8 @@ assert.isTrue(element.editing); element._messageText = ''; + var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + // Save should be disabled on an empty message. var disabled = element.$$('.save').hasAttribute('disabled'); assert.isTrue(disabled, 'save button should be disabled.'); @@ -200,17 +382,42 @@ var numDiscardEvents = 0; element.addEventListener('comment-discard', function(e) { numDiscardEvents++; - if (numDiscardEvents == 3) { + assert.isFalse(eraseMessageDraftSpy.called); + if (numDiscardEvents === 2) { assert.isFalse(updateStub.called); done(); } }); MockInteractions.tap(element.$$('.cancel')); - MockInteractions.tap(element.$$('.discard')); element.flushDebouncer('fire-update'); + element._messageText = ''; MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc }); + test('draft discard removes message from storage', function(done) { + element._messageText = ''; + var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + + var numDiscardEvents = 0; + element.addEventListener('comment-discard', function(e) { + assert.isTrue(eraseMessageDraftSpy.called); + done(); + }); + MockInteractions.tap(element.$$('.discard')); + }); + + 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..2d0786a 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-behavior="[[_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..f1f3810 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
@@ -70,6 +70,10 @@ return Promise.resolve({baseComments: [], comments: []}); }); + sinon.stub(diffElement, '_getDiffRobotComments', function() { + return Promise.resolve({baseComments: [], comments: []}); + }); + var setupDone = function() { cursorElement.moveToFirstChunk(); done(); @@ -98,6 +102,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-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js index ec21fd1..bb5b938 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -32,7 +32,11 @@ * @return {Number} The length of the text. */ getLength: function(node) { - return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length; + return this.getStringLength(node.textContent); + }, + + getStringLength: function(str) { + return str.replace(REGEX_ASTRAL_SYMBOL, '_').length; }, /**
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..c39a89f 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
@@ -87,7 +87,15 @@ </select> </div> <div class="pref"> - <label for="columnsInput">Columns</label> + <label for="lineWrappingInput">Fit to Screen</label> + <input + is="iron-input" + type="checkbox" + id="lineWrappingInput" + on-tap="_handlelineWrappingTap"> + </div> + <div class="pref" id="columnsPref" hidden$="[[_newPrefs.line_wrapping]]"> + <label for="columnsInput">Diff Width</label> <input is="iron-input" type="number" id="columnsInput" prevent-invalid-input allowed-pattern="[0-9]" @@ -100,20 +108,32 @@ allowed-pattern="[0-9]" bind-value="{{_newPrefs.tab_size}}"> </div> + <div class="pref" hidden$="[[!_newPrefs.font_size]]"> + <label for="fontSizeInput">Font Size</label> + <input is="iron-input" type="number" id="fontSizeInput" + prevent-invalid-input + allowed-pattern="[0-9]" + bind-value="{{_newPrefs.font_size}}"> + </div> <div class="pref"> <label for="showTabsInput">Show tabs</label> <input is="iron-input" type="checkbox" id="showTabsInput" on-tap="_handleShowTabsTap"> </div> <div class="pref"> + <label for="showTrailingWhitespaceInput">Show Trailing Whitespace</label> + <input is="iron-input" type="checkbox" id="showTrailingWhitespaceInput" + on-tap="_handleShowTrailingWhitespaceTap"> + </div> + <div class="pref"> <label for="syntaxHighlightInput">Syntax highlighting</label> <input is="iron-input" type="checkbox" id="syntaxHighlightInput" on-tap="_handleSyntaxHighlightTap"> </div> </div> <div class="actions"> - <gr-button 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..fd2a6f5 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. @@ -61,6 +72,8 @@ this._newPrefs = Object.assign({}, prefs); this.$.contextSelect.value = prefs.context; this.$.showTabsInput.checked = prefs.show_tabs; + this.$.showTrailingWhitespaceInput.checked = prefs.show_whitespace_errors; + this.$.lineWrappingInput.checked = prefs.line_wrapping; this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting; }, @@ -79,11 +92,20 @@ this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked); }, + _handleShowTrailingWhitespaceTap: function(e) { + this.set('_newPrefs.show_whitespace_errors', + Polymer.dom(e).rootTarget.checked); + }, + _handleSyntaxHighlightTap: function(e) { this.set('_newPrefs.syntax_highlighting', Polymer.dom(e).rootTarget.checked); }, + _handlelineWrappingTap: function(e) { + this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked); + }, + _handleSave: function() { this.prefs = this._newPrefs; this.localPrefs = this._newLocalPrefs;
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..999f005 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
@@ -41,9 +41,11 @@ test('model changes', function() { element.prefs = { context: 10, + font_size: 12, line_length: 100, show_tabs: true, tab_size: 8, + show_whitespace_errors: true, syntax_highlighting: true, }; assert.deepEqual(element.prefs, element._newPrefs); @@ -51,17 +53,43 @@ element.$.contextSelect.value = '50'; element.fire('change', {}, {node: element.$.contextSelect}); element.$.columnsInput.bindValue = 80; + element.$.fontSizeInput.bindValue = 10; element.$.tabSizeInput.bindValue = 4; MockInteractions.tap(element.$.showTabsInput); + MockInteractions.tap(element.$.showTrailingWhitespaceInput); MockInteractions.tap(element.$.syntaxHighlightInput); + MockInteractions.tap(element.$.lineWrappingInput); assert.equal(element._newPrefs.context, 50); + assert.equal(element._newPrefs.font_size, 10); assert.equal(element._newPrefs.line_length, 80); assert.equal(element._newPrefs.tab_size, 4); assert.isFalse(element._newPrefs.show_tabs); + assert.isFalse(element._newPrefs.show_whitespace_errors); + assert.isTrue(element._newPrefs.line_wrapping); assert.isFalse(element._newPrefs.syntax_highlighting); }); + test('clicking fit to screen hides line length input', function() { + element.prefs = {line_wrapping: false}; + + assert.isFalse(element.$.columnsPref.hidden); + + MockInteractions.tap(element.$.lineWrappingInput); + assert.isTrue(element.$.columnsPref.hidden); + + MockInteractions.tap(element.$.lineWrappingInput); + assert.isFalse(element.$.columnsPref.hidden); + }); + + 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..d3cc461 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>} @@ -236,7 +258,7 @@ } // If there is a range to hide. - if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) { + if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) { var linesBeforeCtx = lines.slice(0, hiddenRange[0]); var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); var linesAfterCtx = lines.slice(hiddenRange[1]); @@ -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..f6d0e37 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; @@ -485,6 +511,17 @@ assert.equal(result[0].lines.length, rows.length); }); + test('_sharedGroupsFromRows no single line collapse', function() { + rows = rows.slice(0, 7); + var context = 3; + var result = element._sharedGroupsFromRows( + rows, context, 10, 100); + + // Results in one uncollapsed group with all rows. + assert.equal(result.length, 1, 'Results in one group'); + assert.equal(result[0].lines.length, rows.length); + }); + test('_deltaLinesFromRows', function() { var startLineNum = 10; var result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows, @@ -512,15 +549,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 +602,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..6ba3ff8 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,192 @@ if (!lineEl) { return; } + var commentSelected = + this._elementDescendedFromClass(e.target, '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 (target.type === 'textarea') { return; } + if (!this._elementDescendedFromClass(target, 'content')) { return; } + if (this.classList.contains(SelectionClass.COMMENT)) { + commentSelected = true; + } + 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); + + 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[lines.length - 1] = lines[lines.length - 1] + .substring(0, endOffset); + lines[0] = lines[0].substring(startOffset); + } + 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 = []; + // 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)) { + // Padded elements require newlines for accurate spacing. + if (el.parentElement.id === 'container' || + el.parentElement.nodeName === 'BLOCKQUOTE') { + if (content.length && content[content.length - 1] !== '') { + content.push(''); + } + } + + if (!el.children.length) { + content.push(el.textContent); + } + } } - var text = ''; - for (var i = 0; i < contentEls.length; i++) { - text += contentEls[i].textContent + '\n'; + if (range.endOffset) { + content[content.length - 1] = + content[content.length - 1].substring(0, range.endOffset); } - return text; + content[0] = content[0].substring(range.startOffset); + 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..a5c26e1 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,69 @@ <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="gr-formatted-text message"> + <span class="gr-linked-text">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="gr-formatted-text message"> + <span class="gr-linked-text">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="gr-formatted-text message"> + <span class="gr-linked-text">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> + <tr> + <td class="lineNum left" data-value="4">4</td> + <td class="content"> + <div class="contentText" data-side="left">ga ga</div> + <div data-side="left"> + <div class="gr-diff-comment-thread"> + <textarea data-side="right">test for textarea copying</textarea> + </div> + </div> + </td> + <td class="lineNum right" data-value="4">4</td> </tr> </table> </gr-diff-selection> @@ -54,25 +99,50 @@ <script> suite('gr-diff-selection', function() { var element; + var sandbox; var emulateCopyOn = function(target) { var fakeEvent = { target: target, - preventDefault: sinon.stub(), + preventDefault: sandbox.stub(), clipboardData: { - setData: sinon.stub(), + setData: sandbox.stub(), }, }; + element._getCopyEventTarget.returns(target); element._handleCopy(fakeEvent); return fakeEvent; }; setup(function() { element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(element, '_getCopyEventTarget'); element._cachedDiffBuilder = { - getLineElByChild: sinon.stub().returns({}), - getSideByLineEl: sinon.stub(), + getLineElByChild: sandbox.stub().returns({}), + getSideByLineEl: sandbox.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'], + }, + ], + }; + }); + + teardown(function() { + sandbox.restore(); }); test('applies selected-left on left side click', function() { @@ -97,46 +167,105 @@ }); test('ignores copy for non-content Element', function() { - sinon.stub(element, '_getSelectedText'); + sandbox.stub(element, '_getSelectedText'); emulateCopyOn(element.querySelector('.other')); 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); + sandbox.stub(element, '_getSelectedText'); + emulateCopyOn(element.querySelector('div.contentText')); + assert.deepEqual(['left', false], element._getSelectedText.lastCall.args); }); test('reacts to copy for content Elements', function() { - sinon.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('td.content')); + sandbox.stub(element, '_getSelectedText'); + 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')); + sandbox.stub(element, '_getSelectedText'); + 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')); + sandbox.stub(element, '_getSelectedText').returns('the text'); + 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(); + selection.removeAllRanges(); 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'); + }); + + test('copies comments', function() { + element.classList.add('selected-left'); + element.classList.add('selected-comment'); + element.classList.remove('selected-right'); + var selection = window.getSelection(); + selection.removeAllRanges(); + var range = document.createRange(); + range.setStart( + element.querySelector('.gr-formatted-text *').firstChild, 3); + range.setEnd( + element.querySelectorAll('.gr-formatted-text *')[2].firstChild, 16); + selection.addRange(range); + assert.equal('s is a comment\nThis is a differ', + element._getSelectedText('left', true)); + }); + + test('defers to default behavior for textarea', function() { + element.classList.add('selected-left'); + element.classList.remove('selected-right'); + var selectedTextSpy = sandbox.spy(element, '_getSelectedText'); + emulateCopyOn(element.querySelector('textarea')); + assert.isFalse(selectedTextSpy.called); + }); + + test('regression test for 4794', function() { + element._cachedDiffBuilder.getLineElByChild = function(child) { + while (!child.classList.contains('content') && child.parentElement) { + child = child.parentElement; + } + return child.previousElementSibling; + }; + + element.classList.add('selected-right'); + element.classList.remove('selected-left'); + + var selection = window.getSelection(); + selection.removeAllRanges(); + var range = document.createRange(); + range.setStart( + element.querySelectorAll('div.contentText')[1].firstChild, 4); + range.setEnd( + element.querySelectorAll('div.contentText')[1].firstChild, 10); + selection.addRange(range); + assert.equal(element._getSelectedText('right'), ' other'); }); }); </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..52ac524 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
@@ -16,7 +16,9 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> @@ -33,9 +35,22 @@ 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); } + .patchRangeLeft { + display: flex; + } + .navLink:not([href]), + .downloadLink:not([href]) { + color: #999; + } .reviewed { display: inline-block; margin: 0 .25em; @@ -97,10 +112,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 { @@ -109,6 +121,9 @@ #modeSelect { margin-left: .5em; } + .separator { + margin: 0 .25em; + } @media screen and (max-width: 50em) { .dash { display: none; @@ -123,58 +138,83 @@ display: block; width: 100%; } + .mobileJumpToFileContainer select { + width: 100%; + } } </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" + allow-outside-scroll="true"> + <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)]]"> + [[_computeTruncatedFileDisplayName(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"> - <gr-patch-range-select - path="[[_path]]" - change-num="[[_changeNum]]" - patch-range="[[_patchRange]]" - files-weblinks="[[_filesWeblinks]]" - available-patches="[[_computeAvailablePatches(_change.revisions)]]"> - </gr-patch-range-select> + <div class="subHeader"> + <div class="patchRangeLeft"> + <gr-patch-range-select + path="[[_path]]" + change-num="[[_changeNum]]" + patch-range="[[_patchRange]]" + files-weblinks="[[_filesWeblinks]]" + available-patches="[[_computeAvailablePatches(_change.revisions)]]" + revisions="[[_change.revisions]]"> + </gr-patch-range-select> + <span class="separator">/</span> + <a class="downloadLink" + href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]"> + Download + </a> + </div> <div> <select id="modeSelect" @@ -195,6 +235,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..5dc1d95 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
@@ -16,17 +16,12 @@ var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - var DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; - var DiffSides = { LEFT: 'left', RIGHT: 'right', }; - var HASH_PATTERN = /^b?\d+$/; + var HASH_PATTERN = /^[ab]?\d+$/; Polymer({ is: 'gr-diff-view', @@ -89,14 +84,31 @@ behaviors: [ Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, ], observers: [ - '_getChangeDetail(_changeNum)', '_getProjectConfig(_change.project)', '_getFiles(_changeNum, _patchRange.*)', ], + keyBindings: { + 'esc': '_handleEscKey', + 'shift+left': '_handleShiftLeftKey', + 'shift+right': '_handleShiftRightKey', + 'up k': '_handleUpKey', + 'down j': '_handleDownKey', + 'c': '_handleCKey', + '[': '_handleLeftBracketKey', + ']': '_handleRightBracketKey', + 'n shift+n': '_handleNKey', + 'p shift+p': '_handlePKey', + 'a shift+a': '_handleAKey', + 'u': '_handleUKey', + ',': '_handleCommaKey', + }, + attached: function() { this._getLoggedIn().then(function(loggedIn) { this._loggedIn = loggedIn; @@ -104,6 +116,12 @@ this._setReviewed(true); } }.bind(this)); + if (this.changeViewState.diffMode === null) { + // If screen size is small, always default to unified view. + this.$.restAPI.getPreferences().then(function(prefs) { + this.set('changeViewState.diffMode', prefs.default_diff_view); + }.bind(this)); + } if (this._path) { this.fire('title-change', @@ -113,11 +131,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(); }, @@ -152,6 +165,10 @@ return this.$.restAPI.getPreferences(); }, + _getWindowWidth: function() { + return window.innerWidth; + }, + _handleReviewedChange: function(e) { this._setReviewed(Polymer.dom(e).rootTarget.checked); }, @@ -170,106 +187,185 @@ this._patchRange.patchNum, this._path, reviewed); }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } + _handleEscKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } - switch (e.keyCode) { - case 37: // left - if (e.shiftKey) { - e.preventDefault(); - this.$.cursor.moveLeft(); - } - break; - case 39: // right - if (e.shiftKey) { - e.preventDefault(); - this.$.cursor.moveRight(); - } - break; - case 40: // down - case 74: // 'j' - e.preventDefault(); - this.$.cursor.moveDown(); - break; - case 38: // up - case 75: // 'k' - e.preventDefault(); - this.$.cursor.moveUp(); - break; - case 67: // 'c' - if (!this.$.diff.isRangeSelected()) { - e.preventDefault(); - var line = this.$.cursor.getTargetLineElement(); - if (line) { - this.$.diff.addDraftAtLine(line); - } - } - break; - case 219: // '[' - e.preventDefault(); - this._navToFile(this._fileList, -1); - break; - case 221: // ']' - e.preventDefault(); - this._navToFile(this._fileList, 1); - break; - case 78: // 'n' - e.preventDefault(); - if (e.shiftKey) { - this.$.cursor.moveToNextCommentThread(); - } else { - this.$.cursor.moveToNextChunk(); - } - break; - case 80: // 'p' - e.preventDefault(); - if (e.shiftKey) { - this.$.cursor.moveToPreviousCommentThread(); - } else { - this.$.cursor.moveToPreviousChunk(); - } - break; - case 65: // 'a' - if (e.shiftKey) { // Hide left diff. - e.preventDefault(); - this.$.diff.toggleLeftDiff(); - break; - } + e.preventDefault(); + this.$.diff.displayLine = false; + }, - if (!this._loggedIn) { break; } + _handleShiftLeftKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } - this.set('changeViewState.showReplyDialog', true); - /* falls through */ // required by JSHint - case 85: // 'u' - if (this._changeNum && this._patchRange.patchNum) { - e.preventDefault(); - page.show(this._getChangePath( - this._changeNum, - this._patchRange, - this._change && this._change.revisions)); - } - break; - case 188: // ',' - e.preventDefault(); - this.$.prefsOverlay.open(); - break; + e.preventDefault(); + this.$.cursor.moveLeft(); + }, + + _handleShiftRightKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.$.cursor.moveRight(); + }, + + _handleUpKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.diff.displayLine = true; + this.$.cursor.moveUp(); + }, + + _handleDownKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.diff.displayLine = true; + this.$.cursor.moveDown(); + }, + + _handleCKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (this.$.diff.isRangeSelected()) { return; } + if (this.modifierPressed(e)) { return; } + + e.preventDefault(); + var line = this.$.cursor.getTargetLineElement(); + if (line) { + this.$.diff.addDraftAtLine(line); } }, - _navToFile: function(fileList, direction) { - if (fileList.length == 0) { return; } + _handleLeftBracketKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } - var idx = fileList.indexOf(this._path) + direction; - if (idx < 0 || idx > fileList.length - 1) { - page.show(this._getChangePath( - this._changeNum, - this._patchRange, - this._change && this._change.revisions)); + e.preventDefault(); + this._navToFile(this._path, this._fileList, -1); + }, + + _handleRightBracketKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._navToFile(this._path, this._fileList, 1); + }, + + _handleNKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + if (e.detail.keyboardEvent.shiftKey) { + this.$.cursor.moveToNextCommentThread(); + } else { + if (this.modifierPressed(e)) { return; } + this.$.cursor.moveToNextChunk(); + } + }, + + _handlePKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + if (e.detail.keyboardEvent.shiftKey) { + this.$.cursor.moveToPreviousCommentThread(); + } else { + if (this.modifierPressed(e)) { return; } + this.$.cursor.moveToPreviousChunk(); + } + }, + + _handleAKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + if (e.detail.keyboardEvent.shiftKey) { // Hide left diff. + e.preventDefault(); + this.$.diff.toggleLeftDiff(); return; } - page.show(this._getDiffURL(this._changeNum, - this._patchRange, - fileList[idx])); + + if (this.modifierPressed(e)) { return; } + + if (!this._loggedIn) { return; } + + this.set('changeViewState.showReplyDialog', true); + e.preventDefault(); + this._navToChangeView(); + }, + + _handleUKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._navToChangeView(); + }, + + _handleCommaKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._openPrefs(); + }, + + _navToChangeView: function() { + if (!this._changeNum || !this._patchRange.patchNum) { return; } + + page.show(this._getChangePath( + this._changeNum, + this._patchRange, + this._change && this._change.revisions)); + }, + + _navToFile: function(path, fileList, direction) { + var url = this._computeNavLinkURL(path, fileList, direction); + if (!url) { return; } + + page.show(this._computeNavLinkURL(path, fileList, direction)); + }, + + _openPrefs: function() { + this.$.prefsOverlay.open().then(function() { + var diffPreferences = this.$.diffPreferences; + var focusStops = diffPreferences.getFocusStops(); + this.$.prefsOverlay.setFocusStops(focusStops); + this.$.diffPreferences.resetFocus(); + }.bind(this)); + }, + + /** + * @param {?string} path The path of the current file being shown. + * @param {Array.<string>} fileList The list of files in this change and + * 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) { + if (opt_noUp) { return null; } + return this._getChangePath( + this._changeNum, + this._patchRange, + this._change && this._change.revisions); + } + return this._getDiffURL(this._changeNum, this._patchRange, fileList[idx]); }, _paramsChanged: function(value) { @@ -318,7 +414,7 @@ _loadHash: function(hash) { var hash = hash.replace(/^#/, ''); if (!HASH_PATTERN.test(hash)) { return; } - if (hash[0] === 'b') { + if (hash[0] === 'a' || hash[0] === 'b') { this.$.cursor.side = DiffSides.LEFT; hash = hash.substring(1); } else { @@ -340,7 +436,7 @@ _getDiffURL: function(changeNum, patchRange, path) { return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' + - path; + this.encodeURL(path, true); }, _computeDiffURL: function(changeNum, patchRangeRecord, path) { @@ -389,7 +485,12 @@ }, _computeFileDisplayName: function(path) { - return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path; + return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path; + }, + + _computeTruncatedFileDisplayName: function(path) { + return path === COMMIT_MESSAGE_PATH ? + 'Commit message' : util.truncatePath(path); }, _computeFileSelected: function(path, currentPath) { @@ -426,7 +527,7 @@ _handlePrefsTap: function(e) { e.preventDefault(); - this.$.prefsOverlay.open(); + this._openPrefs(); }, _handlePrefsSave: function(e) { @@ -458,9 +559,10 @@ * 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. + * preferences unless they have manually chosen the alternative view or they + * are on a mobile device. 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. * @@ -469,11 +571,12 @@ _getDiffViewMode: function() { if (this.changeViewState.diffMode) { return this.changeViewState.diffMode; - } else if (this._userPrefs && this._userPrefs.diff_view) { - return this.changeViewState.diffMode = this._userPrefs.diff_view; + } else if (this._userPrefs) { + return this.changeViewState.diffMode = + this._userPrefs.default_diff_view; + } else { + return 'SIDE_BY_SIDE'; } - - return DiffViewMode.SIDE_BY_SIDE; }, _computeModeSelectHidden: function() { @@ -482,7 +585,13 @@ _onLineSelected: function(e, detail) { this.$.cursor.moveToLineNumber(detail.number, detail.side); - history.pushState(null, null, '#' + this.$.cursor.getAddress()); + history.replaceState(null, null, '#' + this.$.cursor.getAddress()); + }, + + _computeDownloadLink: function(changeNum, patchRange, path) { + var url = this.changeBaseURL(changeNum, patchRange.patchNum); + url += '/patch?zip&path=' + encodeURIComponent(path); + return url; }, }); })();
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..29dd8ae 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,11 +32,20 @@ </template> </test-fixture> +<test-fixture id="blank"> + <template> + <div></div> + </template> +</test-fixture> + <script> suite('gr-diff-view tests', function() { var element; + var sandbox; setup(function() { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { getLoggedIn: function() { return Promise.resolve(false); }, getProjectConfig: function() { return Promise.resolve({}); }, @@ -47,11 +56,14 @@ element = fixture('basic'); }); + teardown(function() { + sandbox.restore(); + }); + test('toggle left diff with a hotkey', function() { - var toggleLeftDiffStub = sinon.stub(element.$.diff, 'toggleLeftDiff'); - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift'); // 'a' + var toggleLeftDiffStub = sandbox.stub(element.$.diff, 'toggleLeftDiff'); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); assert.isTrue(toggleLeftDiffStub.calledOnce); - toggleLeftDiffStub.restore(); }); test('keyboard shortcuts', function() { @@ -69,60 +81,82 @@ element._path = 'glados.txt'; element.changeViewState.selectedFileIndex = 1; - var showStub = sinon.stub(page, 'show'); - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + var showStub = sandbox.stub(page, 'show'); + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/c/42/'), 'Should navigate to /c/42/'); - MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); assert(showStub.lastCall.calledWithExactly('/c/42/10/wheatley.md'), 'Should navigate to /c/42/10/wheatley.md'); element._path = 'wheatley.md'; assert.equal(element.changeViewState.selectedFileIndex, 2); - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/10/glados.txt'), 'Should navigate to /c/42/10/glados.txt'); element._path = 'glados.txt'; assert.equal(element.changeViewState.selectedFileIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/10/chell.go'), 'Should navigate to /c/42/10/chell.go'); element._path = 'chell.go'; assert.equal(element.changeViewState.selectedFileIndex, 0); - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/'), 'Should navigate to /c/42/'); assert.equal(element.changeViewState.selectedFileIndex, 0); - var showPrefsStub = sinon.stub(element.$.prefsOverlay, 'open'); - MockInteractions.pressAndReleaseKeyOn(element, 188); // ',' + var showPrefsStub = sandbox.stub(element.$.prefsOverlay, 'open', + function() { return Promise.resolve({}); }); + + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); assert(showPrefsStub.calledOnce); - var scrollStub = sinon.stub(element.$.cursor, 'moveToNextChunk'); - MockInteractions.pressAndReleaseKeyOn(element, 78); // 'n' + var scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); assert(scrollStub.calledOnce); - scrollStub.restore(); - scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousChunk'); - MockInteractions.pressAndReleaseKeyOn(element, 80); // 'p' + scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p'); assert(scrollStub.calledOnce); - scrollStub.restore(); - scrollStub = sinon.stub(element.$.cursor, 'moveToNextCommentThread'); - MockInteractions.pressAndReleaseKeyOn(element, 78, ['shift']); // 'N' + scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); assert(scrollStub.calledOnce); - scrollStub.restore(); - scrollStub = sinon.stub(element.$.cursor, 'moveToPreviousCommentThread'); - MockInteractions.pressAndReleaseKeyOn(element, 80, ['shift']); // 'P' + scrollStub = sandbox.stub(element.$.cursor, + 'moveToPreviousCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p'); assert(scrollStub.calledOnce); - scrollStub.restore(); - showPrefsStub.restore(); - showStub.restore(); + var computeContainerClassStub = sandbox.stub(element.$.diff, + '_computeContainerClass'); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert(computeContainerClassStub.lastCall.calledWithExactly( + false, 'SIDE_BY_SIDE', true)); + + MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); + assert(computeContainerClassStub.lastCall.calledWithExactly( + false, 'SIDE_BY_SIDE', false)); + }); + + test('saving diff preferences', function() { + var savePrefs = sandbox.stub(element, '_handlePrefsSave'); + var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel'); + element.$.diffPreferences._handleSave(); + assert(savePrefs.calledOnce); + assert(cancelPrefs.notCalled); + }); + + test('cancelling diff preferences', function() { + var savePrefs = sandbox.stub(element, '_handlePrefsSave'); + var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel'); + element.$.diffPreferences._handleCancel(); + assert(cancelPrefs.calledOnce); + assert(savePrefs.notCalled); }); test('keyboard shortcuts with patch range', function() { @@ -139,45 +173,43 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sinon.stub(page, 'show'); + var showStub = sandbox.stub(page, 'show'); - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + 'only work when the user is logged in.'); assert.isNull(window.sessionStorage.getItem( 'changeView.showReplyDialog')); element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(element.changeViewState.showReplyDialog); assert(showStub.lastCall.calledWithExactly('/c/42/5..10'), 'Should navigate to /c/42/5..10'); - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/c/42/5..10'), 'Should navigate to /c/42/5..10'); - MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); assert(showStub.lastCall.calledWithExactly('/c/42/5..10/wheatley.md'), 'Should navigate to /c/42/5..10/wheatley.md'); element._path = 'wheatley.md'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/5..10/glados.txt'), 'Should navigate to /c/42/5..10/glados.txt'); element._path = 'glados.txt'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/5..10/chell.go'), 'Should navigate to /c/42/5..10/chell.go'); element._path = 'chell.go'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/5..10'), 'Should navigate to /c/42/5..10'); - - showStub.restore(); }); test('keyboard shortcuts with old patch number', function() { @@ -195,45 +227,43 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sinon.stub(page, 'show'); + var showStub = sandbox.stub(page, 'show'); - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + 'only work when the user is logged in.'); assert.isNull(window.sessionStorage.getItem( 'changeView.showReplyDialog')); element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(element.changeViewState.showReplyDialog); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'), 'Should navigate to /c/42/1/wheatley.md'); element._path = 'wheatley.md'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'), 'Should navigate to /c/42/1/glados.txt'); element._path = 'glados.txt'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'), 'Should navigate to /c/42/1/chell.go'); element._path = 'chell.go'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - - showStub.restore(); }); test('go up to change via kb without change loaded', function() { @@ -246,45 +276,43 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sinon.stub(page, 'show'); + var showStub = sandbox.stub(page, 'show'); - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + 'only work when the user is logged in.'); assert.isNull(window.sessionStorage.getItem( 'changeView.showReplyDialog')); element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'a' + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(element.changeViewState.showReplyDialog); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u' + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - MockInteractions.pressAndReleaseKeyOn(element, 221); // ']' + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); assert(showStub.lastCall.calledWithExactly('/c/42/1/wheatley.md'), 'Should navigate to /c/42/1/wheatley.md'); element._path = 'wheatley.md'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1/glados.txt'), 'Should navigate to /c/42/1/glados.txt'); element._path = 'glados.txt'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1/chell.go'), 'Should navigate to /c/42/1/chell.go'); element._path = 'chell.go'; - MockInteractions.pressAndReleaseKeyOn(element, 219); // '[' + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); assert(showStub.lastCall.calledWithExactly('/c/42/1'), 'Should navigate to /c/42/1'); - - showStub.restore(); }); test('jump to file dropdown', function() { @@ -338,6 +366,65 @@ 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('download link', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: '10', + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + flushAsynchronousOperations(); + assert.equal(element.$$('.downloadLink').getAttribute('href'), + '/changes/42/revisions/10/patch?zip&path=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'; @@ -347,7 +434,7 @@ }; element._fileList = ['/COMMIT_MSG']; element._path = '/COMMIT_MSG'; - var saveReviewedStub = sinon.stub(element, '_saveReviewedState', + var saveReviewedStub = sandbox.stub(element, '_saveReviewedState', function() { return Promise.resolve(); }); flush(function() { @@ -363,7 +450,6 @@ assert.isTrue(commitMsg.checked); assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true)); - saveReviewedStub.restore(); done(); }); }); @@ -371,8 +457,7 @@ test('diff mode selector correctly toggles the diff', function() { var select = element.$.modeSelect; var diffDisplay = element.$.diff; - - element._userPrefs = {diff_view: 'SIDE_BY_SIDE'}; + element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; // The mode selected in the view state reflects the selected option. assert.equal(element._getDiffViewMode(), select.value); @@ -383,7 +468,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}); @@ -394,6 +478,29 @@ assert.equal(element._getDiffViewMode(), diffDisplay.viewMode); }); + test('diff mode selector initializes from preferences', function() { + var resolvePrefs; + var prefsPromise = new Promise(function(resolve) { + resolvePrefs = resolve; + }); + var getPreferencesStub = sandbox.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({default_diff_view: 'UNIFIED'}); + flushAsynchronousOperations(); + assert.equal(select.value, 'SIDE_BY_SIDE'); + }); + test('_loadHash', function() { assert.isNotOk(element.$.cursor.initialLineNumber); @@ -410,6 +517,68 @@ element._loadHash('b345'); assert.equal(element.$.cursor.initialLineNumber, 345); assert.equal(element.$.cursor.side, 'left'); + + // GWT-style base hash: + element._loadHash('a123'); + assert.equal(element.$.cursor.initialLineNumber, 123); + assert.equal(element.$.cursor.side, 'left'); + }); + + test('_shortenPath with long path should add ellipsis', function() { + var path = + 'level1/level2/level3/level4/file.js'; + var shortenedPath = util.truncatePath(path); + // The expected path is truncated with an ellipsis. + var expectedPath = '\u2026/file.js'; + assert.equal(shortenedPath, expectedPath); + + var path = 'level2/file.js'; + var shortenedPath = util.truncatePath(path); + assert.equal(shortenedPath, expectedPath); + }); + + test('_shortenPath with short path should not add ellipsis', function() { + var path = 'file.js'; + var expectedPath = 'file.js'; + var shortenedPath = util.truncatePath(path); + assert.equal(shortenedPath, expectedPath); + }); + + test('_onLineSelected', function() { + var replaceStateStub = sandbox.stub(history, 'replaceState'); + var moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber'); + + var e = {}; + var detail = {number: 123, side: 'right'}; + + element._onLineSelected(e, detail); + + assert.isTrue(moveStub.called); + assert.equal(moveStub.lastCall.args[0], detail.number); + assert.equal(moveStub.lastCall.args[1], detail.side); + + assert.isTrue(replaceStateStub.called); + }); + + test('_getDiffURL encodes special characters', function() { + var changeNum = 123; + var patchRange = {basePatchNum: 123, patchNum: 456}; + var path = 'c++/cpp.cpp'; + assert.equal(element._getDiffURL(changeNum, patchRange, path), + '/c/123/123..456/c%252B%252B/cpp.cpp'); + }); + + test('_getDiffViewMode', function() { + // No user prefs or change view state set. + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + + // User prefs but no change view state set. + element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'}; + assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF'); + + // User prefs and change view state set. + element.changeViewState = {diffMode: 'SIDE_BY_SIDE'}; + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); }); }); </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..21f8f32 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -28,9 +28,10 @@ <style> :host { --light-remove-highlight-color: #fee; - --dark-remove-highlight-color: #ffd4d4; + --dark-remove-highlight-color: rgba(255, 0, 0, 0.15); --light-add-highlight-color: #efe; - --dark-add-highlight-color: #d4ffd4; + --dark-add-highlight-color: rgba(0, 255, 0, 0.15); + } :host.no-left .sideBySide ::content .left, :host.no-left .sideBySide ::content .left + td, @@ -50,8 +51,8 @@ border-collapse: collapse; border-right: 1px solid #ddd; table-layout: fixed; - } - table tbody { + + /* Hint GPU acceleration */ -webkit-transform: translateZ(0); -moz-transform: translateZ(0); -ms-transform: translateZ(0); @@ -86,18 +87,20 @@ .content { background-color: #fff; } + .full-width { + width: 100%; + } + .full-width .contentText { + white-space: pre-wrap; + word-wrap: break-word; + } .lineNum, .content { + /* Set font size based the user's diff preference. */ + font-size: var(--font-size, 12px); 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; @@ -119,6 +122,7 @@ allows them to shrink. */ max-width: var(--content-width, 80ch); min-width: var(--content-width, 80ch); + width: var(--content-width, 80ch); } .content.add .intraline, .content.add.darkHighlight { @@ -134,6 +138,10 @@ .content.remove.lightHighlight { background-color: var(--light-remove-highlight-color); } + .content .contentText:after { + /* Newline, to ensure all lines are one line-height tall. */ + content: '\A'; + } .contextControl { background-color: #fef; color: #849; @@ -146,23 +154,30 @@ .contextControl td:not(.lineNum) { text-align: center; } + .displayLine .diff-row.target-row { + border-bottom: 1px solid #bbb; + } .br:after { /* Line feed */ content: '\A'; } .tab { display: inline-block; - position: relative; } - .tab.withIndicator { - color: #D68E47; - text-decoration: line-through; + .tab-indicator:before { + color: #C62828; + /* >> character */ + content: '\00BB'; + } + .trailing-whitespace { + border-radius: .4em; + background-color: #FF9AD2; } </style> <style include="gr-theme-default"></style> - <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]" + <div class$="[[_computeContainerClass(_loggedIn, viewMode, displayLine)]]" on-tap="_handleTap"> - <gr-diff-selection> + <gr-diff-selection diff="[[_diff]]"> <gr-diff-highlight id="highlights" logged-in="[[_loggedIn]]" @@ -172,10 +187,11 @@ comments="[[_comments]]" diff="[[_diff]]" view-mode="[[viewMode]]" + line-wrapping="[[lineWrapping]]" is-image-diff="[[isImageDiff]]" base-image="[[_baseImage]]" revision-image="[[_revisionImage]]"> - <table id="diffTable"></table> + <table id="diffTable" class$="[[_diffTableClass]]"></table> </gr-diff-builder> </gr-diff-highlight> </gr-diff-selection>
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..4b5e381 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: { @@ -46,6 +51,10 @@ }, project: String, commit: String, + displayLine: { + type: Boolean, + value: false, + }, isImageDiff: { type: Boolean, computed: '_computeIsImageDiff(_diff)', @@ -61,12 +70,21 @@ type: Boolean, value: false, }, + lineWrapping: { + type: Boolean, + value: false, + observer: '_lineWrappingObserver', + }, viewMode: { type: String, value: DiffViewMode.SIDE_BY_SIDE, observer: '_viewModeObserver', }, _diff: Object, + _diffTableClass: { + type: String, + value: '', + }, _comments: Object, _baseImage: Object, _revisionImage: Object, @@ -84,6 +102,13 @@ this._getLoggedIn().then(function(loggedIn) { this._loggedIn = loggedIn; }.bind(this)); + + }, + + ready: function() { + if (this._canRender()) { + this.reload(); + } }, reload: function() { @@ -108,7 +133,7 @@ }, getCursorStops: function() { - if (this.hidden) { + if (!this.expanded) { return []; } @@ -141,11 +166,21 @@ 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'); }, - _computeContainerClass: function(loggedIn, viewMode) { + _computeContainerClass: function(loggedIn, viewMode, displayLine) { var classes = ['diffContainer']; switch (viewMode) { case DiffViewMode.UNIFIED: @@ -160,6 +195,9 @@ if (loggedIn) { classes.push('canComment'); } + if (displayLine) { + classes.push('displayLine'); + } return classes.join(' '); }, @@ -334,9 +372,26 @@ this._prefsChanged(this.prefs); }, + _lineWrappingObserver: function() { + this._prefsChanged(this.prefs); + }, + _prefsChanged: function(prefs) { if (!prefs) { return; } - this.customStyle['--content-width'] = prefs.line_length + 'ch'; + if (prefs.line_wrapping) { + this._diffTableClass = 'full-width'; + if (this.viewMode === 'SIDE_BY_SIDE') { + this.customStyle['--content-width'] = 'none'; + } + } else { + this._diffTableClass = ''; + this.customStyle['--content-width'] = prefs.line_length + 'ch'; + } + + if (!!prefs.font_size) { + this.customStyle['--font-size'] = prefs.font_size + 'px'; + } + this.updateStyles(); if (this._diff && this._comments) { @@ -353,6 +408,12 @@ }, _handleGetDiffError: function(response) { + // Loading the diff may respond with 409 if the file is too large. In this + // case, use a toast error.. + if (response.status === 409) { + this.fire('server-error', {response: response}); + return; + } this.fire('page-error', {response: response}); }, @@ -363,12 +424,12 @@ this.patchRange.patchNum, this.path, this._handleGetDiffError.bind(this)).then(function(diff) { - this.filesWeblinks = { - meta_a: diff.meta_a && diff.meta_a.web_links, - meta_b: diff.meta_b && diff.meta_b.web_links, - }; - return diff; - }.bind(this)); + this.filesWeblinks = { + meta_a: diff && diff.meta_a && diff.meta_a.web_links, + meta_b: diff && diff.meta_b && diff.meta_b.web_links, + }; + return diff; + }.bind(this)); }, _getDiffComments: function() { @@ -392,14 +453,24 @@ }.bind(this)); }, + _getDiffRobotComments: function() { + return this.$.restAPI.getDiffRobotComments( + this.changeNum, + this.patchRange.basePatchNum, + this.patchRange.patchNum, + this.path); + }, + _getDiffCommentsAndDrafts: function() { var promises = []; promises.push(this._getDiffComments()); promises.push(this._getDiffDrafts()); + promises.push(this._getDiffRobotComments()); return Promise.all(promises).then(function(results) { return Promise.resolve({ comments: results[0], drafts: results[1], + robotComments: results[2], }); }).then(this._normalizeDiffCommentsAndDrafts.bind(this)); }, @@ -411,6 +482,9 @@ } var baseDrafts = results.drafts.baseComments.map(markAsDraft); var drafts = results.drafts.comments.map(markAsDraft); + + var baseRobotComments = results.robotComments.baseComments; + var robotComments = results.robotComments.comments; return Promise.resolve({ meta: { path: this.path, @@ -418,8 +492,10 @@ patchRange: this.patchRange, projectConfig: this.projectConfig, }, - left: results.comments.baseComments.concat(baseDrafts), - right: results.comments.comments.concat(drafts), + left: results.comments.baseComments.concat(baseDrafts) + .concat(baseRobotComments), + right: results.comments.comments.concat(drafts) + .concat(robotComments), }); },
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..9e10bf9 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
@@ -36,7 +36,6 @@ var element; suite('not logged in', function() { - setup(function() { stub('gr-rest-api-interface', { getLoggedIn: function() { return Promise.resolve(false); }, @@ -51,6 +50,21 @@ assert.isFalse(element.classList.contains('no-left')); }); + test('view does not start with displayLine classList', function() { + assert.isFalse( + element.$$('.diffContainer').classList.contains('displayLine')); + }); + + test('displayLine class added called when displayLine is true', + function() { + var spy = sinon.spy(element, '_computeContainerClass'); + element.displayLine = true; + assert.isTrue(spy.called); + assert.isTrue( + element.$$('.diffContainer').classList.contains('displayLine')); + spy.restore(); + }); + test('get drafts', function(done) { element.patchRange = {basePatchNum: 0, patchNum: 0}; @@ -63,6 +77,19 @@ }); }); + test('get robot comments', function(done) { + element.patchRange = {basePatchNum: 0, patchNum: 0}; + + var getDraftsStub = sinon.stub(element.$.restAPI, + 'getDiffRobotComments'); + element._getDiffDrafts().then(function(result) { + assert.deepEqual(result, {baseComments: [], comments: []}); + sinon.assert.notCalled(getDraftsStub); + getDraftsStub.restore(); + done(); + }); + }); + test('loads files weblinks', function(done) { var diffStub = sinon.stub(element.$.restAPI, 'getDiff').returns( Promise.resolve({ @@ -327,13 +354,25 @@ }); content.click(); }); + + test('_getDiff handles null diff responses', function(done) { + stub('gr-rest-api-interface', { + getDiff: function() { return Promise.resolve(null); }, + }); + element.changeNum = 123; + element.patchRange = {basePatchNum: 1, patchNum: 2}; + element.path = 'file.txt'; + element._getDiff().then(done); + }); }); suite('logged in', function() { - setup(function() { stub('gr-rest-api-interface', { getLoggedIn: function() { return Promise.resolve(true); }, + getPreferences: function() { + return Promise.resolve({time_format: 'HHMM_12'}); + }, }); element = fixture('basic'); }); @@ -377,9 +416,24 @@ {id: 'd2'}, ], }; + var diffDraftsStub = sinon.stub(element, '_getDiffDrafts', function() { return Promise.resolve(drafts); }); + var robotComments = { + baseComments: [ + {id: 'br1'}, + {id: 'br2'}, + ], + comments: [ + {id: 'r1'}, + {id: 'r2'}, + ], + }; + + var diffRobotCommentStub = sinon.stub(element, '_getDiffRobotComments', + function() { return Promise.resolve(robotComments); }); + element.changeNum = '42'; element.patchRange = { basePatchNum: 'PARENT', @@ -404,17 +458,22 @@ {id: 'bc2'}, {id: 'bd1', __draft: true}, {id: 'bd2', __draft: true}, + {id: 'br1'}, + {id: 'br2'}, ], right: [ {id: 'c1'}, {id: 'c2'}, {id: 'd1', __draft: true}, {id: 'd2', __draft: true}, + {id: 'r1'}, + {id: 'r2'}, ], }); diffCommentsStub.restore(); diffDraftsStub.restore(); + diffRobotCommentStub.restore(); done(); }); }); @@ -467,6 +526,23 @@ 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); + assert.isTrue(spy.called); + }); }); }); });
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html index c496703..759b3a2 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -14,7 +14,9 @@ limitations under the License. --> +<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> <dom-module id="gr-patch-range-select"> <template> @@ -25,16 +27,21 @@ .patchRange { display: inline-block; } + select { + max-width: 8em; + } </style> Patch set: <span class="patchRange"> - <select id="leftPatchSelect" on-change="_handlePatchChange"> - <option value="PARENT" - selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option> + <select id="leftPatchSelect" bind-value="{{_leftSelected}}" + on-change="_handlePatchChange" is="gr-select"> + <option value="PARENT">Base</option> <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> <option value$="[[patchNum]]" - selected$="[[_computeLeftSelected(patchNum, patchRange)]]" - disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option> + disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]"> + [[patchNum]] + [[_computePatchSetDescription(revisions, patchNum)]] + </option> </template> </select> </span> @@ -46,11 +53,14 @@ </span> → <span class="patchRange"> - <select id="rightPatchSelect" on-change="_handlePatchChange"> + <select id="rightPatchSelect" bind-value="{{_rightSelected}}" + on-change="_handlePatchChange" is="gr-select"> <template is="dom-repeat" items="{{availablePatches}}" as="patchNum"> <option value$="[[patchNum]]" - selected$="[[_computeRightSelected(patchNum, patchRange)]]" - disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option> + disabled$="[[_computeRightDisabled(patchNum, patchRange)]]"> + [[patchNum]] + [[_computePatchSetDescription(revisions, patchNum)]] + </option> </template> </select> <span is="dom-if" if="[[filesWeblinks.meta_b]]">
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..ca38a8c 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
@@ -21,26 +21,32 @@ availablePatches: Array, changeNum: String, filesWeblinks: Object, - patchRange: Object, path: String, + patchRange: { + type: Object, + observer: '_updateSelected', + }, + revisions: Object, + _rightSelected: String, + _leftSelected: String, + }, + + behaviors: [Gerrit.PatchSetBehavior], + + _updateSelected: function() { + this._rightSelected = this.patchRange.patchNum; + this._leftSelected = this.patchRange.basePatchNum; }, _handlePatchChange: function(e) { - var leftPatch = this.$.leftPatchSelect.value; - var rightPatch = this.$.rightPatchSelect.value; + var leftPatch = this._leftSelected; + var rightPatch = this._rightSelected; var rangeStr = rightPatch; if (leftPatch != 'PARENT') { rangeStr = leftPatch + '..' + rangeStr; } page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path); - }, - - _computeLeftSelected: function(patchNum, patchRange) { - return patchNum == patchRange.basePatchNum; - }, - - _computeRightSelected: function(patchNum, patchRange) { - return patchNum == patchRange.patchNum; + e.target.blur(); }, _computeLeftDisabled: function(patchNum, patchRange) { @@ -51,5 +57,23 @@ if (patchRange.basePatchNum == 'PARENT') { return false; } return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10); }, + + // On page load, the dom-if for options getting added occurs after + // the value was set in the select. This ensures that after they + // are loaded, the correct value will get selected. I attempted to + // debounce these, but because they are detecting two different + // events, sometimes the timing was off and one ended up missing. + _synchronizeSelectionRight: function() { + this.$.rightPatchSelect.value = this._rightSelected; + }, + + _synchronizeSelectionLeft: function() { + this.$.leftPatchSelect.value = this._leftSelected; + }, + + _computePatchSetDescription: function(revisions, patchNum) { + var rev = this.getRevisionByPatchNum(revisions, patchNum); + return (rev && rev.description) ? rev.description : ''; + }, }); })();
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..68eeaa9 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,9 +63,14 @@ 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']; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '3', + }; flushAsynchronousOperations(); var numEvents = 0; @@ -77,6 +82,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-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js index 7496e59..90c37cd 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -32,7 +32,7 @@ _commentMap: { type: Object, value: function() { return {left: [], right: []}; }, - } + }, }, observers: [
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html index 68b7528..eae77ef 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -173,9 +173,6 @@ line.beforeNumber = 36; el.setAttribute('data-side', 'right'); - var expectedStart = 6; - var expectedLength = line.text.length - expectedStart; - element.annotate(el, line); assert.isFalse(annotateElementStub.called);
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html index 9a8ea37..5f74f1f 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -15,32 +15,31 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <dom-module id="gr-selection-action-box"> <template> <style> :host { - --gr-arrow-size: .6em; + --gr-arrow-size: .65em; - background-color: #fff; - border: 1px solid #000; - border-radius: .5em; + background-color: rgba(22, 22, 22, .9); + border-radius: 3px; + color: #fff; cursor: pointer; - padding: .3em; + font-family: var(--font-family); + padding: .5em .75em; position: absolute; white-space: nowrap; } .arrow { - background: #fff; - border: var(--gr-arrow-size) solid #000; - border-width: 0 1px 1px 0; - height: var(--gr-arrow-size); - left: calc(50% - 1em); - margin-top: .05em; + border: var(--gr-arrow-size) solid transparent; + border-top: var(--gr-arrow-size) solid rgba(22, 22, 22, 0.9); + height: 0; + left: calc(50% - var(--gr-arrow-size)); + margin-top: .5em; position: absolute; - transform: rotate(45deg); - width: var(--gr-arrow-size); + width: 0; } </style> Press <strong>C</strong> to comment.
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..0f7f2f2 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
@@ -48,7 +48,11 @@ ], listeners: { - 'tap': '_handleTap', + 'mousedown': '_handleMouseDown', // See https://crbug.com/gerrit/4767 + }, + + keyBindings: { + 'c': '_handleCKey', }, placeAbove: function(el) { @@ -74,15 +78,17 @@ return rect; }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - if (e.keyCode === 67) { // 'c' - e.preventDefault(); - this._fireCreateComment(); - } + _handleCKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._fireCreateComment(); }, - _handleTap: function() { + _handleMouseDown: function(e) { + e.preventDefault(); + e.stopPropagation(); 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..79ff2a5 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
@@ -49,12 +49,12 @@ }); test('ignores regular keys', function() { - MockInteractions.pressAndReleaseKeyOn(document.body, 27); // 'esc' + MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc'); assert.isFalse(element.fire.called); }); test('reacts to hotkey', function() { - MockInteractions.pressAndReleaseKeyOn(document.body, 67); // 'c' + MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c'); assert.isTrue(element.fire.called); }); @@ -68,7 +68,7 @@ }; element.side = 'left'; element.range = range; - MockInteractions.pressAndReleaseKeyOn(document.body, 67); // 'c' + MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c'); assert(element.fire.calledWithExactly( 'create-comment', {
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..eca40a6 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
@@ -47,7 +47,6 @@ 'text/x-yaml': 'yaml', }; var ASYNC_DELAY = 10; - var HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; var CLASS_WHITELIST = { 'gr-diff gr-syntax gr-syntax-literal': true, @@ -79,6 +78,10 @@ 'gr-diff gr-syntax gr-syntax-selector-class': true, }; + var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; + var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; + var GLOBAL_LT_PATTERN = /</g; + Polymer({ is: 'gr-syntax-layer', @@ -106,6 +109,7 @@ value: function() { return []; }, }, _processHandle: Number, + _hljs: Object, }, addListener: function(fn) { @@ -254,13 +258,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)); } @@ -276,9 +281,8 @@ * @param {!Object} state The processing state for the layer. */ _processNextLine: function(state) { - var baseLine = undefined; - var revisionLine = undefined; - var hljs = this._getHighlightLib(); + var baseLine; + var revisionLine; var section = this.diff.content[state.sectionIndex]; if (section.ab) { @@ -301,21 +305,67 @@ var result; if (this._baseLanguage && baseLine !== undefined) { - result = hljs.highlight(this._baseLanguage, baseLine, true, + baseLine = this._workaround(this._baseLanguage, baseLine); + 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); + revisionLine = this._workaround(this._revisionLanguage, revisionLine); + result = this._hljs.highlight(this._revisionLanguage, revisionLine, + true, state.revisionContext); this.push('_revisionRanges', this._rangesFromString(result.value)); state.revisionContext = result.top; } }, /** + * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained + * cases before sending them into HLJS so that they parse correctly. + * + * Important notes: + * * These tests should be as constrained as possible to avoid interfering + * with code it shouldn't AND to avoid executing regexes as much as + * possible. + * * These tests should document the issue clearly enough that the test can + * be condidently removed when the issue is solved in HLJS. + * * These tests should rewrite the line of code to have the same number of + * characters. This method rewrites the string that gets parsed, but NOT + * the string that gets displayed and highlighted. Thus, the positions + * must be consistent. + * + * @param {!string} language The name of the HLJS language plugin in use. + * @param {!string} line The line of code to potentially rewrite. + * @return {string} A potentially-rewritten line of code. + */ + _workaround: function(language, line) { + /** + * Prevent confusing < and << operators for the start of a meta string by + * converting them to a different operator. + * {@see Issue 4864} + * {@see https://github.com/isagalaev/highlight.js/issues/1341} + */ + if (language === 'cpp' && CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) { + return line.replace(GLOBAL_LT_PATTERN, '|'); + } + + /** + * Prevent confusing the closing paren of a parameterized Java annotation + * being applied to a formal argument as the closing paren of the argument + * list. Rewrite the parens as spaces. + * {@see Issue 4776} + * {@see https://github.com/isagalaev/highlight.js/issues/1324} + */ + if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) { + return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 '); + } + + return line; + }, + + /** * Tells whether the state has exhausted its current section. * @param {!Object} state * @return {boolean} @@ -358,45 +408,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..01e9325 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)); @@ -395,5 +404,41 @@ state = {sectionIndex: 3, lineIndex: 4}; assert.isTrue(element._isSectionDone(state)); }); + + test('workaround CPP LT directive', function() { + // Does nothing to regular line. + var line = 'int main(int argc, char** argv) { return 0; }'; + assert.equal(element._workaround('cpp', line), line); + + // Does nothing to include directive. + line = '#include <stdio>'; + assert.equal(element._workaround('cpp', line), line); + + // Converts left-shift operator in #define. + line = '#define GiB (1ull << 30)'; + var expected = '#define GiB (1ull || 30)'; + assert.equal(element._workaround('cpp', line), expected); + + // Converts less-than operator in #if. + line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)'; + expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)'; + assert.equal(element._workaround('cpp', line), expected); + }); + + test('workaround Java param-annotation', function() { + // Does nothing to regular line. + var line = 'public static void foo(int bar) { }'; + assert.equal(element._workaround('java', line), line); + + // Does nothing to regular annotation. + var line = 'public static void foo(@Nullable int bar) { }'; + assert.equal(element._workaround('java', line), line); + + // Converts parameterized annotation. + line = 'public static void foo(@SuppressWarnings("unused") int bar) { }'; + var expected = 'public static void foo(@SuppressWarnings "unused" ' + + ' int bar) { }'; + assert.equal(element._workaround('java', line), expected); + }); }); </script>
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/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html index e2abc52..f796475 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -59,7 +59,6 @@ } .gr-syntax-comment { color: #af72a9; - font-style: italic; } .gr-syntax-meta { color: #0091AD;
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index c20795b..d62571d 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,18 +15,20 @@ --> <link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../styles/app-theme.html"> <link rel="import" href="./core/gr-error-manager/gr-error-manager.html"> <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> <link rel="import" href="./core/gr-main-header/gr-main-header.html"> <link rel="import" href="./core/gr-router/gr-router.html"> +<link rel="import" href="./core/gr-reporting/gr-reporting.html"> <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> <link rel="import" href="./change/gr-change-view/gr-change-view.html"> <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html"> +<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html"> <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html"> <link rel="import" href="./shared/gr-overlay/gr-overlay.html"> @@ -45,13 +47,14 @@ gr-main-header, footer { color: var(--primary-text-color); - padding: .5rem var(--default-horizontal-margin); } gr-main-header { background-color: var(--header-background-color, #eee); + padding: 0 var(--default-horizontal-margin); } footer { background-color: var(--footer-background-color, #eee); + padding: .5rem var(--default-horizontal-margin); } main { flex: 1; @@ -85,7 +88,8 @@ color: #b71c1c; } </style> - <gr-main-header search-query="{{params.query}}"></gr-main-header> + <gr-main-header id="mainHeader" search-query="{{params.query}}"> + </gr-main-header> <main> <template is="dom-if" if="[[_showChangeListView]]" restamp="true"> <gr-change-list-view @@ -103,7 +107,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 @@ -111,7 +116,9 @@ change-view-state="{{_viewState.changeView}}"></gr-diff-view> </template> <template is="dom-if" if="[[_showSettingsView]]" restamp="true"> - <gr-settings-view></gr-settings-view> + <gr-settings-view + on-account-detail-update="_handleAccountDetailUpdate"> + </gr-settings-view> </template> <div id="errorView" class="errorView" hidden> <div class="errorEmoji">[[_lastError.emoji]]</div> @@ -125,17 +132,26 @@ | <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 view="[[params.view]]" on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog> </gr-overlay> + <gr-overlay id="registration" with-backdrop> + <gr-registration-dialog + on-account-detail-update="_handleAccountDetailUpdate" + on-close="_handleRegistrationDialogClose"> + </gr-registration-dialog> + </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..72f5254 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: [ @@ -59,6 +62,10 @@ Gerrit.KeyboardShortcutBehavior, ], + keyBindings: { + '?': '_showKeyboardShortcuts', + }, + attached: function() { this.$.restAPI.getAccount().then(function(account) { this._account = account; @@ -72,6 +79,7 @@ }, ready: function() { + this.$.reporting.appStarted(); this._viewState = { changeView: { changeNum: null, @@ -104,13 +112,18 @@ this.set('_showChangeView', view === 'gr-change-view'); this.set('_showDiffView', view === 'gr-diff-view'); this.set('_showSettingsView', view === 'gr-settings-view'); + if (this.params.justRegistered) { + this.$.registration.open(); + } }, _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 +139,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 +170,26 @@ } }, + _handleLocationChange: function(e) { + var hash = e.detail.hash.substring(1); + var pathname = e.detail.pathname; + if (pathname.indexOf('/c/') === 0 && parseInt(hash, 10) > 0) { + pathname += '@' + hash; + } + this.set('_path', pathname); + this._handleSearchPageChange(); + }, + + _handleSearchPageChange: function() { + if (!this.params) { + return; + } + 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'; @@ -160,16 +198,25 @@ } }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - if (e.keyCode === 191 && e.shiftKey) { // '/' or '?' with shift key. - this.$.keyboardShortcuts.open(); - } + _showKeyboardShortcuts: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + this.$.keyboardShortcuts.open(); }, _handleKeyboardShortcutDialogClose: function() { this.$.keyboardShortcuts.close(); }, + + _handleAccountDetailUpdate: function(e) { + this.$.mainHeader.reload(); + if (this.params.view === 'gr-settings-view') { + this.$$('gr-settings-view').reloadAccountDetail(); + } + }, + + _handleRegistrationDialogClose: function(e) { + this.params.justRegistered = false; + this.$.registration.close(); + }, }); })();
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html new file mode 100644 index 0000000..d03ab79 --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,98 @@ +<!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(), + }); + stub('gr-rest-api-interface', { + getAccount: function() { return Promise.resolve(null); }, + getConfig: function() { + return Promise.resolve({ + gerrit: {web_uis: ['GWT', 'POLYGERRIT']}, + plugin: {js_resource_paths: []}, + }); + }, + getVersion: function() { return Promise.resolve(42); }, + }); + + 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(done) { + element._path = '/test/path'; + flush(function() { + var gwtLink = element.$$('#gwtLink'); + assert.equal(gwtLink.href, + 'http://' + location.host + '/?polygerrit=0#/test/path'); + done(); + }); + }); + + test('_handleLocationChange handles hashes', function(done) { + var curLocation = { + pathname: '/c/1/1/testfile.txt', + hash: '#2', + host: location.host, + }; + sandbox.stub(element, '_handleSearchPageChange'); + element._handleLocationChange({detail: curLocation}); + + flush(function() { + var gwtLink = element.$$('#gwtLink'); + assert.equal(gwtLink.href, + 'http://' + location.host + '/?polygerrit=0#/c/1/1/testfile.txt@2'); + done(); + }); + }); + + test('sets plugins count', function() { + sandbox.stub(Gerrit, '_setPluginsCount'); + element._loadPlugins([]); + assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0)); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js index 3930a78..2704ce5 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -17,6 +17,12 @@ Polymer({ is: 'gr-account-info', + /** + * Fired when account details are changed. + * + * @event account-detail-update + */ + properties: { mutable: { type: Boolean, @@ -72,6 +78,7 @@ return this.$.restAPI.setAccountName(this._account.name).then(function() { this.hasUnsavedChanges = false; this._saving = false; + this.fire('account-detail-update'); }.bind(this)); },
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html index 0eace7d..e603e8c 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -32,6 +32,9 @@ tbody tr:last-of-type td .move-down-button { display: none; } + td.urlCell { + word-break: break-word; + } .newTitleInput { width: 10em; } @@ -52,7 +55,7 @@ <template is="dom-repeat" items="[[menuItems]]"> <tr> <td>[[item.name]]</td> - <td>[[item.url]]</td> + <td class="urlCell">[[item.url]]</td> <td> <gr-button data-index="[[index]]"
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html new file mode 100644 index 0000000..ee358d5 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -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. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<dom-module id="gr-registration-dialog"> + <template> + <style include="gr-settings-styles"></style> + <style> + :host { + display: block; + } + main { + max-width: 46em; + } + hr { + margin-top: 1em; + margin-bottom: 1em; + } + header { + border-bottom: 1px solid #ddd; + font-weight: bold; + } + header, + main, + footer { + padding: .5em .65em; + } + footer { + display: flex; + justify-content: space-between; + } + </style> + <main class="gr-settings-styles"> + <header>Please confirm your contact information</header> + <main> + <p> + The following contact information was automatically obtained when you + signed in to the site. This information is used to display who you are + to others, and to send updates to code reviews you have either started + or subscribed to. + </p> + <hr> + <section> + <div class="title">Full Name</div> + <input + is="iron-input" + id="name" + bind-value="{{_account.name}}" + disabled="[[_saving]]" + on-keydown="_handleNameKeydown"> + </section> + <section> + <div class="title">Preferred Email</div> + <select + is="gr-select" + id="email" + bind-value="{{_account.email}}" + disabled="[[_saving]]"> + <option value="[[_account.email]]">[[_account.email]]</option> + <template is="dom-repeat" items="[[_account.secondary_emails]]"> + <option value="[[item]]">[[item]]</option> + </template> + </select> + </section> + </main> + <footer> + <gr-button + id="saveButton" + primary + disabled="[[_saving]]" + on-tap="_handleSave">Save</gr-button> + <gr-button + id="closeButton" + disabled="[[_saving]]" + on-tap="_handleClose">Close</gr-button> + </footer> + </main> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-registration-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js new file mode 100644 index 0000000..9acdba9 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-registration-dialog', + + /** + * Fired when account details are changed. + * + * @event account-detail-update + */ + + /** + * Fired when the close button is pressed. + * + * @event close + */ + + properties: { + _account: Object, + _saving: Boolean, + }, + + hostAttributes: { + role: 'dialog', + }, + + attached: function() { + this.$.restAPI.getAccount().then(function(account) { + this._account = account; + }.bind(this)); + }, + + _handleNameKeydown: function(e) { + if (e.keyCode === 13) { // Enter + e.stopPropagation(); + this._save(); + } + }, + + _save: function() { + this._saving = true; + var promises = [ + this.$.restAPI.setAccountName(this.$.name.value), + this.$.restAPI.setPreferredAccountEmail(this.$.email.value), + ]; + return Promise.all(promises).then(function() { + this._saving = false; + this.fire('account-detail-update'); + }.bind(this)); + }, + + _handleSave: function(e) { + e.preventDefault(); + this._save().then(function() { + this.fire('close'); + }.bind(this)); + }, + + _handleClose: function(e) { + e.preventDefault(); + this._saving = true; // disable buttons indefinitely + this.fire('close'); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html new file mode 100644 index 0000000..33f6aed --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -0,0 +1,147 @@ +<!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-registration-dialog</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-registration-dialog.html"> + +<test-fixture id="basic"> + <template> + <gr-registration-dialog></gr-registration-dialog> + </template> +</test-fixture> + +<test-fixture id="blank"> + <template> + <div></div> + </template> +</test-fixture> + +<script> + suite('gr-registration-dialog tests', function() { + var element; + var account; + var _listeners; + + setup(function(done) { + _listeners = {}; + + account = { + name: 'name', + email: 'email', + secondary_emails: [ + 'email2', + 'email3', + ], + }; + + stub('gr-rest-api-interface', { + getAccount: function() { + // Once the account is resolved, we can let the test proceed. + flush(done); + return Promise.resolve(account); + }, + setAccountName: function(name) { + account.name = name; + return Promise.resolve(); + }, + setPreferredAccountEmail: function(email) { + account.email = email; + return Promise.resolve(); + }, + }); + + element = fixture('basic'); + }); + + teardown(function() { + for (var eventType in _listeners) { + if (_listeners.hasOwnProperty(eventType)) { + element.removeEventListener(eventType, _listeners[eventType]); + } + } + }); + + function listen(eventType) { + return new Promise(function(resolve) { + _listeners[eventType] = function() { resolve(); }; + element.addEventListener(eventType, _listeners[eventType]); + }); + } + + function save(opt_action) { + var promise = listen('account-detail-update'); + if (opt_action) { + opt_action(); + } else { + MockInteractions.tap(element.$.saveButton); + } + return promise; + } + + function close(opt_action) { + var promise = listen('close'); + if (opt_action) { + opt_action(); + } else { + MockInteractions.tap(element.$.closeButton); + } + return promise; + } + + test('fires the close event on close', function(done) { + close().then(done); + }); + + test('fires the close event on save', function(done) { + close(function() { + MockInteractions.tap(element.$.saveButton); + }).then(done); + }); + + test('saves name and preferred email', function(done) { + flush(function() { + element.$.name.value = 'new name'; + element.$.email.value = 'email3'; + + // Nothing should be committed yet. + assert.equal(account.name, 'name'); + assert.equal(account.email, 'email'); + + // Save and verify new values are committed. + save().then(function() { + assert.equal(account.name, 'new name'); + assert.equal(account.email, 'email3'); + }).then(done); + }); + }); + + test('pressing enter saves name', function(done) { + element.$.name.value = 'entered name'; + save(function() { + MockInteractions.pressAndReleaseKeyOn(element.$.name, 13); // 'enter' + }).then(function() { + assert.equal(account.name, 'entered name'); + }).then(done); + }); + }); +</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..ab65a51 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> @@ -211,7 +210,17 @@ </span> </section> <section> - <span class="title">Columns</span> + <span class="title">Fit to Screen</span> + <span class="value"> + <input + id="lineWrapping" + type="checkbox" + checked$="[[_diffPrefs.line_wrapping]]" + on-change="_handleLineWrappingChanged"> + </span> + </section> + <section id="columnsPref" hidden$="[[_diffPrefs.line_wrapping]]"> + <span class="title">Diff Width</span> <span class="value"> <input is="iron-input" @@ -232,6 +241,17 @@ bind-value="{{_diffPrefs.tab_size}}"> </span> </section> + <section hidden$="[[!_diffPrefs.font_size]]"> + <span class="title">Font Size</span> + <span class="value"> + <input + is="iron-input" + type="number" + prevent-invalid-input + allowed-pattern="[0-9]" + bind-value="{{_diffPrefs.font_size}}"> + </span> + </section> <section> <span class="title">Show Tabs</span> <span class="value"> @@ -243,6 +263,16 @@ </span> </section> <section> + <span class="title">Show Trailing Whitespace</span> + <span class="value"> + <input + id="showTrailingWhitespace" + type="checkbox" + checked$="[[_diffPrefs.show_whitespace_errors]]" + on-change="_handleShowTrailingWhitespaceChanged"> + </span> + </section> + <section> <span class="title">Syntax Highlighting</span> <span class="value"> <input
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js index 6c62408..34ef958 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -134,6 +134,13 @@ this.unlisten(window, 'scroll', '_handleBodyScroll'); }, + reloadAccountDetail: function() { + Promise.all([ + this.$.accountInfo.loadData(), + this.$.emailEditor.loadData(), + ]); + }, + _handleBodyScroll: function(e) { if (this._headerHeight === undefined) { var top = this.$.settingsNav.offsetTop; @@ -199,10 +206,19 @@ }.bind(this)); }, + _handleLineWrappingChanged: function() { + this.set('_diffPrefs.line_wrapping', this.$.lineWrapping.checked); + }, + _handleShowTabsChanged: function() { this.set('_diffPrefs.show_tabs', this.$.showTabs.checked); }, + _handleShowTrailingWhitespaceChanged: function() { + this.set('_diffPrefs.show_whitespace_errors', + this.$.showTrailingWhitespace.checked); + }, + _handleSyntaxHighlightingChanged: function() { this.set('_diffPrefs.syntax_highlighting', this.$.syntaxHighlighting.checked);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html index 4e98b43..b9b049a 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -92,8 +92,10 @@ diffPreferences = { context: 10, tab_size: 8, + font_size: 12, line_length: 100, cursor_blink_rate: 0, + line_wrapping: false, intraline_difference: true, show_line_endings: true, show_tabs: true, @@ -188,12 +190,18 @@ // Rendered with the expected preferences selected. assert.equal(valueOf('Context', 'diffPreferences') .firstElementChild.bindValue, diffPreferences.context); - assert.equal(valueOf('Columns', 'diffPreferences') + assert.equal(valueOf('Diff Width', 'diffPreferences') .firstElementChild.bindValue, diffPreferences.line_length); assert.equal(valueOf('Tab Width', 'diffPreferences') .firstElementChild.bindValue, diffPreferences.tab_size); + assert.equal(valueOf('Font Size', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.font_size); assert.equal(valueOf('Show Tabs', 'diffPreferences') .firstElementChild.checked, diffPreferences.show_tabs); + assert.equal(valueOf('Show Trailing Whitespace', 'diffPreferences') + .firstElementChild.checked, diffPreferences.show_whitespace_errors); + assert.equal(valueOf('Fit to Screen', 'diffPreferences') + .firstElementChild.checked, diffPreferences.line_wrapping); assert.isFalse(element._diffPrefsChanged); @@ -218,6 +226,16 @@ }); }); + test('columns input is hidden with fit to scsreen is selected', function() { + assert.isFalse(element.$.columnsPref.hidden); + + MockInteractions.tap(element.$.lineWrapping); + assert.isTrue(element.$.columnsPref.hidden); + + MockInteractions.tap(element.$.lineWrapping); + assert.isFalse(element.$.columnsPref.hidden); + }); + test('menu', function(done) { assert.isFalse(element._menuChanged); assert.isFalse(element._prefsChanged);
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html index 28de9d4..96d1414 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -109,6 +109,7 @@ <span class="value"> <iron-autogrow-textarea id="newKey" + autocomplete="on" bind-value="{{_newKey}}" placeholder="New SSH Key"></iron-autogrow-textarea> </span>
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..496de03 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,27 @@ padding: 0; text-decoration: none; } + :host:focus { + border-color: transparent; + box-shadow: none; + outline: none; + } + :host:focus .container, + :host:focus gr-button { + background: #ccc; + } + .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 + id="remove" 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..33fc50e 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
@@ -18,6 +18,19 @@ Polymer({ is: 'gr-account-chip', + /** + * Fired to indicate a key was pressed while this chip was focused. + * + * @event account-chip-keydown + */ + + /** + * Fired to indicate this chip should be removed, i.e. when the x button is + * clicked or when the remove function is called. + * + * @event remove + */ + properties: { account: Object, removable: { @@ -28,6 +41,10 @@ type: Boolean, reflectToAttribute: true, }, + transparentBackground: { + type: Boolean, + value: false, + }, }, ready: function() { @@ -36,6 +53,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..1d2beab 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
@@ -42,10 +42,11 @@ <span class="text"> <span>[[account.name]]</span> <span hidden$="[[!_computeShowEmail(showEmail, account)]]"> - ([[account.email]]) + [[_computeEmailStr(account)]] </span> </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..b9aea66 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) + '>'; } @@ -41,5 +44,15 @@ _computeShowEmail: function(showEmail, account) { return !!(showEmail && account && account.email); }, + + _computeEmailStr: function(account) { + if (!account || !account.email) { + return ''; + } + if (account.name) { + return '(' + account.email + ')'; + } + return account.email; + }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html index eacd710..5f94658 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -35,9 +35,23 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + getLoggedIn: function() { return Promise.resolve(false); }, + }); element = fixture('basic'); }); + test('null guard', function() { + assert.doesNotThrow(function() { + element.account = null; + }); + }); + + test('missing email', function() { + assert.equal('', element._computeEmailStr({name: 'foo'})); + }); + test('computed fields', function() { assert.equal(element._computeAccountTitle( { @@ -67,6 +81,10 @@ assert.equal(element._computeShowEmail( false, undefined), false); + + assert.equal( + element._computeEmailStr({name: 'test', email: 'test'}), '(test)'); + assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test'); }); });
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..e111d9b 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -20,7 +20,6 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../scripts/util.js"></script> <link rel="import" href="gr-account-link.html"> @@ -35,6 +34,9 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); element = fixture('basic'); }); @@ -48,6 +50,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..d7017ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -14,6 +14,7 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> @@ -55,11 +56,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 @@ -72,6 +73,7 @@ id="cursor" index="{{_index}}" cursor-target-class="selected" + scroll-behavior="keep-visible" stops="[[_getSuggestionElems(_suggestions)]]"></gr-cursor-manager> </template> <script src="gr-autocomplete.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js index 0fc6b07..863b85a 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', @@ -29,6 +31,13 @@ * @event cancel */ + /** + * Fired on keydown to allow for custom hooks into autocomplete textbox + * behavior. + * + * @event input-keydown + */ + properties: { /** @@ -76,6 +85,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,14 +117,19 @@ value: false, }, + _focused: { + type: Boolean, + value: false, + }, + }, attached: function() { - this.listen(document.body, 'click', '_handleBodyClick'); + this.listen(document.body, 'tap', '_handleBodyTap'); }, detached: function() { - this.unlisten(document.body, 'click', '_handleBodyClick'); + this.unlisten(document.body, 'tap', '_handleBodyTap'); }, get focusStart() { @@ -117,6 +140,10 @@ this.$.input.focus(); }, + selectAll: function() { + this.$.input.setSelectionRange(0, this.$.input.value.length); + }, + clear: function() { this.text = ''; }, @@ -131,6 +158,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 +185,8 @@ }.bind(this)); }, - _computeSuggestionsHidden: function(suggestions) { - return !suggestions.length; + _computeSuggestionsHidden: function(suggestions, focused) { + return !(suggestions.length && focused); }, _computeClass: function(borderless) { @@ -167,6 +199,7 @@ }, _handleInputKeydown: function(e) { + this._focused = true; switch (e.keyCode) { case 38: // Up e.preventDefault(); @@ -181,12 +214,17 @@ 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; } + this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); }, _cancel: function() { @@ -199,34 +237,45 @@ 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; } }, - _handleBodyClick: function(e) { + _handleBodyTap: 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) { + e.stopPropagation(); 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 +291,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..394b2c6 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() { @@ -93,8 +94,7 @@ var cancelHandler = sinon.spy(); element.addEventListener('cancel', cancelHandler); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 27); // Esc - + MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, '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() { @@ -128,19 +128,22 @@ assert.equal(element.$.cursor.index, 0); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down + MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, + 'down'); assert.equal(element.$.cursor.index, 1); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 40); // Down + MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, + 'down'); assert.equal(element.$.cursor.index, 2); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 38); // Up + MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up'); assert.equal(element.$.cursor.index, 1); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); assert.equal(element.value, 1); assert.isTrue(commitHandler.called); @@ -163,7 +166,8 @@ var commitHandler = sinon.spy(); element.addEventListener('commit', commitHandler); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); assert.isTrue(commitHandler.called); assert.equal(element.text, 'suggestion'); @@ -184,7 +188,8 @@ var commitHandler = sinon.spy(); element.addEventListener('commit', commitHandler); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); assert.isTrue(commitHandler.called); assert.equal(element.text, ''); @@ -234,12 +239,77 @@ var commitHandler = sinon.spy(); element.addEventListener('commit', commitHandler); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); assert.isTrue(commitHandler.called); assert.equal(element.text, 'blah 0'); done(); }); }); + + test('tab key completes only when suggestions exist', function() { + var commitStub = sinon.stub(element, '_commit'); + element._suggestions = []; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isFalse(commitStub.called); + element._suggestions = ['tunnel snakes rule!']; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isTrue(commitStub.called); + commitStub.restore(); + }); + + 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, null, 'tab'); + assert.isFalse(commitHandler.called); + element.tabCompleteWithoutCommit = false; + element._suggestions = ['tunnel snakes rule!']; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isTrue(commitHandler.called); + }); + + test('_focused flag properly triggered', function(done) { + flush(function() { + assert.isFalse(element._focused); + var input = element.$$('input'); + MockInteractions.focus(input); + assert.isTrue(element._focused); + done(); + }); + }); + + 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')); + assert.isTrue(element._focused); + focusSpy.restore(); + commitSpy.restore(); + }); + + test('input-keydown event fired', function() { + var listener = sinon.spy(); + element.addEventListener('input-keydown', listener); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + flushAsynchronousOperations(); + assert.isTrue(listener.called); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html index ae514ba..5ab27af 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -34,6 +34,9 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); element = fixture('basic'); });
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..c8dfea3 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -16,13 +16,13 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <dom-module id="gr-button"> <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; @@ -68,38 +75,58 @@ } :host([disabled]) { cursor: default; - pointer-events: none; } :host([loading]), :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-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js index e109896..7e91b0e 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -29,6 +29,11 @@ }, }, + listeners: { + 'tap': '_handleAction', + 'click': '_handleAction', + }, + behaviors: [ Gerrit.KeyboardShortcutBehavior, Gerrit.TooltipBehavior, @@ -39,6 +44,17 @@ tabindex: '0', }, + keyBindings: { + 'space enter': '_handleCommitKey', + }, + + _handleAction: function(e) { + if (this.disabled) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + }, + _disabledChanged: function(disabled) { if (disabled) { this._enabledTabindex = this.getAttribute('tabindex'); @@ -46,13 +62,9 @@ this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex); }, - _handleKey: function(e) { - switch (e.keyCode) { - case 32: // 'spacebar' - case 13: // 'enter' - e.preventDefault(); - this.click(); - } + _handleCommitKey: function(e) { + e.preventDefault(); + this.click(); }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html new file mode 100644 index 0000000..70cf636 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -0,0 +1,93 @@ +<!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-button</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-button.html"> + +<test-fixture id="basic"> + <template> + <gr-button></gr-button> + </template> +</test-fixture> + +<script> + suite('gr-select tests', function() { + var element; + var sandbox; + + var addSpyOn = function(eventName) { + var spy = sandbox.spy(); + element.addEventListener(eventName, spy); + return spy; + }; + + setup(function() { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); + + ['tap', 'click'].forEach(function(eventName) { + test('dispatches ' + eventName + ' event', function() { + var spy = addSpyOn(eventName); + MockInteractions.tap(element); + assert.isTrue(spy.calledOnce); + }); + }); + + // Keycodes: 32 for Space, 13 for Enter. + [32, 13].forEach(function(key) { + test('dispatches tap event on keycode ' + key, function() { + var tapSpy = sandbox.spy(); + element.addEventListener('tap', tapSpy); + MockInteractions.pressAndReleaseKeyOn(element, key); + assert.isTrue(tapSpy.calledOnce); + })}); + + suite('disabled', function() { + setup(function() { + element.disabled = true; + }); + + ['tap', 'click'].forEach(function(eventName) { + test('stops ' + eventName + ' event', function() { + var spy = addSpyOn(eventName); + MockInteractions.tap(element); + assert.isFalse(spy.called); + }); + }); + + // Keycodes: 32 for Space, 13 for Enter. + [32, 13].forEach(function(key) { + test('stops tap event on keycode ' + key, function() { + var tapSpy = sandbox.spy(); + element.addEventListener('tap', tapSpy); + MockInteractions.pressAndReleaseKeyOn(element, key); + assert.isFalse(tapSpy.called); + })}); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html index d8fc1df..54fddf5 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -22,11 +22,23 @@ <style> :host { display: block; + max-height: 90vh; + } + .container { + display: flex; + flex-direction: column; + max-height: 90vh; } header { border-bottom: 1px solid #ddd; + flex-shrink: 0; font-weight: bold; } + main { + display: flex; + flex-shrink: 1; + width: 100%; + } header, main, footer { @@ -34,15 +46,20 @@ } footer { display: flex; + flex-shrink: 0; justify-content: space-between; } </style> - <header><content select=".header"></content></header> - <main><content select=".main"></content></main> - <footer> - <gr-button primary on-tap="_handleConfirmTap">[[confirmLabel]]</gr-button> - <gr-button on-tap="_handleCancelTap">Cancel</gr-button> - </footer> + <div class="container"> + <header><content select=".header"></content></header> + <main><content select=".main"></content></main> + <footer> + <gr-button primary on-tap="_handleConfirmTap"> + [[confirmLabel]] + </gr-button> + <gr-button on-tap="_handleCancelTap">Cancel</gr-button> + </footer> + </div> </template> <script src="gr-confirm-dialog.js"></script> </dom-module>
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..81f5186 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
@@ -59,19 +59,10 @@ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond * the viewport. */ - scroll: { + scrollBehavior: { 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() { @@ -117,6 +108,10 @@ } }, + setCursorAtIndex: function(index) { + this.setCursor(this.stops[index]); + }, + /** * Move the cursor forward or backward by delta. Noop if moving past either * end of the stop list. @@ -203,7 +198,9 @@ }, _scrollToTarget: function() { - if (!this.target || this.scroll === ScrollBehavior.NEVER) { return; } + if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { + return; + } // Calculate where the element is relative to the window. var top = this.target.offsetTop; @@ -213,8 +210,8 @@ top += offsetParent.offsetTop; } - if (this.scroll === ScrollBehavior.KEEP_VISIBLE && - top > window.pageYOffset + this.foldOffsetTop && + if (this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && + 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-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html new file mode 100644 index 0000000..55209af --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -0,0 +1,116 @@ +<!-- +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="../../../bower_components/iron-dropdown/iron-dropdown.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-dropdown"> + <template> + <style> + :host { + display: inline-block; + } + .dropdown-trigger { + text-decoration: none; + } + .dropdown-content { + background-color: #fff; + box-shadow: 0 1px 5px rgba(0, 0, 0, .3); + } + button { + background: none; + border: none; + font: inherit; + padding: .3em 0; + } + gr-avatar { + height: 2em; + width: 2em; + vertical-align: middle; + } + gr-button[link] { + padding: 1em 0; + } + ul { + list-style: none; + } + ul .accountName { + font-weight: bold; + } + li .accountInfo, + li a { + display: block; + padding: .85em 1em; + } + li a:link, + li a:visited { + color: #00e; + text-decoration: none; + } + li a:hover { + background-color: #6B82D6; + color: #fff; + } + .topContent { + display: block; + padding: .85em 1em; + } + .bold-text { + font-weight: bold; + } + </style> + <gr-button link class="dropdown-trigger" id="trigger" + on-tap="_showDropdownTapHandler"> + <content></content> + </gr-button> + <iron-dropdown id="dropdown" + vertical-align="top" + vertical-offset="40" + allow-outside-scroll="true" + horizontal-align="[[horizontalAlign]]" + on-tap="_handleDropdownTap"> + <div class="dropdown-content"> + <ul> + <template is="dom-if" if="[[topContent]]"> + <div class="topContent"> + <template + is="dom-repeat" + items="[[topContent]]" + as="item" + initial-count="75"> + <div class$="[[_getClassIfBold(item.bold)]] top-item"> + [[item.text]] + </div> + </template> + </div> + </template> + <template + is="dom-repeat" + items="[[items]]" + as="link" + initial-count="75"> + <li><a href$="[[_computeRelativeURL(link.url)]]">[[link.name]]</a> + </li> + </template> + </ul> + </div> + </iron-dropdown> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-dropdown.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js new file mode 100644 index 0000000..d10d219 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-dropdown', + + properties: { + items: Array, + topContent: Object, + horizontalAlign: { + type: String, + value: 'left', + }, + _hasAvatars: String, + }, + + attached: function() { + this.$.restAPI.getConfig().then(function(cfg) { + this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + }.bind(this)); + }, + + _handleDropdownTap: function(e) { + this.$.dropdown.close(); + }, + + _showDropdownTapHandler: function(e) { + this.$.dropdown.open(); + }, + + _getClassIfBold: function(bold) { + return bold ? 'bold-text' : ''; + }, + + _computeURLHelper: function(host, path) { + return '//' + host + path; + }, + + _computeRelativeURL: function(path) { + var host = window.location.host; + return this._computeURLHelper(host, path); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html new file mode 100644 index 0000000..b2f2d21 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -0,0 +1,74 @@ +<!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-dropdown</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-dropdown.html"> + +<test-fixture id="basic"> + <template> + <gr-dropdown></gr-dropdown> + </template> +</test-fixture> + +<script> + suite('gr-dropdown tests', function() { + var element; + + setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + }); + + test('tap on trigger opens menu', function() { + assert.isFalse(element.$.dropdown.opened); + MockInteractions.tap(element.$.trigger); + assert.isTrue(element.$.dropdown.opened); + }); + + test('_computeRelativeURL', function() { + var path = '/test'; + var host = 'http://www.testsite.com'; + var computedPath = element._computeURLHelper(host, path); + assert.equal(computedPath, '//http://www.testsite.com/test'); + }); + + test('_getClassIfBold', function() { + var bold = true; + assert.equal(element._getClassIfBold(bold), 'bold-text'); + + bold = false; + assert.equal(element._getClassIfBold(bold), ''); + }); + + test('Top text exists and is bolded correctly', function() { + element.topContent = [{text: 'User', bold: true}, {text: 'email'}]; + flushAsynchronousOperations(); + var topItems = Polymer.dom(element.root).querySelectorAll('.top-item'); + assert.equal(topItems.length, 2); + assert.isTrue(topItems[0].classList.contains('bold-text')); + assert.isFalse(topItems[1].classList.contains('bold-text')); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html index 5b49dcc..bd87db3 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
@@ -43,6 +43,7 @@ </div> <div class="editor" hidden$="[[!editing]]"> <iron-autogrow-textarea + autocomplete="on" bind-value="{{_newContent}}" disabled="[[disabled]]"></iron-autogrow-textarea> <div class="editButtons">
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..9b6c572 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,21 @@ <dom-module id="gr-editable-label"> <template> <style> + :host { + align-items: center; + display: inline-flex; + } + input, + label, + .container { + 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; @@ -38,16 +45,16 @@ text-decoration: underline; } </style> - <input - is="iron-input" - id="input" - hidden$="[[!editing]]" - on-keydown="_handleInputKeydown" - bind-value="{{_inputText}}"> - <label - hidden$="[[editing]]" - class$="[[_computeLabelClass(readOnly, value, placeholder)]]" - on-tap="_open">[[_computeLabel(value, placeholder)]]</label> + <input + is="iron-input" + id="input" + hidden$="[[!editing]]" + on-keydown="_handleInputKeydown" + bind-value="{{_inputText}}"> + <label + hidden$="[[editing]]" + class$="[[_computeLabelClass(readOnly, value, placeholder)]]" + on-tap="_open">[[_computeLabel(value, placeholder)]]</label> </template> <script src="gr-editable-label.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html new file mode 100644 index 0000000..11560f1 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -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. +--> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-linked-text/gr-linked-text.html"> + +<dom-module id="gr-formatted-text"> + <template> + <style> + :host { + display: block; + font-family: var(--font-family); + } + p, + ul, + blockquote, + gr-linked-text.pre { + margin: 0 0 1.4em 0; + } + blockquote { + border-left: 1px solid #aaa; + padding: 0 .7em; + } + li { + margin-left: 1.4em; + } + gr-linked-text.pre { + font-family: var(--monospace-font-family); + } + </style> + <div id="container"></div> + </template> + <script src="gr-formatted-text.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js new file mode 100644 index 0000000..1b129a7 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -0,0 +1,266 @@ +// 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 QUOTE_MARKER_PATTERN = /\n\s?>\s/g; + + Polymer({ + is: 'gr-formatted-text', + + properties: { + content: String, + config: Object, + }, + + observers: [ + '_contentOrConfigChanged(content, config)', + ], + + /** + * Get the plain text as it appears in the generated DOM. + * + * This differs from the `content` property in that it will not include + * formatting markers such as > characters to make quotes or * and - markers + * to make list items. + * + * @return {string} + */ + getTextContent: function() { + return this._blocksToText(this._computeBlocks(this.content)); + }, + + /** + * Given a source string, update the DOM inside #container. + */ + _contentOrConfigChanged: function(content) { + var container = Polymer.dom(this.$.container); + + // Remove existing content. + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + // Add new content. + this._computeNodes(this._computeBlocks(content)).forEach(function(node) { + container.appendChild(node); + }); + }, + + /** + * Given a source string, parse into an array of block objects. Each block + * has a `type` property which takes any of the follwoing values. + * * 'paragraph' + * * 'quote' (Block quote.) + * * 'pre' (Pre-formatted text.) + * * 'list' (Unordered list.) + * + * For blocks of type 'paragraph' and 'pre' there is a `text` property that + * maps to a string of the block's content. + * + * For blocks of type 'list', there is an `items` property that maps to a + * list of strings representing the list items. + * + * For blocks of type 'quote', there is a `blocks` property that maps to a + * list of blocks contained in the quote. + * + * NOTE: Strings appearing in all block objects are NOT escaped. + * + * @param {string} content + * @return {!Array<!Object>} + */ + _computeBlocks: function(content) { + if (!content) { return []; } + + var result = []; + var split = content.split('\n\n'); + var p; + + for (var i = 0; i < split.length; i++) { + p = split[i]; + if (!p.length) { continue; } + + if (this._isQuote(p)) { + result.push(this._makeQuote(p)); + } else if (this._isPreFormat(p)) { + result.push({type: 'pre', text: p}); + } else if (this._isList(p)) { + this._makeList(p, result); + } else { + result.push({type: 'paragraph', text: p}); + } + } + return result; + }, + + /** + * Take a block of comment text that contains a list and potentially + * a paragraph (but does not contain blank lines), generate appropriate + * block objects and append them to the output list. + * + * In simple cases, this will generate a single list block. For example, on + * the following input. + * + * * Item one. + * * Item two. + * * item three. + * + * However, if the list starts with a paragraph, it will need to also + * generate that paragraph. Consider the following input. + * + * A bit of text describing the context of the list: + * * List item one. + * * List item two. + * * Et cetera. + * + * In this case, `_makeList` generates a paragraph block object + * containing the non-bullet-prefixed text, followed by a list block. + * + * @param {!string} p The block containing the list (as well as a + * potential paragraph). + * @param {!Array<!Object>} out The list of blocks to append to. + */ + _makeList: function(p, out) { + var block = null; + var inList = false; + var inParagraph = false; + var lines = p.split('\n'); + var line; + + for (var i = 0; i < lines.length; i++) { + line = lines[i]; + + if (line[0] === '-' || line[0] === '*') { + // The next line looks like a list item. If not building a list + // already, then create one. Remove the list item marker (* or -) from + // the line. + if (!inList) { + if (inParagraph) { + // Add the finished paragraph block to the result. + inParagraph = false; + out.push(block); + } + inList = true; + block = {type: 'list', items: []}; + } + line = line.substring(1).trim(); + } else if (!inList) { + // Otherwise, if a list has not yet been started, but the next line + // does not look like a list item, then add the line to a paragraph + // block. If a paragraph block has not yet been started, then create + // one. + if (!inParagraph) { + inParagraph = true; + block = {type: 'paragraph', text: ''}; + } else { + block.text += ' '; + } + block.text += line; + continue; + } + block.items.push(line); + } + if (block != null) { + out.push(block); + } + }, + + _makeQuote: function(p) { + var quotedLines = p + .split('\n') + .map(function(l) { return l.replace(/^[ ]?>[ ]?/, ''); }) + .join('\n'); + return { + type: 'quote', + blocks: this._computeBlocks(quotedLines), + }; + }, + + _isQuote: function(p) { + return p.indexOf('> ') === 0 || p.indexOf(' > ') === 0; + }, + + _isPreFormat: function(p) { + return p.indexOf('\n ') !== -1 || p.indexOf('\n\t') !== -1 || + p.indexOf(' ') === 0 || p.indexOf('\t') === 0; + }, + + _isList: function(p) { + return p.indexOf('\n- ') !== -1 || p.indexOf('\n* ') !== -1 || + p.indexOf('- ') === 0 || p.indexOf('* ') === 0; + }, + + _makeLinkedText: function(content, isPre) { + var text = document.createElement('gr-linked-text'); + text.config = this.config; + text.content = content; + text.pre = true; + if (isPre) { + text.classList.add('pre'); + } + return text; + }, + + /** + * Map an array of block objects to an array of DOM nodes. + * @param {!Array<!Object>} blocks + * @return {!Array<!HTMLElement>} + */ + _computeNodes: function(blocks) { + return blocks.map(function(block) { + if (block.type === 'paragraph') { + var p = document.createElement('p'); + p.appendChild(this._makeLinkedText(block.text)); + return p; + } + + if (block.type === 'quote') { + var bq = document.createElement('blockquote'); + this._computeNodes(block.blocks).forEach(function(node) { + bq.appendChild(node); + }); + return bq; + } + + if (block.type === 'pre') { + return this._makeLinkedText(block.text, true); + } + + if (block.type === 'list') { + var ul = document.createElement('ul'); + block.items.forEach(function(item) { + var li = document.createElement('li'); + li.appendChild(this._makeLinkedText(item)); + ul.appendChild(li); + }.bind(this)); + return ul; + } + }.bind(this)); + }, + + _blocksToText: function(blocks) { + return blocks.map(function(block) { + if (block.type === 'paragraph' || block.type === 'pre') { + return block.text; + } + if (block.type === 'quote') { + return this._blocksToText(block.blocks); + } + if (block.type === 'list') { + return block.items.join('\n'); + } + }.bind(this)).join('\n\n'); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html new file mode 100644 index 0000000..1477d43 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -0,0 +1,358 @@ +<!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-editable-label</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-formatted-text.html"> + +<test-fixture id="basic"> + <template> + <gr-formatted-text></gr-formatted-text> + </template> +</test-fixture> + +<script> + suite('gr-formatted-text tests', function() { + var element; + + function assertBlock(result, index, type, text) { + assert.equal(result[index].type, type); + assert.equal(result[index].text, text); + } + + function assertListBlock(result, resultIndex, itemIndex, text) { + assert.equal(result[resultIndex].type, 'list'); + assert.equal(result[resultIndex].items[itemIndex], text); + } + + setup(function() { + element = fixture('basic'); + }); + + test('parse null undefined and empty', function() { + assert.lengthOf(element._computeBlocks(null), 0); + assert.lengthOf(element._computeBlocks(undefined), 0); + assert.lengthOf(element._computeBlocks(''), 0); + }); + + test('parse simple', function() { + var comment = 'Para1'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'paragraph', comment); + }); + + test('parse multiline para', function() { + var comment = 'Para 1\nStill para 1'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'paragraph', comment); + }); + + test('parse para break', function() { + var comment = 'Para 1\n\nPara 2\n\nPara 3'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'Para 1'); + assertBlock(result, 1, 'paragraph', 'Para 2'); + assertBlock(result, 2, 'paragraph', 'Para 3'); + }); + + test('parse quote', function() { + var comment = '> Quote text'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); + }); + + test('parse quote lead space', function() { + var comment = ' > Quote text'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); + }); + + test('parse excludes empty', function() { + var comment = 'Para 1\n\n\n\nPara 2'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'Para 1'); + assertBlock(result, 1, 'paragraph', 'Para 2'); + }); + + test('parse multiline quote', function() { + var comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', + 'Quote line 1\nQuote line 2\nQuote line 3\n'); + }); + + test('parse pre', function() { + var comment = ' Four space indent.'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse one space pre', function() { + var comment = ' One space indent.\n Another line.'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse tab pre', function() { + var comment = '\tOne tab indent.\n\tAnother line.\n Yet another!'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse intermediate leading whitespace pre', function() { + var comment = 'No indent.\n\tNonzero indent.\nNo indent again.'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse star list', function() { + var comment = '* Item 1\n* Item 2\n* Item 3'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + }); + + test('parse dash list', function() { + var comment = '- Item 1\n- Item 2\n- Item 3'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + }); + + test('parse mixed list', function() { + var comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + assertListBlock(result, 0, 3, 'Item 4'); + }); + + test('parse mixed block types', function() { + var comment = 'Paragraph\nacross\na\nfew\nlines.' + + '\n\n' + + '> Quote\n> across\n> not many lines.' + + '\n\n' + + 'Another paragraph' + + '\n\n' + + '* Series\n* of\n* list\n* items' + + '\n\n' + + 'Yet another paragraph' + + '\n\n' + + '\tPreformatted text.' + + '\n\n' + + 'Parting words.'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 7); + assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.'); + + assert.equal(result[1].type, 'quote'); + assert.lengthOf(result[1].blocks, 1); + assertBlock(result[1].blocks, 0, 'paragraph', + 'Quote\nacross\nnot many lines.'); + + assertBlock(result, 2, 'paragraph', 'Another paragraph'); + assertListBlock(result, 3, 0, 'Series'); + assertListBlock(result, 3, 1, 'of'); + assertListBlock(result, 3, 2, 'list'); + assertListBlock(result, 3, 3, 'items'); + assertBlock(result, 4, 'paragraph', 'Yet another paragraph'); + assertBlock(result, 5, 'pre', '\tPreformatted text.'); + assertBlock(result, 6, 'paragraph', 'Parting words.'); + }); + + test('bullet list 1', function() { + var comment = 'A\n\n* line 1\n* 2nd line'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + }); + + test('bullet list 2', function() { + var comment = 'A\n\n* line 1\n* 2nd line\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('bullet list 3', function() { + var comment = '* line 1\n* 2nd line\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertListBlock(result, 0, 0, 'line 1'); + assertListBlock(result, 0, 1, '2nd line'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('bullet list 4', function() { + var comment = 'To see this bug, you have to:\n' + + '* Be on IMAP or EAS (not on POP)\n' + + '* Be very unlucky\n'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); + assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); + assertListBlock(result, 1, 1, 'Be very unlucky'); + }); + + test('bullet list 5', function() { + var comment = 'To see this bug,\n' + + 'you have to:\n' + + '* Be on IMAP or EAS (not on POP)\n' + + '* Be very unlucky\n'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); + assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); + assertListBlock(result, 1, 1, 'Be very unlucky'); + }); + + test('dash list 1', function() { + var comment = 'A\n\n- line 1\n- 2nd line'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + }); + + test('dash list 2', function() { + var comment = 'A\n\n- line 1\n- 2nd line\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('dash list 3', function() { + var comment = '- line 1\n- 2nd line\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertListBlock(result, 0, 0, 'line 1'); + assertListBlock(result, 0, 1, '2nd line'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('pre format 1', function() { + var comment = 'A\n\n This is pre\n formatted'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' This is pre\n formatted'); + }); + + test('pre format 2', function() { + var comment = 'A\n\n This is pre\n formatted\n\nbut this is not'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' This is pre\n formatted'); + assertBlock(result, 2, 'paragraph', 'but this is not'); + }); + + test('pre format 3', function() { + var comment = 'A\n\n Q\n <R>\n S\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' Q\n <R>\n S'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('pre format 4', function() { + var comment = ' Q\n <R>\n S\n\nB'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'pre', ' Q\n <R>\n S'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('quote 1', function() { + var comment = '> I\'m happy\n > with quotes!\n\nSee above.'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!'); + assertBlock(result, 1, 'paragraph', 'See above.'); + }); + + test('quote 2', function() { + var comment = 'See this said:\n\n > a quoted\n > string block\n\nOK?'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'See this said:'); + assert.equal(result[1].type, 'quote'); + assert.lengthOf(result[1].blocks, 1); + assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block'); + assertBlock(result, 2, 'paragraph', 'OK?'); + }); + + test('nested quotes', function() { + var comment = ' > > prior\n > \n > next\n'; + var result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 2); + assert.equal(result[0].blocks[0].type, 'quote'); + assert.lengthOf(result[0].blocks[0].blocks, 1); + assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior'); + assertBlock(result[0].blocks, 1, 'paragraph', 'next\n'); + }); + + test('getTextContent', function() { + var comment = 'Paragraph\n\n pre\n\n* List\n* Of\n* Items\n\n> Quote'; + element.content = comment; + var result = element.getTextContent(); + var expected = 'Paragraph\n\n pre\n\nList\nOf\nItems\n\nQuote'; + assert.equal(result, expected); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js index f7c337b..72c7f6e 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -33,6 +33,11 @@ }); }; + GrChangeActionsInterface.prototype.setActionHidden = function(type, key, + hidden) { + return this._el.setActionHidden(type, key, hidden); + }; + GrChangeActionsInterface.prototype.add = function(type, label) { return this._el.addActionButton(type, label); };
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html index 4919a5a..93e676c 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -119,5 +119,22 @@ }); }); }); + + test('hide action buttons', function(done) { + var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(function() { + var button = element.$$('[data-action-key="' + key + '"]'); + assert.isOk(button); + assert.isFalse(button.hasAttribute('hidden')); + changeActions.setActionHidden(changeActions.ActionType.REVISION, key, + true); + flush(function() { + var button = element.$$('[data-action-key="' + key + '"]'); + assert.isOk(button); + assert.isTrue(button.hasAttribute('hidden')); + done(); + }); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html index 2e5aa56..304982c 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -42,6 +42,7 @@ setup(function() { stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, getAccount: function() { return Promise.resolve(null); }, }); element = fixture('basic');
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..34ca728 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
@@ -19,8 +19,10 @@ LABEL_CHANGE: 'labelchange', SHOW_CHANGE: 'showchange', SUBMIT_CHANGE: 'submitchange', + COMMIT_MSG_EDIT: 'commitmsgedit', COMMENT: 'comment', REVERT: 'revert', + POST_REVERT: 'postrevert', }; var Element = { @@ -46,23 +48,26 @@ EventType: EventType, handleEvent: function(type, detail) { - switch (type) { - case EventType.HISTORY: - this._handleHistory(detail); - break; - case EventType.SHOW_CHANGE: - this._handleShowChange(detail); - break; - case EventType.COMMENT: - this._handleComment(detail); - break; - case EventType.LABEL_CHANGE: - this._handleLabelChange(detail); - break; - default: - console.warn('handleEvent called with unsupported event type:', type); - break; - } + Gerrit.awaitPluginsLoaded().then(function() { + switch (type) { + case EventType.HISTORY: + this._handleHistory(detail); + break; + case EventType.SHOW_CHANGE: + this._handleShowChange(detail); + break; + case EventType.COMMENT: + this._handleComment(detail); + break; + case EventType.LABEL_CHANGE: + this._handleLabelChange(detail); + break; + default: + console.warn('handleEvent called with unsupported event type:', + type); + break; + } + }.bind(this)); }, addElement: function(key, el) { @@ -80,11 +85,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); } @@ -129,6 +134,18 @@ }); }, + handleCommitMessage: function(change, msg) { + this._getEventCallbacks(EventType.COMMIT_MSG_EDIT).forEach( + function(cb) { + try { + cb(change, msg); + } catch (err) { + console.error(err); + } + } + ); + }, + _handleComment: function(detail) { this._getEventCallbacks(EventType.COMMENT).forEach(function(cb) { try { @@ -149,15 +166,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..e742e1c 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
@@ -45,6 +45,7 @@ }); element = fixture('basic'); errorStub = sinon.stub(console, 'error'); + Gerrit._setPluginsCount(1); Gerrit.install(function(p) { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); }); @@ -75,10 +76,7 @@ test('showchange event', function(done) { var testChange = { _number: 42, - revisions: { - def: {_number: 2}, - abc: {_number: 1}, - }, + revisions: {def: {_number: 2}, abc: {_number: 1}}, }; plugin.on(element.EventType.SHOW_CHANGE, throwErrFn); plugin.on(element.EventType.SHOW_CHANGE, function(change, revision) { @@ -91,6 +89,24 @@ {change: testChange, patchNum: 1}); }); + test('handleEvent awaits plugins load', function(done) { + var testChange = { + _number: 42, + revisions: {def: {_number: 2}, abc: {_number: 1}}, + }; + var spy = sinon.spy(); + Gerrit._setPluginsCount(1); + plugin.on(element.EventType.SHOW_CHANGE, spy); + element.handleEvent(element.EventType.SHOW_CHANGE, + {change: testChange, patchNum: 1}); + assert.isFalse(spy.called); + Gerrit._setPluginsCount(0); + flush(function() { + assert.isTrue(spy.called); + done(); + }); + }); + test('comment event', function(done) { var testCommentNode = {foo: 'bar'}; plugin.on(element.EventType.COMMENT, throwErrFn); @@ -103,24 +119,53 @@ }); 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('commitmsgedit event', function(done) { + var testMsg = 'Test CL commit message'; + plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn); + plugin.on(element.EventType.COMMIT_MSG_EDIT, function(change, msg) { + assert.deepEqual(msg, testMsg); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleCommitMessage(null, testMsg); + }); + test('labelchange event', function(done) { var testChange = {_number: 42}; plugin.on(element.EventType.LABEL_CHANGE, throwErrFn); @@ -156,5 +201,49 @@ }); }); + test('_setPluginsCount', function(done) { + stub('gr-reporting', { + pluginsLoaded: function() { + assert.equal(Gerrit._pluginsPending, 0); + done(); + } + }); + Gerrit._setPluginsCount(0); + }); + + test('_arePluginsLoaded', function() { + assert.isTrue(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..588dc8b 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
@@ -44,6 +44,10 @@ return this._name; }; + Plugin.prototype.getServerInfo = function() { + return document.createElement('gr-rest-api-interface').getConfig(); + }; + Plugin.prototype.on = function(eventName, callback) { Plugin._sharedAPIElement.addEventCallback(eventName, callback); }; @@ -64,6 +68,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 +92,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() { @@ -99,6 +108,41 @@ Gerrit.installGwt = function() { // NOOP since PolyGerrit doesn’t support GWT plugins. + Gerrit._pluginInstalled(); + }; + + Gerrit._allPluginsPromise = null; + Gerrit._resolveAllPluginsLoaded = null; + + Gerrit.awaitPluginsLoaded = function() { + if (!Gerrit._allPluginsPromise) { + if (Gerrit._arePluginsLoaded()) { + Gerrit._allPluginsPromise = Promise.resolve(); + } else { + Gerrit._allPluginsPromise = new Promise(function(resolve) { + Gerrit._resolveAllPluginsLoaded = resolve; + }); + } + } + return Gerrit._allPluginsPromise; + }; + + Gerrit._setPluginsCount = function(count) { + Gerrit._pluginsPending = count; + if (Gerrit._arePluginsLoaded()) { + document.createElement('gr-reporting').pluginsLoaded(); + if (Gerrit._resolveAllPluginsLoaded) { + Gerrit._resolveAllPluginsLoaded(); + } + } + }; + + Gerrit._pluginInstalled = function() { + Gerrit._setPluginsCount(Gerrit._pluginsPending - 1); + }; + + Gerrit._arePluginsLoaded = function() { + return Gerrit._pluginsPending === 0; }; window.Gerrit = Gerrit;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html new file mode 100644 index 0000000..a3d2769 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -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. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-button/gr-button.html"> +<dom-module id="gr-linked-chip"> + <template> + <style> + :host { + display: block; + overflow: hidden; + } + .container { + align-items: center; + background: #eee; + border-radius: .75em; + display: inline-flex; + padding: 0 .5em; + } + gr-button.remove, + gr-button.remove:hover, + gr-button.remove:focus { + border-color: transparent; + color: #333; + } + gr-button.remove { + background: #eee; + color: #666; + font-size: 1.7em; + font-weight: normal; + height: .6em; + line-height: .6em; + margin-left: .15em; + padding: 0; + text-decoration: none; + } + .transparentBackground, + gr-button.transparentBackground { + background-color: transparent; + } + </style> + <div class$="container [[_getBackgroundClass(transparentBackground)]]"> + <a href$="[[href]]">[[text]]</a> + <gr-button + id="remove" + hidden$="[[!removable]]" + hidden + class$="remove [[_getBackgroundClass(transparentBackground)]]" + on-tap="_handleRemoveTap">×</gr-button> + </div> + </template> + <script src="gr-linked-chip.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js new file mode 100644 index 0000000..c6a5e4e --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-linked-chip', + + properties: { + href: String, + removable: { + type: Boolean, + value: false, + }, + text: String, + transparentBackground: { + type: Boolean, + value: false, + }, + }, + + _getBackgroundClass: function(transparent) { + return transparent ? 'transparentBackground' : ''; + }, + + _handleRemoveTap: function(e) { + e.preventDefault(); + this.fire('remove'); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html new file mode 100644 index 0000000..5e2cac5 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -0,0 +1,56 @@ +<!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-linked-chip</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script> + +<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="gr-linked-chip.html"> + +<test-fixture id="basic"> + <template> + <gr-linked-chip></gr-linked-chip> + </template> +</test-fixture> + +<script> + suite('gr-linked-chip tests', function() { + var element; + var sandbox; + + setup(function() { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('remove fired', function() { + var spy = sandbox.spy(); + element.addEventListener('remove', spy); + flushAsynchronousOperations(); + MockInteractions.tap(element.$.remove); + assert.isTrue(spy.called); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js index cb852fd..a3eccb8 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -50,27 +50,19 @@ _contentOrConfigChanged: function(content, config) { var output = Polymer.dom(this.$.output); output.textContent = ''; - var parser = new GrLinkTextParser(config, function(text, href, html) { + var parser = new GrLinkTextParser( + config, function(text, href, fragment) { if (href) { var a = document.createElement('a'); a.href = href; a.textContent = text; a.target = '_blank'; output.appendChild(a); - } else if (html) { - var fragment = document.createDocumentFragment(); - // Create temporary div to hold the nodes in. - var div = document.createElement('div'); - div.innerHTML = html; - while (div.firstChild) { - fragment.appendChild(div.firstChild); - } + } else if (fragment) { output.appendChild(fragment); - } else { - output.appendChild(document.createTextNode(text)); } }); parser.parse(content); - } + }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html index 5203520..90b09ab 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -47,6 +47,10 @@ match: '(I[0-9a-f]{8,40})', link: '#/q/$1' }, + changeid2: { + match: 'Change-Id: +(I[0-9a-f]{8,40})', + link: '#/q/$1' + }, googlesearch: { match: 'google:(.+)', link: 'https://bing.com/search?q=$1', // html should supercede link. @@ -123,6 +127,34 @@ assert.equal(linkEl2.textContent, 'Issue 3450'); }); + test('Change-Id pattern parsed before bug pattern', function() { + // "Change-Id:" pattern. + var changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; + var prefix = 'Change-Id: '; + + // "Issue/Bug" pattern. + var bug = 'Issue 3650'; + + var changeUrl = '/q/' + changeID; + var bugUrl = 'https://code.google.com/p/gerrit/issues/detail?id=3650'; + + element.content = prefix + changeID + bug; + + var textNode = element.$.output.childNodes[0]; + var changeLinkEl = element.$.output.childNodes[1]; + var bugLinkEl = element.$.output.childNodes[2]; + + assert.equal(textNode.textContent, prefix); + + assert.equal(changeLinkEl.target, '_blank'); + assert.isTrue(changeLinkEl.href.endsWith(changeUrl)); + assert.equal(changeLinkEl.textContent, changeID); + + assert.equal(bugLinkEl.target, '_blank'); + assert.equal(bugLinkEl.href, bugUrl); + assert.equal(bugLinkEl.textContent, 'Issue 3650'); + }); + test('html field in link config', function() { element.content = 'google:do a barrel roll'; var linkEl = element.$.output.childNodes[0]; @@ -143,5 +175,28 @@ assert.equal(element.$.output.innerHTML, 'foo:baz'); }); + test('overlapping links', function() { + element.config = { + b1: { + match: '(B:\\s*)(\\d+)', + html: '$1<a href="ftp://foo/$2">$2</a>', + }, + b2: { + match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)', + html: '$1<a href="ftp://foo/$2">$2</a>', + }, + }; + element.content = '- B: 123, 45'; + var links = Polymer.dom(element.root).querySelectorAll('a'); + + assert.equal(links.length, 2); + assert.equal(element.$$('span').textContent, '- B: 123, 45'); + + assert.equal(links[0].href, 'ftp://foo/123'); + assert.equal(links[0].textContent, '123'); + + assert.equal(links[1].href, 'ftp://foo/45'); + assert.equal(links[1].textContent, '45'); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js index b4b1678..b28097a 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -27,13 +27,102 @@ this.callback(text, href); }; -GrLinkTextParser.prototype.addHTML = function(html) { - this.callback(null, null, html); +GrLinkTextParser.prototype.processLinks = function(text, outputArray) { + this.sortArrayReverse(outputArray); + var fragment = document.createDocumentFragment(); + var cursor = text.length; + + // Start inserting linkified URLs from the end of the String. That way, the + // string positions of the items don't change as we iterate through. + outputArray.forEach(function(item) { + // Add any text between the current linkified item and the item added before + // if it exists. + if (item.position + item.length !== cursor) { + fragment.insertBefore( + document.createTextNode( + text.slice(item.position + item.length, cursor)), + fragment.firstChild); + } + fragment.insertBefore(item.html, fragment.firstChild); + cursor = item.position; + }); + + // Add the beginning portion at the end. + if (cursor !== 0) { + fragment.insertBefore( + document.createTextNode(text.slice(0, cursor)), fragment.firstChild); + } + + this.callback(null, null, fragment); +}; + +GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) { + outputArray.sort(function(a, b) {return b.position - a.position}); +}; + +GrLinkTextParser.prototype.addItem = + function(text, href, html, position, length, outputArray) { + var htmlOutput = ''; + + if (href) { + var a = document.createElement('a'); + a.href = href; + a.textContent = text; + a.target = '_blank'; + htmlOutput = a; + } else if (html) { + var fragment = document.createDocumentFragment(); + // Create temporary div to hold the nodes in. + var div = document.createElement('div'); + div.innerHTML = html; + while (div.firstChild) { + fragment.appendChild(div.firstChild); + } + htmlOutput = fragment; + } + + outputArray.push({ + html: htmlOutput, + position: position, + length: length, + }); +}; + +GrLinkTextParser.prototype.addLink = + function(text, href, position, length, outputArray) { + if (!text) { + return; + } + if (!this.hasOverlap(position, length, outputArray)) { + this.addItem(text, href, null, position, length, outputArray); + } +}; + +GrLinkTextParser.prototype.addHTML = + function(html, position, length, outputArray) { + if (!this.hasOverlap(position, length, outputArray)) { + this.addItem(null, null, html, position, length, outputArray); + } +}; + +GrLinkTextParser.prototype.hasOverlap = + function(position, length, outputArray) { + var endPosition = position + length; + for (var i = 0; i < outputArray.length; i++) { + var arrayItemStart = outputArray[i].position; + var arrayItemEnd = outputArray[i].position + outputArray[i].length; + if ((position >= arrayItemStart && position < arrayItemEnd) || + (endPosition > arrayItemStart && endPosition <= arrayItemEnd) || + (position === arrayItemStart && position === arrayItemEnd)) { + return true; + } + } + return false; }; GrLinkTextParser.prototype.parse = function(text) { linkify(text, { - callback: this.parseChunk.bind(this) + callback: this.parseChunk.bind(this), }); }; @@ -46,6 +135,8 @@ }; GrLinkTextParser.prototype.parseLinks = function(text, patterns) { + // The outputArray is used to store all of the matches found for all patterns. + var outputArray = []; for (var p in patterns) { if (patterns[p].enabled != null && patterns[p].enabled == false) { continue; @@ -66,22 +157,44 @@ var pattern = new RegExp(patterns[p].match, 'g'); var match; - while ((match = pattern.exec(text)) != null) { - var before = text.substr(0, match.index); - this.addText(before); - text = text.substr(match.index + match[0].length); + var textToCheck = text; + var susbtrIndex = 0; + + while ((match = pattern.exec(textToCheck)) != null) { + textToCheck = textToCheck.substr(match.index + match[0].length); var result = match[0].replace(pattern, patterns[p].html || patterns[p].link); + // Skip portion of replacement string that is equal to original. + for (var i = 0; i < result.length; i++) { + if (result[i] !== match[0][i]) { + break; + } + } + result = result.slice(i); + if (patterns[p].html) { - this.addHTML(result); + this.addHTML( + result, + susbtrIndex + match.index + i, + match[0].length - i, + outputArray); } else if (patterns[p].link) { - this.addText(match[0], result); + this.addLink( + match[0], + result, + susbtrIndex + match.index + i, + match[0].length - i, + outputArray); } else { throw Error('linkconfig entry ' + p + ' doesn’t contain a link or html attribute.'); } + + // Update the substring location so we know where we are in relation to + // the initial full text string. + susbtrIndex = susbtrIndex + match.index + match[0].length; } } - this.addText(text); + this.processLinks(text, outputArray); };
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html index 817d8c5..9aa80b5 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -16,7 +16,6 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> <dom-module id="gr-overlay"> <template>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js index da28e49..9f271ed 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -24,29 +24,13 @@ Polymer.IronOverlayBehavior, ], - detached: function() { - // For good measure. - Gerrit.KeyboardShortcutBehavior.enabled = true; - }, - open: function() { return new Promise(function(resolve) { - Gerrit.KeyboardShortcutBehavior.enabled = false; Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments); this._awaitOpen(resolve); }.bind(this)); }, - close: function() { - Gerrit.KeyboardShortcutBehavior.enabled = true; - Polymer.IronOverlayBehaviorImpl.close.apply(this, arguments); - }, - - cancel: function() { - Gerrit.KeyboardShortcutBehavior.enabled = true; - Polymer.IronOverlayBehaviorImpl.cancel.apply(this, arguments); - }, - /** * Override the focus stops that iron-overlay-behavior tries to find. */ @@ -72,5 +56,9 @@ }.bind(this); step.call(this); }, + + _id: function() { + return this.getAttribute('id') || 'global'; + }, }); })();
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..b5c9f0c 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> @@ -21,4 +22,3 @@ <dom-module id="gr-rest-api-interface"> <script src="gr-rest-api-interface.js"></script> </dom-module> -
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index 2f109c9..dee0cad 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,7 +14,12 @@ (function() { 'use strict'; + var DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', + }; var JSON_PREFIX = ')]}\''; + var MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900; var PARENT_PATCH_NUM = 'PARENT'; // Must be kept in sync with the ListChangesOption enum and protobuf. @@ -62,11 +67,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 +106,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, @@ -185,9 +196,11 @@ auto_hide_diff_table_header: true, context: 10, cursor_blink_rate: 0, + font_size: 12, ignore_whitespace: 'IGNORE_NONE', intraline_difference: true, line_length: 100, + line_wrapping: false, show_line_endings: true, show_tabs: true, show_whitespace_errors: true, @@ -204,6 +217,8 @@ }, saveDiffPreferences: function(prefs, opt_errFn, opt_ctx) { + // Invalidate the cache. + this._cache['/accounts/self/preferences.diff'] = undefined; return this.send('PUT', '/accounts/self/preferences.diff', prefs, opt_errFn, opt_ctx); }, @@ -232,12 +247,39 @@ setPreferredAccountEmail: function(email, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/emails/' + - encodeURIComponent(email) + '/preferred', null, opt_errFn, opt_ctx); + encodeURIComponent(email) + '/preferred', null, + opt_errFn, opt_ctx).then(function() { + // If result of getAccountEmails is in cache, update it in the cache + // so we don't have to invalidate it. + var cachedEmails = this._cache['/accounts/self/emails']; + if (cachedEmails) { + var emails = cachedEmails.map(function(entry) { + if (entry.email === email) { + return {email: email, preferred: true}; + } else { + return {email: email}; + } + }); + this._cache['/accounts/self/emails'] = emails; + } + }.bind(this)); }, setAccountName: function(name, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/name', {name: name}, opt_errFn, - opt_ctx); + opt_ctx).then(function(response) { + // If result of getAccount is in cache, update it in the cache + // so we don't have to invalidate it. + var cachedAccount = this._cache['/accounts/self/detail']; + if (cachedAccount) { + return this.getResponseObject(response).then(function(newName) { + // Replace object in cache with new object to force UI updates. + // TODO(logan): Polyfill for Object.assign in IE + this._cache['/accounts/self/detail'] = Object.assign( + {}, cachedAccount, {name: newName}); + }.bind(this)); + } + }.bind(this)); }, getAccountGroups: function() { @@ -258,11 +300,21 @@ getPreferences: function() { return this.getLoggedIn().then(function(loggedIn) { if (loggedIn) { - return this._fetchSharedCacheURL('/accounts/self/preferences'); + return this._fetchSharedCacheURL('/accounts/self/preferences').then( + function(res) { + if (this._isNarrowScreen()) { + res.default_diff_view = DiffViewMode.UNIFIED; + } else { + res.default_diff_view = res.diff_view; + } + return Promise.resolve(res); + }.bind(this)); } return Promise.resolve({ changes_per_page: 25, + default_diff_view: this._isNarrowScreen() ? + DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, diff_view: 'SIDE_BY_SIDE', }); }.bind(this)); @@ -307,11 +359,19 @@ return this._sharedFetchPromises[url]; }, + _isNarrowScreen: function() { + return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; + }, + getChanges: function(changesPerPage, opt_query, opt_offset) { var options = this._listChangesOptionsToHex( ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS ); + // Issue 4524: respect legacy token with max sortkey. + if (opt_offset === 'n,z') { + opt_offset = 0; + } var params = { n: changesPerPage, O: options, @@ -333,8 +393,9 @@ O: options, q: [ 'is:open owner:self', - 'is:open reviewer:self -owner:self', - 'is:closed (owner:self OR reviewer:self) -age:4w limit:10', + 'is:open ((reviewer:self -owner:self -star:ignore) OR assignee:self)', + 'is:closed (owner:self OR reviewer:self OR assignee:self) -age:4w ' + + 'limit:10', ], }; return this.fetchJSON('/changes/', null, null, params); @@ -348,10 +409,14 @@ var options = this._listChangesOptionsToHex( ListChangesOption.ALL_REVISIONS, ListChangesOption.CHANGE_ACTIONS, - ListChangesOption.DOWNLOAD_COMMANDS + ListChangesOption.CURRENT_ACTIONS, + ListChangesOption.CURRENT_COMMIT, + ListChangesOption.DOWNLOAD_COMMANDS, + ListChangesOption.SUBMITTABLE, + ListChangesOption.WEB_LINKS ); - return this._getChangeDetail(changeNum, options, opt_errFn, - opt_cancelCondition); + return this._getChangeDetail( + changeNum, options, opt_errFn, opt_cancelCondition); }, getDiffChangeDetail: function(changeNum, opt_errFn, opt_cancelCondition) { @@ -392,13 +457,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 +474,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 +495,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 +573,7 @@ ].join(' '); var params = { O: options, - q: query + q: query, }; return this.fetchJSON('/changes/', null, null, params); }, @@ -648,6 +690,12 @@ opt_patchNum, opt_path); }, + getDiffRobotComments: function(changeNum, basePatchNum, patchNum, + opt_path) { + return this._getDiffComments(changeNum, '/robotcomments', basePatchNum, + patchNum, opt_path); + }, + getDiffDrafts: function(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, @@ -877,5 +925,16 @@ 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)); + }, + + setDescription: function(changeNum, patchNum, desc) { + return this.send('PUT', + this.getChangeActionURL(changeNum, patchNum, '/description'), + {description: desc}); + }, }); })();
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..27d5d32 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) { @@ -285,7 +311,7 @@ text: function() { return Promise.resolve(')]}\'{}'); } }, ]; - var fetchStub = sandbox.stub(window, 'fetch', function(url) { + sandbox.stub(window, 'fetch', function(url) { if (url === '/accounts/self/detail') { return Promise.resolve(responses.shift()); } @@ -298,5 +324,93 @@ }); }); }); + + 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); + }); + + test('saveDiffPreferences invalidates cache line', function() { + var cacheKey = '/accounts/self/preferences.diff'; + var sendStub = sandbox.stub(element, 'send'); + element._cache[cacheKey] = {tab_size: 4}; + element.saveDiffPreferences({tab_size: 8}); + assert.isTrue(sendStub.called); + assert.notOk(element._cache[cacheKey]); + }); + + var preferenceSetup = function(testJSON, loggedIn, smallScreen) { + sandbox.stub(element, 'getLoggedIn', function() { + return Promise.resolve(loggedIn); + }); + sandbox.stub(element, '_isNarrowScreen', function() { + return smallScreen; + }); + sandbox.stub(element, '_fetchSharedCacheURL', function() { + return Promise.resolve(testJSON); + }); + }; + + test('getPreferences returns correctly on small screens logged in', + function(done) { + + var testJSON = {diff_view: 'SIDE_BY_SIDE'}; + var loggedIn = true; + var smallScreen = true; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(function(obj) { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('getPreferences returns correctly on small screens not logged in', + function(done) { + + var testJSON = {diff_view: 'SIDE_BY_SIDE'}; + var loggedIn = false; + var smallScreen = true; + + preferenceSetup(testJSON, loggedIn, smallScreen); + element.getPreferences().then(function(obj) { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('getPreferences returns correctly on larger screens logged in', + function(done) { + var testJSON = {diff_view: 'UNIFIED_DIFF'}; + var loggedIn = true; + var smallScreen = false; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(function(obj) { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'UNIFIED_DIFF'); + done(); + }); + }); + + test('getPreferences returns correctly on larger screens not logged in', + function(done) { + var testJSON = {diff_view: 'UNIFIED_DIFF'}; + var loggedIn = false; + var smallScreen = false; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(function(obj) { + assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js index 9e14f08..bef260e9 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -16,40 +16,33 @@ Polymer({ is: 'gr-select', - extends: 'select', - properties: { bindValue: { type: String, notify: true, + observer: '_updateValue', }, }, - observers: [ - '_valueChanged(bindValue)', - ], + listeners: { + change: '_valueChanged', + 'dom-change': '_updateValue', + }, - attached: function() { - this.addEventListener('change', function() { - this.bindValue = this.value; - }); + _updateValue: function() { + if (this.bindValue) { + this.value = this.bindValue; + } + }, + + _valueChanged: function() { + this.bindValue = this.value; }, ready: function() { // If not set via the property, set bind-value to the element value. if (!this.bindValue) { this.bindValue = this.value; } }, - - _valueChanged: function(bindValue) { - var options = Polymer.dom(this.root).querySelectorAll('option'); - for (var i = 0; i < options.length; i++) { - if (options[i].getAttribute('value') === bindValue + '') { - options[i].setAttribute('selected', true); - this.value = bindValue; - break; - } - } - }, }); })();
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html index 0e00f77..2649c21 100644 --- a/polygerrit-ui/app/index.html +++ b/polygerrit-ui/app/index.html
@@ -20,9 +20,16 @@ <meta name="description" content="Gerrit Code Review"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> +<!-- +SourceCodePro fonts are used in styles/fonts.css +@see https://github.com/w3c/preload/issues/32 regarding crossorigin +--> +<link rel="preload" href="/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin> +<link rel="preload" href="/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin> <link rel="stylesheet" href="/styles/fonts.css"> <link rel="stylesheet" href="/styles/main.css"> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> +<link rel="preload" href="/elements/gr-app.js"> <link rel="import" href="/elements/gr-app.html"> <body unresolved>
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh new file mode 100644 index 0000000..b557db8 --- /dev/null +++ b/polygerrit-ui/app/run_test.sh
@@ -0,0 +1,24 @@ +#!/bin/sh + +wct_bin=$(which wct) +if [[ -z "$wct_bin" ]]; then + echo "WCT must be on the path." + exit 1 +fi + +npm_bin=$(which npm) +if [[ -z "$npm_bin" ]]; then + echo "NPM must be on the path." + exit 1 +fi + +# WCT tests are not hermetic, and need extra environment variables. +# TODO(hanwen): does $DISPLAY even work on OSX? +bazel test \ + --test_env="HOME=$HOME" \ + --test_env="WCT=${wct_bin}" \ + --test_env="WCT_ARGS=${WCT_ARGS}" \ + --test_env="NPM=${npm_bin}" \ + --test_env="DISPLAY=${DISPLAY}" \ + "$@" \ + //polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js index 13f3243..76c8493 100644 --- a/polygerrit-ui/app/scripts/util.js +++ b/polygerrit-ui/app/scripts/util.js
@@ -55,5 +55,25 @@ return ''; }; + /** + * Truncates URLs to display filename only + * Example + * // returns '.../text.html' + * util.truncatePath.('dir/text.html'); + * Example + * // returns 'text.html' + * util.truncatePath.('text.html'); + * @return {String} Returns the truncated value of a URL. + */ + util.truncatePath = function(path) { + var pathPieces = path.split('/'); + + if (pathPieces.length < 2) { + return path; + } + // Character is an ellipsis. + return '\u2026/' + pathPieces[pathPieces.length - 1]; + }; + window.util = util; })(window);
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..ffd3b8e 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -22,10 +22,13 @@ <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-view/gr-change-list-view_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', @@ -33,7 +36,9 @@ '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', @@ -44,13 +49,14 @@ '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-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-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', @@ -61,37 +67,53 @@ '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', 'settings/gr-http-password/gr-http-password_test.html', 'settings/gr-menu-editor/gr-menu-editor_test.html', + 'settings/gr-registration-dialog/gr-registration-dialog_test.html', '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-button/gr-button_test.html', 'shared/gr-change-star/gr-change-star_test.html', 'shared/gr-confirm-dialog/gr-confirm-dialog_test.html', 'shared/gr-cursor-manager/gr-cursor-manager_test.html', 'shared/gr-date-formatter/gr-date-formatter_test.html', 'shared/gr-editable-content/gr-editable-content_test.html', 'shared/gr-editable-label/gr-editable-label_test.html', + 'shared/gr-formatted-text/gr-formatted-text_test.html', 'shared/gr-js-api-interface/gr-change-actions-js-api_test.html', 'shared/gr-js-api-interface/gr-change-reply-js-api_test.html', 'shared/gr-js-api-interface/gr-js-api-interface_test.html', + 'shared/gr-linked-chip/gr-linked-chip_test.html', 'shared/gr-linked-text/gr-linked-text_test.html', 'shared/gr-rest-api-interface/gr-rest-api-interface_test.html', '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-patch-set-behavior/gr-patch-set-behavior_test.html', + 'gr-path-list-behavior/gr-path-list-behavior_test.html', + ].forEach(function(file) { + // Behaviors do not utilize the DOM, so no shadow DOM test is necessary. + file = behaviorsPath + file; + testFiles.push(file); + }); + WCT.loadSuites(testFiles); </script>
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh new file mode 100755 index 0000000..10de424 --- /dev/null +++ b/polygerrit-ui/app/wct_test.sh
@@ -0,0 +1,56 @@ +#!/bin/sh + +set -ex + +t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX) +components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip +code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/pg_code.zip + +echo $t +unzip -qd $t $components +unzip -qd $t $code +mkdir -p $t/test +cp $TEST_SRCDIR/gerrit/polygerrit-ui/app/test/index.html $t/test/ + +# For some reason wct tries to install selenium into its node_modules +# directory on first run. If you've installed into /usr/local and +# aren't running wct as root, you're screwed. Turning this option off +# through skipSeleniumInstall seems to still work, so there's that. + +# Sauce tests are disabled by default in order to run local tests +# only. Run it with (saucelabs.com account required; free for open +# source): WCT_ARGS='--plugin sauce' buck test --no-results-cache +# --include web + +cat <<EOF > $t/wct.conf.js +module.exports = { + 'suites': ['test'], + 'webserver': { + 'pathMappings': [ + {'/components/bower_components': 'bower_components'} + ] + }, + 'plugins': { + 'local': { + 'skipSeleniumInstall': true + }, + 'sauce': { + 'disabled': true, + 'browsers': [ + 'OS X 10.11/chrome', + 'Windows 10/chrome', + 'Linux/firefox', + 'OS X 10.11/safari', + 'Windows 10/microsoftedge' + ] + } + } + }; +EOF + +export PATH="$(dirname $WCT):$(dirname $NPM):$PATH" + +cd $t +test -n "${WCT}" + +$(basename ${WCT}) ${WCT_ARGS}
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go index cb6d236..33f33b7 100644 --- a/polygerrit-ui/server.go +++ b/polygerrit-ui/server.go
@@ -32,6 +32,7 @@ restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to") port = flag.String("port", ":8081", "Port to serve HTTP requests on") prod = flag.Bool("prod", false, "Serve production assets") + scheme = flag.String("scheme", "https", "URL scheme") ) func main() { @@ -57,7 +58,7 @@ req := &http.Request{ Method: "GET", URL: &url.URL{ - Scheme: "https", + Scheme: *scheme, Host: *restHost, Opaque: r.URL.EscapedPath(), RawQuery: r.URL.RawQuery,
diff --git a/tools/BUILD b/tools/BUILD index ff64faa..060cbd8 100644 --- a/tools/BUILD +++ b/tools/BUILD
@@ -1,6 +1,6 @@ py_binary( - name = 'merge_jars', - srcs = ['merge_jars.py'], - main = 'merge_jars.py', - visibility = ['//visibility:public'], + name = "merge_jars", + srcs = ["merge_jars.py"], + main = "merge_jars.py", + visibility = ["//visibility:public"], )
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/bazel.rc b/tools/bazel.rc new file mode 100644 index 0000000..4ed16cf --- /dev/null +++ b/tools/bazel.rc
@@ -0,0 +1,2 @@ +build --workspace_status_command=./tools/workspace-status.sh +test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD index e69de29..a0f5bd1 100644 --- a/tools/bzl/BUILD +++ b/tools/bzl/BUILD
@@ -0,0 +1,5 @@ +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..c39541d --- /dev/null +++ b/tools/bzl/asciidoc.bzl
@@ -0,0 +1,324 @@ +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( + 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), + }, + implementation = _replace_macros_impl, +) + +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_attrs = { + "_exe": attr.label( + default = Label("//lib/asciidoctor:asciidoc"), + cfg = "host", + 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(), +} + +_asciidoc = rule( + attrs = _asciidoc_attrs + { + "outs": attr.output_list(mandatory = True), + }, + implementation = _asciidoc_impl, +) + +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( + attrs = _asciidoc_attrs, + outputs = { + "out": "%{name}.zip", + }, + implementation = _asciidoc_html_zip_impl, +) + +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", + "rm -rf %s" % tmpdir, + "mkdir %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( + 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", + }, + implementation = _asciidoc_zip_impl, +) + +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/classpath.bzl b/tools/bzl/classpath.bzl new file mode 100644 index 0000000..5b9242e --- /dev/null +++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,22 @@ +def _classpath_collector(ctx): + all = set() + for d in ctx.attr.deps: + if hasattr(d, 'java'): + all += d.java.transitive_runtime_deps + all += d.java.compilation_info.runtime_classpath + elif hasattr(d, 'files'): + all += d.files + + as_strs = [c.path for c in all] + ctx.file_action(output= ctx.outputs.runtime, + content="\n".join(sorted(as_strs))) + +classpath_collector = rule( + attrs = { + "deps": attr.label_list(), + }, + outputs = { + "runtime": "%{name}.runtime_classpath", + }, + implementation = _classpath_collector, +)
diff --git a/tools/bzl/genrule2.bzl b/tools/bzl/genrule2.bzl index e67ee30..563a9ef 100644 --- a/tools/bzl/genrule2.bzl +++ b/tools/bzl/genrule2.bzl
@@ -15,15 +15,13 @@ # Syntactic sugar for native genrule() rule: # expose ROOT shell variable # expose TMP shell variable -# accept single output -def genrule2(out, cmd, **kwargs): +def genrule2(cmd, **kwargs): cmd = ' && '.join([ 'ROOT=$$PWD', - 'TMP=$$(mktemp -d)', + 'TMP=$$(mktemp -d || mktemp -d -t bazel-tmp)', '(' + cmd + ')', ]) native.genrule( cmd = cmd, - outs = [out], **kwargs)
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl index 29987ef..67a45cc 100644 --- a/tools/bzl/gwt.bzl +++ b/tools/bzl/gwt.bzl
@@ -12,17 +12,297 @@ # See the License for the specific language governing permissions and # limitations under the License. -# GWT Rules Skylark rules for building [GWT](http://www.gwtproject.org/) -# modules using Bazel. -load('//tools/bzl:java.bzl', 'java_library2') +# 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:java.bzl", "java_library2") + +jar_filetype = FileType([".jar"]) + +BROWSERS = [ + "chrome", + "firefox", + "gecko1_8", + "safari", + "msie", + "ie8", + "ie9", + "ie10", + "edge", +] + +ALIASES = { + "chrome": "safari", + "firefox": "gecko1_8", + "msie": "ie10", + "edge": "gecko1_8", +} + +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", +] + +PLUGIN_DEPS_NEVERLINK = [ + "//gerrit-plugin-api:lib-neverlink", +] + +GWT_PLUGIN_DEPS_NEVERLINK = [ + "//gerrit-plugin-gwtui:gwtui-api-lib-neverlink", + "//lib/gwt:user-neverlink", +] + +GWT_PLUGIN_DEPS = [ + "//gerrit-plugin-gwtui:gwtui-api-lib", +] + +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//jar:src", +] + +USER_AGENT_XML = """<module rename-to='gerrit_ui'> +<inherits name='%s'/> +<set-property name='user.agent' value='%s'/> +<set-property name='locale' value='default'/> +</module> +""" def gwt_module(gwt_xml=None, resources=[], srcs=[], **kwargs): if gwt_xml: resources += [gwt_xml] - if srcs: - resources += srcs java_library2( srcs = srcs, resources = resources, **kwargs) + +def _gwt_user_agent_module(ctx): + """Generate user agent specific GWT module.""" + if not ctx.attr.user_agent: + return None + + ua = ctx.attr.user_agent + impl = ua + if ua in ALIASES: + impl = ALIASES[ua] + + # intermediate artifact: user agent speific GWT xml file + gwt_user_agent_xml = ctx.new_file(ctx.label.name + "_gwt.xml") + ctx.file_action(output = gwt_user_agent_xml, + content=USER_AGENT_XML % (MODULE, impl)) + + # intermediate artifact: user agent specific zip with GWT module + gwt_user_agent_zip = ctx.new_file(ctx.label.name + "_gwt.zip") + gwt = '%s_%s.gwt.xml' % (MODULE.replace('.', '/'), ua) + dir = gwt_user_agent_zip.path + ".dir" + cmd = " && ".join([ + "p=$PWD", + "mkdir -p %s" % dir, + "cd %s" % dir, + "mkdir -p $(dirname %s)" % gwt, + "cp $p/%s %s" % (gwt_user_agent_xml.path, gwt), + "$p/%s cC $p/%s $(find . | sed 's|^./||')" % (ctx.executable._zip.path, gwt_user_agent_zip.path) + ]) + ctx.action( + inputs = [gwt_user_agent_xml] + ctx.files._zip, + outputs = [gwt_user_agent_zip], + command = cmd, + mnemonic = "GenerateUserAgentGWTModule") + + return struct( + zip=gwt_user_agent_zip, + module=MODULE + '_' + ua + ) + +def _gwt_binary_impl(ctx): + module = ctx.attr.module[0] + 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) + + gwt_user_agent_modules = [] + ua = _gwt_user_agent_module(ctx) + if ua: + paths.append(ua.zip.path) + gwt_user_agent_modules.append(ua.zip) + module = ua.module + + 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, + ) + # TODO(davido): clean up command concatenation + cmd += " ".join([ + "-style %s" % ctx.attr.style, + "-optimize %s" % ctx.attr.optimize, + "-strict", + " ".join(ctx.attr.compiler_args), + module + "\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 + gwt_user_agent_modules, + 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( + attrs = { + "user_agent": attr.string(), + "style": attr.string(default = "OBF"), + "optimize": attr.string(default = "9"), + "deps": attr.label_list(allow_files = jar_filetype), + "module": attr.string_list(default = [MODULE]), + "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"), + cfg = "host", + executable = True, + single_file = True, + ), + }, + outputs = { + "output": "%{name}.zip", + }, + implementation = _gwt_binary_impl, +) + +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/$@ .', + outs = ['ui_optdbg' + suffix + '.zip'], + visibility = ['//visibility:public'], + ) + + gwt_binary( + name = opt, + module = [MODULE], + module_deps = [module_dep], + deps = DEPS, + compiler_args = args, + jvm_args = GWT_JVM_ARGS, + ) + + gwt_binary( + name = dbg, + 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'], + ) + +def gwt_user_agent_permutations(): + for ua in BROWSERS: + gwt_binary( + name = "ui_%s" % ua, + user_agent = ua, + style = 'PRETTY', + optimize = "0", + module = [MODULE], + module_deps = [':ui_module'], + deps = DEPS, + compiler_args = GWT_COMPILER_ARGS, + jvm_args = GWT_JVM_ARGS, + )
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl new file mode 100644 index 0000000..341b9c1 --- /dev/null +++ b/tools/bzl/javadoc.bzl
@@ -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. + +# 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" + external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs + cmd = [ + "rm -rf %s" % source, + "mkdir %s" % source, + " && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]), + "rm -rf %s" % dir, + "mkdir %s" % dir, + " ".join([ + ctx.file._javadoc.path, + "-Xdoclint:-missing", + "-protected", + "-encoding UTF-8", + "-charset UTF-8", + "-notimestamp", + "-quiet", + "-windowtitle '%s'" % ctx.attr.title, + " ".join(['-link %s' % url for url in external_docs]), + "-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(), + "external_docs": attr.string_list(), + "_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, + ), + }, + outputs = {"zip": "%{name}.zip"}, + implementation = _impl, +)
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl new file mode 100644 index 0000000..1aa9d5b --- /dev/null +++ b/tools/bzl/js.bzl
@@ -0,0 +1,374 @@ +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( + attrs = { + # Label resolves within repo of the .bzl file. + "_download_script": attr.label(default = Label("//tools:download_file.py")), + "repository": attr.string(default = NPMJS), + }, + local = True, + implementation = _npm_binary_impl, +) + +# 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 || mktemp -d -t bazel-tmp)", + "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" % (" ".join(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): + # intermediate artifact. + vulcanized = ctx.new_file( + ctx.configuration.genfiles_dir, ctx.outputs.html, ".vulcanized.html") + destdir = ctx.outputs.html.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/" + 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, + ]) + + # Node/NPM is not (yet) hermeticized, so we have to get the binary + # from the environment, and it may be under $HOME, so we can't run + # in the sandbox. + node_tweaks = dict( + use_default_shell_env = True, + execution_requirements = {"local": "1"}, + ) + ctx.action( + mnemonic = "Vulcanize", + inputs = [ctx.file._run_npm, ctx.file.app, + ctx.file._vulcanize_archive + ] + list(zips) + ctx.files.srcs, + outputs = [vulcanized], + command = cmd, + **node_tweaks) + + hermetic_npm_command = "export PATH && " + " ".join([ + 'python', + ctx.file._run_npm.path, + ctx.file._crisper_archive.path, + "--always-write-script", + "--source", 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, vulcanized], + outputs = [ctx.outputs.js, ctx.outputs.html], + command = hermetic_npm_command, + **node_tweaks) + +_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 = { + "html": "%{name}.html", + "js": "%{name}.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..836572a --- /dev/null +++ b/tools/bzl/license-map.py
@@ -0,0 +1,142 @@ +#!/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() + +if args.asciidoctor: + print( +""" +GERRIT +------ +Part of link:index.html[Gerrit Code Review] +""")
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl new file mode 100644 index 0000000..38dfbe5 --- /dev/null +++ b/tools/bzl/license.bzl
@@ -0,0 +1,57 @@ +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/maven_jar.bzl b/tools/bzl/maven_jar.bzl new file mode 100644 index 0000000..5a6707b --- /dev/null +++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,140 @@ +GERRIT = "GERRIT:" + +GERRIT_API = "GERRIT_API:" + +MAVEN_CENTRAL = "MAVEN_CENTRAL:" + +MAVEN_LOCAL = "MAVEN_LOCAL:" + +def _maven_release(ctx, parts): + """induce jar and url name from maven coordinates.""" + if len(parts) not in [3, 4]: + fail('%s:\nexpected id="groupId:artifactId:version[:classifier]"' + % ctx.attr.artifact) + if len(parts) == 4: + group, artifact, version, classifier = parts + file_version = version + '-' + classifier + else: + group, artifact, version = parts + file_version = version + + jar = artifact.lower() + '-' + file_version + url = '/'.join([ + ctx.attr.repository, + group.replace('.', '/'), + artifact, + version, + artifact + '-' + file_version]) + + return jar, url + +# Creates a struct containing the different parts of an artifact's FQN +def _create_coordinates(fully_qualified_name): + parts = fully_qualified_name.split(":") + packaging = None + classifier = None + + if len(parts) == 3: + group_id, artifact_id, version = parts + elif len(parts) == 4: + group_id, artifact_id, version, packaging = parts + elif len(parts) == 5: + group_id, artifact_id, version, packaging, classifier = parts + else: + fail("Invalid fully qualified name for artifact: %s" % fully_qualified_name) + + return struct( + fully_qualified_name = fully_qualified_name, + group_id = group_id, + artifact_id = artifact_id, + packaging = packaging, + classifier = classifier, + version = version, + ) + +def _generate_build_file(ctx, binjar, srcjar): + srcjar_attr = "" + if srcjar: + srcjar_attr = 'srcjar = "%s",' % srcjar + contents = """ +# DO NOT EDIT: automatically generated BUILD file for maven_jar rule {rule_name} +package(default_visibility = ['//visibility:public']) +java_import( + name = 'jar', + {srcjar_attr} + jars = ['{binjar}'], +) +java_import( + name = 'neverlink', + jars = ['{binjar}'], + neverlink = 1, +) +\n""".format(srcjar_attr = srcjar_attr, + rule_name = ctx.name, + binjar = binjar) + if srcjar: + contents += """ +java_import( + name = 'src', + jars = ['{srcjar}'], +) +""".format(srcjar = srcjar) + ctx.file('%s/BUILD' % ctx.path("jar"), contents, False) + +def _maven_jar_impl(ctx): + """rule to download a Maven archive.""" + coordinates = _create_coordinates(ctx.attr.artifact) + + name = ctx.name + sha1 = ctx.attr.sha1 + + parts = ctx.attr.artifact.split(':') + # TODO(davido): Only releases for now, implement handling snapshots + jar, url = _maven_release(ctx, parts) + + binjar = jar + '.jar' + binjar_path = ctx.path('/'.join(['jar', binjar])) + binurl = url + '.jar' + + python = ctx.which("python") + script = ctx.path(ctx.attr._download_script) + + args = [python, script, "-o", binjar_path, "-u", binurl, "-v", sha1] + if ctx.attr.unsign: + args.append('--unsign') + for x in ctx.attr.exclude: + args.extend(['-x', x]) + + out = ctx.execute(args) + + if out.return_code: + fail("failed %s: %s" % (' '.join(args), out.stderr)) + + srcjar = None + if ctx.attr.src_sha1 or ctx.attr.attach_source: + srcjar = jar + '-src.jar' + srcurl = url + '-sources.jar' + srcjar_path = ctx.path('jar/' + srcjar) + args = [python, script, "-o", srcjar_path, "-u", srcurl] + if ctx.attr.src_sha1: + args.extend(['-v', ctx.attr.src_sha1]) + out = ctx.execute(args) + if out.return_code: + fail("failed %s: %s" % (args, out.stderr)) + + _generate_build_file(ctx, binjar, srcjar) + +maven_jar = repository_rule( + attrs = { + "artifact": attr.string(mandatory = True), + "sha1": attr.string(mandatory = True), + "src_sha1": attr.string(), + "_download_script": attr.label(default = Label("//tools:download_file.py")), + "repository": attr.string(default = MAVEN_CENTRAL), + "attach_source": attr.bool(default = True), + "unsign": attr.bool(default = False), + "exclude": attr.string_list(), + }, + local = True, + implementation = _maven_jar_impl, +)
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl new file mode 100644 index 0000000..70ca190 --- /dev/null +++ b/tools/bzl/pkg_war.bzl
@@ -0,0 +1,155 @@ +# 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 '(%s)' % ' && '.join([ + 'root=$(pwd)', + 'cd %s' % input_dir, + "find . -exec touch -t 198001010000 '{}' ';' 2> /dev/null", + '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: + if hasattr(l, 'java'): + transitive_lib_deps += l.java.transitive_runtime_deps + elif hasattr(l, 'files'): + transitive_lib_deps += l.files + + 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, + ) + +# context: go to the root directory +# libs: go to the WEB-INF/lib directory +# pgmlibs: go to the WEB-INF/pgm-lib directory +_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), + }, + outputs = {"war": "%{name}.war"}, + implementation = _war_impl, +) + +def pkg_war(name, ui = 'ui_optdbg', context = [], doc = False, **kwargs): + doc_ctx = [] + doc_lib = [] + ui_deps = [] + if ui == 'polygerrit' or ui == 'ui_optdbg' or ui == 'ui_optdbg_r': + ui_deps.append('//polygerrit-ui/app:polygerrit_ui') + if ui and ui != 'polygerrit': + ui_deps.append('//gerrit-gwtui:%s' % ui) + if doc: + doc_ctx.append('//Documentation:html') + doc_lib.append('//Documentation:index') + + _pkg_war( + name = name, + libs = LIBS + doc_lib, + pgmlibs = PGMLIBS, + context = doc_ctx + context + ui_deps + [ + '//gerrit-main:main_bin_deploy.jar', + '//gerrit-war:webapp_assets', + ], + **kwargs + )
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl new file mode 100644 index 0000000..098c3cd --- /dev/null +++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,92 @@ +load("//tools/bzl:genrule2.bzl", "genrule2") +load( + "//tools/bzl:gwt.bzl", + "GWT_PLUGIN_DEPS", + "GWT_PLUGIN_DEPS_NEVERLINK", + "GWT_TRANSITIVE_DEPS", + "GWT_COMPILER_ARGS", + "PLUGIN_DEPS_NEVERLINK", + "GWT_JVM_ARGS", + "gwt_binary", +) + +PLUGIN_DEPS = ["//gerrit-plugin-api:lib"] + +PLUGIN_TEST_DEPS = ["//gerrit-acceptance-framework:lib"] + +def gerrit_plugin( + name, + deps = [], + provided_deps = [], + srcs = [], + gwt_module = [], + resources = [], + manifest_entries = [], + **kwargs): + native.java_library( + name = name + '__plugin', + srcs = srcs, + resources = resources, + deps = provided_deps + deps + GWT_PLUGIN_DEPS_NEVERLINK + PLUGIN_DEPS_NEVERLINK, + visibility = ['//visibility:public'], + ) + + static_jars = [] + if gwt_module: + static_jars = [':%s-static' % name] + + native.java_binary( + name = '%s__non_stamped' % name, + deploy_manifest_lines = manifest_entries + [ + "Gerrit-ApiType: plugin", + "Implementation-Vendor: Gerrit Code Review", + ], + main_class = 'Dummy', + runtime_deps = [ + ':%s__plugin' % name, + ] + static_jars, + visibility = ['//visibility:public'], + **kwargs + ) + + if gwt_module: + native.java_library( + name = name + '__gwt_module', + resources = list(set(srcs + resources)), + runtime_deps = deps + GWT_PLUGIN_DEPS, + visibility = ['//visibility:public'], + ) + genrule2( + name = '%s-static' % name, + cmd = ' && '.join([ + 'mkdir -p $$TMP/static', + 'unzip -qd $$TMP/static $(location %s__gwt_application)' % name, + 'cd $$TMP', + 'zip -qr $$ROOT/$@ .']), + tools = [':%s__gwt_application' % name], + outs = ['%s-static.jar' % name], + ) + gwt_binary( + name = name + '__gwt_application', + module = [gwt_module], + deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ['//lib/gwt:dev'], + module_deps = [':%s__gwt_module' % name], + compiler_args = GWT_COMPILER_ARGS, + jvm_args = GWT_JVM_ARGS, + ) + + # TODO(davido): Remove manual merge of manifest file when this feature + # request is implemented: https://github.com/bazelbuild/bazel/issues/2009 + genrule2( + name = name, + stamp = 1, + srcs = ['%s__non_stamped_deploy.jar' % name], + cmd = " && ".join([ + "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep %s | cut -d ' ' -f 2)" % name.upper(), + "cd $$TMP", + "unzip -q $$ROOT/$<", + "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF", + "zip -qr $$ROOT/$@ ."]), + outs = ['%s.jar' % name], + visibility = ['//visibility:public'], + )
diff --git a/tools/bzl/plugins.bzl b/tools/bzl/plugins.bzl new file mode 100644 index 0000000..f53652c --- /dev/null +++ b/tools/bzl/plugins.bzl
@@ -0,0 +1,12 @@ +CORE_PLUGINS = [ + "commit-message-length-validator", + "download-commands", + "hooks", + "replication", + "reviewnotes", + "singleusergroup", +] + +CUSTOM_PLUGINS = [ + "cookbook-plugin", +]
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/default.defs b/tools/default.defs index 191dfe5..fa1800b 100644 --- a/tools/default.defs +++ b/tools/default.defs
@@ -41,9 +41,15 @@ # Munge kwargs to set Gerrit-specific defaults. def _munge_args(kwargs): + if read_config('sanitizers', 'error_prone'): + _set_error_prone(kwargs) _set_auto_value(kwargs) _set_extra_arguments(kwargs) +def _set_error_prone(kwargs): + kwargs['javac_jar'] = '//lib:errorprone' + kwargs['compiler_class_name'] = 'com.google.errorprone.ErrorProneJavaCompiler' + def _set_extra_arguments(kwargs): ext = 'extra_arguments' if ext not in kwargs: @@ -201,7 +207,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..c9c6ef0 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)) @@ -128,7 +103,7 @@ print('Download %s' % src_url, file=stderr) try: - check_call(['curl', '--proxy-anyauth', '-ksfo', cache_ent, src_url]) + check_call(['curl', '--proxy-anyauth', '-ksSfo', cache_ent, src_url]) except OSError as err: print('could not invoke curl: %s\nis curl installed?' % err, file=stderr) exit(1)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK index 0bcde9d..a8b3f01 100644 --- a/tools/eclipse/BUCK +++ b/tools/eclipse/BUCK
@@ -21,8 +21,14 @@ '//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/BUILD b/tools/eclipse/BUILD new file mode 100644 index 0000000..2b3f77a --- /dev/null +++ b/tools/eclipse/BUILD
@@ -0,0 +1,69 @@ +load("//tools/bzl:pkg_war.bzl", "LIBS", "PGMLIBS") +load("//tools/bzl:classpath.bzl", "classpath_collector") +load( + "//tools/bzl:plugins.bzl", + "CORE_PLUGINS", + "CUSTOM_PLUGINS", +) + +PROVIDED_DEPS = [ + "//lib/bouncycastle:bcprov", + "//lib/bouncycastle:bcpg", + "//lib/bouncycastle:bcpkix", +] + +TEST_DEPS = [ + "//gerrit-gpg:gpg_tests", + "//gerrit-gwtui:ui_tests", + "//gerrit-httpd:httpd_tests", + "//gerrit-patch-jgit:jgit_patch_tests", + "//gerrit-reviewdb:client_tests", + "//gerrit-server:server_tests", +] + +DEPS = [ + "//gerrit-acceptance-tests:lib", + "//gerrit-gwtdebug:gwtdebug", + "//gerrit-gwtui:ui_module", + "//gerrit-main:main_lib", + "//gerrit-plugin-gwtui:gwtui-api-lib", + "//gerrit-server:server", + "//lib/asciidoctor:asciidoc_lib", + "//lib/asciidoctor:doc_indexer_lib", + "//lib/auto:auto-value", + "//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", + # TODO(davido): I do not understand why it must be on the Eclipse classpath + #'//Documentation:index', +] + +java_library( + name = "classpath", + testonly = 1, + runtime_deps = LIBS + PGMLIBS + DEPS, +) + +classpath_collector( + name = "main_classpath_collect", + testonly = 1, + deps = LIBS + PGMLIBS + DEPS + TEST_DEPS + PROVIDED_DEPS + + ["//plugins/%s:%s__plugin" % (n, n) for n in CORE_PLUGINS + CUSTOM_PLUGINS], +) + +classpath_collector( + name = "gwt_classpath_collect", + deps = ["//gerrit-gwtui:ui_module"], +) + +classpath_collector( + name = "autovalue_classpath_collect", + deps = ["//lib/auto:auto-value"], +)
diff --git a/tools/eclipse/gerrit_daemon.launch b/tools/eclipse/gerrit_daemon.launch index cbc6204..9495884 100644 --- a/tools/eclipse/gerrit_daemon.launch +++ b/tools/eclipse/gerrit_daemon.launch
@@ -13,5 +13,5 @@ <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/> <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/> <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/> -<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.plugin-classes=${resource_loc:/gerrit/buck-out}/eclipse/plugins"/> +<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.plugin-classes=${resource_loc:/gerrit/eclipse-out}/plugins"/> </launchConfiguration>
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch index b2ab320..9f2bf2b 100644 --- a/tools/eclipse/gerrit_gwt_debug.launch +++ b/tools/eclipse/gerrit_gwt_debug.launch
@@ -16,7 +16,7 @@ </listAttribute> <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/> <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gerrit.gwtdebug.GerritGwtDebugLauncher"/> -<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-strict -noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/> +<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-strict -noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/.gwt_work_dir com.google.gerrit.GerritGwtUI -src ${resource_loc:/gerrit}/gerrit-plugin-gwtui/src/main/java -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/> <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/> <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024M -XX:MaxPermSize=256M -Dgerrit.disable-gwtui-recompile=true"/> </launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py index 46f5680..46cd9fc 100755 --- a/tools/eclipse/project.py +++ b/tools/eclipse/project.py
@@ -17,7 +17,7 @@ from __future__ import print_function from optparse import OptionParser -from os import path +from os import makedirs, path from subprocess import Popen, PIPE, CalledProcessError, check_call from xml.dom import minidom import re @@ -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__) @@ -75,19 +75,23 @@ </projectDescription>\ """, file=fd) +def gen_primary_build_tool(): + with open(path.join(ROOT, ".primary_build_tool"), 'w') as fd: + fd.write("buck") + def gen_plugin_classpath(root): p = path.join(root, '.classpath') with open(p, 'w') as fd: if path.exists(path.join(root, 'src', 'test', 'java')): testpath = """ - <classpathentry kind="src" path="src/test/java"\ + <classpathentry excluding="**/BUILD" kind="src" path="src/test/java"\ out="eclipse-out/test"/>""" else: testpath = "" print("""\ <?xml version="1.0" encoding="UTF-8"?> <classpath> - <classpathentry kind="src" path="src/main/java"/>%(testpath)s + <classpathentry excluding="**/BUILD" kind="src" path="src/main/java"/>%(testpath)s <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry combineaccessrules="false" kind="src" path="/gerrit"/> <classpathentry kind="output" path="eclipse-out/classes"/> @@ -101,6 +105,11 @@ def classpathentry(kind, path, src=None, out=None, exported=None): e = doc.createElement('classpathentry') e.setAttribute('kind', kind) + # TODO(davido): Remove this and other exclude BUILD files hack + # when this Bazel bug is fixed: + # https://github.com/bazelbuild/bazel/issues/1083 + if kind == 'src': + e.setAttribute('excluding', '**/BUILD') e.setAttribute('path', path) if src: e.setAttribute('sourcepath', src) @@ -119,6 +128,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 +185,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: @@ -228,6 +248,12 @@ gen_project(args.project_name) gen_classpath() gen_factorypath() + gen_primary_build_tool() + + # TODO(davido): Remove this when GWT gone + gwt_working_dir = ".gwt_work_dir" + if not path.isdir(gwt_working_dir): + makedirs(path.join(ROOT, gwt_working_dir)) try: targets = ['//tools:buck'] + MAIN + GWT
diff --git a/tools/eclipse/project_bzl.py b/tools/eclipse/project_bzl.py new file mode 100755 index 0000000..a7ddf6f --- /dev/null +++ b/tools/eclipse/project_bzl.py
@@ -0,0 +1,280 @@ +#!/usr/bin/env python +# 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. +# +# TODO(sop): Remove hack after Buck supports Eclipse + +from __future__ import print_function +# TODO(davido): use Google style for importing instead: +# import optparse +# ... +# optparse.OptionParser +from optparse import OptionParser +from os import environ, path, makedirs +from subprocess import CalledProcessError, check_call, check_output +from xml.dom import minidom +import re +import sys + +MAIN = '//tools/eclipse:classpath' +GWT = '//gerrit-gwtui:ui_module' +AUTO = '//lib/auto:auto-value' +JRE = '/'.join([ + 'org.eclipse.jdt.launching.JRE_CONTAINER', + 'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType', + 'JavaSE-1.8', +]) +# Map of targets to corresponding classpath collector rules +cp_targets = { + AUTO: '//tools/eclipse:autovalue_classpath_collect', + GWT: '//tools/eclipse:gwt_classpath_collect', + MAIN: '//tools/eclipse:main_classpath_collect', +} + +ROOT = path.abspath(__file__) +while not path.exists(path.join(ROOT, 'WORKSPACE')): + ROOT = path.dirname(ROOT) + +opts = OptionParser() +opts.add_option('--plugins', help='create eclipse projects for plugins', + action='store_true') +opts.add_option('--name', help='name of the generated project', + action='store', default='gerrit', dest='project_name') +args, _ = opts.parse_args() + +def retrieve_ext_location(): + return check_output(['bazel', 'info', 'output_base']).strip() + +def gen_primary_build_tool(): + bazel = check_output(['which', 'bazel']).strip() + with open(path.join(ROOT, ".primary_build_tool"), 'w') as fd: + fd.write("bazel=%s\n" % bazel) + fd.write("PATH=%s\n" % environ["PATH"]) + +def _query_classpath(target): + deps = [] + t = cp_targets[target] + try: + check_call(['bazel', 'build', t]) + except CalledProcessError: + exit(1) + name = 'bazel-bin/tools/eclipse/' + t.split(':')[1] + '.runtime_classpath' + deps = [line.rstrip('\n') for line in open(name)] + return deps + +def gen_project(name='gerrit', root=ROOT): + p = path.join(root, '.project') + with open(p, 'w') as fd: + print("""\ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>%(name)s</name> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription>\ + """ % {"name": name}, file=fd) + +def gen_plugin_classpath(root): + p = path.join(root, '.classpath') + with open(p, 'w') as fd: + if path.exists(path.join(root, 'src', 'test', 'java')): + testpath = """ + <classpathentry excluding="**/BUILD" kind="src" path="src/test/java"\ + out="eclipse-out/test"/>""" + else: + testpath = "" + print("""\ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="**/BUILD" kind="src" path="src/main/java"/>%(testpath)s + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/gerrit"/> + <classpathentry kind="output" path="eclipse-out/classes"/> +</classpath>""" % {"testpath": testpath}, file=fd) + +def gen_classpath(ext): + def make_classpath(): + impl = minidom.getDOMImplementation() + return impl.createDocument(None, 'classpath', None) + + def classpathentry(kind, path, src=None, out=None, exported=None): + e = doc.createElement('classpathentry') + e.setAttribute('kind', kind) + # TODO(davido): Remove this and other exclude BUILD files hack + # when this Bazel bug is fixed: + # https://github.com/bazelbuild/bazel/issues/1083 + if kind == 'src': + e.setAttribute('excluding', '**/BUILD') + e.setAttribute('path', path) + if src: + e.setAttribute('sourcepath', src) + if out: + e.setAttribute('output', out) + if exported: + e.setAttribute('exported', 'true') + doc.documentElement.appendChild(e) + + doc = make_classpath() + src = set() + lib = set() + gwt_src = set() + gwt_lib = set() + plugins = set() + + # Classpath entries are absolute for cross-cell support + java_library = re.compile('bazel-out/local-fastbuild/bin/(.*)/[^/]+[.]jar$') + srcs = re.compile('(.*/external/[^/]+)/jar/(.*)[.]jar') + for p in _query_classpath(MAIN): + if p.endswith('-src.jar'): + # gwt_module() depends on -src.jar for Java to JavaScript compiles. + if p.startswith("external"): + p = path.join(ext, p) + gwt_lib.add(p) + continue + + + m = java_library.match(p) + if m: + src.add(m.group(1)) + # Exceptions: both source and lib + if p.endswith('libquery_parser.jar') or \ + p.endswith('prolog/libcommon.jar'): + lib.add(p) + else: + # Don't mess up with Bazel internal test runner dependencies. + # When we use Eclipse we rely on it for running the tests + if p.endswith("external/bazel_tools/tools/jdk/TestRunner_deploy.jar"): + continue + if p.startswith("external"): + p = path.join(ext, p) + lib.add(p) + + for p in _query_classpath(GWT): + m = java_library.match(p) + if m: + gwt_src.add(m.group(1)) + + for s in sorted(src): + out = None + + if s.startswith('lib/'): + out = 'eclipse-out/lib' + elif s.startswith('plugins/'): + if args.plugins: + plugins.add(s) + continue + out = 'eclipse-out/' + s + + p = path.join(s, 'java') + if path.exists(p): + classpathentry('src', p, out=out) + continue + + for env in ['main', 'test']: + o = None + if out: + o = out + '/' + env + elif env == 'test': + o = 'eclipse-out/test' + + for srctype in ['java', 'resources']: + p = path.join(s, 'src', env, srctype) + if path.exists(p): + classpathentry('src', p, out=o) + + for libs in [lib, gwt_lib]: + for j in sorted(libs): + s = None + m = srcs.match(j) + if m: + prefix = m.group(1) + suffix = m.group(2) + p = path.join(prefix, "jar", "%s-src.jar" % suffix) + if path.exists(p): + s = p + if args.plugins: + classpathentry('lib', j, s, exported=True) + else: + # Filter out the source JARs that we pull through transitive closure of + # GWT plugin API (we add source directories themself). Exception is + # libEdit-src.jar, that is needed for GWT SDM to work. + m = java_library.match(j) + if m: + if m.group(1).startswith("gerrit-") and \ + j.endswith("-src.jar") and \ + not j.endswith("libEdit-src.jar"): + continue + classpathentry('lib', j, s) + + for s in sorted(gwt_src): + p = path.join(ROOT, s, 'src', 'main', 'java') + if path.exists(p): + classpathentry('lib', p, out='eclipse-out/gwtsrc') + + classpathentry('con', JRE) + classpathentry('output', 'eclipse-out/classes') + + p = path.join(ROOT, '.classpath') + with open(p, 'w') as fd: + doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8') + + if args.plugins: + for plugin in plugins: + plugindir = path.join(ROOT, plugin) + try: + gen_project(plugin.replace('plugins/', ""), plugindir) + gen_plugin_classpath(plugindir) + except (IOError, OSError) as err: + print('error generating project for %s: %s' % (plugin, err), + file=sys.stderr) + +def gen_factorypath(ext): + doc = minidom.getDOMImplementation().createDocument(None, 'factorypath', None) + for jar in _query_classpath(AUTO): + e = doc.createElement('factorypathentry') + e.setAttribute('kind', 'EXTJAR') + e.setAttribute('id', path.join(ext, jar)) + e.setAttribute('enabled', 'true') + e.setAttribute('runInBatchMode', 'false') + doc.documentElement.appendChild(e) + + p = path.join(ROOT, '.factorypath') + with open(p, 'w') as fd: + doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8') + +try: + ext_location = retrieve_ext_location() + gen_project(args.project_name) + gen_classpath(ext_location) + gen_factorypath(ext_location) + gen_primary_build_tool() + + # TODO(davido): Remove this when GWT gone + gwt_working_dir = ".gwt_work_dir" + if not path.isdir(gwt_working_dir): + makedirs(path.join(ROOT, gwt_working_dir)) + + try: + check_call(['bazel', 'build', MAIN, GWT, '//gerrit-patch-jgit:libEdit-src.jar']) + except CalledProcessError: + exit(1) +except KeyboardInterrupt: + print('Interrupted by user', file=sys.stderr) + exit(1)
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/intellij/Gerrit_Code_Style.xml b/tools/intellij/Gerrit_Code_Style.xml new file mode 100644 index 0000000..b913e09 --- /dev/null +++ b/tools/intellij/Gerrit_Code_Style.xml
@@ -0,0 +1,531 @@ +<code_scheme name="Google Format (Gerrit)"> + <option name="OTHER_INDENT_OPTIONS"> + <value> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + <option name="USE_TAB_CHARACTER" value="false" /> + <option name="SMART_TABS" value="false" /> + <option name="LABEL_INDENT_SIZE" value="0" /> + <option name="LABEL_INDENT_ABSOLUTE" value="false" /> + <option name="USE_RELATIVE_INDENTS" value="false" /> + </value> + </option> + <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> + <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" /> + <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND"> + <value /> + </option> + <option name="IMPORT_LAYOUT_TABLE"> + <value> + <package name="" withSubpackages="true" static="true" /> + <emptyLine /> + <package name="com.google" withSubpackages="true" static="false" /> + <emptyLine /> + <package name="org" withSubpackages="true" static="false" /> + <emptyLine /> + <package name="java" withSubpackages="true" static="false" /> + <emptyLine /> + <package name="" withSubpackages="true" static="false" /> + </value> + </option> + <option name="RIGHT_MARGIN" value="80" /> + <option name="JD_ALIGN_PARAM_COMMENTS" value="false" /> + <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" /> + <option name="JD_P_AT_EMPTY_LINES" value="false" /> + <option name="JD_KEEP_EMPTY_PARAMETER" value="false" /> + <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" /> + <option name="JD_KEEP_EMPTY_RETURN" value="false" /> + <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" /> + <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> + <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="ALIGN_MULTILINE_FOR" value="false" /> + <option name="CALL_PARAMETERS_WRAP" value="1" /> + <option name="METHOD_PARAMETERS_WRAP" value="1" /> + <option name="EXTENDS_LIST_WRAP" value="1" /> + <option name="THROWS_KEYWORD_WRAP" value="1" /> + <option name="METHOD_CALL_CHAIN_WRAP" value="1" /> + <option name="BINARY_OPERATION_WRAP" value="1" /> + <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" /> + <option name="TERNARY_OPERATION_WRAP" value="1" /> + <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> + <option name="FOR_STATEMENT_WRAP" value="1" /> + <option name="ARRAY_INITIALIZER_WRAP" value="1" /> + <option name="WRAP_COMMENTS" value="true" /> + <option name="IF_BRACE_FORCE" value="3" /> + <option name="DOWHILE_BRACE_FORCE" value="3" /> + <option name="WHILE_BRACE_FORCE" value="3" /> + <option name="FOR_BRACE_FORCE" value="3" /> + <AndroidXmlCodeStyleSettings> + <option name="USE_CUSTOM_SETTINGS" value="true" /> + <option name="LAYOUT_SETTINGS"> + <value> + <option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" /> + </value> + </option> + </AndroidXmlCodeStyleSettings> + <JSCodeStyleSettings> + <option name="INDENT_CHAINED_CALLS" value="false" /> + </JSCodeStyleSettings> + <Python> + <option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" /> + </Python> + <TypeScriptCodeStyleSettings> + <option name="INDENT_CHAINED_CALLS" value="false" /> + </TypeScriptCodeStyleSettings> + <XML> + <option name="XML_ALIGN_ATTRIBUTES" value="false" /> + <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> + </XML> + <codeStyleSettings language="CSS"> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="ECMA Script Level 4"> + <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="ALIGN_MULTILINE_FOR" value="false" /> + <option name="CALL_PARAMETERS_WRAP" value="1" /> + <option name="METHOD_PARAMETERS_WRAP" value="1" /> + <option name="EXTENDS_LIST_WRAP" value="1" /> + <option name="BINARY_OPERATION_WRAP" value="1" /> + <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" /> + <option name="TERNARY_OPERATION_WRAP" value="1" /> + <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> + <option name="FOR_STATEMENT_WRAP" value="1" /> + <option name="ARRAY_INITIALIZER_WRAP" value="1" /> + <option name="IF_BRACE_FORCE" value="3" /> + <option name="DOWHILE_BRACE_FORCE" value="3" /> + <option name="WHILE_BRACE_FORCE" value="3" /> + <option name="FOR_BRACE_FORCE" value="3" /> + <option name="PARENT_SETTINGS_INSTALLED" value="true" /> + </codeStyleSettings> + <codeStyleSettings language="HTML"> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="JAVA"> + <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" /> + <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" /> + <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="3" /> + <option name="KEEP_BLANK_LINES_IN_CODE" value="3" /> + <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="3" /> + <option name="BLANK_LINES_BEFORE_IMPORTS" value="0" /> + <option name="BLANK_LINES_AROUND_CLASS" value="2" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="ALIGN_MULTILINE_RESOURCES" value="false" /> + <option name="ALIGN_MULTILINE_FOR" value="false" /> + <option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" /> + <option name="CALL_PARAMETERS_WRAP" value="1" /> + <option name="METHOD_PARAMETERS_WRAP" value="1" /> + <option name="EXTENDS_LIST_WRAP" value="1" /> + <option name="THROWS_LIST_WRAP" value="1" /> + <option name="EXTENDS_KEYWORD_WRAP" value="1" /> + <option name="THROWS_KEYWORD_WRAP" value="1" /> + <option name="METHOD_CALL_CHAIN_WRAP" value="1" /> + <option name="BINARY_OPERATION_WRAP" value="1" /> + <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" /> + <option name="TERNARY_OPERATION_WRAP" value="1" /> + <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> + <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" /> + <option name="FOR_STATEMENT_WRAP" value="1" /> + <option name="ARRAY_INITIALIZER_WRAP" value="1" /> + <option name="ASSIGNMENT_WRAP" value="1" /> + <option name="IF_BRACE_FORCE" value="3" /> + <option name="DOWHILE_BRACE_FORCE" value="3" /> + <option name="WHILE_BRACE_FORCE" value="3" /> + <option name="FOR_BRACE_FORCE" value="3" /> + <option name="PARENT_SETTINGS_INSTALLED" value="true" /> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="JSON"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="JavaScript"> + <option name="RIGHT_MARGIN" value="80" /> + <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="ALIGN_MULTILINE_FOR" value="false" /> + <option name="CALL_PARAMETERS_WRAP" value="1" /> + <option name="METHOD_PARAMETERS_WRAP" value="1" /> + <option name="BINARY_OPERATION_WRAP" value="1" /> + <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" /> + <option name="TERNARY_OPERATION_WRAP" value="1" /> + <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> + <option name="FOR_STATEMENT_WRAP" value="1" /> + <option name="ARRAY_INITIALIZER_WRAP" value="1" /> + <option name="IF_BRACE_FORCE" value="3" /> + <option name="DOWHILE_BRACE_FORCE" value="3" /> + <option name="WHILE_BRACE_FORCE" value="3" /> + <option name="FOR_BRACE_FORCE" value="3" /> + <option name="PARENT_SETTINGS_INSTALLED" value="true" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="Python"> + <option name="RIGHT_MARGIN" value="80" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="PARENT_SETTINGS_INSTALLED" value="true" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="SASS"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="SCSS"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="TypeScript"> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + </codeStyleSettings> + <codeStyleSettings language="XML"> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> + <arrangement> + <rules> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:android</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:id</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:.*Style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_width</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_height</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_weight</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_margin</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginTop</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginBottom</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginStart</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginEnd</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginLeft</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_marginRight</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:padding</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingTop</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingBottom</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingStart</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingEnd</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingLeft</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:paddingRight</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_NAMESPACE>.*</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + </rules> + </arrangement> + </codeStyleSettings> +</code_scheme>
diff --git a/tools/intellij/copyright/Gerrit_Copyright.xml b/tools/intellij/copyright/Gerrit_Copyright.xml new file mode 100644 index 0000000..5609cdc --- /dev/null +++ b/tools/intellij/copyright/Gerrit_Copyright.xml
@@ -0,0 +1,6 @@ +<component name="CopyrightManager"> + <copyright> + <option name="myName" value="Gerrit Copyright" /> + <option name="notice" value="Copyright (C) &#36;today.year The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." /> + </copyright> +</component> \ No newline at end of file
diff --git a/tools/intellij/copyright/profiles_settings.xml b/tools/intellij/copyright/profiles_settings.xml new file mode 100644 index 0000000..dfb94d5 --- /dev/null +++ b/tools/intellij/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@ +<component name="CopyrightManager"> + <settings default="Gerrit Copyright"> + <LanguageOptions name="__TEMPLATE__"> + <option name="block" value="false" /> + </LanguageOptions> + </settings> +</component> \ No newline at end of file
diff --git a/tools/intellij/gerrit_daemon.xml b/tools/intellij/gerrit_daemon.xml new file mode 100644 index 0000000..85dc6a7 --- /dev/null +++ b/tools/intellij/gerrit_daemon.xml
@@ -0,0 +1,16 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="gerrit_daemon" type="Application" factoryName="Application" singleton="true"> + <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" /> + <option name="MAIN_CLASS_NAME" value="Main" /> + <option name="PROGRAM_PARAMETERS" value="daemon --console-log --show-stack-trace -d ${GERRIT_TESTSITE}" /> + <option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$" /> + <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" /> + <option name="ALTERNATIVE_JRE_PATH" /> + <option name="ENABLE_SWING_INSPECTOR" value="false" /> + <option name="ENV_VARIABLES" /> + <option name="PASS_PARENT_ENVS" value="true" /> + <module name=".workspace" /> + <envs /> + <method /> + </configuration> +</component>
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..08a997f --- /dev/null +++ b/tools/js/bower2bazel.py
@@ -0,0 +1,230 @@ +#!/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 collections +import json +import hashlib +import optparse +import os +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", + "BSD-3-Clause": "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", + "lodash": "polymer", # MIT, actually. + "promise-polyfill": "promise-polyfill", + "webcomponentsjs": "polymer", # self-identifies as BSD. + "sinon-chai": "polymer", # WTFPL & BSD. + "sinonjs": "polymer", # 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: %s" % (pkg_name, pkg) + 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..d99b282 100755 --- a/tools/js/bower2buck.py +++ b/tools/js/bower2buck.py
@@ -26,8 +26,7 @@ import sys import tempfile -from tools import util - +from tools.js import bowerutil # This script is run with `buck run`, but needs to shell out to buck; this is # only possible if we avoid buckd. @@ -78,7 +77,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 +105,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 +117,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..8e8e835 --- /dev/null +++ b/tools/js/bowerutil.py
@@ -0,0 +1,46 @@ +# 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 + + +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/maven/BUCK b/tools/maven/BUCK index 322b5a2..0541fc0 100644 --- a/tools/maven/BUCK +++ b/tools/maven/BUCK
@@ -1,6 +1,6 @@ -include_defs('//VERSION') include_defs('//tools/maven/package.defs') include_defs('//tools/maven/repository.defs') +include_defs('//version.bzl') if GERRIT_VERSION.endswith('-SNAPSHOT'): URL = MAVEN_SNAPSHOT_URL
diff --git a/tools/maven/BUILD b/tools/maven/BUILD new file mode 100644 index 0000000..9ac46ac --- /dev/null +++ b/tools/maven/BUILD
@@ -0,0 +1,32 @@ +load("//:version.bzl", "GERRIT_VERSION") +load("//tools/maven:package.bzl", "maven_package") + +MAVEN_REPOSITORY = "sonatype-nexus-staging" + +# TODO(davido): support snapshot repositories +MAVEN_RELEASE_URL = "https://oss.sonatype.org/service/local/staging/deploy/maven2" + +maven_package( + src = { + "gerrit-acceptance-framework": "//gerrit-acceptance-framework:liblib-src.jar", + "gerrit-extension-api": "//gerrit-extension-api:libapi-src.jar", + "gerrit-plugin-api": "//gerrit-plugin-api:plugin-api-sources_deploy.jar", + "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api-source_deploy.jar", + }, + doc = { + "gerrit-acceptance-framework": "//gerrit-acceptance-framework:acceptance-framework-javadoc", + "gerrit-extension-api": "//gerrit-extension-api:extension-api-javadoc", + "gerrit-plugin-api": "//gerrit-plugin-api:plugin-api-javadoc", + "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api-javadoc", + }, + jar = { + "gerrit-acceptance-framework": "//gerrit-acceptance-framework:acceptance-framework_deploy.jar", + "gerrit-extension-api": "//gerrit-extension-api:extension-api_deploy.jar", + "gerrit-plugin-api": "//gerrit-plugin-api:plugin-api_deploy.jar", + "gerrit-plugin-gwtui": "//gerrit-plugin-gwtui:gwtui-api_deploy.jar", + }, + repository = MAVEN_REPOSITORY, + url = MAVEN_RELEASE_URL, + version = GERRIT_VERSION, + war = {"gerrit-war": "//:release"}, +)
diff --git a/tools/maven/api.sh b/tools/maven/api.sh index c7ce65e..93b5f2e 100755 --- a/tools/maven/api.sh +++ b/tools/maven/api.sh
@@ -14,16 +14,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [[ "$#" == "0" ]] ; then +if [[ "$#" != "2" ]] ; then cat <<EOF -Usage: run "$0 COMMAND" from the top of your workspace, where -COMMAND is one of +Usage: run "$0 COMMAND BUILD-TOOL" from the top of your workspace, +where COMMAND is one of install deploy war_install war_deploy +and BUILD-TOOL is one of + + buck + bazel Set VERBOSE in the environment to get more information. EOF @@ -54,16 +58,33 @@ ;; esac +case "$2" in +bazel) + buildProc=bazel + ;; +buck) + buildProc=buck + ;; +*) + echo "unknown build-tool $2. Should be buck or bazel." + exit 1 + ;; +esac + if [[ "${VERBOSE:-x}" != "x" ]]; then set -o xtrace fi -buck build //tools/maven:gen_${command} || \ - { echo "buck failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; } +$buildProc build //tools/maven:gen_${command} || \ + { echo "$buildProc failed to build gen_${command}. Use VERBOSE=1 for more info" ; exit 1 ; } -script="./buck-out/gen/tools/maven/gen_${command}/${command}.sh" - -# The PEX wrapper does some funky exit handling, so even if the script -# does "exit(0)", the return status is '1'. So we can't tell if the -# following invocation was successful. -${script} +if [[ "$buildProc" = "bazel" ]]; then + script="./bazel-genfiles/tools/maven/${command}.sh" + ${script} +else + script="./buck-out/gen/tools/maven/gen_${command}/${command}.sh" + # The PEX wrapper does some funky exit handling, so even if the script + # does "exit(0)", the return status is '1'. So we can't tell if the + # following invocation was successful. + ${script} +fi
diff --git a/tools/maven/package.bzl b/tools/maven/package.bzl new file mode 100644 index 0000000..cf36311 --- /dev/null +++ b/tools/maven/package.bzl
@@ -0,0 +1,96 @@ +# 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. + +sh_bang_template = (" && ".join([ + "echo '#!/bin/bash -e' > $@", + "echo \"# this script should run from the root of your workspace.\" >> $@", + "echo \"\" >> $@", + "echo 'if [[ \"$$VERBOSE\" ]]; then set -x ; fi' >> $@", + "echo \"\" >> $@", + "echo %s >> $@", + "echo \"\" >> $@", + "echo %s >> $@", +])) + +def maven_package( + version, + repository = None, + url = None, + jar = {}, + src = {}, + doc = {}, + war = {}): + + build_cmd = ['bazel', 'build'] + mvn_cmd = ['python', 'tools/maven/mvn.py', '-v', version] + api_cmd = mvn_cmd[:] + api_targets = [] + for type,d in [('jar', jar), ('java-source', src), ('javadoc', doc)]: + for a,t in sorted(d.items()): + api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t)) + api_targets.append(t) + + native.genrule( + name = 'gen_api_install', + cmd = sh_bang_template % ( + ' '.join(build_cmd + api_targets), + ' '.join(api_cmd + ['-a', 'install'])), + srcs = api_targets, + outs = ['api_install.sh'], + executable = True, + testonly = 1, + ) + + if repository and url: + native.genrule( + name = 'gen_api_deploy', + cmd = sh_bang_template % ( + ' '.join(build_cmd + api_targets), + ' '.join(api_cmd + ['-a', 'deploy', + '--repository', repository, + '--url', url])), + srcs = api_targets, + outs = ['api_deploy.sh'], + executable = True, + testonly = 1, + ) + + war_cmd = mvn_cmd[:] + war_targets = [] + for a,t in sorted(war.items()): + war_cmd.append('-s %s:war:$(location %s)' % (a,t)) + war_targets.append(t) + + native.genrule( + name = 'gen_war_install', + cmd = sh_bang_template % (' '.join(build_cmd + war_targets), + ' '.join(war_cmd + ['-a', 'install'])), + srcs = war_targets, + outs = ['war_install.sh'], + executable = True, + ) + + if repository and url: + native.genrule( + name = 'gen_war_deploy', + cmd = sh_bang_template % ( + ' '.join(build_cmd + war_targets), + ' '.join(war_cmd + [ + '-a', 'deploy', + '--repository', repository, + '--url', url])), + srcs = war_targets, + outs = ['war_deploy.sh'], + executable = True, + )
diff --git a/tools/maven/package.defs b/tools/maven/package.defs index c412ebd..a557170 100644 --- a/tools/maven/package.defs +++ b/tools/maven/package.defs
@@ -13,10 +13,10 @@ # limitations under the License. sh_bang_template = (' && '.join([ - "echo '#!/bin/bash -eu' > $OUT", + "echo '#!/bin/bash -e' > $OUT", 'echo "# this script should run from the root of your workspace." >> $OUT', 'echo "" >> $OUT', - "echo 'if [[ -n \"$${VERBOSE:-}\" ]]; then set -x ; fi' >> $OUT", + "echo 'if [[ \"${VERBOSE}\" ]]; then set -x ; fi' >> $OUT", 'echo "" >> $OUT', 'echo %s >> $OUT', 'echo "" >> $OUT',
diff --git a/tools/plugin_archetype_deploy.sh b/tools/plugin_archetype_deploy.sh deleted file mode 100755 index b16ce95..0000000 --- a/tools/plugin_archetype_deploy.sh +++ /dev/null
@@ -1,89 +0,0 @@ -#!/usr/bin/env bash -# 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. - -function help -{ - cat <<'eof' -Usage: plugin_archetype_deploy [option] - -Deploys Gerrit plugin Maven archetypes to Maven Central - -Valid options: - --help show this message. - --dry-run don't execute commands, just print them. - -eof -exit -} - -function getver -{ - grep "$1" $root/VERSION | sed "s/.*'\(.*\)'/\1/" -} - -function instroot -{ - bindir=${0%/*} - - case $bindir in - ./*) bindir=$PWD/$bindir ;; - esac - - cd $bindir/.. - pwd -} - -function doIt -{ - case $dryRun in - true) echo "$@" ;; - *) "$@" ;; - esac -} - -function build_and_deploy -{ - module=${PWD##*/} - doIt mvn package gpg:sign-and-deploy-file \ - -Durl=$url \ - -DrepositoryId=sonatype-nexus-staging \ - -DpomFile=pom.xml \ - -Dfile=target/$module-$ver.jar -} - -function run -{ - test ${dryRun:-'false'} == 'false' - root=$(instroot) - cd "$root" - ver=$(getver GERRIT_VERSION) - [[ $ver == *-SNAPSHOT ]] \ - && url="https://oss.sonatype.org/content/repositories/snapshots" \ - || url="https://oss.sonatype.org/service/local/staging/deploy/maven2" - - for d in gerrit-plugin-archetype \ - gerrit-plugin-js-archetype \ - gerrit-plugin-gwt-archetype ; do - (cd "$d"; build_and_deploy) - done -} - -if [ "$1" == "--dry-run" ]; then - dryRun=true && run -elif [ -z "$1" ]; then - run -else - help -fi
diff --git a/tools/util.py b/tools/util.py index 08a803f..e8182ed 100644 --- a/tools/util.py +++ b/tools/util.py
@@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os from os import path REPO_ROOTS = { @@ -70,34 +69,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/version.py b/tools/version.py index 9f03a59..fee1477 100755 --- a/tools/version.py +++ b/tools/version.py
@@ -46,14 +46,13 @@ src_pattern = re.compile(r'^(\s*<version>)([-.\w]+)(</version>\s*)$', re.MULTILINE) for project in ['gerrit-acceptance-framework', 'gerrit-extension-api', - 'gerrit-plugin-api', 'gerrit-plugin-archetype', - 'gerrit-plugin-gwt-archetype', 'gerrit-plugin-gwtui', - 'gerrit-plugin-js-archetype', 'gerrit-war']: + 'gerrit-plugin-api', 'gerrit-plugin-gwtui', + 'gerrit-war']: pom = os.path.join(project, 'pom.xml') replace_in_file(pom, src_pattern) src_pattern = re.compile(r"^(GERRIT_VERSION = ')([-.\w]+)(')$", re.MULTILINE) -replace_in_file('VERSION', src_pattern) +replace_in_file('version.bzl', src_pattern) src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$', re.MULTILINE)
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh new file mode 100755 index 0000000..cd48a30 --- /dev/null +++ b/tools/workspace-status.sh
@@ -0,0 +1,22 @@ +#!/bin/bash + +# 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. + +function rev() { + cd $1; git describe --always --match "v[0-9].*" --dirty +} + +echo STABLE_BUILD_GERRIT_LABEL $(rev .) +for p in plugins/* ; do + test -d "$p" || continue + echo STABLE_BUILD_$(echo $(basename $p)_LABEL|tr [a-z] [A-Z]) $(rev $p) +done +echo "STABLE_WORKSPACE_ROOT ${PWD}"
diff --git a/VERSION b/version.bzl similarity index 81% rename from VERSION rename to version.bzl index 0f5659a..24a3c27 100644 --- a/VERSION +++ b/version.bzl
@@ -2,4 +2,4 @@ # Used by :api_install and :api_deploy targets # when talking to the destination repository. # -GERRIT_VERSION = '2.13.4' +GERRIT_VERSION = "2.14-SNAPSHOT"