Merge branch 'stable-2.13' * stable-2.13: Allow plugins to set a custom metrics prefix Change-Id: Ia46ff8f66a1b8f18d2c7af08d62d5637f57a49e5
diff --git a/.bazelproject b/.bazelproject new file mode 100644 index 0000000..41bb27f --- /dev/null +++ b/.bazelproject
@@ -0,0 +1,20 @@ +# 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 + +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 deleted file mode 100644 index b347a96..0000000 --- a/.buckconfig +++ /dev/null
@@ -1,33 +0,0 @@ -[alias] - api = //:api - chrome = //:chrome - docs = //Documentation:searchfree - firefox = //:firefox - gerrit = //:gerrit - gwtgerrit = //:gwtgerrit - headless = //:headless - polygerrit = //:polygerrit - release = //:release - releasenotes = //ReleaseNotes:html - safari = //:safari - soyc = //gerrit-gwtui:ui_soyc - soyc_r = //gerrit-gwtui:ui_soyc_r - withdocs = //:withdocs - -[buildfile] - includes = //tools/default.defs - -[java] - jar_spool_mode = direct_to_jar - src_roots = java, resources, src - -[project] - ignore = .git, eclipse-out, bazel-gerrit, bin - parallel_parsing = true - -[cache] - mode = dir - dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts - -[test] - excluded_labels = manual
diff --git a/.buckversion b/.buckversion deleted file mode 100644 index f5fe016..0000000 --- a/.buckversion +++ /dev/null
@@ -1 +0,0 @@ -e64a2e2ada022f81e42be750b774024469551398
diff --git a/.gitignore b/.gitignore index 341d3a5..3fb8768 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,28 +1,27 @@ /.apt_generated +/.buckd /.classpath /.factorypath /.project /.settings/org.maven.ide.eclipse.prefs /.settings/org.eclipse.m2e.core.prefs /.settings/org.eclipse.ltk.core.refactoring.prefs +/.metadata /test_site /.idea *.iml *.eml *.sublime-* /gerrit-package-plugins -/.buckconfig.local -/.buckjavaargs -/.buckd /bazel-bin /bazel-genfiles /bazel-gerrit /bazel-out /bazel-testlogs -/buck-cache /buck-out /eclipse-out /extras +/infer-out /local.properties *.pyc /gwt-unitCache
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/.watchmanconfig b/.watchmanconfig deleted file mode 100644 index 4467aec..0000000 --- a/.watchmanconfig +++ /dev/null
@@ -1,9 +0,0 @@ -{ - "ignore_dirs": [ - "buck-out", - "eclipse-out" - ], - "ignore_vcs": [ - ".git" - ] -}
diff --git a/BUCK b/BUCK deleted file mode 100644 index 9657ff3..0000000 --- a/BUCK +++ /dev/null
@@ -1,31 +0,0 @@ -include_defs('//tools/build.defs') - -gerrit_war(name = 'gerrit') -gerrit_war(name = 'gwtgerrit', ui = 'ui_dbg') -gerrit_war(name = 'headless', ui = None) -gerrit_war(name = 'chrome', ui = 'ui_chrome') -gerrit_war(name = 'firefox', ui = 'ui_firefox') -gerrit_war(name = 'safari', ui = 'ui_safari') -gerrit_war(name = 'polygerrit', ui = 'polygerrit') -gerrit_war(name = 'withdocs', docs = True) -gerrit_war(name = 'release', ui = 'ui_optdbg_r', docs = True, context = ['//plugins:core'], visibility = ['//tools/maven:']) - -API_DEPS = [ - '//gerrit-acceptance-framework:acceptance-framework', - '//gerrit-acceptance-framework:acceptance-framework-src', - '//gerrit-acceptance-framework:acceptance-framework-javadoc', - '//gerrit-extension-api:extension-api', - '//gerrit-extension-api:extension-api-src', - '//gerrit-extension-api:extension-api-javadoc', - '//gerrit-plugin-api:plugin-api', - '//gerrit-plugin-api:plugin-api-src', - '//gerrit-plugin-api:plugin-api-javadoc', - '//gerrit-plugin-gwtui:gwtui-api', - '//gerrit-plugin-gwtui:gwtui-api-src', - '//gerrit-plugin-gwtui:gwtui-api-javadoc', -] - -zip_file( - name = 'api', - srcs = API_DEPS, -)
diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..d806bdb --- /dev/null +++ b/BUILD
@@ -0,0 +1,72 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools/bzl:genrule2.bzl", "genrule2") +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, +) + +API_DEPS = [ + "//gerrit-acceptance-framework:acceptance-framework_deploy.jar", + "//gerrit-acceptance-framework:liblib-src.jar", + "//gerrit-acceptance-framework:acceptance-framework-javadoc", + "//gerrit-extension-api:extension-api_deploy.jar", + "//gerrit-extension-api:libapi-src.jar", + "//gerrit-extension-api:extension-api-javadoc", + "//gerrit-plugin-api:plugin-api_deploy.jar", + "//gerrit-plugin-api:plugin-api-sources_deploy.jar", + "//gerrit-plugin-api:plugin-api-javadoc", + "//gerrit-plugin-gwtui:gwtui-api_deploy.jar", + "//gerrit-plugin-gwtui:gwtui-api-source_deploy.jar", + "//gerrit-plugin-gwtui:gwtui-api-javadoc", +] + +genrule2( + name = 'api', + srcs = API_DEPS, + testonly = 1, + cmd = ' && '.join([ + 'cp $(SRCS) $$TMP', + 'cd $$TMP', + 'zip -qr $$ROOT/$@ .', + ]), + outs = ['api.zip'], +)
diff --git a/Documentation/BUCK b/Documentation/BUCK deleted file mode 100644 index 48ca579..0000000 --- a/Documentation/BUCK +++ /dev/null
@@ -1,80 +0,0 @@ -include_defs('//Documentation/asciidoc.defs') -include_defs('//Documentation/config.defs') -include_defs('//Documentation/license.defs') -include_defs('//tools/git.defs') - -DOC_DIR = 'Documentation' - -JSUI_JAVA_DEPS = ['//gerrit-gwtui:ui_module'] -JSUI_NON_JAVA_DEPS = ['//polygerrit-ui/app:polygerrit_ui'] -MAIN_JAVA_DEPS = ['//gerrit-pgm:pgm'] -SRCS = glob(['*.txt'], excludes = ['licenses.txt']) - - -genasciidoc( - name = 'html', - out = 'html.zip', - directory = DOC_DIR, - srcs = SRCS + [':licenses.txt'], - attributes = documentation_attributes(git_describe()), - backend = 'html5', - visibility = ['PUBLIC'], -) - -genasciidoc( - name = 'searchfree', - out = 'searchfree.zip', - directory = DOC_DIR, - srcs = SRCS + [':licenses.txt'], - attributes = documentation_attributes(git_describe()), - backend = 'html5', - searchbox = False, - visibility = ['PUBLIC'], -) - -genlicenses( - name = 'licenses.txt', - opts = ['--asciidoc'], - java_deps = JSUI_JAVA_DEPS + MAIN_JAVA_DEPS, - non_java_deps = JSUI_NON_JAVA_DEPS, - out = 'licenses.txt', -) - -# Required by Google for gerrit-review. -genlicenses( - name = 'js_licenses.txt', - opts = ['--partial'], - java_deps = JSUI_JAVA_DEPS, - non_java_deps = JSUI_NON_JAVA_DEPS, - out = 'js_licenses.txt', -) - -python_binary( - name = 'gen_licenses', - main = 'gen_licenses.py', -) - -python_binary( - name = 'replace_macros', - main = 'replace_macros.py', - visibility = ['//ReleaseNotes:'], -) - -genrule( - name = 'index', - cmd = '$(exe //lib/asciidoctor:doc_indexer) ' + - '-o $OUT ' + - '--prefix "%s/" ' % DOC_DIR + - '--in-ext ".txt" ' + - '--out-ext ".html" ' + - '$SRCS ' + - '$(location :licenses.txt)', - srcs = SRCS, - out = 'index.jar', -) - -prebuilt_jar( - name = 'index_lib', - binary_jar = ':index', - visibility = ['PUBLIC'], -)
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/asciidoc.defs b/Documentation/asciidoc.defs deleted file mode 100644 index 4b17071..0000000 --- a/Documentation/asciidoc.defs +++ /dev/null
@@ -1,113 +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. - -def genasciidoc_htmlonly( - name, - out, - srcs = [], - attributes = [], - backend = None, - searchbox = True, - visibility = []): - EXPN = '.' + name + '_expn' - - asciidoc = [ - '$(exe //lib/asciidoctor:asciidoc)', - '-z', '$OUT', - '--base-dir', '$SRCDIR', - '--tmp', '$TMP', - '--in-ext', '".txt%s"' % EXPN, - '--out-ext', '".html"', - ] - if backend: - asciidoc.extend(['-b', backend]) - for attribute in attributes: - asciidoc.extend(['-a', attribute]) - asciidoc.append('$SRCS') - newsrcs = [] - for src in srcs: - fn = src - # We have two cases: regular source files and generated files. - # Generated files are passed as targets ':foo', and ':' is removed. - # 1. regular files: cmd = '-s foo', srcs = ['foo'] - # 2. generated files: cmd = '-s $(location :foo)', srcs = [] - srcs = [src] - passed_src = fn - if fn.startswith(':') : - fn = src[1:] - srcs = [] - passed_src = '$(location :%s)' % fn - ex = fn + EXPN - - genrule( - name = ex, - cmd = '$(exe //Documentation:replace_macros) --suffix="%s"' % EXPN + - ' -s ' + passed_src + ' -o $OUT' + - (' --searchbox' if searchbox else ' --no-searchbox'), - srcs = srcs, - out = ex, - ) - - newsrcs.append(':%s' % ex) - - genrule( - name = name, - cmd = ' '.join(asciidoc), - srcs = newsrcs, - out = out, - visibility = visibility, - ) - -def genasciidoc( - name, - out, - directory, - srcs = [], - attributes = [], - backend = None, - searchbox = True, - resources = True, - visibility = []): - SUFFIX = '_htmlonly' - - genasciidoc_htmlonly( - name = name + SUFFIX if resources else name, - srcs = srcs, - attributes = attributes, - backend = backend, - searchbox = searchbox, - out = (name + SUFFIX + '.zip') if resources else (name + '.zip'), - ) - - if resources: - genrule( - name = name, - cmd = 'cd $TMP;' + - 'mkdir -p %s/images;' % directory + - 'unzip -q $(location %s) -d %s/;' - % (':' + name + SUFFIX, directory) + - 'for s in $SRCS;do ln -s $s %s/;done;' % directory + - 'mv %s/*.{jpg,png} %s/images;' % (directory, directory) + - 'cp $(location %s) LICENSES.txt;' % ':licenses.txt' + - 'zip -qr $OUT *', - srcs = glob([ - 'images/*.jpg', - 'images/*.png', - ]) + [ - '//gerrit-prettify:prettify.min.css', - '//gerrit-prettify:prettify.min.js', - ], - out = out, - visibility = visibility, - )
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 5c3e3f9..a0e00a5 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 @@ -642,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) + @@ -657,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. @@ -729,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 @@ -1011,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. @@ -1515,7 +1545,7 @@ + * `MAXDB` + -Connect to an SAP MaxDb database server. +Connect to an SAP MaxDB database server. + * `MYSQL` + @@ -1804,6 +1834,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 @@ -1973,6 +2012,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. @@ -2145,6 +2201,29 @@ + By default, false. +[[groups.uuid.name]]groups.<uuid>.name:: ++ +Display name for group with the given UUID. ++ +This option is only supported for system groups (scheme 'global'). ++ +E.g. this parameter can be used to configure another name for the +`Anonymous Users` group: ++ +---- +[groups "global:Anonymous-Users"] + name = All Users +---- ++ +When setting this parameter it should be verified that there is no +existing group with the same name (case-insensitive). Configuring an +ambiguous name makes Gerrit fail on startup. Once set Gerrit ensures +that it is not possible to create a group with this name. Gerrit also +keeps the default name reserved so that it cannot be used for new +groups either. This means there is no danger of ambiguous group names +when this parameter is removed and the system group uses the default +name again. + [[http]] === Section http @@ -2446,6 +2525,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`. @@ -2570,6 +2653,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 @@ -2581,9 +2702,9 @@ An example LDAP configuration follows, and then discussion of the parameters introduced here. Suitable defaults for most parameters are automatically guessed based on the type of server -detected during startup. The guessed defaults support both -link:http://www.ietf.org/rfc/rfc2307.txt[RFC 2307] and Active -Directory. +detected during startup. The guessed defaults support +link:http://www.ietf.org/rfc/rfc2307.txt[RFC 2307], Active +Directory and link:https://www.freeipa.org[FreeIPA]. ---- [ldap] @@ -2687,7 +2808,7 @@ is `(uid=${username})` or `(cn=${username})`, but the proper setting depends on the LDAP schema used by the directory server. + -Default is `(uid=${username})` for RFC 2307 servers, +Default is `(uid=${username})` for FreeIPA and RFC 2307 servers, and `(&(objectClass=user)(sAMAccountName=${username}))` for Active Directory. @@ -2705,7 +2826,7 @@ If set, users will be unable to modify their full name field, as Gerrit will populate it only from the LDAP data. + -Default is `displayName` for RFC 2307 servers, +Default is `displayName` for FreeIPA and RFC 2307 servers, and `${givenName} ${sn}` for Active Directory. [[ldap.accountEmailAddress]]ldap.accountEmailAddress:: @@ -2748,17 +2869,25 @@ recommended not to make changes to this setting that would cause the value to differ, as this will prevent users from logging in. + -Default is `uid` for RFC 2307 servers, +Default is `uid` for FreeIPA and RFC 2307 servers, and `${sAMAccountName.toLowerCase}` for Active Directory. [[ldap.accountMemberField]]ldap.accountMemberField:: + _(Optional)_ Name of an attribute on the user account object which contains the groups the user is part of. Typically used for Active -Directory servers. +Directory and FreeIPA servers. + Default is unset for RFC 2307 servers (disabled) -and `memberOf` for Active Directory. +and `memberOf` for Active Directory and FreeIPA. + +[[ldap.accountMemberExpandGroups]]ldap.accountMemberExpandGroups:: ++ +_(Optional)_ Whether to expand nested groups recursively. This +setting is used only if `ldap.accountMemberField` is set. ++ +Default is unset for FreeIPA and `true` for RFC 2307 servers +and Active Directory. [[ldap.fetchMemberOfEagerly]]ldap.fetchMemberOfEagerly:: + @@ -2768,7 +2897,7 @@ as this will result in a much faster LDAP login. + Default is unset for RFC 2307 servers (disabled) and `true` for -Active Directory. +Active Directory and FreeIPA. [[ldap.groupBase]]ldap.groupBase:: + @@ -2797,7 +2926,7 @@ `${groupname}` is replaced with the search term supplied by the group owner. + -Default is `(cn=${groupname})` for RFC 2307, +Default is `(cn=${groupname})` for FreeIPA and RFC 2307 servers, and `(&(objectClass=group)(cn=${groupname}))` for Active Directory. [[ldap.groupMemberPattern]]ldap.groupMemberPattern:: @@ -2815,7 +2944,7 @@ Attributes such as `${dn}` or `${uidNumber}` may be useful. + Default is `(|(memberUid=${username})(gidNumber=${gidNumber}))` for -RFC 2307, and unset (disabled) for Active Directory. +RFC 2307, and unset (disabled) for Active Directory and FreeIPA. [[ldap.groupName]]ldap.groupName:: + @@ -2832,6 +2961,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 @@ -3354,6 +3492,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 @@ -3364,6 +3563,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 @@ -3395,7 +3602,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` + @@ -3421,6 +3630,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 @@ -3506,6 +3725,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 @@ -3822,11 +4050,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-login-register.txt b/Documentation/config-login-register.txt index ffeae62..1639c8a 100644 --- a/Documentation/config-login-register.txt +++ b/Documentation/config-login-register.txt
@@ -135,11 +135,3 @@ user@host:~$ ---- - - -GERRIT ------- -Part of link:index.html[Gerrit Code Review] - -SEARCHBOX ----------
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..0d0717e 100644 --- a/Documentation/config-validation.txt +++ b/Documentation/config-validation.txt
@@ -45,6 +45,18 @@ If the commit fails the validation, the plugin can throw an exception which will cause the merge to fail. +[[on-submit-validation]] +== On submit validation + + +Plugins implementing the `OnSubmitValidationListener` interface can +perform additional validation checks against ref operations resuling +from execution of submit operation before they are applied to any git +repositories (there could be more than one in case of topic submits). + +Plugin can throw an exception which will cause submit operation to be +aborted. + [[pre-upload-validation]] == Pre-upload validation @@ -80,6 +92,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/config.defs b/Documentation/config.defs deleted file mode 100644 index 7f814d3..0000000 --- a/Documentation/config.defs +++ /dev/null
@@ -1,22 +0,0 @@ -DOCUMENTATION_DEPS = { - "install-quick.txt": ["config-login-register.txt"], - "install.txt": ["database-setup.txt"], -} - -def documentation_attributes(revision): - return [ - 'toc', - 'newline="\\n"', - 'asterisk="*"', - 'plus="+"', - 'caret="^"', - 'startsb="["', - 'endsb="]"', - 'tilde="~"', - 'last-update-label!', - 'source-highlighter=prettify', - 'stylesheet=DEFAULT', - 'linkcss=true', - 'prettifydir=.', - 'revnumber="%s"' % revision, - ]
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt index 0f73bd3..8c69972 100644 --- a/Documentation/database-setup.txt +++ b/Documentation/database-setup.txt
@@ -250,11 +250,3 @@ Visit SAP HANA's link:http://help.sap.com/hana_appliance/[documentation] for further information regarding using SAP HANA. - - -GERRIT ------- -Part of link:index.html[Gerrit Code Review] - -SEARCHBOX ----------
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt new file mode 100644 index 0000000..c1cd0e8 --- /dev/null +++ b/Documentation/dev-bazel.txt
@@ -0,0 +1,332 @@ += Gerrit Code Review - Building with Bazel + +[[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 +---- + +[NOTE] +PolyGerrit UI may require additional tools (such as npm). Please read +the polygerrit-ui/README.md for more info. + +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 api +---- + +The output archive that contains Java binaries, Java sources and +Java docs will be placed in: + +---- + bazel-genfiles/api.zip +---- + +Install {extension,plugin,gwt}-api to the local maven repository: + +---- + tools/maven/api.sh install +---- + +Install gerrit.war to the local maven repository: + +---- + tools/maven/api.sh war_install +---- + +=== 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.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.py` again. For IntelliJ, you need to click the `Sync Project +with BUILD Files` button of link:https://ij.bazel.io[IntelliJ plugin]. + +[[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 //... +---- + +To run only tests that do not use SSH: + +---- + bazel test --test_env=GERRIT_USE_SSH=NO //... +---- + +== 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> +---- + +Redirection to local mirrors of Maven Central and the Gerrit storage +bucket is supported by defining specific properties in +`local.properties`, a file that is not tracked by Git: + +---- + echo download.GERRIT = http://nexus.my-company.com/ >>local.properties + echo download.MAVEN_CENTRAL = http://nexus.my-company.com/ >>local.properties +---- + +The `local.properties` file may be placed in the root of the gerrit repository +being built, or in `~/.gerritcodereview/`. The file in the root of the gerrit +repository has precedence. + +== 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', + artifact = 'gwtorm:gwtorm:42', + 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', + artifact = 'com.gitblit:gitblit:1.4.0', + sha1 = '1b130dbf5578ace37507430a4a523f6594bf34fa', + 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', + artifact = 'com.gitblit:gitblit:1.4.0', + sha1 = '1b130dbf5578ace37507430a4a523f6594bf34fa', + 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 deleted file mode 100644 index 315c0b0..0000000 --- a/Documentation/dev-buck.txt +++ /dev/null
@@ -1,668 +0,0 @@ -= Gerrit Code Review - Building with Buck - - -== Installation - -You need to use Java 7 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 -and Mac OS are supported. - -Clone the git and build it: - ----- - git clone https://github.com/facebook/buck - cd buck - git checkout $(cat ../gerrit/.buckversion) - ant ----- - -If you don't have a `bin/` directory in your home directory, create one: - ----- - mkdir ~/bin ----- - -Add the `~/bin` folder to the path: - ----- - PATH=~/bin:$PATH ----- - -Note that the buck executable needs to be available in all shell sessions, -so also make sure it is appended to the path globally. - -Add a symbolic link in `~/bin` to the buck and buckd executables: - ----- - ln -s `pwd`/bin/buck ~/bin/ - ln -s `pwd`/bin/buckd ~/bin/ ----- - -Verify that `buck` is accessible: - ----- - which buck ----- - -To enable autocompletion of buck commands, install the autocompletion -script from `./scripts/buck-completion.bash` in the buck project. Refer -to the script's header comments for installation instructions. - -== Prerequisites - -Buck requires Python version 2.7 to be installed. The Maven download toolchain -requires `curl` to be installed. - -[[eclipse]] -== Eclipse Integration - - -=== Generating the Eclipse Project - -Create the Eclipse project: - ----- - tools/eclipse/project.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: - ----- - tools/eclipse/project.py ----- - - -=== Attaching Sources - -Source JARs are downloaded by default. This allows Eclipse to show -documentation or dive into the implementation of a library JAR. - -To save time and bandwidth, download of source JARs can be restricted -to only those that are necessary to compile Java source into JavaScript -using the GWT compiler: - ----- - tools/eclipse/project.py --no-src ----- - - -[[build]] -== Building on the Command Line - - -=== Gerrit Development WAR File - -To build the Gerrit web application that includes GWT UI and PolyGerrit UI: - ----- - buck build gerrit ----- - -[NOTE] -PolyGerrit UI may require additional tools (such as npm). Please read -the polygerrit-ui/README.md for more info. - -The output executable WAR will be placed in: - ----- - buck-out/gen/gerrit/gerrit.war ----- - -To build the Gerrit web application that includes only GWT UI: - ----- - buck build gwtgerrit ----- - -The output executable WAR will be placed in: - ----- - buck-out/gen/gwtgerrit/gwtgerrit.war ----- - - -=== Headless Mode - -To build Gerrit in headless mode, i.e. without the GWT Web UI: - ----- - buck build headless ----- - -The output executable WAR will be placed in: - ----- - buck-out/gen/headless/headless.war ----- - -=== Extension and Plugin API JAR Files - -To build the extension, plugin and GWT API JAR files: - ----- - buck build api ----- - -Java binaries, Java sources and Java docs are generated into corresponding -project directories in `buck-out/gen`, here as example for plugin API: - ----- - buck-out/gen/gerrit-plugin-api/plugin-api.jar - buck-out/gen/gerrit-plugin-api/plugin-api-javadoc/plugin-api-javadoc.jar - buck-out/gen/gerrit-plugin-api/plugin-api-src.jar ----- - -Install {extension,plugin,gwt}-api to the local maven repository: - ----- - tools/maven/api.sh install ----- - -Install gerrit.war to the local maven repository: - ----- - tools/maven/api.sh war_install ----- - -=== Plugins - -To build all core plugins: - ----- - buck build plugins:core ----- - -The output JAR files for individual plugins will be placed in: - ----- - buck-out/gen/plugins/<name>/<name>.jar ----- - -The JAR files will also be packaged in: - ----- - buck-out/gen/plugins/core/core.zip ----- - -To build a specific plugin: - ----- - buck build plugins/<name>:<name> ----- - -The output JAR file will be be placed in: - ----- - buck-out/gen/plugins/<name>/<name>.jar ----- - -Note that when building an individual plugin, the `core.zip` package -is not regenerated. - -Additional plugins with BUCK files can be added to the build -environment by cloning the source repository into the plugins -subdirectory: - ----- - git clone https://gerrit.googlesource.com/plugins/<name> plugins/<name> - echo /plugins/<name> >>.git/info/exclude ----- - -Additional plugin sources will be automatically added to Eclipse the -next time project.py is run: - ----- - tools/eclipse/project.py ----- - - -[[documentation]] -=== Documentation - -To build only the documentation for testing or static hosting: - ----- - buck build docs ----- - -The generated html files will NOT come with the search box, and will be -placed in: - ----- - buck-out/gen/Documentation/searchfree__tmp/Documentation ----- - -The html files will also be bundled into `searchfree.zip` in this location: - ----- - buck-out/gen/Documentation/searchfree/searchfree.zip ----- - -To build the executable WAR with the documentation included: - ----- - buck build withdocs ----- - -The WAR file will be placed in: - ----- - buck-out/gen/withdocs/withdocs.war ----- - -[[soyc]] -=== GWT Compile Report - -The GWT compiler can output a compile report (or "story of your compile"), -describing the size of the JavaScript and which source classes contributed -to the overall download size. - ----- - buck build soyc ----- - -The report will be written as an HTML page to the extras directory, and -can be opened and viewed in any web browser: - ----- - extras/gerrit_ui/soycReport/compile-report/index.html ----- - -Only the "Split Point Report" is created, "Compiler Metrics" are not output. - -[[release]] -=== Gerrit Release WAR File - -To build the release of the Gerrit web application, including documentation and -all core plugins: - ----- - buck build release ----- - -The output release WAR will be placed in: - ----- - buck-out/gen/release/release.war ----- - -[[tests]] -== Running Unit Tests - -To run all tests including acceptance tests (but not flaky tests): - ----- - buck test --exclude flaky ----- - -To exclude flaky and slow tests: - ----- - buck test --exclude flaky slow ----- - -To run only a specific group of acceptance tests: - ----- - buck test --include api ----- - -The following groups of tests are currently supported: - -* acceptance -* api -* edit -* flaky -* git -* pgm -* rest -* server -* ssh -* slow - -To run a specific test group, e.g. the rest-account test group: - ----- - buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account ----- - -To create test coverage report: - ----- - buck test --code-coverage --code-coverage-format html --no-results-cache ----- - -The HTML report is created in `buck-out/gen/jacoco/code-coverage/index.html`. - -== Dependencies - -Dependency JARs are normally downloaded automatically, but Buck can inspect -its graph and download any missing JAR files. This is useful to enable -subsequent builds to run without network access: - ----- - tools/download_all.py ----- - -When downloading from behind a proxy (which is common in some corporate -environments), it might be necessary to explicitly specify the proxy that -is then used by `curl`: - ----- - export http_proxy=http://<proxy_user_id>:<proxy_password>@<proxy_server>:<proxy_port> ----- - -Redirection to local mirrors of Maven Central and the Gerrit storage -bucket is supported by defining specific properties in -`local.properties`, a file that is not tracked by Git: - ----- - echo download.GERRIT = http://nexus.my-company.com/ >>local.properties - echo download.MAVEN_CENTRAL = http://nexus.my-company.com/ >>local.properties ----- - -The `local.properties` file may be placed in the root of the gerrit repository -being built, or in `~/.gerritcodereview/`. The file in the root of the gerrit -repository has precedence. - -== 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 BUCK 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, - ) ----- - -=== Caching Build Results - -Build results can be locally cached, saving rebuild time when -switching between Git branches. Buck's documentation covers -caching in link:http://facebook.github.io/buck/concept/buckconfig.html[buckconfig]. -The trivial case using a local directory is: - ----- - cat >.buckconfig.local <<EOF - [cache] - mode = dir - dir = buck-cache - EOF ----- - -[[clean-cache]] -=== Cleaning The Buck Cache - -The cache for the Gerrit Code Review project is located in -`~/.gerritcodereview/buck-cache/locally-built-artifacts`. - -The Buck cache should never need to be manually deleted. If you find yourself -deleting the Buck cache regularly, then it is likely that there is something -wrong with your environment or your workflow. - -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). - -[[buck-daemon]] -=== Using Buck daemon - -Buck ships with a daemon command `buckd`, which uses the -link:https://github.com/martylamb/nailgun[Nailgun] protocol for running -Java programs from the command line without incurring the JVM startup -overhead. - -Using a Buck daemon can save significant amounts of time as it avoids the -overhead of starting a Java virtual machine, loading the buck class files -and parsing the build files for each command. - -It is safe to run several buck daemons started from different project -directories and they will not interfere with each other. Buck's documentation -covers daemon in http://facebook.github.io/buck/command/buckd.html[buckd]. - -To use `buckd` the additional -link:https://facebook.github.io/watchman[watchman] program must be installed. - -To disable `buckd`, the environment variable `NO_BUCKD` must be set. It's not -recommended to put it in the shell config, as it can be forgotten about it and -then assumed Buck was working as it should when it should be using buckd. -Prepend the variable to Buck invocation instead: - ----- - NO_BUCKD=1 buck build gerrit ----- - -[[watchman]] -=== Installing watchman - -Watchman is used internally by Buck to monitor directory trees and is needed -for buck daemon to work properly. Because buckd is activated by default in the -latest version of Buck, it searches for the watchman executable in the -path and issues a warning when it is not found and kills buckd. - -To prepare watchman installation on Linux: - ----- - git clone https://github.com/facebook/watchman.git - cd watchman - ./autogen.sh ----- - -To install it in user home directory (without root privileges): - ----- - ./configure --prefix $HOME/watchman - make install ----- - -To install it system wide: - ----- - ./configure - make - sudo make install ----- - -Put $HOME/watchman/bin/watchman in path or link to $HOME/bin/watchman. - -To install watchman on OS X: - ----- - brew install --HEAD watchman ----- - -See the original documentation for more information: -link:https://facebook.github.io/watchman/docs/install.html[Watchman -installation]. - -=== Override Buck's settings - -Additional JVM args for Buck can be set in `.buckjavaargs` in the -project root directory. For example to override Buck's default 1GB -heap size: - ----- - cat > .buckjavaargs <<EOF - -XX:MaxPermSize=512m -Xms8000m -Xmx16000m - EOF ----- - -== Rerun unit tests - -Test execution results are cached by Buck. If a test that was already run -needs to be repeated, the unit test cache for that test must be removed first: - ----- - rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/.rest-account/ ----- - -After clearing the cache, the test can be run again: - ----- - buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account - [-] TESTING...FINISHED 12,3s (12 PASS/0 FAIL) - RESULTS FOR //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:rest-account - PASS 970ms 2 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.CapabilitiesIT - PASS 999ms 1 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.EditPreferencesIT - PASS 1,2s 1 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.GetAccountDetailIT - PASS 951ms 2 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.GetAccountIT - PASS 6,4s 2 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.GetDiffPreferencesIT - PASS 1,2s 4 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.PutUsernameIT - TESTS PASSED ----- - -An alternative approach is to use Buck's `--filters` (`-f`) option: - ----- - buck test -f 'com.google.gerrit.acceptance.rest.account.CapabilitiesIT' - Using buckd. - [-] PROCESSING BUCK FILES...FINISHED 1,0s [100%] - [-] BUILDING...FINISHED 2,8s [100%] (334/701 JOBS, 110 UPDATED, 5,1% CACHE MISS) - [-] TESTING...FINISHED 9,2s (6 PASS/0 FAIL) - RESULTS FOR SELECTED TESTS - PASS 8,0s 2 Passed 0 Skipped 0 Failed com.google.gerrit.acceptance.rest.account.CapabilitiesIT - PASS <100ms 4 Passed 0 Skipped 0 Failed //tools:util_test - TESTS PASSED ----- - -When this option is used, the cache is disabled per design and doesn't need to -be explicitly deleted. Note, that this is a known issue, that python tests are -always executed. - -Note that when this option is used, the whole unit test cache is dropped, so -repeating the - ----- -buck test ----- - -causes all tests to be executed again. - -To run tests without using cached results at all, use the `--no-results-cache` -option: - ----- -buck test --no-results-cache ----- - -== Upgrading Buck - -The following tests should be executed, when Buck version is upgraded: - -* buck build release -* tools/maven/api.sh install -* buck test -* buck build gerrit, change some sources in gerrit-server project, - repeat buck build gerrit and verify that gerrit.war was updated -* install and verify new gerrit site -* upgrade and verify existing gerrit site -* reindex existing gerrit site -* verify that tools/eclipse/project.py produces sane Eclipse project -* verify that tools/eclipse/project.py --src generates sources as well -* verify that unit test execution from Eclipse works -* verify that daemon started from Eclipse works -* verify that GWT SDM debug session started from Eclipse works - -== Known issues and bugs - -=== Symbolic links and `watchman` - -`Buck` with activated `Watchman` has currently a -[known bug](https://github.com/facebook/buck/issues/341) related to -symbolic links. The symbolic links are used very often with external -plugins, that are linked per symbolic link to the plugins directory. -With this use case Buck is failing to rebuild the plugin artifact -after it was built. All attempts to convince Buck to rebuild will fail. -The only known way to recover is to weep out `buck-out` directory. The -better workaround is to avoid using Watchman in this specific use case. -Watchman can either be de-installed or disabled. See -link:#buck-daemon[Using Buck daemon] section above how to temporarily -disable `buckd`. - -== Troubleshooting Buck - -In some cases problems with Buck itself need to be investigated. See for example -link:https://gerrit-review.googlesource.com/62411[this attempt to upgrade Buck] -and link:https://github.com/facebook/buck/pull/227[the fix that was needed] to -make the update possible. - -To build Gerrit with a custom version of Buck, the following steps are necessary: - -1. In the Buck git apply any necessary changes from pull requests -2. Compile Buck with `ant` -3. In the root of the Gerrit project create a `.nobuckcheck` file to prevent Buck -from updating itself -4. Replace the sha1 in Gerrit's `.buckversion` file with the required version from -the custom Buck build -5. Build Gerrit as usual - -GERRIT ------- -Part of link:index.html[Gerrit Code Review] - -SEARCHBOX ----------
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt index 13071df..c777327 100644 --- a/Documentation/dev-build-plugins.txt +++ b/Documentation/dev-build-plugins.txt
@@ -4,15 +4,15 @@ From build process perspective there are three types of plugins: * Maven driven -* Buck in tree driven -* Buck standalone driven +* Bazel tree driven +* Bazel standalone These types can be combined: if both files in plugin's root directory exist: -* `BUCK` +* `BUILD` * `pom.xml` -the plugin can be built with both Buck and Maven. +the plugin can be built with both Bazel and Maven. == Maven driven build @@ -52,35 +52,56 @@ Repeat step 1. above. -== Buck in tree driven +== Bazel in tree driven -The fact that plugin contains `BUCK` file doesn't mean that building this -plugin from the plugin directory works. For now it doesn't. Buck in tree driven -means it can only be built from within Gerrit tree. Clone or link the plugin -into gerrit/plugins directory: +The fact that plugin contains `BUILD` file doesn't mean that building this +plugin from the plugin directory works. + +Bazel in tree driven means it can only be built from within Gerrit tree. Clone +or link the plugin into gerrit/plugins directory: ---- cd gerrit -buck build plugins/<plugin-name>:<plugin-name> +bazel build plugins/<plugin-name>:<plugin-name> ---- The output can be normally found in the following directory: ---- -buck-out/gen/plugins/<plugin-name>/<plugin-name>.jar +bazel-genfiles/plugins/<plugin-name>/<plugin-name>.jar ---- Some plugins describe their build process in `src/main/resources/Documentation/build.md` file. It may worth checking. -== Buck standalone driven +=== Plugins with external dependencies === + +If the plugin has external dependencies, then they must be included from Gerrit's +own WORKSPACE file. This can be achieved by including them in `external_plugin_deps.bzl`. +During the build in Gerrit tree, this file must be copied over the dummy one in +`plugins` directory. + +Example for content of `external_plugin_deps.bzl` file: + +---- +load("//tools/bzl:maven_jar.bzl", "maven_jar") + +def external_plugin_deps(): + maven_jar( + name = 'org_apache_tika_tika_core', + artifact = 'org.apache.tika:tika-core:1.12', + sha1 = '5ab95580d22fe1dee79cffbcd98bb509a32da09b', + ) +---- + +== Bazel standalone driven Only few plugins support that mode for now: ---- cd reviewers -buck build plugin +bazel build reviewers ---- GERRIT
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt index 775fe21..8d16ca1 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 @@ -210,10 +211,10 @@ * Define any static interfaces next in your class. * Define non static interfaces after static interfaces in your class. - * Next you should define static types, members, and methods, in - decreasing order of visibility (public to private). - * Finally instance members, then constructors, and then instance - methods. + * Next you should define static types, static members, and + static methods, in decreasing order of visibility (public to private). + * Finally instance types, instance members, then constructors, + and then instance methods. * Some common exceptions are private helper static methods, which might appear near the instance methods which they help (but may also appear at the top). @@ -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-eclipse.txt b/Documentation/dev-eclipse.txt index 4fa542d..5ada1e2 100644 --- a/Documentation/dev-eclipse.txt +++ b/Documentation/dev-eclipse.txt
@@ -33,7 +33,7 @@ In Eclipse, choose 'Import existing project' and select the `gerrit` project from the current working directory. -Expand the `gerrit` project, right-click on the `buck-out` folder, select +Expand the `gerrit` project, right-click on the `eclipse-out` folder, select 'Properties', and then under 'Attributes' check 'Derived'. Note that if you make any changes in the project configuration @@ -55,7 +55,7 @@ == Site Initialization Build once on the command line with -link:dev-buck.html#build[Buck] and then follow +link:dev-bazel.html#build[Bazel] and then follow link:dev-readme.html#init[Site Initialization] in the Developer Setup guide to configure a local site for testing.
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 9752d14..bb9c5c7 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.5 \ - -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 @@ -156,7 +132,7 @@ </manifestEntries> ---- -For Buck driven plugins, the following line must be included in the BUCK +For Bazel driven plugins, the following line must be included in the BUILD configuration file: [source,python] @@ -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`: + @@ -1314,7 +1347,7 @@ </manifestEntries> ---- -or in the `BUCK` configuration file for Buck driven plugins: +or in the `BUILD` configuration file for Bazel driven plugins: [source,python] ---- @@ -1379,7 +1412,7 @@ </manifestEntries> ---- -or in the `BUCK` configuration file for Buck driven plugins +or in the `BUILD` configuration file for Bazel driven plugins [source,python] ---- @@ -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: @@ -2261,6 +2321,7 @@ } ---- + [[documentation]] == Documentation @@ -2413,6 +2474,66 @@ ---- +[[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; + } +} +---- + + +[[mail-filter]] +== Mail Filter Plugins + +Gerrit provides an extension point that enables Plugins to discard incoming +messages and prevent further processing by Gerrit. + +This can be used to implement spam checks, signature validations or organisation +specific checks like IP filters. + +[source, java] +---- +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.server.mail.receive.MailMessage; + +public class MyPlugin implements MailFilter { + boolean shouldProcessMessage(MailMessage message) { + // Implement your filter logic here + return true; + } +} +---- + == SEE ALSO * link:js-api.html[JavaScript API]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt index 4959ced..873b9ac 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 +Google Bazel 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,12 +18,10 @@ 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 <<dev-bazel#,Building with Bazel>>. == Switching between branches @@ -40,33 +38,24 @@ 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 To use the Eclipse IDE for development, please see link:dev-eclipse.html[Eclipse Setup]. -For details on how to configure the Eclipse workspace with Buck, -refer to: link:dev-buck.html#eclipse[Eclipse integration with Buck]. +For details on how to configure the Eclipse workspace with Bazel, +refer to: link:dev-bazel.html#eclipse[Eclipse integration with Bazel]. == Configuring IntelliJ IDEA -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. - -Once you start compiling using both buck and your Gerrit project in -IDEA, you will likely need to mark the below directories as generated -sources roots. You can do so using the IDEA "Project" view. In the -context menu of each one of these, use "Mark Directory As" to mark -them as "Generated Sources Root": - ----- - __auto_value_tests_gen__ - __httpd_gen__ - __server_gen__ ----- +Please refer to <<dev-intellij#,IntelliJ Setup>> for detailed +instructions. == Mac OS X @@ -83,29 +72,62 @@ [[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: ---- - java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/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. +[[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`. -The daemon will automatically start in the background and a web -browser will launch to the start page, enabling login via OpenID. +During initialization, make two changes to the default settings: -Shutdown the daemon after registering the administrator account -through the web interface: +* 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 @@ -119,20 +141,25 @@ started on that site. When the test has finished the Gerrit daemon is shutdown. -For instructions on running the integration tests with Buck, -please refer to: -link:dev-buck.html#tests[Running integration tests with Buck]. +For instructions on running the integration tests with 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: ---- - java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/gerrit.war daemon -d ../gerrit_testsite \ + --console-log ---- +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 @@ -149,9 +176,13 @@ command used to launch the daemon: ---- - java -jar buck-out/gen/gerrit/gerrit.war daemon -d ../gerrit_testsite -s + $(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: @@ -176,9 +207,13 @@ command line. If the daemon is not currently running: ---- - java -jar buck-out/gen/gerrit/gerrit.war gsql -d ../gerrit_testsite + $(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..2a857b2 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] @@ -110,8 +111,8 @@ * link:dev-release-subproject.html#prepare-release[Prepare the Release] * link:dev-release-subproject.html#publish-release[Publish the Release] -* Update the `id`, `bin_sha1`, and `src_sha1` values in the `maven_jar` -for the Subproject in `/lib/BUCK` to the released version. +* Update the `artifact`, `sha1`, and `src_sha1` values in the `maven_jar` +for the Subproject in `WORKSPACE` to the released version. [[update-versions]] === Update Versions and Create Release Tag @@ -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: ---- @@ -151,8 +147,7 @@ * Build the Gerrit WAR, API JARs and documentation + ---- - buck clean - buck build --no-cache release docs + bazel build release Documentation:searchfree ./tools/maven/api.sh install ---- @@ -161,11 +156,10 @@ * Verify plugin versions + -Sometimes `buck` doesn't rebuild plugins after they are tagged, and the -versions don't reflect the tag. Verify the versions: +Verify the versions: + ---- - java -jar ./buck-out/gen/release/release.war init --list-plugins + java -jar bazel-bin/release.war init --list-plugins ---- [[publish-gerrit]] @@ -178,7 +172,7 @@ link:dev-release-deploy-config.html#deploy-configuration-setting-maven-central[ configuration] for deploying to Maven Central -* Make sure that the version is updated in the `VERSION` file and in +* Make sure that the version is updated in the `version.bzl` file and in the `pom.xml` files as described in the link:#update-versions[Update Versions and Create Release Tag] section. @@ -193,21 +187,9 @@ ---- ./tools/maven/api.sh deploy ---- -+ -If no artifacts are uploaded, clean the `buck-out` folder and retry: -+ ----- - 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: +the `version.bzl` file: ** SNAPSHOT versions are directly uploaded into the Sonatype snapshots repository and no further action is needed: @@ -327,23 +309,24 @@ [[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`. +`bazel build searchfree`: `bazel-bin/Documentation/searchfree.zip`. * Upload the files manually via web browser to the appropriate folder in the 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,14 +353,14 @@ * 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. + The SHA1 and MD5 can be taken from the artifact page on Sonatype. The SHA256 can be generated with -`openssl sha -sha256 buck-out/gen/release/release.war` or an equivalent +`openssl sha -sha256 bazel-bin/release.war` or an equivalent command. * Update the new discussion group announcement to be sticky @@ -397,8 +380,7 @@ 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.bzl` 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/gen_licenses.py b/Documentation/gen_licenses.py deleted file mode 100755 index 15f470c..0000000 --- a/Documentation/gen_licenses.py +++ /dev/null
@@ -1,178 +0,0 @@ -#!/usr/bin/env python -# 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. -# -# TODO(sop): Be more detailed: version, link to Maven Central - -from __future__ import print_function - -import argparse -from collections import defaultdict, deque -import json -from os import chdir, path -from shutil import copyfileobj -from subprocess import Popen, PIPE -from sys import stdout, stderr - -parser = argparse.ArgumentParser() -parser.add_argument('--asciidoc', action='store_true') -parser.add_argument('--partial', action='store_true') -parser.add_argument('targets', nargs='+') -args = parser.parse_args() - -KNOWN_PROVIDED_DEPS = [ - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcpkix', - '//lib/bouncycastle:bcprov', -] - -for target in args.targets: - if not target.startswith('//'): - print('Target must be absolute: %s' % target, file=stderr) - -def parse_graph(): - graph = defaultdict(list) - while not path.isfile('.buckconfig'): - chdir('..') - query = ' + '.join('deps(%s)' % t for t in args.targets) - p = Popen([ - 'buck', 'query', query, - '--output-attributes=buck.direct_dependencies'], stdout=PIPE) - obj = json.load(p.stdout) - for target, attrs in obj.iteritems(): - for dep in attrs['buck.direct_dependencies']: - - if target in KNOWN_PROVIDED_DEPS: - continue - - if (args.partial - and dep == '//gerrit-gwtexpui:CSS' - and target == '//gerrit-gwtui:ui_module'): - continue - - graph[target].append(dep) - r = p.wait() - if r != 0: - exit(r) - return graph - -graph = parse_graph() -licenses = defaultdict(set) - -do_not_distribute = False -queue = deque(args.targets) -while queue: - target = queue.popleft() - for dep in graph[target]: - if not dep.startswith('//lib:LICENSE-'): - continue - if 'DO_NOT_DISTRIBUTE' in dep: - do_not_distribute = True - licenses[dep].add(target) - queue.extend(graph[target]) - -if do_not_distribute: - print('DO_NOT_DISTRIBUTE license found', file=stderr) - for target in args.targets: - print('...via %s:' % target) - Popen(['buck', 'query', - 'allpaths(%s, //lib:LICENSE-DO_NOT_DISTRIBUTE)' % target], - stdout=stderr).communicate() - exit(1) - -used = sorted(licenses.keys()) - -if args.asciidoc: - print("""\ -= 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 used: - libs = sorted(licenses[n]) - name = n[len('//lib:LICENSE-'):] - if args.asciidoc: - print() - print('[[%s]]' % name.replace('.', '_')) - print("=== " + name) - print() - else: - print() - print(name) - print() - print('----') - for d in libs: - 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) - if args.asciidoc: - print() - print('[[%s_license]]' % name.replace('.', '_')) - print('----') - with open(n[2:].replace(':', '/')) as fd: - copyfileobj(fd, stdout) - print() - print('----') - -if args.asciidoc: - print(""" -GERRIT ------- -Part of link:index.html[Gerrit Code Review] -""")
diff --git a/Documentation/index.txt b/Documentation/index.txt index f53463c..0471ed8 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,9 @@ == Developer . Getting Started .. link:dev-readme.html[Developer Setup] +.. link:dev-bazel.html[Building with Bazel] .. link:dev-eclipse.html[Eclipse Setup] -.. link:dev-buck.html[Building with Buck] +.. 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..6056e57 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 @@ -596,7 +596,7 @@ + ---- [plugin "project-download-commands"] - Build = git fetch ${url} ${ref} && git checkout FETCH_HEAD && buck build ${project} + Build = git fetch ${url} ${ref} && git checkout FETCH_HEAD && bazel build ${project} Update = git fetch ${url} ${ref} && git checkout FETCH_HEAD && git submodule update ---- +
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/license.defs b/Documentation/license.defs deleted file mode 100644 index 42dd3eb..0000000 --- a/Documentation/license.defs +++ /dev/null
@@ -1,29 +0,0 @@ -def genlicenses( - name, - out, - opts = [], - java_deps = [], - non_java_deps = [], - visibility = []): - cmd = ['$(exe :gen_licenses)'] - cmd.extend(opts) - cmd.append('>$OUT') - cmd.extend(java_deps) - cmd.extend(non_java_deps) - - # Must use $(classpath) for Java deps, since transitive dependencies are not - # first-order dependencies of the output jar, so changes would not cause - # invalidation of the build cache key for the genrule. - cmd.extend('; true $(classpath %s)' % d for d in java_deps) - - # Must use $(location) for non-Java deps, since $(classpath) will fail with an - # error. This is ok, because transitive dependencies are included in the - # output artifacts for everything _except_ Java libraries. - cmd.extend('; true $(location %s)' % d for d in non_java_deps) - - genrule( - name = name, - out = out, - cmd = ' '.join(cmd), - visibility = visibility, - )
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..f1b4abf 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt
@@ -285,6 +285,66 @@ HTTP/1.1 204 No Content ---- +[[get-account-status]] +=== Get Account Status +-- +'GET /accounts/link:#account-id[\{account-id\}]/status' +-- + +Retrieves the status of an account. + +.Request +---- + GET /accounts/self/status HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + "Available" +---- + +If the account does not have a status an empty string is returned. + +[[set-account-status]] +=== Set Account Status +-- +'PUT /accounts/link:#account-id[\{account-id\}]/status' +-- + +Sets the status of an account. + +The new account status must be provided in the request body inside +an link:#account-status-input[AccountStatusInput] entity. + +.Request +---- + PUT /accounts/self/status HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "status": "Out Of Office" + } +---- + +As response the new account status is returned. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + "Out Of Office" +---- + +If the name was deleted the response is "`204 No Content`". + [[get-username]] === Get Username -- @@ -396,7 +456,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 +1273,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 +1298,8 @@ { "url": "#/groups/self", "name": "Groups" - } + }, + change_table: [] ] } ---- @@ -1262,6 +1324,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 +1357,10 @@ "url": "#/groups/self", "name": "Groups" } + ], + "change_table": [ + "Subject", + "Owner" ] } ---- @@ -1312,6 +1379,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 +1412,10 @@ "url": "#/groups/self", "name": "Groups" } + ], + "change_table": [ + "Subject", + "Owner" ] } ---- @@ -1381,7 +1453,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -1412,7 +1485,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -1436,7 +1510,8 @@ "show_tabs": true, "show_whitespace_errors": true, "syntax_highlighting": true, - "tab_size": 8 + "tab_size": 8, + "font_size": 12 } ---- @@ -1660,6 +1735,64 @@ HTTP/1.1 204 No Content ---- +[[get-account-external-ids]] +=== Get Account External IDs +-- +'GET /accounts/link:#account-id[\{account-id\}]/external.ids' +-- + +Retrieves the external ids of a user account. + +.Request +---- + GET /a/accounts/self/external.ids HTTP/1.0 +---- + +As result the external ids of the user are returned as a list of +link:#account-external-id-info[AccountExternalIdInfo] entities. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "identity": "username:john", + "email": "john.doe@example.com", + "trusted": true + } + ] +---- + +[[delete-account-external-ids]] +=== Delete Account External IDs +-- +'POST /accounts/link:#account-id[\{account-id\}]/external.ids:delete' +-- + +Delete a list of external ids for a user account. The target external ids must +be provided as a list in the request body. + +Only external ids belonging to the caller may be deleted. + +.Request +---- + POST /a/accounts/self/external.ids:delete HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "mailto:john.doe@example.com" + } +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + [[default-star-endpoints]] == Default Star Endpoints @@ -2019,6 +2152,22 @@ registered. |================================= +[[account-external-id-info]] +=== AccountExternalIdInfo +The `AccountExternalIdInfo` entity contains information for an external id of +an account. + +[options="header",cols="1,^1,5"] +|============================ +|Field Name ||Description +|`identity` ||The account external id. +|`email` |optional|The email address for the external id. +|`trusted` |not set if `false`| +Whether the external id is trusted. +|`can_delete` |not set if `false`| +Whether the external id can be deleted by the calling user. +|============================ + [[account-info]] === AccountInfo The `AccountInfo` entity contains information about an account. @@ -2084,6 +2233,18 @@ If not set or if set to an empty string, the account name is deleted. |============================= +[[account-status-input]] +=== AccountStatusInput +The `AccountStatusInput` entity contains information for setting a status +for an account. + +[options="header",cols="1,^2,4"] +|============================= +|Field Name ||Description +|`status` |optional|The new status of the account. + +If not set or if set to an empty string, the account status is deleted. +|============================= + [[capability-info]] === CapabilityInfo The `CapabilityInfo` entity contains information about the global @@ -2226,6 +2387,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 +2449,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 +2617,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 +2634,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 +2664,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 +2685,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 +2700,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 +2730,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..6a78d9f 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 ---- @@ -1675,6 +1895,62 @@ } ---- +[[list-change-robot-comments]] +=== List Change Robot Comments +-- +'GET /changes/link:#change-id[\{change-id\}]/robotcomments' +-- + +Lists the robot comments of all revisions of the change. + +Return a map 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/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" + } + ] + } +---- + [[list-change-drafts]] === List Change Drafts -- @@ -2135,9 +2411,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. @@ -2190,6 +2474,79 @@ HTTP/1.1 204 No Content ---- +[[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" + ] +---- + [[reviewer-endpoints]] == Reviewer Endpoints @@ -2408,14 +2765,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 +2832,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 +2921,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 +3664,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 +3752,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 +4158,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 +4262,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 +4762,131 @@ } ---- +[[revision-reviewer-endpoints]] +== Revision Reviewer Endpoints + +[[list-revision-reviewers]] +=== List Revision Reviewers +-- +'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/reviewers/' +-- + +Lists the reviewers of a revision. + +Please note that only the current revision is supported. + +As result a list of link:#reviewer-info[ReviewerInfo] entries is returned. + +.Request +---- + GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/ HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "approvals": { + "Verified": "+1", + "Code-Review": "+2" + }, + "_account_id": 1000096, + "name": "John Doe", + "email": "john.doe@example.com" + }, + { + "approvals": { + "Verified": " 0", + "Code-Review": "-1" + }, + "_account_id": 1000097, + "name": "Jane Roe", + "email": "jane.roe@example.com" + } + ] +---- + +[[list-revision-votes]] +=== List Revision Votes +-- +'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/' +-- + +Lists the votes for a specific reviewer of the revision. + +Please note that only the current revision is supported. + +.Request +---- + GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/ HTTP/1.0 +---- + +As result a map is returned that maps the label name to the label value. +The entries in the map are sorted by label name. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + { + "Code-Review": -1, + "Verified": 1, + "Work-In-Progress": 1 + } +---- + +[[delete-revision-vote]] +=== Delete Revision Vote +-- +'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}] +/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]' + +'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}] +/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]/delete' +-- + +Deletes a single vote from a revision. The deletion will be possible only +if the revision is the current revision. By using this endpoint you can prevent +deleting the vote (with same label) from a newer patch set by mistake. + +Note, that even when the last vote of a reviewer is removed the reviewer itself +is still listed on the change. + +Options can be provided in the request body as a +link:#delete-vote-input[DeleteVoteInput] entity. + +.Request +---- + DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/Code-Review HTTP/1.0 + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/Code-Review/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/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/reviewers/John%20Doe/votes/Code-Review/delete HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "notify": "NONE" + } +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + [[ids]] == IDs @@ -4141,10 +4921,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 +4949,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 +5028,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 +5145,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` || @@ -4424,6 +5232,14 @@ Allow creating a new branch when set to `true`. |`merge` |optional| The detail of a merge commit as a link:#merge-input[MergeInput] entity. +|`notify` |optional| +Notify handling that defines to whom email notifications should be sent +after the change is created. + +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 change creation +as a map of recipient type to link:#notify-info[NotifyInfo] entity. |================================== [[change-message-info]] @@ -4455,11 +5271,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]] @@ -4505,6 +5323,10 @@ while posting the review. NOTE: To apply different tags on on different votes/comments multiple invocations of the REST call are required. +|`unresolved` |optional| +Whether or not the comment must be addressed by the user. The state of +resolution of a comment thread is stored in the last comment in that thread +chronologically. |=========================== [[comment-input]] @@ -4547,6 +5369,10 @@ Value of the `tag` field. Only allowed on link:#create-draft[draft comment] + inputs; for published comments, use the `tag` field in + link#review-input[ReviewInput] +|`unresolved` |optional| +Whether or not the comment must be addressed by the user. This value will +default to false if the comment is an orphan, or the value of the `in_reply_to` +comment if it is supplied. |=========================== [[comment-range]] @@ -4589,23 +5415,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 +5655,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 +5709,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 +5844,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 +5875,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 +5911,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 +6082,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 +6099,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 +6142,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 +6224,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 +6295,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 +6407,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 7b96a1c..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`. @@ -1464,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-documentation.txt b/Documentation/rest-api-documentation.txt index 4c9db2b..0a7ff16 100644 --- a/Documentation/rest-api-documentation.txt +++ b/Documentation/rest-api-documentation.txt
@@ -6,9 +6,9 @@ Please note that this feature is only usable with documentation built-in. You'll need to -`buck build withdocs` +`bazel build withdocs` or -`buck build release` +`bazel build release` to test this feature. [[documentation-endpoints]]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt index 23d4c5b..bef2153 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt
@@ -120,8 +120,13 @@ ==== Check if a group is owned by the calling user By setting the option `owned` and specifying a group to inspect with -the option `q`, it is possible to find out, if this group is owned by -the calling user. +the option `group`/`g`, it is possible to find out if this group is +owned by the calling user. + +[NOTE] Earlier the `group`/`g` option was named `query`/`q`. Using +`query`/`q` still works, but this option is deprecated and may be +removed in future. Hence all users should be adapted to use +`group`/`g` instead. .Request ---- @@ -181,8 +186,8 @@ When using this option, the `project` or `p` option can be used to name the current project, to allow context-dependent suggestions. -Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`, -or `S`. +Not compatible with `visible-to-all`, `owned`, `user`, `match`, +`group`, or `S`. (Attempts to use one of those options combined with `suggest` will error out.) @@ -211,6 +216,88 @@ } ---- +[[query-groups]] +=== Query Groups +-- +'GET /groups/?query2=<query>' +-- + +Queries internal groups visible to the caller. The +link:user-search-groups.html#_search_operators[query string] must be +provided by the `query2` parameter. The `start` and `limit` parameters +can be used to skip/limit results. + +As result a list of link:#group-info[GroupInfo] entities is returned. + +[NOTE] `query2` is a temporary name and in future this option may be +renamed to `query`. `query2` was chosen to maintain backwards +compatibility with the deprecated `query` parameter on the +link:#list-groups[List Groups] endpoint. + +.Request +---- + GET /groups/?query2=inname:test HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "url": "#/admin/groups/uuid-68236a40ca78de8be630312d8ba50250bc5638ae", + "options": {}, + "description": "Group for running tests on MyProject", + "group_id": 20, + "owner": "MyProject-Test-Group", + "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b", + "id": "68236a40ca78de8be630312d8ba50250bc5638ae" + }, + { + "url": "#/admin/groups/uuid-99a534526313324a2667025c3f4e089199b736aa", + "options": {}, + "description": "Testers for ProjectX", + "group_id": 17, + "owner": "ProjectX-Testers", + "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b", + "id": "99a534526313324a2667025c3f4e089199b736aa" + } + ] +---- + +If the number of groups matching the query exceeds either the internal +limit or a supplied `limit` query parameter, the last group object has +a `_more_groups: true` JSON field set. + +[[group-query-limit]] +==== Group Limit +The `/groups/?query2=<query>` URL also accepts a limit integer in the +`limit` parameter. This limits the results to `limit` groups. + +Query the first 25 groups in group list. +---- + GET /groups/?query2=<query>&limit=25 HTTP/1.0 +---- + +The `/groups/` URL also accepts a start integer in the `start` +parameter. The results will skip `start` groups from group list. + +Query 25 groups starting from index 50. +---- + GET /groups/?query2=<query>&limit=25&start=50 HTTP/1.0 +---- + +[[group-query-options]] +==== Group Options +Additional fields can be obtained by adding `o` parameters. Each option +requires more lookups and slows down the query response time to the +client so they are generally disabled by default. The supported fields +are described in the context of the link:#group-options[List Groups] +REST endpoint. + [[get-group]] === Get Group -- @@ -714,6 +801,24 @@ ] ---- +[[index-group]] +=== Index Group +-- +'POST /groups/link:#group-id[\{group-id\}]/index' +-- + +Adds or updates the internal group in the secondary index. + +.Request +---- + POST /groups/fdda826a0815859ab48d22a05a43472f0f55f89a/index HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + [[group-member-endpoints]] == Group Member Endpoints @@ -1207,11 +1312,6 @@ [[ids]] == IDs -[[account-id]] -=== link:rest-api-accounts.html#account-id[\{account-id\}] --- --- - [[group-id]] === \{group-id\} Identifier for a group. @@ -1282,6 +1382,10 @@ |`group_id` |only for internal groups|The numeric ID of the group. |`owner` |only for internal groups|The name of the owner group. |`owner_id` |only for internal groups|The URL encoded UUID of the owner group. +|`_more_groups`|optional, only for internal groups, not set if `false`| +Whether the query would deliver more results if not limited. + +Only set on the last group that is returned by a +link:#query-groups[group query]. |`members` |optional, only for internal groups| A list of link:rest-api-accounts.html#account-info[AccountInfo] entities describing the direct members. + @@ -1319,7 +1423,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 +1464,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 +1472,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..f69b955 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 @@ -1940,6 +1989,35 @@ } ---- +[[get-included-in]] +=== Get Included In +-- +'GET /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/in' +-- + +Retrieves the branches and tags in which a change is included. As result +an link:rest-api-changes.html#included-in-info[IncludedInInfo] entity is returned. + +.Request +---- + GET /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/in HTTP/1.0 +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json;charset=UTF-8 + + )]}' + { + "branches": [ + "master" + ], + "tags": [] + } +---- + [[get-content-from-commit]] === Get Content -- @@ -2408,8 +2486,7 @@ |====================================================== [[config-parameter-info]] -ConfigParameterInfo -~~~~~~~~~~~~~~~~~~~ +=== ConfigParameterInfo The `ConfigParameterInfo` entity describes a project configuration parameter. @@ -2523,6 +2600,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-accounts.txt b/Documentation/user-search-accounts.txt index 15d87b0..04f40c0 100644 --- a/Documentation/user-search-accounts.txt +++ b/Documentation/user-search-accounts.txt
@@ -1,6 +1,6 @@ = Gerrit Code Review - Searching Accounts -== Basic Change Search +== Basic Account Search Similar to many popular search engines on the web, just enter some text and let Gerrit figure out the meaning: @@ -57,7 +57,7 @@ is:visible:: + Magical internal flag to prove the current user has access to read -the change. This flag is always added to any query. +the account. This flag is always added to any query. [[is-active-magic]] is:active::
diff --git a/Documentation/user-search-groups.txt b/Documentation/user-search-groups.txt new file mode 100644 index 0000000..3d8c51c --- /dev/null +++ b/Documentation/user-search-groups.txt
@@ -0,0 +1,82 @@ += Gerrit Code Review - Searching Groups + +Group queries only match internal groups. External groups and system +groups are not included in the query result. + +== Basic Group Search + +Similar to many popular search engines on the web, just enter some +text and let Gerrit figure out the meaning: + +[options="header"] +|====================================================== +|Description | Examples +|Name | Foo-Verifiers +|UUID | 6a1e70e1a88782771a91808c8af9bbb7a9871389 +|Description | deprecated +|====================================================== + +[[search-operators]] +== Search Operators + +Operators act as restrictions on the search. As more operators +are added to the same query string, they further restrict the +returned results. Search can also be performed by typing only a text +with no operator, which will match against a variety of fields. + +[[description]] +description:'DESCRIPTION':: ++ +Matches groups that have a description that contains 'DESCRIPTION' +(case-insensitive). + +[[inname]] +inname:'NAMEPART':: ++ +Matches groups that have a name part that starts with 'NAMEPART' +(case-insensitive). + +[[is]] +[[is-visibletoall]] +is:visibletoall:: ++ +Matches groups that are in the groups options marked as visible to all +registered users. + +[[name]] +name:'NAME':: ++ +Matches groups that have the name 'NAME' (case-insensitive). + +[[owner]] +owner:'UUID':: ++ +Matches groups that are owned by a group that has the UUID 'UUID'. + +[[uuid]] +uuid:'UUID':: ++ +Matches groups that have the UUID 'UUID'. + +== Magical Operators + +[[is-visible]] +is:visible:: ++ +Magical internal flag to prove the current user has access to read +the group. This flag is always added to any query. + +[[limit]] +limit:'CNT':: ++ +Limit the returned results to no more than 'CNT' records. This is +automatically set to the page size configured in the current user's +preferences. Including it in a web query may lead to unpredictable +results with regards to pagination. + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] + +SEARCHBOX +---------
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 ba3445a..6d0c47a 100644 --- a/Documentation/user-upload.txt +++ b/Documentation/user-upload.txt
@@ -177,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]] @@ -405,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. @@ -458,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..78c8477 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, @@ -48,10 +50,10 @@ ## Build -Install [Buck](http://facebook.github.io/buck/setup/install.html) and run the following: +Install [Bazel](https://bazel.build/versions/master/docs/install.html) and run the following: git clone --recursive https://gerrit.googlesource.com/gerrit - cd gerrit && buck build release + cd gerrit && bazel build release ## Install binary packages (Deb/Rpm) @@ -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/BUCK b/ReleaseNotes/BUCK deleted file mode 100644 index 0f47808..0000000 --- a/ReleaseNotes/BUCK +++ /dev/null
@@ -1,19 +0,0 @@ -include_defs('//Documentation/asciidoc.defs') -include_defs('//ReleaseNotes/config.defs') - -DIR = 'ReleaseNotes' - -SRCS = glob(['*.txt']) - - -genasciidoc( - name = 'html', - out = 'html.zip', - directory = DIR, - srcs = SRCS, - attributes = release_notes_attributes(), - backend = 'html5', - searchbox = False, - resources = False, - visibility = ['PUBLIC'], -)
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/config.defs b/ReleaseNotes/config.defs deleted file mode 100644 index 86b7603..0000000 --- a/ReleaseNotes/config.defs +++ /dev/null
@@ -1,14 +0,0 @@ -def release_notes_attributes(): - return [ - 'toc', - 'newline="\\n"', - 'asterisk="*"', - 'plus="+"', - 'caret="^"', - 'startsb="["', - 'endsb="]"', - 'tilde="~"', - 'last-update-label!', - 'stylesheet=DEFAULT', - 'linkcss=true', - ]
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/VERSION b/VERSION deleted file mode 100644 index ce4b95d..0000000 --- a/VERSION +++ /dev/null
@@ -1,5 +0,0 @@ -# Maven style API version (e.g. '2.x-SNAPSHOT'). -# Used by :api_install and :api_deploy targets -# when talking to the destination repository. -# -GERRIT_VERSION = '2.13.5'
diff --git a/WORKSPACE b/WORKSPACE index d465b37..5775df8 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -1,699 +1,1109 @@ -ANTLR_VERS = '3.5.2' +workspace(name = "gerrit") + +load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL") +load("//lib/codemirror:cm.bzl", "CM_VERSION", "DIFF_MATCH_PATCH_VERSION") +load("//plugins:external_plugin_deps.bzl", "external_plugin_deps") + +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 = "a2b5970b853f8fee64589fc1103c0ceb7677ba63", + src_sha1 = "765f955774c36c226aa41fba7c20119451de2db7", + 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 = "d3aa54bd610db9a5c246aa8fef13989982c98628", + 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 = "a728cf277396f1227c5a8dffcf5dee0188fc0821", ) 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 = "6c2b2f192c95d25a2e1576aee5d1169dd8bd2266", + 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.17", + sha1 = "97bdc872f00388910c9af70771f07bbb32f1b949", + src_sha1 = "889e35d7295b1af49161a28daaea9905ffa76a63", ) 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.3" +PROLOG_REPO = GERRIT + 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 = PROLOG_REPO, + sha1 = "d5206556cbc76ffeab21313ffc47b586a1efbcbb", ) -PROLOG_VERS = '1.4.1' +maven_jar( + name = "prolog_compiler", + artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS, + attach_source = False, + repository = PROLOG_REPO, + sha1 = "f37032cf1dec3e064427745bc59da5a12757a3b2", +) 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 = PROLOG_REPO, + sha1 = "d02b2640b26f64036b6ba2b45e4acc79281cea17", ) 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 = PROLOG_REPO, + sha1 = "e3b1860c63e57265e5435f890263ad82dafa724f", ) 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.2", + attach_source = False, + repository = GERRIT, + sha1 = "519fc548df920123bce986056d2f644663665ae4", ) +# 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.56" maven_jar( - name = 'bcprov', - artifact = 'org.bouncycastle:bcprov-jdk15on:' + BC_VERS, - sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269', + name = "bcprov", + artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS, + sha1 = "a153c6f9744a3e9dd6feab5e210e1c9861362ec7", ) maven_jar( - name = 'bcpg', - artifact = 'org.bouncycastle:bcpg-jdk15on:' + BC_VERS, - sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858', + name = "bcpg", + artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS, + sha1 = "9c3f2e7072c8cc1152079b5c25291a9f462631f1", ) maven_jar( - name = 'bcpkix', - artifact = 'org.bouncycastle:bcpkix-jdk15on:' + BC_VERS, - sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504', + name = "bcpkix", + artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS, + sha1 = "4648af70268b6fdb24674fb1fd7c1fcc73db1231", ) 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.15.v20161220" 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 = "e730f488cc01b3d566d3ad052cdb4ee383048410", ) 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 = "976a4560814e33ad72554c33281eb39413f03644", ) 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 = "2118bdbc41fee57279d44da7679d1dbf5b511e99", ) 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 = "598b96d749ebf99b1790dff64f0e6e2b6990cdcd", ) 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 = "8bc2f2b269bcbecc16d61a9a530c0b2fb5d5486e", ) 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 = "7d56b0381b9ed43ae7a160e558e7e16dcf621aa9", ) 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 = "8e56b579c4ef70c6b4e6a60e8074c024ceecbd1f", ) 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 = "3b100e43d2dc4206762c7af4514199d9fca153bb", ) 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 = "fc69fc2b0b476e5b5b14fc4a35a2c5edc539be62", ) 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 = "org.postgresql:postgresql:9.4.1211.jre7", + sha1 = "56b01e9e667f408818a6ef06a89598dbab80687d", +) + +maven_jar( + name = "codemirror_minified", + artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION, + sha1 = "3e8767c9293614968176fcf66cb873d6eb8b3051", +) + +maven_jar( + name = "codemirror_original", + artifact = "org.webjars.npm:codemirror:" + CM_VERSION, + sha1 = "879c49085a44f062554a4e4a9ac248b7083d37cf", +) + +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 = "f2563ed9c8571057814b78d8f6cf275eeb953eeb", + version = "1.7.1", +) + +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() +external_plugin_deps()
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet deleted file mode 100644 index 367fe71..0000000 --- a/bucklets/gerrit_plugin.bucklet +++ /dev/null
@@ -1,21 +0,0 @@ -# -# Dummy to make the co-existence of core and standalone plugins possible. -# Intentionaly left empty as this doesn't suppose to have any side effects -# in tree build, i. e.: -# -# cookbook-plugin/BUCK include this line: -# include_defs('//bucklets/gerrit_plugin.bucklet') -# -# When executing from the Gerrit tree: -# buck build plugins/cookbook-plugin -# -# this line has no effect. -# -# When compiling from standalone cookbook-plugin, bucklets directory points -# to cloned bucklets library that includes real gerrit_plugin.bucklet code. - -GERRIT_GWT_API = ['//gerrit-plugin-gwtui:gwtui-api'] -GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib'] -GERRIT_TESTS = ['//gerrit-acceptance-framework:lib'] - -STANDALONE_MODE = False
diff --git a/bucklets/java_doc.bucklet b/bucklets/java_doc.bucklet deleted file mode 120000 index cc8b6db..0000000 --- a/bucklets/java_doc.bucklet +++ /dev/null
@@ -1 +0,0 @@ -../tools/java_doc.defs \ No newline at end of file
diff --git a/bucklets/java_sources.bucklet b/bucklets/java_sources.bucklet deleted file mode 120000 index 8a1a5dd..0000000 --- a/bucklets/java_sources.bucklet +++ /dev/null
@@ -1 +0,0 @@ -../tools/java_sources.defs \ No newline at end of file
diff --git a/bucklets/maven_jar.bucklet b/bucklets/maven_jar.bucklet deleted file mode 120000 index 130a747..0000000 --- a/bucklets/maven_jar.bucklet +++ /dev/null
@@ -1 +0,0 @@ -../lib/maven.defs \ No newline at end of file
diff --git a/bucklets/maven_package.bucklet b/bucklets/maven_package.bucklet deleted file mode 120000 index b5f5ea8..0000000 --- a/bucklets/maven_package.bucklet +++ /dev/null
@@ -1 +0,0 @@ -../tools/maven/package.defs \ No newline at end of file
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/convertkey/BUCK b/contrib/convertkey/BUCK deleted file mode 100644 index 40ad9c4..0000000 --- a/contrib/convertkey/BUCK +++ /dev/null
@@ -1,50 +0,0 @@ -include_defs('//lib/maven.defs') - -genrule( - name = 'bcprov__unsign', - cmd = ' && '.join([ - 'unzip -qd $TMP $(location //lib/bouncycastle:bcprov)', - 'cd $TMP', - 'zip -Drq $OUT . -x META-INF/\*.RSA META-INF/\*.DSA META-INF/\*.SF META-INF/\*.LIST', - ]), - out = 'bcprov-unsigned.jar', -) - -prebuilt_jar( - name = 'bcprov', - binary_jar = ':bcprov__unsign', -) - -genrule( - name = 'bcpkix__unsign', - cmd = ' && '.join([ - 'unzip -qd $TMP $(location //lib/bouncycastle:bcpkix)', - 'cd $TMP', - 'zip -Drq $OUT . -x META-INF/\*.RSA META-INF/\*.DSA META-INF/\*.SF META-INF/\*.LIST', - ]), - out = 'bcpkix-unsigned.jar', -) - -prebuilt_jar( - name = 'bcpkix', - binary_jar = ':bcpkix__unsign', -) - -java_library( - name = 'convertkey__lib', - srcs = glob(['src/main/java/**/*.java']), - deps = [ - ':bcprov', - ':bcpkix', - '//lib:jsch', - '//lib/log:nop', - '//lib/mina:sshd', - ], -) - -java_binary( - name = 'convertkey', - deps = [':convertkey__lib'], - main_class = 'com.googlesource.gerrit.convertkey.ConvertKey', -) -
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 deleted file mode 100644 index ba68fa3..0000000 --- a/gerrit-acceptance-framework/BUCK +++ /dev/null
@@ -1,92 +0,0 @@ -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', - '//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', - deps = [':lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'lib', - srcs = SRCS, - exported_deps = DEPS + [ - '//lib:truth', - ], - provided_deps = PROVIDED + [ - '//lib:gwtorm', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - ], - 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' ], - visibility = ['PUBLIC'], -) - -java_doc( - name = 'acceptance-framework-javadoc', - title = 'Gerrit Acceptance Test Framework Documentation', - pkgs = [' com.google.gerrit.acceptance'], - paths = ['src/test/java'], - srcs = SRCS, - deps = DEPS + PROVIDED + [ - '//lib:guava', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice_library', - '//lib/guice:guice-servlet', - '//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 2f7dbf9..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.5</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..2a0245b 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
@@ -15,38 +15,49 @@ package com.google.gerrit.acceptance; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; 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 static org.eclipse.jgit.lib.Constants.R_TAGS; -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; import com.google.gerrit.extensions.api.projects.ProjectInput; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; +import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.client.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 +73,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 +84,11 @@ 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.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; @@ -83,6 +99,7 @@ import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.FakeEmailSender; import com.google.gerrit.testutil.FakeEmailSender.Message; +import com.google.gerrit.testutil.SshMode; import com.google.gerrit.testutil.TempFileUtil; import com.google.gerrit.testutil.TestNotesMigration; import com.google.gson.Gson; @@ -98,12 +115,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 +139,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 { @@ -216,8 +248,14 @@ protected ChangeResource.Factory changeResourceFactory; @Inject + protected SystemGroupBackend systemGroupBackend; + + @Inject private EventRecorder.Factory eventRecorderFactory; + @Inject + private ChangeIndexCollection changeIndexes; + protected TestRepository<InMemoryRepository> testRepo; protected GerritServer server; protected TestAccount admin; @@ -236,11 +274,15 @@ @Inject protected ChangeNotes.Factory notesFactory; + @Inject + protected Abandon changeAbandoner; + @Rule public ExpectedException exception = ExpectedException.none(); private String resourcePrefix; private List<Repository> toClose; + private boolean useSsh; @Rule public TestRule testRunner = new TestRule() { @@ -273,6 +315,16 @@ eventRecorder = eventRecorderFactory.create(admin); } + @Before + public void assumeSshIfRequired() { + if (useSsh) { + // If the test uses ssh, we use assume() to make sure ssh is enabled on + // the test suite. JUnit will skip tests annotated with @UseSsh if we + // disable them using the command line flag. + assume().that(SshMode.useSsh()).isTrue(); + } + } + @After public void closeEventRecorder() { eventRecorder.close(); @@ -346,20 +398,34 @@ adminRestSession = new RestSession(server, admin); userRestSession = new RestSession(server, user); - initSsh(admin); + db = reviewDbProvider.open(); - Context ctx = newRequestContext(user); - atrScope.set(ctx); - userSshSession = ctx.getSession(); - userSshSession.open(); - ctx = newRequestContext(admin); - atrScope.set(ctx); - adminSshSession = ctx.getSession(); - adminSshSession.open(); + + if (classDesc.useSsh() || methodDesc.useSsh()) { + useSsh = true; + if (SshMode.useSsh() && (adminSshSession == null || + userSshSession == null)) { + // Create Ssh sessions + initSsh(admin); + Context ctx = newRequestContext(user); + atrScope.set(ctx); + userSshSession = ctx.getSession(); + userSshSession.open(); + ctx = newRequestContext(admin); + atrScope.set(ctx); + adminSshSession = ctx.getSession(); + adminSshSession.open(); + } + } else { + useSsh = false; + } + resourcePrefix = UNSAFE_PROJECT_NAME.matcher( description.getClassName() + "_" + description.getMethodName() + "_").replaceAll(""); + Context ctx = newRequestContext(admin); + atrScope.set(ctx); project = createProject(projectInput(description)); testRepo = cloneProject(project, getCloneAsAccount(description)); } @@ -484,8 +550,12 @@ repo.close(); } db.close(); - adminSshSession.close(); - userSshSession.close(); + if (adminSshSession != null) { + adminSshSession.close(); + } + if (userSshSession != null) { + userSshSession.close(); + } if (server != commonServer) { server.stop(); } @@ -522,21 +592,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(); @@ -629,9 +704,12 @@ return gApi.changes().id(id).get(); } - protected EditInfo getEdit(String id) + protected Optional<EditInfo> getEdit(String id) throws RestApiException { - return gApi.changes().id(id).getEdit(); + return gApi.changes() + .id(id) + .edit() + .get(); } protected ChangeInfo get(String id, ListChangesOption... options) @@ -668,6 +746,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 +896,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 +932,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 +946,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 +1025,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; } } @@ -935,6 +1051,12 @@ return getRemoteHead(project, "master"); } + protected void grantTagPermissions() throws Exception { + grant(Permission.CREATE, project, R_TAGS + "*"); + grant(Permission.CREATE_TAG, project, R_TAGS + "*"); + grant(Permission.CREATE_SIGNED_TAG, project, R_TAGS + "*"); + } + protected void assertMailFrom(Message message, String email) throws Exception { assertThat(message.headers()).containsKey("Reply-To"); @@ -943,6 +1065,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 +1210,59 @@ 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(); + } + + protected void watch(String project, String filter) + throws RestApiException { + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); + ProjectWatchInfo pwi = new ProjectWatchInfo(); + pwi.project = project; + pwi.filter = filter; + pwi.notifyAbandonedChanges = true; + pwi.notifyNewChanges = true; + pwi.notifyAllComments = true; + projectsToWatch.add(pwi); + gApi.accounts().self().setWatchedProjects(projectsToWatch); + } }
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..b271f8a 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; @@ -29,6 +30,7 @@ import com.google.gerrit.server.account.VersionedAuthorizedKeys; import com.google.gerrit.server.index.account.AccountIndexer; import com.google.gerrit.server.ssh.SshKeyCache; +import com.google.gerrit.testutil.SshMode; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -104,15 +106,19 @@ 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)); } } - KeyPair sshKey = genSshKey(); - authorizedKeys.addKey(id, publicKey(sshKey, email)); - sshKeyCache.evict(username); + KeyPair sshKey = null; + if (SshMode.useSsh()) { + sshKey = genSshKey(); + authorizedKeys.addKey(id, publicKey(sshKey, email)); + sshKeyCache.evict(username); + } accountCache.evictByUsername(username); byEmailCache.evict(email);
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..606ad61 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,11 +16,10 @@ 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; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.common.UserScopedEventListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.registration.RegistrationHandle; @@ -40,7 +39,7 @@ public class EventRecorder { private final RegistrationHandle eventListenerRegistration; - private final Multimap<String, RefEvent> recordedEvents; + private final ListMultimap<String, RefEvent> recordedEvents; @Singleton public static class Factory { @@ -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..39d06f3 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
@@ -25,11 +25,14 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.AsyncReceiveCommits; import com.google.gerrit.server.ssh.NoSshModule; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.gerrit.server.util.SocketUtil; import com.google.gerrit.server.util.SystemLog; import com.google.gerrit.testutil.FakeEmailSender; import com.google.gerrit.testutil.NoteDbChecker; import com.google.gerrit.testutil.NoteDbMode; +import com.google.gerrit.testutil.SshMode; import com.google.gerrit.testutil.TempFileUtil; import com.google.inject.Injector; import com.google.inject.Key; @@ -47,6 +50,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; @@ -60,10 +64,12 @@ static Description forTestClass(org.junit.runner.Description testDesc, String configName) { return new AutoValue_GerritServer_Description( + testDesc, configName, true, // @UseLocalDisk is only valid on methods. !has(NoHttpd.class, testDesc.getTestClass()), has(Sandboxed.class, testDesc.getTestClass()), + has(UseSsh.class, testDesc.getTestClass()), null, // @GerritConfig is only valid on methods. null); // @GerritConfigs is only valid on methods. @@ -72,12 +78,15 @@ static Description forTestMethod(org.junit.runner.Description testDesc, String configName) { return new AutoValue_GerritServer_Description( + testDesc, configName, testDesc.getAnnotation(UseLocalDisk.class) == null, testDesc.getAnnotation(NoHttpd.class) == null && !has(NoHttpd.class, testDesc.getTestClass()), testDesc.getAnnotation(Sandboxed.class) != null || has(Sandboxed.class, testDesc.getTestClass()), + testDesc.getAnnotation(UseSsh.class) != null || + has(UseSsh.class, testDesc.getTestClass()), testDesc.getAnnotation(GerritConfig.class), testDesc.getAnnotation(GerritConfigs.class)); } @@ -92,10 +101,12 @@ return false; } + abstract org.junit.runner.Description testDescription(); @Nullable abstract String configName(); abstract boolean memory(); abstract boolean httpd(); abstract boolean sandboxed(); + abstract boolean useSsh(); @Nullable abstract GerritConfig config(); @Nullable abstract GerritConfigs configs(); @@ -129,8 +140,9 @@ throw new RuntimeException(e); } } - }); + }, Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir"))); daemon.setEmailModuleForTesting(new FakeEmailSender.Module()); + daemon.setEnableSshd(SshMode.useSsh()); final File site; ExecutorService daemonService = null; @@ -290,10 +302,7 @@ void stop() throws Exception { try { - if (NoteDbMode.get().equals(NoteDbMode.CHECK)) { - testInjector.getInstance(NoteDbChecker.class) - .rebuildAndCheckAllChanges(); - } + checkNoteDbState(); } finally { daemon.getLifecycleManager().stop(); if (daemonService != null) { @@ -305,6 +314,23 @@ } } + private void checkNoteDbState() throws Exception { + NoteDbMode mode = NoteDbMode.get(); + if (mode != NoteDbMode.CHECK && mode != NoteDbMode.PRIMARY) { + return; + } + NoteDbChecker checker = testInjector.getInstance(NoteDbChecker.class); + OneOffRequestContext oneOffRequestContext = + testInjector.getInstance(OneOffRequestContext.class); + try (ManualRequestContext ctx = oneOffRequestContext.open()) { + if (mode == NoteDbMode.CHECK) { + checker.rebuildAndCheckAllChanges(); + } else if (mode == NoteDbMode.PRIMARY) { + checker.assertNoReviewDbChanges(desc.testDescription()); + } + } + } + @Override public String toString() { return MoreObjects.toStringHelper(this).addValue(desc).toString();
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 f1700a7..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(); @@ -156,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(); } @@ -180,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 443c580..bdc1bb3 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; @@ -90,6 +92,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..9c86391 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.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.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() { + if (plugin != null) { + // plugin will be null if the plugin test requires ssh, but the command + // line flag says we are running tests without ssh as the assume() + // statement in AbstractDaemonTest will prevent the execution of setUp() + // in this class + 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..8620c6d 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; @@ -37,10 +38,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * @deprecated use {@link LightweightPluginDaemonTest} instead. + */ +@Deprecated 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 +56,8 @@ private Path pluginSubPath; private Path pluginSource; private boolean standalone; + private boolean bazel; + private Path basePath; protected String pluginName; protected Path testSite; @@ -87,10 +96,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 +108,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 +136,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 +162,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 +192,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 +213,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 +233,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..3f5ada8 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; @@ -28,7 +29,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.notedb.ChangeNotes; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; @@ -51,15 +51,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 +60,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 +137,7 @@ private String changeId; private Tag tag; private boolean force; + private List<String> pushOptions; private final TestRepository<?>.CommitBuilder commitBuilder; @@ -275,8 +277,8 @@ } tagCommand.call(); } - return new Result(ref, pushHead(testRepo, ref, tag != null, force), c, - subject); + return new Result(ref, + pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject); } public void setTag(final Tag tag) { @@ -287,6 +289,14 @@ this.force = force; } + public List<String> getPushOptions() { + return pushOptions; + } + + public void setPushOptions(List<String> pushOptions) { + this.pushOptions = pushOptions; + } + public void noParents() { commitBuilder.noParents(); } @@ -326,9 +336,13 @@ return commit; } + public void assertPushOptions(List<String> pushOptions) { + assertEquals(pushOptions, getPushOptions()); + } + public void assertChange(Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) - throws OrmException, NoSuchChangeException { + throws OrmException { Change c = getChange().change(); assertThat(c.getSubject()).isEqualTo(resSubj); assertThat(c.getStatus()).isEqualTo(expectedStatus); @@ -337,7 +351,7 @@ } private void assertReviewers(Change c, TestAccount... expectedReviewers) - throws OrmException, NoSuchChangeException { + throws OrmException { Iterable<Account.Id> actualIds = approvalsUtil .getReviewers(db, notesFactory.createChecked(db, c)) .all();
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..f6bdf7a 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,10 +18,12 @@ 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; import org.apache.http.Header; +import org.apache.http.HttpStatus; import org.apache.http.client.fluent.Request; import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.InputStreamEntity; @@ -32,10 +34,22 @@ public class RestSession extends HttpSession { - public RestSession(GerritServer server, TestAccount account) { + public RestSession(GerritServer server, @Nullable TestAccount account) { super(server, account); } + public RestResponse getOK(String endPoint) + throws Exception { + return get(endPoint, HttpStatus.SC_OK); + } + + public RestResponse get(String endPoint, int expectedStatus) + throws Exception { + RestResponse r = get(endPoint); + r.assertStatus(expectedStatus); + return r; + } + public RestResponse get(String endPoint) throws IOException { return getWithHeader(endPoint, null); } @@ -45,9 +59,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 +69,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 +87,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 +102,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( @@ -101,8 +115,28 @@ return post(endPoint, null); } + public RestResponse postOK(String endPoint, Object content) + throws Exception { + return post(endPoint, content, HttpStatus.SC_OK); + } + + public RestResponse post(String endPoint, Object content, int expectedStatus) + throws Exception { + RestResponse r = post(endPoint, content); + r.assertStatus(expectedStatus); + return r; + } + 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 +147,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/SshSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java index 794f832..4ce3301 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -49,9 +49,10 @@ channel.setCommand(command); channel.setInputStream(opt); InputStream in = channel.getInputStream(); + InputStream err = channel.getErrStream(); channel.connect(); - Scanner s = new Scanner(channel.getErrStream()).useDelimiter("\\A"); + Scanner s = new Scanner(err).useDelimiter("\\A"); error = s.hasNext() ? s.next() : null; s = new Scanner(in).useDelimiter("\\A");
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..563dd3c 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)); } @@ -63,6 +51,7 @@ public final String fullName; public final KeyPair sshKey; public final String httpPassword; + public String status; TestAccount(Account.Id id, String username, String email, String fullName, KeyPair sshKey, String httpPassword) {
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-framework/src/test/java/com/google/gerrit/acceptance/UseSsh.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseSsh.java new file mode 100644 index 0000000..b5ca4b2 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseSsh.java
@@ -0,0 +1,27 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +public @interface UseSsh { +}
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK deleted file mode 100644 index d5d0b0d..0000000 --- a/gerrit-acceptance-tests/BUCK +++ /dev/null
@@ -1,44 +0,0 @@ -java_library( - 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', - - '//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 = [ - '//gerrit-plugin-api/...', - '//tools/eclipse:classpath', - '//gerrit-acceptance-tests/...', - ], -)
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD index 2ec7a05..769baeb 100644 --- a/gerrit-acceptance-tests/BUILD +++ b/gerrit-acceptance-tests/BUILD
@@ -1,42 +1,44 @@ -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:guava-retrying", + "//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..3efbab3 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; @@ -39,6 +39,7 @@ import com.google.gerrit.acceptance.AccountCreator; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.accounts.EmailInput; import com.google.gerrit.extensions.api.changes.AddReviewerInput; @@ -59,9 +60,9 @@ import com.google.gerrit.gpg.testutil.TestKey; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.account.WatchConfig; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.project.RefPattern; @@ -122,9 +123,14 @@ @After public void restoreExternalIds() throws Exception { - db.accountExternalIds().delete(getExternalIds(admin)); - db.accountExternalIds().delete(getExternalIds(user)); - db.accountExternalIds().insert(savedExternalIds); + if (savedExternalIds != null) { + // savedExternalIds is null when we don't run SSH tests and the assume in + // @Before in AbstractDaemonTest prevents this class' @Before method from + // being executed. + db.accountExternalIds().delete(getExternalIds(admin)); + db.accountExternalIds().delete(getExternalIds(user)); + db.accountExternalIds().insert(savedExternalIds); + } accountCache.evict(admin.getId()); accountCache.evict(user.getId()); } @@ -199,6 +205,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(); @@ -360,7 +396,11 @@ @Test public void addEmail() throws Exception { List<String> emails = ImmutableList.of( - "new.email@example.com", "new.email@example.systems"); + "new.email@example.com", + "new.email@example.systems", + + // Not in the list of TLDs but added to override in OutgoingEmailValidator + "new.email@example.local"); for (String email : emails) { EmailInput input = new EmailInput(); input.email = email; @@ -370,14 +410,44 @@ } @Test - public void addInvalidEmail() throws Exception { - EmailInput input = new EmailInput(); - input.email = "invalid@"; - input.noConfirmation = true; + public void putStatus() throws Exception { + List<String> statuses = ImmutableList.of( + "OOO", "Busy"); + AccountInfo info; + for (String status : statuses) { + gApi.accounts().self().setStatus(status); + admin.status = status; + info = gApi.accounts().self().get(); + assertUser(info, admin); + } + } - exception.expect(BadRequestException.class); - exception.expectMessage("invalid email address"); - gApi.accounts().self().addEmail(input); + @Test + public void addInvalidEmail() throws Exception { + List<String> emails = ImmutableList.of( + // Missing domain part + "new.email", + + // Missing domain part + "new.email@", + + // Missing user part + "@example.com", + + // Non-supported TLD (see tlds-alpha-by-domain.txt) + "new.email@example.blog" + ); + for (String email : emails) { + EmailInput input = new EmailInput(); + input.email = email; + input.noConfirmation = true; + try { + gApi.accounts().self().addEmail(input); + fail("Expected BadRequestException for invalid email address: " + email); + } catch (BadRequestException e) { + assertThat(e).hasMessage("invalid email address"); + } + } } @Test @@ -636,7 +706,9 @@ } @Test + @UseSsh public void sshKeys() throws Exception { + // // The test account should initially have exactly one ssh key List<SshKeyInfo> info = gApi.accounts().self().listSshKeys(); assertThat(info).hasSize(1); @@ -739,13 +811,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 +823,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) { @@ -819,5 +875,6 @@ assertThat(info.name).isEqualTo(account.fullName); assertThat(info.email).isEqualTo(account.email); assertThat(info.username).isEqualTo(account.username); + assertThat(info.status).isEqualTo(account.status); } }
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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK deleted file mode 100644 index 4e3c880..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'api_account', - srcs = glob(['*IT.java']), - labels = ['api'], -)
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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK deleted file mode 100644 index e8963be..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'api_change', - srcs = glob(['*IT.java']), - labels = ['api'], -)
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 7bea801..b336ab4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -30,59 +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.notedb.NoteDbChangeState.PrimaryStorage; 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; @@ -100,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; @@ -109,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"); @@ -188,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(); @@ -304,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, @@ -313,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); @@ -334,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); @@ -345,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 @@ -584,6 +768,149 @@ } @Test + public void pushCommitOfOtherUser() throws Exception { + // admin pushes commit of user + PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.owner._accountId).isEqualTo(admin.id.get()); + CommitInfo commit = change.revisions.get(change.currentRevision).commit; + assertThat(commit.author.email).isEqualTo(user.email); + assertThat(commit.committer.email).isEqualTo(user.email); + + // check that the author/committer was added as reviewer + Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER); + assertThat(reviewers).isNotNull(); + assertThat(reviewers).hasSize(1); + assertThat(reviewers.iterator().next()._accountId) + .isEqualTo(user.getId().get()); + assertThat(change.reviewers.get(CC)).isNull(); + + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()) + .contains(admin.fullName + " has uploaded this 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"); @@ -623,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(); @@ -665,6 +1008,28 @@ } @Test + public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists() + throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.changePrimaryStorage()) + .isEqualTo(PrimaryStorage.REVIEW_DB); + + 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(); @@ -702,6 +1067,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() @@ -742,6 +1166,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() @@ -781,7 +1230,7 @@ cfg.getLabelSections().put(verified.getName(), verified); AccountGroup.UUID registeredUsers = - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1, registeredUsers, heads); @@ -801,11 +1250,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() @@ -831,6 +1287,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() @@ -856,11 +1321,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()).isEmpty(); + } reviewers = gApi.changes() .id(changeId) @@ -901,10 +1381,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(); @@ -928,18 +1405,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()) @@ -963,10 +1430,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(); @@ -977,7 +1441,63 @@ .id(r.getChangeId()) .reviewer(user.getId().toString()) .deleteVote(in); - assertThat(sender.getMessages()).hasSize(0); + assertThat(sender.getMessages()).isEmpty(); + } + + @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 @@ -1005,9 +1525,9 @@ cfg.getLabelSections().put(verified.getName(), verified); String heads = "refs/heads/*"; AccountGroup.UUID owners = - SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID(); + systemGroupBackend.getGroup(CHANGE_OWNER).getUUID(); AccountGroup.UUID registered = - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, owners, heads); Util.allow(cfg, @@ -1251,6 +1771,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(); @@ -1279,7 +1824,7 @@ cfg.getLabelSections().put(custom2.getName(), custom2); String heads = "refs/heads/*"; AccountGroup.UUID anon = - SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); + systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads); Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads); Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads); @@ -1329,6 +1874,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(); @@ -1387,10 +1967,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()); @@ -1717,14 +2295,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) @@ -1734,4 +2682,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..2f9b4aa --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/MergeListIT.java
@@ -0,0 +1,206 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.api.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.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.extensions.restapi.ResourceConflictException; + +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; + + @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 { + gApi.changes() + .id(changeId) + .edit() + .create(); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("Invalid path: " + MERGE_LIST); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(MERGE_LIST, RawInputUtil.create("new content")); + } + + @Test + public void deleteMergeList() throws Exception { + gApi.changes() + .id(changeId) + .edit() + .create(); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("no changes were made"); + gApi.changes() + .id(changeId) + .edit() + .deleteFile(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..32a7c9e 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; @@ -44,7 +45,6 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.Util; import org.eclipse.jgit.junit.TestRepository; @@ -72,14 +72,16 @@ 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 = - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow(cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, registeredUsers, heads); @@ -91,7 +93,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 +103,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 +124,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 +149,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 +196,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 +221,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 +243,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 +328,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 +400,8 @@ updateFirstParent(changeId); return; case NO_CHANGE: + noChange(changeId); + return; default: fail("unexpected change kind: " + changeKind); } @@ -379,6 +418,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 +549,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 +567,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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK deleted file mode 100644 index 3b3d362..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'api_config', - srcs = glob(['*IT.java']), - labels = ['api'], -)
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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK deleted file mode 100644 index cea23dd..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/BUCK +++ /dev/null
@@ -1,23 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -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'], -) - -java_library( - 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/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..604d6bf 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,15 @@ 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 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.Iterables; -import com.google.common.collect.Ordering; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.TestAccount; -import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.common.AccountInfo; @@ -47,8 +47,9 @@ import org.junit.Test; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -125,7 +126,7 @@ String p = createGroup("parent"); String g1 = createGroup("newGroup1"); String g2 = createGroup("newGroup2"); - List<String> groups = new LinkedList<>(); + List<String> groups = new ArrayList<>(); groups.add(g1); groups.add(g2); gApi.groups().id(p).addGroups(g1, g2); @@ -133,14 +134,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 +151,7 @@ } @Test - public void testCreateDuplicateInternalGroupCaseInsensitiveName() + public void createDuplicateInternalGroupCaseInsensitiveName() throws Exception { String dupGroupName = name("dupGroupA"); String dupGroupNameLowerCase = name("dupGroupA").toLowerCase(); @@ -161,7 +162,7 @@ } @Test - public void testCreateDuplicateSystemGroupCaseSensitiveName_Conflict() + public void createDuplicateSystemGroupCaseSensitiveName_Conflict() throws Exception { String newGroupName = "Registered Users"; exception.expect(ResourceConflictException.class); @@ -170,7 +171,7 @@ } @Test - public void testCreateDuplicateSystemGroupCaseInsensitiveName_Conflict() + public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception { String newGroupName = "registered users"; exception.expect(ResourceConflictException.class); @@ -179,7 +180,25 @@ } @Test - public void testCreateGroupWithProperties() throws Exception { + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void createGroupWithConfiguredNameOfSystemGroup_Conflict() + throws Exception { + exception.expect(ResourceConflictException.class); + exception.expectMessage("group 'All Users' already exists"); + gApi.groups().create("all users"); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users") + public void createGroupWithDefaultNameOfSystemGroup_Conflict() + throws Exception { + exception.expect(ResourceConflictException.class); + exception.expectMessage("group name 'Anonymous Users' is reserved"); + gApi.groups().create("anonymous users"); + } + + @Test + public void createGroupWithProperties() throws Exception { GroupInput in = new GroupInput(); in.name = name("newGroup"); in.description = "Test description"; @@ -192,14 +211,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 +232,42 @@ } @Test - public void testGroupName() throws Exception { + @GerritConfig(name = "groups.global:Anonymous-Users.name", + value = "All Users") + public void getSystemGroupByConfiguredName() throws Exception { + GroupReference anonymousUsersGroup = + systemGroupBackend.getGroup(ANONYMOUS_USERS); + assertThat(anonymousUsersGroup.getName()).isEqualTo("All Users"); + + GroupInfo group = + gApi.groups().id(anonymousUsersGroup.getUUID().get()).get(); + assertThat(group.name).isEqualTo(anonymousUsersGroup.getName()); + + group = gApi.groups().id(anonymousUsersGroup.getName()).get(); + assertThat(group.id) + .isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get()))); + } + + @Test + public void getSystemGroupByDefaultName() throws Exception { + GroupReference anonymousUsersGroup = + systemGroupBackend.getGroup(ANONYMOUS_USERS); + GroupInfo group = gApi.groups().id("Anonymous Users").get(); + assertThat(group.name).isEqualTo(anonymousUsersGroup.getName()); + assertThat(group.id) + .isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get()))); + } + + @Test + @GerritConfig(name = "groups.global:Anonymous-Users.name", + value = "All Users") + public void getSystemGroupByDefaultName_NotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.groups().id("Anonymous-Users").get(); + } + + @Test + public void groupName() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -232,7 +286,7 @@ } @Test - public void testGroupRename() throws Exception { + public void groupRename() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -247,7 +301,7 @@ } @Test - public void testGroupDescription() throws Exception { + public void groupDescription() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -269,7 +323,7 @@ } @Test - public void testGroupOptions() throws Exception { + public void groupOptions() throws Exception { String name = name("group"); gApi.groups().create(name); @@ -284,7 +338,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 +452,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 +484,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(); @@ -488,6 +538,32 @@ } } + // reindex is tested by {@link AbstractQueryGroupsTest#reindex} + @Test + public void reindexPermissions() throws Exception { + TestAccount groupOwner = accounts.user2(); + GroupInput in = new GroupInput(); + in.name = name("group"); + in.members = Collections.singleton(groupOwner).stream() + .map(u -> u.id.toString()).collect(toList()); + in.visibleToAll = true; + GroupInfo group = gApi.groups().create(in).get(); + + // admin can reindex any group + setApiUser(admin); + gApi.groups().id(group.id).index(); + + // group owner can reindex own group (group is owned by itself) + setApiUser(groupOwner); + gApi.groups().id(group.id).index(); + + // user cannot reindex any group + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to index group"); + gApi.groups().id(group.id).index(); + } + private void assertAuditEvent(GroupAuditEventInfo info, Type expectedType, Account.Id expectedUser, Account.Id expectedMember) { assertThat(info.user._accountId).isEqualTo(expectedUser.get()); @@ -510,7 +586,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 +594,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 +609,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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK deleted file mode 100644 index 0b293f3..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'api_project', - srcs = glob(['*IT.java']), - labels = ['api'], -)
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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK deleted file mode 100644 index 76ae637..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'api_revision', - srcs = glob(['*IT.java']), - 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..d8fcc99 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,26 @@ 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.extensions.client.ReviewerState.REVIEWER; +import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG; +import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST; import static 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.ImmutableSet; 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 +45,60 @@ 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.AccountInfo; +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.MethodNotAllowedException; 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.Account; +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 +148,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 +541,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 +724,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 +745,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 +753,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 +761,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 +830,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 +1009,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()) @@ -800,6 +1058,71 @@ oldETag = checkETag(getRevisionActions, r2, oldETag); } + @Test + public void deleteVoteOnNonCurrentPatchSet() throws Exception { + PushOneCommit.Result r = createChange(); // patch set 1 + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + // patch set 2 + amendChange(r.getChangeId()); + + // code-review + setApiUser(user); + recommend(r.getChangeId()); + + // check if it's blocked to delete a vote on a non-current patch set. + exception.expect(MethodNotAllowedException.class); + exception.expectMessage("Cannot access on non-current patch set"); + setApiUser(admin); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().getName()) + .reviewer(user.getId().toString()) + .deleteVote("Code-Review"); + } + + @Test + public void deleteVoteOnCurrentPatchSet() throws Exception { + PushOneCommit.Result r = createChange(); // patch set 1 + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + // patch set 2 + amendChange(r.getChangeId()); + + // code-review + setApiUser(user); + recommend(r.getChangeId()); + + setApiUser(admin); + gApi.changes() + .id(r.getChangeId()) + .current() + .reviewer(user.getId().toString()) + .deleteVote("Code-Review"); + + Map<String, Short> m = gApi.changes() + .id(r.getChangeId()) + .current() + .reviewer(user.getId().toString()) + .votes(); + + assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0)); + + ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); + ChangeMessageInfo message = Iterables.getLast(c.messages); + assertThat(message.author._accountId).isEqualTo(admin.getId().get()); + assertThat(message.message).isEqualTo( + "Removed Code-Review+1 by User <user@example.com>\n"); + assertThat(getReviewers(c.reviewers.get(REVIEWER))) + .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId())); + } + private PushOneCommit.Result updateChange(PushOneCommit.Result r, String content) throws Exception { PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, @@ -822,4 +1145,112 @@ 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(); + } + + private static Iterable<Account.Id> getReviewers( + Collection<AccountInfo> r) { + return Iterables.transform(r, a -> new Account.Id(a._accountId)); + } }
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..f23667e --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.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.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 addedRobotCommentsCanBeRetrievedByChange() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + RobotCommentInput in = createRobotCommentInput(); + addRobotComment(changeId, in); + + pushFactory.create(db, admin.getIdent(), testRepo, changeId) + .to("refs/for/master"); + + RobotCommentInput in2 = createRobotCommentInput(); + addRobotComment(changeId, in2); + + Map<String, List<RobotCommentInfo>> out = + gApi.changes().id(changeId).robotComments(); + + assertThat(out).hasSize(1); + assertThat(out.get(in.path)).hasSize(2); + + RobotCommentInfo comment1 = out.get(in.path).get(0); + assertRobotComment(comment1, in, false); + RobotCommentInfo comment2 = out.get(in.path).get(1); + assertRobotComment(comment2, in2, 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 deleted file mode 100644 index c3274db..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK +++ /dev/null
@@ -1,11 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - 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..02d5cc7 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
@@ -16,14 +16,14 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat; +import static com.google.gerrit.extensions.restapi.BinaryResultSubject.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; 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.ImmutableMap; -import com.google.common.collect.Lists; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; @@ -31,31 +31,28 @@ 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; 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.DiffInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FileInfo; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BinaryResult; -import com.google.gerrit.extensions.restapi.ResourceNotFoundException; -import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +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.server.ReviewDb; import com.google.gerrit.server.change.ChangeEdits.EditMessage; import com.google.gerrit.server.change.ChangeEdits.Post; import com.google.gerrit.server.change.ChangeEdits.Put; -import com.google.gerrit.server.change.FileContentUtil; -import com.google.gerrit.server.edit.ChangeEdit; -import com.google.gerrit.server.edit.ChangeEditModifier; -import com.google.gerrit.server.edit.ChangeEditUtil; -import com.google.gerrit.server.edit.UnchangedCommitMessageException; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.Util; import com.google.gerrit.testutil.TestTimeUtil; import com.google.gson.reflect.TypeToken; @@ -63,12 +60,10 @@ 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; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -78,12 +73,12 @@ import org.junit.BeforeClass; import org.junit.Test; -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; +import java.io.IOException; +import java.sql.Timestamp; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; public class ChangeEditIT extends AbstractDaemonTest { @@ -98,21 +93,9 @@ @Inject private SchemaFactory<ReviewDb> reviewDbProvider; - @Inject - private ChangeEditUtil editUtil; - - @Inject - private ChangeEditModifier modifier; - - @Inject - private FileContentUtil fileUtil; - - private Change change; private String changeId; - private Change change2; private String changeId2; private PatchSet ps; - private PatchSet ps2; @BeforeClass public static void setTimeForTesting() { @@ -129,14 +112,9 @@ db = reviewDbProvider.open(); changeId = newChange(admin.getIdent()); ps = getCurrentPatchSet(changeId); - amendChange(admin.getIdent(), changeId); - change = getChange(changeId); assertThat(ps).isNotNull(); + amendChange(admin.getIdent(), changeId); changeId2 = newChange2(admin.getIdent()); - change2 = getChange(changeId2); - assertThat(change2).isNotNull(); - ps2 = getCurrentPatchSet(changeId2); - assertThat(ps2).isNotNull(); } @After @@ -146,36 +124,50 @@ @Test public void parseEditRevision() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); + createArbitraryEditFor(changeId); // check that '0' is parsed as edit revision - gApi.changes().id(change.getChangeId()).revision(0).comments(); + gApi.changes().id(changeId).revision(0).comments(); // check that 'edit' is parsed as edit revision - gApi.changes().id(change.getChangeId()).revision("edit").comments(); + gApi.changes().id(changeId).revision("edit").comments(); } @Test - public void deleteEdit() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - editUtil.delete(editUtil.byChange(change).get()); - assertThat(editUtil.byChange(change).isPresent()).isFalse(); + public void deleteEditOfCurrentPatchSet() throws Exception { + createArbitraryEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .delete(); + assertThat(getEdit(changeId)).isAbsent(); + } + + @Test + public void deleteEditOfOlderPatchSet() throws Exception { + createArbitraryEditFor(changeId2); + amendChange(admin.getIdent(), changeId2); + + gApi.changes() + .id(changeId2) + .edit() + .delete(); + assertThat(getEdit(changeId2)).isAbsent(); } @Test public void publishEdit() throws Exception { - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .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()); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(edit.isPresent()).isFalse(); - assertChangeMessages(change, + createArbitraryEditFor(changeId); + + PublishChangeEditInput publishInput = new PublishChangeEditInput(); + publishInput.notify = NotifyHandling.NONE; + gApi.changes() + .id(changeId) + .edit() + .publish(publishInput); + + assertThat(getEdit(changeId)).isAbsent(); + assertChangeMessages(changeId, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", "Patch Set 3: Published edit on patch set 2.")); @@ -184,124 +176,149 @@ @Test public void publishEditRest() throws Exception { PatchSet oldCurrentPatchSet = getCurrentPatchSet(changeId); - assertThat(modifier.createEdit(change, oldCurrentPatchSet)).isEqualTo( - RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - Optional<ChangeEdit> edit = editUtil.byChange(change); - adminRestSession.post(urlPublish()).assertNoContent(); - edit = editUtil.byChange(change); - assertThat(edit.isPresent()).isFalse(); + createArbitraryEditFor(changeId); + + adminRestSession.post(urlPublish(changeId)).assertNoContent(); + assertThat(getEdit(changeId)).isAbsent(); PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId); - assertThat(newCurrentPatchSet.getId()).isNotEqualTo(oldCurrentPatchSet.getId()); - assertChangeMessages(change, + assertThat(newCurrentPatchSet.getId()) + .isNotEqualTo(oldCurrentPatchSet.getId()); + assertChangeMessages(changeId, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", "Patch Set 3: Published edit on patch set 2.")); } @Test + public void publishEditNotifyRest() throws Exception { + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = user.email; + gApi.changes().id(changeId).addReviewer(in); + + createArbitraryEditFor(changeId); + + sender.clear(); + PublishChangeEditInput input = new PublishChangeEditInput(); + input.notify = NotifyHandling.NONE; + adminRestSession.post(urlPublish(changeId), input).assertNoContent(); + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void publishEditWithDefaultNotify() throws Exception { + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = user.email; + gApi.changes() + .id(changeId) + .addReviewer(in); + + createArbitraryEditFor(changeId); + + sender.clear(); + gApi.changes() + .id(changeId) + .edit() + .publish(); + assertThat(sender.getMessages()).isNotEmpty(); + } + + @Test public void deleteEditRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - Optional<ChangeEdit> edit = editUtil.byChange(change); - adminRestSession.delete(urlEdit()).assertNoContent(); - edit = editUtil.byChange(change); - assertThat(edit.isPresent()).isFalse(); + createArbitraryEditFor(changeId); + adminRestSession.delete(urlEdit(changeId)).assertNoContent(); + assertThat(getEdit(changeId)).isAbsent(); } @Test public void publishEditRestWithoutCLA() throws Exception { + createArbitraryEditFor(changeId); setUseContributorAgreements(InheritableBoolean.TRUE); - PatchSet oldCurrentPatchSet = getCurrentPatchSet(changeId); - assertThat(modifier.createEdit(change, oldCurrentPatchSet)).isEqualTo( - RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - adminRestSession.post(urlPublish()).assertForbidden(); + adminRestSession.post(urlPublish(changeId)).assertForbidden(); setUseContributorAgreements(InheritableBoolean.FALSE); - adminRestSession.post(urlPublish()).assertNoContent(); + adminRestSession.post(urlPublish(changeId)).assertNoContent(); } @Test public void rebaseEdit() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - ChangeEdit edit = editUtil.byChange(change).get(); - PatchSet current = getCurrentPatchSet(changeId); - assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo( - current.getPatchSetId() - 1); - Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen(); - modifier.rebaseEdit(edit, current); - edit = editUtil.byChange(change).get(); - assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()), - ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW); - assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()), - ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2); - assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo( - current.getPatchSetId()); - Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen(); - assertThat(beforeRebase.equals(afterRebase)).isFalse(); + PatchSet previousPatchSet = getCurrentPatchSet(changeId2); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + amendChange(admin.getIdent(), changeId2); + PatchSet currentPatchSet = getCurrentPatchSet(changeId2); + + Optional<EditInfo> originalEdit = getEdit(changeId2); + assertThat(originalEdit).value().baseRevision() + .isEqualTo(previousPatchSet.getRevision().get()); + Timestamp beforeRebase = originalEdit.get().commit.committer.date; + gApi.changes() + .id(changeId2) + .edit() + .rebase(); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2); + Optional<EditInfo> rebasedEdit = getEdit(changeId2); + assertThat(rebasedEdit).value().baseRevision() + .isEqualTo(currentPatchSet.getRevision().get()); + assertThat(rebasedEdit).value().commit().committer().creationDate() + .isNotEqualTo(beforeRebase); } @Test public void rebaseEditRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - ChangeEdit edit = editUtil.byChange(change).get(); - PatchSet current = getCurrentPatchSet(changeId); - assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo( - current.getPatchSetId() - 1); - Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen(); - adminRestSession.post(urlRebase()).assertNoContent(); - edit = editUtil.byChange(change).get(); - assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()), - ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW); - assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()), - ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2); - assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo( - current.getPatchSetId()); - Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen(); - assertThat(afterRebase).isNotEqualTo(beforeRebase); + PatchSet previousPatchSet = getCurrentPatchSet(changeId2); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + amendChange(admin.getIdent(), changeId2); + PatchSet currentPatchSet = getCurrentPatchSet(changeId2); + + Optional<EditInfo> originalEdit = getEdit(changeId2); + assertThat(originalEdit).value().baseRevision() + .isEqualTo(previousPatchSet.getRevision().get()); + Timestamp beforeRebase = originalEdit.get().commit.committer.date; + adminRestSession.post(urlRebase(changeId2)).assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME2), CONTENT_NEW2); + Optional<EditInfo> rebasedEdit = getEdit(changeId2); + assertThat(rebasedEdit).value().baseRevision() + .isEqualTo(currentPatchSet.getRevision().get()); + assertThat(rebasedEdit).value().commit().committer().creationDate() + .isNotEqualTo(beforeRebase); } @Test public void rebaseEditWithConflictsRest_Conflict() throws Exception { - PatchSet current = getCurrentPatchSet(changeId2); - assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - ChangeEdit edit = editUtil.byChange(change2).get(); - assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo( - current.getPatchSetId()); - PushOneCommit push = - pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, FILE_NAME, - new String(CONTENT_NEW2), changeId2); + PatchSet currentPatchSet = getCurrentPatchSet(changeId2); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + Optional<EditInfo> edit = getEdit(changeId2); + assertThat(edit).value().baseRevision() + .isEqualTo(currentPatchSet.getRevision().get()); + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, + PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_NEW2, UTF_8), + changeId2); push.to("refs/for/master").assertOkStatus(); - adminRestSession.post(urlRebase()).assertConflict(); + adminRestSession.post(urlRebase(changeId2)).assertConflict(); } @Test public void updateExistingFile() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW); - editUtil.delete(edit.get()); - edit = editUtil.byChange(change); - assertThat(edit.isPresent()).isFalse(); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + assertThat(getEdit(changeId)).isPresent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); } @Test @@ -310,70 +327,89 @@ // Re-clone empty repo; TestRepository doesn't let us reset to unborn head. testRepo = cloneProject(project); changeId = newChange(admin.getIdent()); - change = getChange(changeId); - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(edit.get().getEditCommit().getParentCount()).isEqualTo(0); + createEmptyEditFor(changeId); + Optional<EditInfo> edit = getEdit(changeId); + assertThat(edit).value().commit().parents().isEmpty(); String msg = String.format("New commit message\n\nChange-Id: %s\n", - change.getKey()); - assertThat(modifier.modifyMessage(edit.get(), msg)) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg); + changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyCommitMessage(msg); + String commitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); + assertThat(commitMessage).isEqualTo(msg); } @Test public void updateMessageNoChange() throws Exception { - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); + createEmptyEditFor(changeId); + String commitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); - exception.expect(UnchangedCommitMessageException.class); + exception.expect(ResourceConflictException.class); exception.expectMessage( "New commit message cannot be same as existing commit message"); - modifier.modifyMessage( - edit.get(), - edit.get().getEditCommit().getFullMessage()); + gApi.changes() + .id(changeId) + .edit() + .modifyCommitMessage(commitMessage); } @Test public void updateMessageOnlyAddTrailingNewLines() throws Exception { - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); + createEmptyEditFor(changeId); + String commitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); - exception.expect(UnchangedCommitMessageException.class); + exception.expect(ResourceConflictException.class); exception.expectMessage( "New commit message cannot be same as existing commit message"); - modifier.modifyMessage( - edit.get(), - edit.get().getEditCommit().getFullMessage() + "\n\n"); + gApi.changes() + .id(changeId) + .edit() + .modifyCommitMessage(commitMessage + "\n\n"); } @Test public void updateMessage() throws Exception { - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); + createEmptyEditFor(changeId); String msg = String.format("New commit message\n\nChange-Id: %s\n", - change.getKey()); - assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo( - RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg); + changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyCommitMessage(msg); + String commitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); + assertThat(commitMessage).isEqualTo(msg); - editUtil.publish(edit.get()); - assertThat(editUtil.byChange(change).isPresent()).isFalse(); + PublishChangeEditInput publishInput = new PublishChangeEditInput(); + publishInput.notify = NotifyHandling.NONE; + gApi.changes() + .id(changeId) + .edit() + .publish(publishInput); + assertThat(getEdit(changeId)).isAbsent(); ChangeInfo info = get(changeId, ListChangesOption.CURRENT_COMMIT, ListChangesOption.CURRENT_REVISION); assertThat(info.revisions.get(info.currentRevision).commit.message) .isEqualTo(msg); + assertThat(info.revisions.get(info.currentRevision).description) + .isEqualTo("Edit commit message"); - assertChangeMessages(change, + assertChangeMessages(changeId, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", "Patch Set 3: Commit message was updated.")); @@ -381,26 +417,31 @@ @Test public void updateMessageRest() throws Exception { - adminRestSession.get(urlEditMessage(false)).assertNotFound(); + adminRestSession.get(urlEditMessage(changeId, false)).assertNotFound(); EditMessage.Input in = new EditMessage.Input(); in.message = String.format("New commit message\n\n" + CONTENT_NEW2_STR + "\n\nChange-Id: %s\n", - change.getKey()); - adminRestSession.put(urlEditMessage(false), in).assertNoContent(); - RestResponse r = adminRestSession.getJsonAccept(urlEditMessage(false)); + changeId); + adminRestSession.put(urlEditMessage(changeId, false), in).assertNoContent(); + RestResponse r = adminRestSession.getJsonAccept(urlEditMessage(changeId, + false)); r.assertOK(); assertThat(readContentFromJson(r)).isEqualTo(in.message); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(edit.get().getEditCommit().getFullMessage()) - .isEqualTo(in.message); + String commitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); + assertThat(commitMessage).isEqualTo(in.message); in.message = String.format("New commit message2\n\nChange-Id: %s\n", - change.getKey()); - adminRestSession.put(urlEditMessage(false), in).assertNoContent(); - edit = editUtil.byChange(change); - assertThat(edit.get().getEditCommit().getFullMessage()) - .isEqualTo(in.message); + changeId); + adminRestSession.put(urlEditMessage(changeId, false), in).assertNoContent(); + String updatedCommitMessage = gApi.changes() + .id(changeId) + .edit() + .getCommitMessage(); + assertThat(updatedCommitMessage).isEqualTo(in.message); - r = adminRestSession.getJsonAccept(urlEditMessage(true)); + r = adminRestSession.getJsonAccept(urlEditMessage(changeId, true)); try (Repository repo = repoManager.openRepository(project); RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit( @@ -408,8 +449,13 @@ assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage()); } - editUtil.publish(edit.get()); - assertChangeMessages(change, + PublishChangeEditInput publishInput = new PublishChangeEditInput(); + publishInput.notify = NotifyHandling.NONE; + gApi.changes() + .id(changeId) + .edit() + .publish(publishInput); + assertChangeMessages(changeId, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", "Patch Set 3: Commit message was updated.")); @@ -417,273 +463,240 @@ @Test public void retrieveEdit() throws Exception { - adminRestSession.get(urlEdit()).assertNoContent(); - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - EditInfo info = toEditInfo(false); - assertThat(info.commit.commit).isEqualTo(edit.get().getRevision().get()); - assertThat(info.commit.parents).hasSize(1); + adminRestSession.get(urlEdit(changeId)).assertNoContent(); + createArbitraryEditFor(changeId); + EditInfo editInfo = getEditInfo(changeId, false); + ChangeInfo changeInfo = get(changeId); + assertThat(editInfo.commit.commit).isNotEqualTo(changeInfo.currentRevision); + assertThat(editInfo).commit().parents().hasSize(1); + assertThat(editInfo).baseRevision().isEqualTo(changeInfo.currentRevision); - edit = editUtil.byChange(change); - editUtil.delete(edit.get()); + gApi.changes() + .id(changeId) + .edit() + .delete(); - adminRestSession.get(urlEdit()).assertNoContent(); + adminRestSession.get(urlEdit(changeId)).assertNoContent(); } @Test public void retrieveFilesInEdit() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); - EditInfo info = toEditInfo(true); - assertThat(info.files).hasSize(2); - List<String> l = Lists.newArrayList(info.files.keySet()); - assertThat(l.get(0)).isEqualTo("/COMMIT_MSG"); - assertThat(l.get(1)).isEqualTo("foo"); + EditInfo info = getEditInfo(changeId, true); + assertThat(info.files).isNotNull(); + assertThat(info.files.keySet()).containsExactly(Patch.COMMIT_MSG, + FILE_NAME, FILE_NAME2); } @Test public void deleteExistingFile() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.deleteFile(edit.get(), FILE_NAME)).isEqualTo( - RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .deleteFile(FILE_NAME); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void renameExistingFile() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.renameFile(edit.get(), FILE_NAME, FILE_NAME3)) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .renameFile(FILE_NAME, FILE_NAME3); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_OLD); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void createEditByDeletingExistingFileRest() throws Exception { - adminRestSession.delete(urlEditFile()).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + adminRestSession.delete(urlEditFile(changeId, FILE_NAME)).assertNoContent(); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void deletingNonExistingEditRest() throws Exception { - adminRestSession.delete(urlEdit()).assertNotFound(); + adminRestSession.delete(urlEdit(changeId)).assertNotFound(); } @Test public void deleteExistingFileRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - adminRestSession.delete(urlEditFile()).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + createEmptyEditFor(changeId); + adminRestSession.delete(urlEditFile(changeId, FILE_NAME)).assertNoContent(); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void restoreDeletedFileInPatchSet() throws Exception { - assertThat(modifier.createEdit(change2, ps2)).isEqualTo( - RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change2); - assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo( - RefUpdate.Result.FORCED); - edit = editUtil.byChange(change2); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .restoreFile(FILE_NAME); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_OLD); } @Test public void revertChanges() throws Exception { - assertThat(modifier.createEdit(change2, ps2)).isEqualTo( - RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change2); - assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo( - RefUpdate.Result.FORCED); - edit = editUtil.byChange(change2); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD); - assertThat( - modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change2); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW); - assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo( - RefUpdate.Result.FORCED); - edit = editUtil.byChange(change2); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD); - editUtil.delete(edit.get()); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .restoreFile(FILE_NAME); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_OLD); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_NEW); + gApi.changes() + .id(changeId2) + .edit() + .restoreFile(FILE_NAME); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_OLD); } @Test public void renameFileRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); + createEmptyEditFor(changeId); Post.Input in = new Post.Input(); in.oldPath = FILE_NAME; in.newPath = FILE_NAME3; - adminRestSession.post(urlEdit(), in).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + adminRestSession.post(urlEdit(changeId), in).assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_OLD); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void restoreDeletedFileInPatchSetRest() throws Exception { Post.Input in = new Post.Input(); in.restorePath = FILE_NAME; - adminRestSession.post(urlEdit2(), in).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change2); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD); + adminRestSession.post(urlEdit(changeId2), in).assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId2, FILE_NAME), CONTENT_OLD); } @Test public void amendExistingFile() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW2)); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW2); } @Test public void createAndChangeEditInOneRequestRest() throws Exception { Put.Input in = new Put.Input(); in.content = RawInputUtil.create(CONTENT_NEW); - adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW); + adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content) + .assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); in.content = RawInputUtil.create(CONTENT_NEW2); - adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent(); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2); + adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content) + .assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW2); } @Test public void changeEditRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); + createEmptyEditFor(changeId); Put.Input in = new Put.Input(); in.content = RawInputUtil.create(CONTENT_NEW); - adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW); + adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content) + .assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_NEW); } @Test public void emptyPutRequest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - adminRestSession.put(urlEditFile()).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), "".getBytes()); + createEmptyEditFor(changeId); + adminRestSession.put(urlEditFile(changeId, FILE_NAME)).assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), + "".getBytes(UTF_8)); } @Test public void createEmptyEditRest() throws Exception { - adminRestSession.post(urlEdit()).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD); + adminRestSession.post(urlEdit(changeId)).assertNoContent(); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME), CONTENT_OLD); } @Test public void getFileContentRest() throws Exception { Put.Input in = new Put.Input(); in.content = RawInputUtil.create(CONTENT_NEW); - adminRestSession.putRaw(urlEditFile(), in.content).assertNoContent(); - 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()); + adminRestSession.putRaw(urlEditFile(changeId, FILE_NAME), in.content) + .assertNoContent(); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW2)); + RestResponse r = adminRestSession.getJsonAccept(urlEditFile(changeId, + FILE_NAME)); r.assertOK(); assertThat(readContentFromJson(r)).isEqualTo( - StringUtils.newStringUtf8(CONTENT_NEW2)); + new String(CONTENT_NEW2, UTF_8)); - r = adminRestSession.getJsonAccept(urlEditFile(true)); + r = adminRestSession.getJsonAccept(urlEditFile(changeId, FILE_NAME, true)); r.assertOK(); assertThat(readContentFromJson(r)).isEqualTo( - StringUtils.newStringUtf8(CONTENT_OLD)); + new String(CONTENT_OLD, UTF_8)); } @Test public void getFileNotFoundRest() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - adminRestSession.delete(urlEditFile()).assertNoContent(); - Optional<ChangeEdit> edit = editUtil.byChange(change); - adminRestSession.get(urlEditFile()).assertNoContent(); - exception.expect(ResourceNotFoundException.class); - fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME); + createEmptyEditFor(changeId); + adminRestSession.delete(urlEditFile(changeId, FILE_NAME)).assertNoContent(); + adminRestSession.get(urlEditFile(changeId, FILE_NAME)).assertNoContent(); + assertThat(getFileContentOfEdit(changeId, FILE_NAME)).isAbsent(); } @Test public void addNewFile() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME3, RawInputUtil.create(CONTENT_NEW)); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_NEW); } @Test public void addNewFileAndAmend() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW); - assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RawInputUtil.create(CONTENT_NEW2))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()), - ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW2); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME3, RawInputUtil.create(CONTENT_NEW)); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_NEW); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME3, RawInputUtil.create(CONTENT_NEW2)); + ensureSameBytes(getFileContentOfEdit(changeId, FILE_NAME3), CONTENT_NEW2); } @Test public void writeNoChanges() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - exception.expect(InvalidChangeOperationException.class); + createEmptyEditFor(changeId); + exception.expect(ResourceConflictException.class); exception.expectMessage("no changes were made"); - modifier.modifyFile( - editUtil.byChange(change).get(), - FILE_NAME, - RawInputUtil.create(CONTENT_OLD)); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_OLD)); } @Test @@ -695,23 +708,26 @@ cfg.getLabelSections().put(cr, codeReview); saveProjectConfig(project, cfg); - String changeId = change.getKey().get(); ReviewInput r = new ReviewInput(); - r.labels = ImmutableMap.<String, Short> of(cr, (short) 1); + r.labels = ImmutableMap.of(cr, (short) 1); gApi.changes() .id(changeId) - .revision(change.currentPatchSetId().get()) + .current() .review(r); - assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId))) - .isEqualTo(RefUpdate.Result.NEW); - Optional<ChangeEdit> edit = editUtil.byChange(change); + createEmptyEditFor(changeId); String newSubj = "New commit message"; String newMsg = newSubj + "\n\nChange-Id: " + changeId + "\n"; - assertThat(modifier.modifyMessage(edit.get(), newMsg)) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change); - editUtil.publish(edit.get()); + gApi.changes() + .id(changeId) + .edit() + .modifyCommitMessage(newMsg); + PublishChangeEditInput publishInput = new PublishChangeEditInput(); + publishInput.notify = NotifyHandling.NONE; + gApi.changes() + .id(changeId) + .edit() + .publish(publishInput); ChangeInfo info = get(changeId); assertThat(info.subject).isEqualTo(newSubj); @@ -721,65 +737,82 @@ } @Test - public void testHasEditPredicate() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); + public void hasEditPredicate() throws Exception { + createEmptyEditFor(changeId); assertThat(queryEdits()).hasSize(1); - PatchSet current = getCurrentPatchSet(changeId2); - assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW); - assertThat( - modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); + createEmptyEditFor(changeId2); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); assertThat(queryEdits()).hasSize(2); - assertThat( - modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, - RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); - editUtil.delete(editUtil.byChange(change).get()); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + gApi.changes() + .id(changeId) + .edit() + .delete(); assertThat(queryEdits()).hasSize(1); - editUtil.publish(editUtil.byChange(change2).get()); - assertThat(queryEdits()).hasSize(0); + PublishChangeEditInput publishInput = new PublishChangeEditInput(); + publishInput.notify = NotifyHandling.NONE; + gApi.changes() + .id(changeId2) + .edit() + .publish(publishInput); + assertThat(queryEdits()).isEmpty(); setApiUser(user); - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); + createEmptyEditFor(changeId); assertThat(queryEdits()).hasSize(1); setApiUser(admin); - assertThat(queryEdits()).hasSize(0); + assertThat(queryEdits()).isEmpty(); } @Test public void files() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - ChangeEdit edit = editUtil.byChange(change).get(); - assertThat(modifier.modifyFile(edit, FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change).get(); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + Optional<EditInfo> edit = getEdit(changeId); + assertThat(edit).isPresent(); + String editCommitId = edit.get().commit.commit; - RestResponse r = adminRestSession.getJsonAccept(urlRevisionFiles(edit)); + RestResponse r = adminRestSession.getJsonAccept(urlRevisionFiles(changeId, + editCommitId)); Map<String, FileInfo> files = readContentFromJson( r, new TypeToken<Map<String, FileInfo>>() {}); assertThat(files).containsKey(FILE_NAME); - r = adminRestSession.getJsonAccept(urlRevisionFiles()); + r = adminRestSession.getJsonAccept(urlRevisionFiles(changeId)); files = readContentFromJson(r, new TypeToken<Map<String, FileInfo>>() {}); assertThat(files).containsKey(FILE_NAME); } @Test public void diff() throws Exception { - assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); - ChangeEdit edit = editUtil.byChange(change).get(); - assertThat(modifier.modifyFile(edit, FILE_NAME, RawInputUtil.create(CONTENT_NEW))) - .isEqualTo(RefUpdate.Result.FORCED); - edit = editUtil.byChange(change).get(); + createEmptyEditFor(changeId); + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + Optional<EditInfo> edit = getEdit(changeId); + assertThat(edit).isPresent(); + String editCommitId = edit.get().commit.commit; - RestResponse r = adminRestSession.getJsonAccept(urlDiff(edit)); + RestResponse r = adminRestSession.getJsonAccept(urlDiff(changeId, + editCommitId, FILE_NAME)); DiffInfo diff = readContentFromJson(r, DiffInfo.class); assertThat(diff.diffHeader.get(0)).contains(FILE_NAME); - r = adminRestSession.getJsonAccept(urlDiff()); + r = adminRestSession.getJsonAccept(urlDiff(changeId, FILE_NAME)); diff = readContentFromJson(r, DiffInfo.class); assertThat(diff.diffHeader.get(0)).contains(FILE_NAME); } @@ -802,8 +835,35 @@ r1.assertOkStatus(); // Try to create edit as admin - assertThat(modifier.createEdit(r1.getChange().change(), - r1.getPatchSet())).isEqualTo(RefUpdate.Result.REJECTED); + exception.expect(AuthException.class); + createEmptyEditFor(r1.getChangeId()); + } + + private void createArbitraryEditFor(String changeId) throws Exception { + createEmptyEditFor(changeId); + arbitrarilyModifyEditOf(changeId); + } + + private void createEmptyEditFor(String changeId) throws Exception { + gApi.changes() + .id(changeId) + .edit() + .create(); + } + + private void arbitrarilyModifyEditOf(String changeId) throws Exception { + gApi.changes() + .id(changeId) + .edit() + .modifyFile(FILE_NAME, RawInputUtil.create(CONTENT_NEW)); + } + + private Optional<BinaryResult> getFileContentOfEdit(String changeId, + String filePath) throws Exception { + return gApi.changes() + .id(changeId) + .edit() + .getFile(filePath); } private List<ChangeInfo> queryEdits() throws Exception { @@ -817,117 +877,107 @@ return push.to("refs/for/master").getChangeId(); } - private String amendChange(PersonIdent ident, String changeId) throws Exception { - PushOneCommit push = - pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME2, - new String(CONTENT_NEW2, UTF_8), changeId); + private String amendChange(PersonIdent ident, String changeId) + throws Exception { + PushOneCommit push = pushFactory.create(db, ident, testRepo, + PushOneCommit.SUBJECT, FILE_NAME2, new String(CONTENT_NEW2, UTF_8), + changeId); return push.to("refs/for/master").getChangeId(); } private String newChange2(PersonIdent ident) throws Exception { - PushOneCommit push = - pushFactory.create(db, ident, testRepo, PushOneCommit.SUBJECT, FILE_NAME, - new String(CONTENT_OLD, UTF_8)); + PushOneCommit push = pushFactory.create(db, ident, testRepo, + PushOneCommit.SUBJECT, FILE_NAME, new String(CONTENT_OLD, UTF_8)); return push.rm("refs/for/master").getChangeId(); } - private Change getChange(String changeId) throws Exception { - return getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change(); - } - private PatchSet getCurrentPatchSet(String changeId) throws Exception { return getOnlyElement(queryProvider.get().byKeyPrefix(changeId)) .currentPatchSet(); } - private static void assertByteArray(BinaryResult result, byte[] expected) - throws Exception { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - result.writeTo(os); - assertThat(os.toByteArray()).isEqualTo(expected); + private void ensureSameBytes(Optional<BinaryResult> fileContent, + byte[] expectedFileBytes) throws IOException { + assertThat(fileContent).value().bytes().isEqualTo(expectedFileBytes); } - private String urlEdit() { + private String urlEdit(String changeId) { return "/changes/" - + change.getChangeId() + + changeId + "/edit"; } - private String urlEdit2() { + private String urlEditMessage(String changeId, boolean base) { return "/changes/" - + change2.getChangeId() - + "/edit/"; - } - - private String urlEditMessage(boolean base) { - return "/changes/" - + change.getChangeId() + + changeId + "/edit:message" + (base ? "?base" : ""); } - private String urlEditFile() { - return urlEditFile(false); + private String urlEditFile(String changeId, String fileName) { + return urlEditFile(changeId, fileName, false); } - private String urlEditFile(boolean base) { - return urlEdit() + private String urlEditFile(String changeId, String fileName, boolean base) { + return urlEdit(changeId) + "/" - + FILE_NAME + + fileName + (base ? "?base" : ""); } - private String urlGetFiles() { - return urlEdit() + private String urlGetFiles(String changeId) { + return urlEdit(changeId) + "?list"; } - private String urlRevisionFiles(ChangeEdit edit) { + private String urlRevisionFiles(String changeId, String revisionId) { return "/changes/" - + change.getChangeId() + + changeId + "/revisions/" - + edit.getRevision().get() + + revisionId + "/files"; } - private String urlRevisionFiles() { + private String urlRevisionFiles(String changeId) { return "/changes/" - + change.getChangeId() + + changeId + "/revisions/0/files"; } - private String urlPublish() { + private String urlPublish(String changeId) { return "/changes/" - + change.getChangeId() + + changeId + "/edit:publish"; } - private String urlRebase() { + private String urlRebase(String changeId) { return "/changes/" - + change.getChangeId() + + changeId + "/edit:rebase"; } - private String urlDiff() { + private String urlDiff(String changeId, String fileName) { return "/changes/" - + change.getChangeId() + + changeId + "/revisions/0/files/" - + FILE_NAME + + fileName + "/diff?context=ALL&intraline"; } - private String urlDiff(ChangeEdit edit) { + private String urlDiff(String changeId, String revisionId, String fileName) { return "/changes/" - + change.getChangeId() + + changeId + "/revisions/" - + edit.getRevision().get() + + revisionId + "/files/" - + FILE_NAME + + fileName + "/diff?context=ALL&intraline"; } - private EditInfo toEditInfo(boolean files) throws Exception { - RestResponse r = adminRestSession.get(files ? urlGetFiles() : urlEdit()); + private EditInfo getEditInfo(String changeId, boolean files) + throws Exception { + RestResponse r = adminRestSession.get(files ? urlGetFiles(changeId) + : urlEdit(changeId)); return readContentFromJson(r, EditInfo.class); } @@ -951,16 +1001,15 @@ return readContentFromJson(r, String.class); } - private void assertChangeMessages(Change c, List<String> expectedMessages) + private void assertChangeMessages(String changeId, + List<String> expectedMessages) throws Exception { - ChangeInfo ci = get(c.getId().toString()); + ChangeInfo ci = get(changeId); assertThat(ci.messages).isNotNull(); assertThat(ci.messages).hasSize(expectedMessages.size()); - List<String> actualMessages = new ArrayList<>(); - Iterator<ChangeMessageInfo> it = ci.messages.iterator(); - while (it.hasNext()) { - actualMessages.add(it.next().message); - } + List<String> actualMessages = ci.messages.stream() + .map(message -> message.message) + .collect(Collectors.toList()); assertThat(actualMessages) .containsExactlyElementsIn(expectedMessages) .inOrder();
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 57ae8b1..6fde56d 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,8 @@ 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.extensions.common.EditInfoSubject.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; import static com.google.gerrit.server.project.Util.category; import static com.google.gerrit.server.project.Util.value; @@ -29,10 +31,10 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GerritConfig; 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,15 +42,17 @@ 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; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.project.Util; import com.google.gerrit.server.query.change.ChangeData; @@ -70,18 +74,20 @@ 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.Set; + public abstract class AbstractPushForReview extends AbstractDaemonTest { protected enum Protocol { // TODO(dborowitz): TEST. SSH, HTTP } - private String sshUrl; private LabelType patchSetLock; @BeforeClass @@ -96,12 +102,11 @@ @Before public void setUp() throws Exception { - sshUrl = adminSshSession.getUrl(); ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); patchSetLock = Util.patchSetLock(); cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock); AccountGroup.UUID anonymousUsers = - SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); + systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers, "refs/heads/*"); saveProjectConfig(cfg); @@ -112,7 +117,7 @@ String url; switch (p) { case SSH: - url = sshUrl; + url = adminSshSession.getUrl(); break; case HTTP: url = admin.getHttpUrl(server); @@ -131,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"); @@ -178,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 @@ -188,13 +240,13 @@ PushOneCommit.Result r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE); r.assertOkStatus(); - assertThat(sender.getMessages()).hasSize(0); + assertThat(sender.getMessages()).isEmpty(); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER); r.assertOkStatus(); // no email notification about own changes - assertThat(sender.getMessages()).hasSize(0); + assertThat(sender.getMessages()).isEmpty(); sender.clear(); r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER_REVIEWERS); @@ -208,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 @@ -288,18 +378,19 @@ public void pushForMasterAsEdit() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); - EditInfo edit = getEdit(r.getChangeId()); - assertThat(edit).isNull(); + Optional<EditInfo> edit = getEdit(r.getChangeId()); + assertThat(edit).isAbsent(); // specify edit as option r = amendChange(r.getChangeId(), "refs/for/master%edit"); r.assertOkStatus(); edit = getEdit(r.getChangeId()); - assertThat(edit).isNotNull(); + assertThat(edit).isPresent(); + EditInfo editInfo = edit.get(); r.assertMessage("Updated Changes:\n " + canonicalWebUrl.get() + r.getChange().getId() - + " " + edit.commit.subject + " [EDIT]\n"); + + " " + editInfo.commit.subject + " [EDIT]\n"); } @Test @@ -314,6 +405,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 @@ -404,7 +534,7 @@ value(-1, "Negative")); ProjectConfig config = projectCache.checkedGet(project).getConfig(); AccountGroup.UUID anon = - SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); + systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); String heads = "refs/heads/*"; Util.allow(config, Permission.forLabel("Custom-Label"), -1, 1, anon, heads); config.getLabelSections().put(Q.getName(), Q); @@ -613,6 +743,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()); @@ -844,11 +1026,176 @@ } @Test - // TODO(dborowitz): This is to exercise a specific case in the database search - // path. Once the account index becomes obligatory this method can be removed. - @GerritConfig(name = "index.testDisable", value = "accounts") - public void pushWithNameInFooterNotFoundWithDbSearch() throws Exception { - pushWithReviewerInFooter("Notauser", null); + 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,
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 8b77238..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; @@ -60,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); @@ -78,8 +86,8 @@ @Nullable Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType) throws Exception { Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType); - grant("push", project, "refs/heads/*"); - grant("submit", project, "refs/for/refs/heads/*"); + grant(Permission.PUSH, project, "refs/heads/*"); + grant(Permission.SUBMIT, project, "refs/for/refs/heads/*"); return cloneProject(project); }
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 deleted file mode 100644 index f6796a5..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK +++ /dev/null
@@ -1,26 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'git', - srcs = glob(['*IT.java']), - deps = [ - ':submodule_util', - ':push_for_review', - ], - labels = ['git'], -) - -java_library( - name = 'push_for_review', - 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',] -)
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/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java new file mode 100644 index 0000000..78fa70d --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -0,0 +1,568 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.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; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.AcceptanceTestRequestScope; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.AccessSection; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.AnonymousCowardName; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook; +import com.google.gerrit.server.git.SearchingChangeCacheImpl; +import com.google.gerrit.server.git.TagCache; +import com.google.gerrit.server.git.VisibleRefFilter; +import com.google.gerrit.server.notedb.ChangeNoteUtil; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.Util; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.testutil.DisabledReviewDb; +import com.google.gerrit.testutil.TestChanges; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import 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; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@NoHttpd +public class RefAdvertisementIT extends AbstractDaemonTest { + @Inject + private ProjectControl.GenericFactory projectControlFactory; + + @Inject + @Nullable + private SearchingChangeCacheImpl changeCache; + + @Inject + private TagCache tagCache; + + @Inject + private Provider<CurrentUser> userProvider; + + @Inject + private ChangeNoteUtil noteUtil; + + @Inject + @AnonymousCowardName + private String anonymousCowardName; + + private AccountGroup.UUID admins; + + 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 { + admins = groupCache.get(new AccountGroup.NameKey("Administrators")) + .getGroupUUID(); + setUpPermissions(); + setUpChanges(); + } + + private void setUpPermissions() throws Exception { + // Remove read permissions for all users besides admin. This method is + // idempotent, so is safe to call on every test setup. + ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig(); + for (AccessSection sec : pc.getAccessSections()) { + sec.removePermission(Permission.READ); + } + Util.allow(pc, Permission.READ, admins, "refs/*"); + saveProjectConfig(allProjects, pc); + } + + private static String changeRefPrefix(Change.Id id) { + String ps = new PatchSet.Id(id, 1).toRefName(); + return ps.substring(0, ps.length() - 1); + } + + private void setUpChanges() throws Exception { + gApi.projects() + .name(project.get()) + .branch("branch") + .create(new BranchInput()); + + // First 2 changes are merged, which means the tags pointing to them are + // visible. + 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(); + r1 = changeRefPrefix(c1.getId()); + PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo) + .to("refs/for/branch%submit"); + br.assertOkStatus(); + 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 + RefUpdate mtu = repo.updateRef("refs/tags/master-tag"); + mtu.setExpectedOldObjectId(ObjectId.zeroId()); + mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId()); + assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW); + + // branch-tag -> branch + RefUpdate btu = repo.updateRef("refs/tags/branch-tag"); + btu.setExpectedOldObjectId(ObjectId.zeroId()); + btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId()); + assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW); + } + } + + @Test + 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); + Util.doNotInherit(cfg, Permission.READ, RefNames.REFS_CONFIG); + saveProjectConfig(project, cfg); + + setApiUser(user); + 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", + "refs/tags/master-tag"); + } + + @Test + public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/*"); + allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG); + + 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, + "refs/tags/branch-tag", + "refs/tags/master-tag"); + } + + @Test + public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + + setApiUser(user); + assertUploadPackRefs( + "HEAD", + r1 + "1", + r1 + "meta", + r3 + "1", + r3 + "meta", + "refs/heads/master", + "refs/tags/master-tag"); + } + + @Test + public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception { + deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + + setApiUser(user); + 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 + // (since PushOneCommit always bases changes on each other). + "refs/tags/master-tag"); + } + + @Test + public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + + Change c = notesFactory.createChecked(db, project, c1.getId()).getChange(); + String changeId = c.getKey().get(); + + // Admin's edit is not visible. + setApiUser(admin); + gApi.changes() + .id(changeId) + .edit() + .create(); + + // User's edit is visible. + setApiUser(user); + gApi.changes() + .id(changeId) + .edit() + .create(); + + assertUploadPackRefs( + "HEAD", + r1 + "1", + r1 + "meta", + r3 + "1", + r3 + "meta", + "refs/heads/master", + "refs/tags/master-tag", + "refs/users/01/1000001/edit-" + c1.getId() + "/1"); + } + + @Test + public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + try { + deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + + String changeId = c1.change().getKey().get(); + setApiUser(admin); + gApi.changes() + .id(changeId) + .edit() + .create(); + setApiUser(user); + + 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.getId() + "/1"); + } finally { + removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + } + } + + @Test + 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 c5 = br.getChange().getId(); + String r5 = changeRefPrefix(c5); + + // Only admin can see admin's draft change (5). + setApiUser(admin); + assertUploadPackRefs( + "HEAD", + r1 + "1", + r1 + "meta", + r2 + "1", + r2 + "meta", + r3 + "1", + r3 + "meta", + r4 + "1", + r4 + "meta", + r5 + "1", + r5 + "meta", + "refs/heads/branch", + "refs/heads/master", + RefNames.REFS_CONFIG, + "refs/tags/branch-tag", + "refs/tags/master-tag"); + + // user can't. + setApiUser(user); + 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", + "refs/tags/master-tag"); + } + + @Test + public void uploadPackNoSearchingChangeCacheImpl() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/*"); + + setApiUser(user); + try (Repository repo = repoManager.openRepository(project)) { + assertRefs( + repo, + new VisibleRefFilter(tagCache, notesFactory, null, repo, + projectControl(), db, true), + // Can't use stored values from the index so DB must be enabled. + false, + "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", + "refs/tags/master-tag"); + } + } + + @Test + public void uploadPackSequencesWithAccessDatabase() throws Exception { + assume().that(notesMigration.readChangeSequence()).isTrue(); + try (Repository repo = repoManager.openRepository(allProjects)) { + setApiUser(user); + assertRefs(repo, newFilter(db, repo, allProjects), true); + + allowGlobalCapabilities( + REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + try { + setApiUser(user); + assertRefs( + repo, newFilter(db, repo, allProjects), true, + "refs/sequences/changes"); + } finally { + removeGlobalCapabilities( + REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + } + } + } + + @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 { + 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()); + + if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) { + 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. + * + * @param expectedWithMeta expected refs, in order. If NoteDb is disabled by + * the configuration, any NoteDb refs (i.e. ending in "/meta") are removed + * from the expected list before comparing to the actual results. + * @throws Exception + */ + private void assertUploadPackRefs(String... expectedWithMeta) + throws Exception { + try (Repository repo = repoManager.openRepository(project)) { + assertRefs( + repo, + new VisibleRefFilter(tagCache, notesFactory, changeCache, repo, + projectControl(), new DisabledReviewDb(), true), + true, + expectedWithMeta); + } + } + + private void assertRefs(Repository repo, VisibleRefFilter filter, + boolean disableDb, String... expectedWithMeta) throws Exception { + List<String> expected = new ArrayList<>(expectedWithMeta.length); + for (String r : expectedWithMeta) { + if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) { + expected.add(r); + } + } + + AcceptanceTestRequestScope.Context ctx = null; + if (disableDb) { + ctx = disableDb(); + } + try { + Map<String, Ref> all = repo.getAllRefs(); + assertThat(filter.filter(all, false).keySet()) + .containsExactlyElementsIn(expected); + } finally { + if (disableDb) { + enableDb(ctx); + } + } + } + + 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()); + } + + private VisibleRefFilter newFilter(ReviewDb db, Repository repo, + Project.NameKey project) throws Exception { + return new VisibleRefFilter( + tagCache, notesFactory, null, repo, + projectControlFactory.controlFor(project, userProvider.get()), + db, true); + } + + private 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/SshPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java index c7da993..ede1d4f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
@@ -15,10 +15,12 @@ package com.google.gerrit.acceptance.git; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.UseSsh; import org.junit.Before; @NoHttpd +@UseSsh public class SshPushForReviewIT extends AbstractPushForReview { @Before public void selectSshUrl() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java index 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 98405d7..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,7 @@ } @Test - public void testDoNotUseFastForward() throws Exception { + public void doNotUseFastForward() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project", false); TestRepository<?> sub = createProjectWithPush("sub", false); @@ -251,7 +292,7 @@ } @Test - public void testUseFastForwardWhenNoSubmodule() throws Exception { + public void useFastForwardWhenNoSubmodule() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project", false); TestRepository<?> sub = createProjectWithPush("sub", false); @@ -273,7 +314,7 @@ } @Test - public void testSameProjectSameBranchDifferentPaths() throws Exception { + public void sameProjectSameBranchDifferentPaths() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub = createProjectWithPush("sub"); @@ -306,7 +347,7 @@ } @Test - public void testSameProjectDifferentBranchDifferentPaths() throws Exception { + public void sameProjectDifferentBranchDifferentPaths() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub = createProjectWithPush("sub"); @@ -350,7 +391,7 @@ } @Test - public void testNonSubmoduleInSameTopic() throws Exception { + public void nonSubmoduleInSameTopic() throws Exception { TestRepository<?> superRepo = createProjectWithPush("super-project"); TestRepository<?> sub = createProjectWithPush("sub"); TestRepository<?> standAlone = createProjectWithPush("standalone"); @@ -390,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"); @@ -404,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"); @@ -416,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"); @@ -440,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"); @@ -452,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"); @@ -479,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"); @@ -529,7 +585,7 @@ } @Test - public void testProjectNoSubscriptionWholeTopic() throws Exception { + public void projectNoSubscriptionWholeTopic() throws Exception { TestRepository<?> repoA = createProjectWithPush("project-a"); TestRepository<?> repoB = createProjectWithPush("project-b"); // bootstrap the dev branch @@ -581,7 +637,7 @@ } @Test - public void testTwoProjectsMultipleBranchesWholeTopic() throws Exception { + public void twoProjectsMultipleBranchesWholeTopic() throws Exception { TestRepository<?> repoA = createProjectWithPush("project-a"); TestRepository<?> repoB = createProjectWithPush("project-b"); // bootstrap the dev branch
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/VisibleRefFilterIT.java deleted file mode 100644 index fd2385b..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.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.acceptance.git; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.TruthJUnit.assume; -import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; - -import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.AcceptanceTestRequestScope; -import com.google.gerrit.acceptance.NoHttpd; -import com.google.gerrit.acceptance.PushOneCommit; -import com.google.gerrit.common.Nullable; -import com.google.gerrit.common.data.AccessSection; -import com.google.gerrit.common.data.GlobalCapability; -import com.google.gerrit.common.data.Permission; -import com.google.gerrit.extensions.api.projects.BranchInput; -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.client.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.edit.ChangeEditModifier; -import com.google.gerrit.server.git.ProjectConfig; -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.project.ProjectControl; -import com.google.gerrit.server.project.Util; -import com.google.gerrit.testutil.DisabledReviewDb; -import com.google.inject.Inject; -import com.google.inject.Provider; - -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.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@NoHttpd -public class VisibleRefFilterIT extends AbstractDaemonTest { - @Inject - private ChangeEditModifier editModifier; - - @Inject - private ProjectControl.GenericFactory projectControlFactory; - - @Inject - @Nullable - private SearchingChangeCacheImpl changeCache; - - @Inject - private TagCache tagCache; - - @Inject - private Provider<CurrentUser> userProvider; - - private AccountGroup.UUID admins; - - private Change.Id c1; - private Change.Id c2; - private String r1; - private String r2; - - @Before - public void setUp() throws Exception { - admins = groupCache.get(new AccountGroup.NameKey("Administrators")) - .getGroupUUID(); - setUpPermissions(); - setUpChanges(); - } - - private void setUpPermissions() throws Exception { - // Remove read permissions for all users besides admin. This method is - // idempotent, so is safe to call on every test setup. - ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig(); - for (AccessSection sec : pc.getAccessSections()) { - sec.removePermission(Permission.READ); - } - Util.allow(pc, Permission.READ, admins, "refs/*"); - saveProjectConfig(allProjects, pc); - } - - private static String changeRefPrefix(Change.Id id) { - String ps = new PatchSet.Id(id, 1).toRefName(); - return ps.substring(0, ps.length() - 1); - } - - private void setUpChanges() throws Exception { - gApi.projects() - .name(project.get()) - .branch("branch") - .create(new BranchInput()); - - 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); - PushOneCommit.Result br = pushFactory.create(db, admin.getIdent(), testRepo) - .to("refs/for/branch%submit"); - br.assertOkStatus(); - c2 = br.getChange().getId(); - r2 = changeRefPrefix(c2); - - try (Repository repo = repoManager.openRepository(project)) { - // master-tag -> master - RefUpdate mtu = repo.updateRef("refs/tags/master-tag"); - mtu.setExpectedOldObjectId(ObjectId.zeroId()); - mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId()); - assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW); - - // branch-tag -> branch - RefUpdate btu = repo.updateRef("refs/tags/branch-tag"); - btu.setExpectedOldObjectId(ObjectId.zeroId()); - btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId()); - assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW); - } - } - - @Test - public void allRefsVisibleNoRefsMetaConfig() 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); - Util.doNotInherit(cfg, Permission.READ, RefNames.REFS_CONFIG); - saveProjectConfig(project, cfg); - - setApiUser(user); - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "meta", - "refs/heads/branch", - "refs/heads/master", - "refs/tags/branch-tag", - "refs/tags/master-tag"); - } - - @Test - public void allRefsVisibleWithRefsMetaConfig() throws Exception { - allow(Permission.READ, REGISTERED_USERS, "refs/*"); - allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG); - - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "meta", - "refs/heads/branch", - "refs/heads/master", - RefNames.REFS_CONFIG, - "refs/tags/branch-tag", - "refs/tags/master-tag"); - } - - @Test - public void subsetOfBranchesVisibleIncludingHead() throws Exception { - allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); - deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); - - setApiUser(user); - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - "refs/heads/master", - "refs/tags/master-tag"); - } - - @Test - public void subsetOfBranchesVisibleNotIncludingHead() throws Exception { - deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); - allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); - - setApiUser(user); - assertRefs( - r2 + "1", - r2 + "meta", - "refs/heads/branch", - "refs/tags/branch-tag", - // master branch is not visible but master-tag is reachable from branch - // (since PushOneCommit always bases changes on each other). - "refs/tags/master-tag"); - } - - @Test - public void subsetOfBranchesVisibleWithEdit() 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)); - - // Admin's edit is not visible. - setApiUser(admin); - editModifier.createEdit(c, ps1); - - // User's edit is visible. - setApiUser(user); - editModifier.createEdit(c, ps1); - - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - "refs/heads/master", - "refs/tags/master-tag", - "refs/users/01/1000001/edit-" + c1.get() + "/1"); - } - - @Test - public void subsetOfRefsVisibleWithAccessDatabase() 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)); - setApiUser(admin); - editModifier.createEdit(c, ps1); - setApiUser(user); - - assertRefs( - // Change 1 is visible due to accessDatabase capability, even though - // refs/heads/master is not. - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "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"); - } finally { - removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); - } - } - - @Test - public void draftRefs() 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); - - // Only admin can see admin's draft change. - setApiUser(admin); - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "meta", - r3 + "1", - r3 + "meta", - "refs/heads/branch", - "refs/heads/master", - RefNames.REFS_CONFIG, - "refs/tags/branch-tag", - "refs/tags/master-tag"); - - // user can't. - setApiUser(user); - assertRefs( - "HEAD", - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "meta", - "refs/heads/branch", - "refs/heads/master", - "refs/tags/branch-tag", - "refs/tags/master-tag"); - } - - @Test - public void noSearchingChangeCacheImpl() throws Exception { - allow(Permission.READ, REGISTERED_USERS, "refs/heads/*"); - - setApiUser(user); - try (Repository repo = repoManager.openRepository(project)) { - assertRefs( - repo, - new VisibleRefFilter(tagCache, notesFactory, null, repo, - projectControl(), db, true), - // Can't use stored values from the index so DB must be enabled. - false, - "HEAD", - r1 + "1", - r1 + "meta", - r2 + "1", - r2 + "meta", - "refs/heads/branch", - "refs/heads/master", - "refs/tags/branch-tag", - "refs/tags/master-tag"); - } - } - - @Test - public void sequencesWithAccessDatabase() throws Exception { - assume().that(notesMigration.readChangeSequence()).isTrue(); - try (Repository repo = repoManager.openRepository(allProjects)) { - setApiUser(user); - assertRefs(repo, newFilter(db, repo, allProjects), true); - - allowGlobalCapabilities( - REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); - try { - setApiUser(user); - assertRefs( - repo, newFilter(db, repo, allProjects), true, - "refs/sequences/changes"); - } finally { - removeGlobalCapabilities( - REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); - } - } - } - - /** - * Assert that refs seen by a non-admin user match expected. - * - * @param expectedWithMeta expected refs, in order. If NoteDb is disabled by - * the configuration, any NoteDb refs (i.e. ending in "/meta") are removed - * from the expected list before comparing to the actual results. - * @throws Exception - */ - private void assertRefs(String... expectedWithMeta) throws Exception { - try (Repository repo = repoManager.openRepository(project)) { - assertRefs( - repo, - new VisibleRefFilter(tagCache, notesFactory, changeCache, repo, - projectControl(), new DisabledReviewDb(), true), - true, - expectedWithMeta); - } - } - - private void assertRefs(Repository repo, VisibleRefFilter filter, - boolean disableDb, String... expectedWithMeta) throws Exception { - List<String> expected = new ArrayList<>(expectedWithMeta.length); - for (String r : expectedWithMeta) { - if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) { - expected.add(r); - } - } - - AcceptanceTestRequestScope.Context ctx = null; - if (disableDb) { - ctx = disableDb(); - } - try { - Map<String, Ref> all = repo.getAllRefs(); - assertThat(filter.filter(all, false).keySet()) - .containsExactlyElementsIn(expected); - } finally { - if (disableDb) { - enableDb(ctx); - } - } - } - - private ProjectControl projectControl() throws Exception { - return projectControlFactory.controlFor(project, userProvider.get()); - } - - private VisibleRefFilter newFilter(ReviewDb db, Repository repo, - Project.NameKey project) throws Exception { - return new VisibleRefFilter( - tagCache, notesFactory, null, repo, - projectControlFactory.controlFor(project, userProvider.get()), - db, true); - } -}
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 deleted file mode 100644 index ff167ac..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUCK +++ /dev/null
@@ -1,8 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK deleted file mode 100644 index 76c918b..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK +++ /dev/null
@@ -1,23 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'rest_account', - srcs = glob(['*IT.java']), - deps = [':util'], - labels = ['rest'], -) - -java_library( - name = 'util', - srcs = [ - 'AccountAssert.java', - 'CapabilityInfo.java', - ], - deps = [ - '//gerrit-acceptance-tests:lib', - '//gerrit-reviewdb:server', - '//lib:gwtorm', - '//lib:junit', - ], - visibility = ['//gerrit-acceptance-tests/...'], -)
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/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java new file mode 100644 index 0000000..c94fe1f --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -0,0 +1,120 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.account; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.acceptance.Sandboxed; +import com.google.gerrit.extensions.common.AccountExternalIdInfo; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gson.reflect.TypeToken; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Sandboxed +public class ExternalIdIT extends AbstractDaemonTest { + @Test + public void getExternalIDs() throws Exception { + Collection<AccountExternalId> expectedIds = + accountCache.get(user.getId()).getExternalIds(); + + List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>(); + for (AccountExternalId id : expectedIds) { + id.setCanDelete(!id.getExternalId().equals("username:" + user.username)); + id.setTrusted(true); + expectedIdInfos.add(toInfo(id)); + } + + RestResponse response = userRestSession.get("/accounts/self/external.ids"); + response.assertOK(); + + List<AccountExternalIdInfo> results = + newGson().fromJson(response.getReader(), + new TypeToken<List<AccountExternalIdInfo>>() {}.getType()); + + Collections.sort(expectedIdInfos); + Collections.sort(results); + assertThat(results).containsExactlyElementsIn(expectedIdInfos); + } + + @Test + public void deleteExternalIDs() throws Exception { + setApiUser(user); + List<AccountExternalIdInfo> externalIds = + gApi.accounts().self().getExternalIds(); + + List<String> toDelete = new ArrayList<>(); + List<AccountExternalIdInfo> expectedIds = new ArrayList<>(); + for (AccountExternalIdInfo id : externalIds) { + if (id.canDelete != null && id.canDelete) { + toDelete.add(id.identity); + continue; + } + expectedIds.add(id); + } + + assertThat(toDelete).hasSize(1); + + RestResponse response = userRestSession.post( + "/accounts/self/external.ids:delete", toDelete); + response.assertNoContent(); + List<AccountExternalIdInfo> results = + gApi.accounts().self().getExternalIds(); + // The external ID in WebSession will not be set for tests, resulting that + // "mailto:user@example.com" can be deleted while "username:user" can't. + assertThat(results).hasSize(1); + assertThat(results).containsExactlyElementsIn(expectedIds); + } + + @Test + public void deleteExternalIDs_Conflict() throws Exception { + List<String> toDelete = new ArrayList<>(); + String externalIdStr = "username:" + user.username; + toDelete.add(externalIdStr); + RestResponse response = userRestSession.post( + "/accounts/self/external.ids:delete", toDelete); + response.assertConflict(); + assertThat(response.getEntityContent()).isEqualTo( + String.format("External id %s cannot be deleted", externalIdStr)); + } + + @Test + public void deleteExternalIDs_UnprocessableEntity() throws Exception { + List<String> toDelete = new ArrayList<>(); + String externalIdStr = "mailto:user@domain.com"; + toDelete.add(externalIdStr); + RestResponse response = userRestSession.post( + "/accounts/self/external.ids:delete", toDelete); + response.assertUnprocessableEntity(); + assertThat(response.getEntityContent()).isEqualTo( + String.format("External id %s does not exist", externalIdStr)); + } + + private static AccountExternalIdInfo toInfo(AccountExternalId id) { + AccountExternalIdInfo info = new AccountExternalIdInfo(); + info.identity = id.getExternalId(); + info.emailAddress = id.getEmailAddress(); + info.trusted = id.isTrusted() ? true : null; + info.canDelete = id.canDelete() ? true : null; + return info; + } +}
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..5772ec9 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -0,0 +1,655 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF 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.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/account/WatchedProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java index 32cfc9b..36a95f1 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/WatchedProjectsIT.java
@@ -25,7 +25,6 @@ import org.junit.Test; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; public class WatchedProjectsIT extends AbstractDaemonTest { @@ -64,7 +63,7 @@ String projectName1 = createProject(NEW_PROJECT_NAME).get(); String projectName2 = createProject(NEW_PROJECT_NAME + "2").get(); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName1; @@ -98,7 +97,7 @@ public void setConflictingWatches() throws Exception { String projectName = createProject(NEW_PROJECT_NAME).get(); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName; @@ -122,7 +121,7 @@ public void setAndGetEmptyWatch() throws Exception { String projectName = createProject(NEW_PROJECT_NAME).get(); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName; @@ -157,7 +156,7 @@ // Let another user watch a project setApiUser(admin); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName; @@ -183,7 +182,7 @@ // Let another user watch a project setApiUser(admin); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName; @@ -214,7 +213,7 @@ throws Exception { String projectName = project.get(); - List<ProjectWatchInfo> projectsToWatch = new LinkedList<>(); + List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = projectName;
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..2a39ffd 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,17 +20,23 @@ 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; +import com.google.common.collect.Sets; import com.google.gerrit.acceptance.AbstractDaemonTest; 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.TimeUtil; +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 +45,11 @@ 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.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; 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; @@ -51,16 +59,25 @@ 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.ApprovalsUtil; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.change.Submit; +import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.git.validators.OnSubmitValidationListener; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.project.Util; +import com.google.gerrit.server.validators.ValidationException; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.TestTimeUtil; +import com.google.gwtorm.server.OrmException; 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 +85,21 @@ 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.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; @NoHttpd public abstract class AbstractSubmit extends AbstractDaemonTest { @@ -89,6 +114,16 @@ @Inject private Submit submitHandler; + @Inject + private IdentifiedUser.GenericFactory userFactory; + + @Inject + private BatchUpdate.Factory updateFactory; + + @Inject + private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners; + private RegistrationHandle onSubmitValidatorHandle; + private String systemTimeZone; @Before @@ -108,14 +143,243 @@ db.close(); } + @After + public void removeOnSubmitValidator(){ + if (onSubmitValidatorHandle != null){ + onSubmitValidatorHandle.remove(); + } + } + protected abstract SubmitType getSubmitType(); @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 +489,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 @@ -343,21 +603,190 @@ assertThat(log).contains(mergeReview.getCommit()); } + @Test + public void submitChangeWithCommitThatWasAlreadyMerged() throws Exception { + // create and submit a change + PushOneCommit.Result change = createChange(); + submit(change.getChangeId()); + RevCommit headAfterSubmit = getRemoteHead(); + + // set the status of the change back to NEW to simulate a failed submit that + // merged the commit but failed to update the change status + setChangeStatusToNew(change); + + // submitting the change again should detect that the commit was already + // merged and just fix the change status to be MERGED + submit(change.getChangeId()); + assertThat(getRemoteHead()).isEqualTo(headAfterSubmit); + } + + @Test + public void submitChangesWithCommitsThatWereAlreadyMerged() throws Exception { + // create and submit 2 changes + PushOneCommit.Result change1 = createChange(); + PushOneCommit.Result change2 = createChange(); + approve(change1.getChangeId()); + if (getSubmitType() == SubmitType.CHERRY_PICK) { + submit(change1.getChangeId()); + } + submit(change2.getChangeId()); + assertMerged(change1.getChangeId()); + RevCommit headAfterSubmit = getRemoteHead(); + + // set the status of the changes back to NEW to simulate a failed submit that + // merged the commits but failed to update the change status + setChangeStatusToNew(change1, change2); + + // submitting the changes again should detect that the commits were already + // merged and just fix the change status to be MERGED + submit(change1.getChangeId()); + submit(change2.getChangeId()); + assertThat(getRemoteHead()).isEqualTo(headAfterSubmit); + } + + @Test + public void submitTopicWithCommitsThatWereAlreadyMerged() throws Exception { + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + + // create and submit 2 changes with the same topic + String topic = name("topic"); + PushOneCommit.Result change1 = createChange("refs/for/master/" + topic); + PushOneCommit.Result change2 = createChange("refs/for/master/" + topic); + approve(change1.getChangeId()); + submit(change2.getChangeId()); + assertMerged(change1.getChangeId()); + RevCommit headAfterSubmit = getRemoteHead(); + + // set the status of the second change back to NEW to simulate a failed + // submit that merged the commits but failed to update the change status of + // some changes in the topic + setChangeStatusToNew(change2); + + // submitting the topic again should detect that the commits were already + // merged and just fix the change status to be MERGED + submit(change2.getChangeId()); + assertThat(getRemoteHead()).isEqualTo(headAfterSubmit); + } + + @Test + public void submitWithValidation() throws Exception { + AtomicBoolean called = new AtomicBoolean(false); + this.addOnSubmitValidationListener(new OnSubmitValidationListener() { + @Override + public void preBranchUpdate(Arguments args) throws ValidationException { + called.set(true); + HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet()); + assertThat(refs).contains("refs/heads/master"); + refs.remove("refs/heads/master"); + if (!refs.isEmpty()){ + // Some submit strategies need to insert new patchset. + assertThat(refs).hasSize(1); + assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES); + } + } + }); + + PushOneCommit.Result change = createChange(); + approve(change.getChangeId()); + submit(change.getChangeId()); + assertThat(called.get()).isTrue(); + } + + @Test + public void submitWithValidationMultiRepo() throws Exception { + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + String topic = "test-topic"; + + // Create test projects + TestRepository<?> repoA = + createProjectWithPush("project-a", null, getSubmitType()); + TestRepository<?> repoB = + createProjectWithPush("project-b", null, getSubmitType()); + + // Create changes on project-a + PushOneCommit.Result change1 = + createChange(repoA, "master", "Change 1", "a.txt", "content", topic); + PushOneCommit.Result change2 = + createChange(repoA, "master", "Change 2", "b.txt", "content", topic); + + // Create changes on project-b + PushOneCommit.Result change3 = + createChange(repoB, "master", "Change 3", "a.txt", "content", topic); + PushOneCommit.Result change4 = + createChange(repoB, "master", "Change 4", "b.txt", "content", topic); + + List<PushOneCommit.Result> changes = + Lists.newArrayList(change1, change2, change3, change4); + for (PushOneCommit.Result change : changes) { + approve(change.getChangeId()); + } + + // Construct validator which will throw on a second call. + // Since there are 2 repos, first submit attempt will fail, the second will + // succeed. + List<String> projectsCalled = new ArrayList<>(4); + this.addOnSubmitValidationListener(new OnSubmitValidationListener() { + @Override + public void preBranchUpdate(Arguments args) throws ValidationException { + assertThat(args.getCommands().keySet()).contains("refs/heads/master"); + try (RevWalk rw = args.newRevWalk()) { + rw.parseBody(rw.parseCommit( + args.getCommands().get("refs/heads/master").getNewId())); + } catch (IOException e) { + assertThat(e).isNull(); + } + projectsCalled.add(args.getProject().get()); + if (projectsCalled.size() == 2) { + throw new ValidationException("time to fail"); + } + } + }); + submitWithConflict(change4.getChangeId(), "time to fail"); + assertThat(projectsCalled).containsExactly(name("project-a"), + name("project-b")); + for (PushOneCommit.Result change : changes) { + change.assertChange(Change.Status.NEW, name(topic), admin); + } + + submit(change4.getChangeId()); + assertThat(projectsCalled).containsExactly(name("project-a"), + name("project-b"), name("project-a"), name("project-b")); + for (PushOneCommit.Result change : changes) { + change.assertChange(Change.Status.MERGED, name(topic), admin); + } + } + + private void setChangeStatusToNew(PushOneCommit.Result... changes) + throws Exception { + for (PushOneCommit.Result change : changes) { + try (BatchUpdate bu = updateFactory.create(db, project, + userFactory.create(admin.id), TimeUtil.nowTs())) { + bu.addOp(change.getChange().getId(), new BatchUpdate.Op() { + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + ctx.getChange().setStatus(Change.Status.NEW); + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .setStatus(Change.Status.NEW); + return true; + } + }); + bu.execute(); + } + } + } + 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 +848,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); @@ -460,11 +898,17 @@ } protected void assertApproved(String changeId) throws Exception { + assertApproved(changeId, admin); + } + + protected void assertApproved(String changeId, TestAccount user) + throws Exception { ChangeInfo c = get(changeId, DETAILED_LABELS); LabelInfo cr = c.labels.get("Code-Review"); assertThat(cr.all).hasSize(1); assertThat(cr.all.get(0).value).isEqualTo(2); - assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(admin.getId()); + assertThat(new Account.Id(cr.all.get(0)._accountId)) + .isEqualTo(user.getId()); } protected void assertMerged(String changeId) throws RestApiException { @@ -484,14 +928,19 @@ protected void assertSubmitter(String changeId, int psId) throws Exception { + assertSubmitter(changeId, psId, admin); + } + + protected void assertSubmitter(String changeId, int psId, TestAccount user) + throws Exception { Change c = getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change(); ChangeNotes cn = notesFactory.createChecked(db, c); - PatchSetApproval submitter = approvalsUtil.getSubmitter( - db, cn, new PatchSet.Id(cn.getChangeId(), psId)); + PatchSetApproval submitter = approvalsUtil.getSubmitter(db, cn, + new PatchSet.Id(cn.getChangeId(), psId)); assertThat(submitter).isNotNull(); assertThat(submitter.isLegacySubmit()).isTrue(); - assertThat(submitter.getAccountId()).isEqualTo(admin.getId()); + assertThat(submitter.getAccountId()).isEqualTo(user.getId()); } protected void assertNoSubmitter(String changeId, int psId) @@ -541,6 +990,11 @@ return getRemoteLog(project, "master"); } + protected void addOnSubmitValidationListener(OnSubmitValidationListener listener){ + assertThat(onSubmitValidatorHandle).isNull(); + onSubmitValidatorHandle = onSubmitValidationListeners.add(listener); + } + private String getLatestDiff(Repository repo) throws Exception { ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}"); ObjectId newTreeId = repo.resolve("HEAD^{tree}");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java index 741864a..e14f153 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -18,9 +18,11 @@ import static com.google.common.truth.TruthJUnit.assume; import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.extensions.api.changes.SubmitInput; +import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.common.ChangeInfo; @@ -33,6 +35,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.RefSpec; import org.junit.Test; public abstract class AbstractSubmitByMerge extends AbstractSubmit { @@ -180,4 +183,81 @@ .isEqualTo(tip); } } + + @Test + public void submitWithCommitAndItsMergeCommitTogether() throws Exception { + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + + RevCommit initialHead = getRemoteHead(); + + // Create a stable branch and bootstrap it. + gApi.projects() + .name(project.get()) + .branch("stable") + .create(new BranchInput()); + PushOneCommit push = pushFactory.create( + db, user.getIdent(), testRepo, "initial commit", "a.txt", "a"); + PushOneCommit.Result change = push.to("refs/heads/stable"); + + RevCommit stable = getRemoteHead(project, "stable"); + RevCommit master = getRemoteHead(project, "master"); + + assertThat(master).isEqualTo(initialHead); + assertThat(stable).isEqualTo(change.getCommit()); + + testRepo.git().fetch().call(); + testRepo.git() + .branchCreate() + .setName("stable") + .setStartPoint(stable) + .call(); + testRepo.git() + .branchCreate() + .setName("master") + .setStartPoint(master) + .call(); + + // Create a fix in stable branch. + testRepo.reset(stable); + RevCommit fix = testRepo.commit() + .parent(stable) + .message("small fix") + .add("b.txt", "b") + .insertChangeId() + .create(); + testRepo.branch("refs/heads/stable").update(fix); + testRepo.git() + .push() + .setRefSpecs( + new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic"))) + .call(); + + // Merge the fix into master. + testRepo.reset(master); + RevCommit merge = testRepo.commit() + .parent(master) + .parent(fix) + .message("Merge stable into master") + .insertChangeId() + .create(); + testRepo.branch("refs/heads/master").update(merge); + testRepo.git().push() + .setRefSpecs( + new RefSpec("refs/heads/master:refs/for/master/" + name("topic"))) + .call(); + + // Submit together. + String fixId = GitUtil.getChangeId(testRepo, fix).get(); + String mergeId = GitUtil.getChangeId(testRepo, merge).get(); + approve(fixId); + approve(mergeId); + submit(mergeId); + assertMerged(fixId); + assertMerged(mergeId); + testRepo.git().fetch().call(); + RevWalk rw = testRepo.getRevWalk(); + master = rw.parseCommit(getRemoteHead(project, "master")); + assertThat(rw.isMergedInto(merge, master)).isTrue(); + assertThat(rw.isMergedInto(fix, master)).isTrue(); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java new file mode 100644 index 0000000..3cb8040 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -0,0 +1,442 @@ +// 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 static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.changes.SubmitInput; +import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.SubmitType; +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 com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.project.Util; + +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 { + submitWithRebase(admin); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitWithRebaseWithoutAddPatchSetPermission() throws Exception { + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + Util.block(cfg, Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*"); + Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*"); + Util.allow(cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, + REGISTERED_USERS, "refs/heads/*"); + saveProjectConfig(project, cfg); + + submitWithRebase(user); + } + + private void submitWithRebase(TestAccount submitter) throws Exception { + setApiUser(submitter); + 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(), submitter); + assertCurrentRevision(change2.getChangeId(), 2, headAfterSecondSubmit); + assertSubmitter(change2.getChangeId(), 1, submitter); + assertSubmitter(change2.getChangeId(), 2, submitter); + assertPersonEquals(admin.getIdent(), + headAfterSecondSubmit.getAuthorIdent()); + assertPersonEquals(submitter.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()); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitFastForwardIdenticalTree() throws Exception { + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "a"); + PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "a"); + + assertThat(change1.getCommit().getTree()) + .isEqualTo(change2.getCommit().getTree()); + + // for rebase if necessary, otherwise, the manual rebase of change2 will + // fail since change1 would be merged as fast forward + testRepo.reset(initialHead); + PushOneCommit.Result change0 = createChange("Change 0", "b.txt", "b"); + submit(change0.getChangeId()); + RevCommit headAfterChange0 = getRemoteHead(); + assertThat(headAfterChange0.getShortMessage()).isEqualTo("Change 0"); + + submit(change1.getChangeId()); + RevCommit headAfterChange1 = getRemoteHead(); + assertThat(headAfterChange1.getShortMessage()).isEqualTo("Change 1"); + assertThat(headAfterChange0).isEqualTo(headAfterChange1.getParent(0)); + + // Do manual rebase first. + gApi.changes().id(change2.getChangeId()).current().rebase(); + submit(change2.getChangeId()); + RevCommit headAfterChange2 = getRemoteHead(); + assertThat(headAfterChange2.getShortMessage()).isEqualTo("Change 2"); + assertThat(headAfterChange1).isEqualTo(headAfterChange2.getParent(0)); + + ChangeInfo info2 = get(change2.getChangeId()); + assertThat(info2.status).isEqualTo(ChangeStatus.MERGED); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitChainOneByOne() throws Exception { + PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", + "content 1"); + PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", + "content 2"); + submit(change1.getChangeId()); + submit(change2.getChangeId()); + } + + @Test + @TestProjectInput(useContentMerge = InheritableBoolean.TRUE) + public void submitChainOneByOneManualRebase() throws Exception { + RevCommit initialHead = getRemoteHead(); + PushOneCommit.Result change1 = createChange("subject 1", "fileName 1", + "content 1"); + PushOneCommit.Result change2 = createChange("subject 2", "fileName 2", + "content 2"); + + // for rebase if necessary, otherwise, the manual rebase of change2 will + // fail since change1 would be merged as fast forward + testRepo.reset(initialHead); + PushOneCommit.Result change = createChange(); + submit(change.getChangeId()); + + submit(change1.getChangeId()); + // Do manual rebase first. + gApi.changes().id(change2.getChangeId()).current().rebase(); + submit(change2.getChangeId()); + } +}
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 deleted file mode 100644 index 04e71eb..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK +++ /dev/null
@@ -1,38 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -SUBMIT_UTIL_SRCS = [ - 'AbstractSubmit.java', - 'AbstractSubmitByMerge.java', -] - -SUBMIT_TESTS = glob(['Submit*IT.java']) -OTHER_TESTS = glob(['*IT.java'], excludes = SUBMIT_TESTS) - -acceptance_tests( - group = 'rest_change_other', - srcs = OTHER_TESTS, - deps = [ - ':submit_util', - '//gerrit-server:server', - '//lib/guice:guice', - '//lib/joda:joda-time', - ], - labels = ['rest'], -) - -acceptance_tests( - group = 'rest_change_submit', - srcs = SUBMIT_TESTS, - deps = [ - ':submit_util', - ], - labels = ['rest'], -) - -java_library( - name = 'submit_util', - srcs = SUBMIT_UTIL_SRCS, - deps = [ - '//gerrit-acceptance-tests:lib', - ], -)
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/ChangeIncludedInIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java new file mode 100644 index 0000000..c37501d --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
@@ -0,0 +1,64 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.change; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.projects.TagInput; +import com.google.gerrit.reviewdb.client.Branch; + +import org.junit.Test; + +@NoHttpd +public class ChangeIncludedInIT extends AbstractDaemonTest { + + @Test + public void includedInOpenChange() throws Exception { + Result result = createChange(); + assertThat(gApi.changes().id(result.getChangeId()).includedIn().branches) + .isEmpty(); + assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags) + .isEmpty(); + } + + @Test + public void includedInMergedChange() throws Exception { + Result result = createChange(); + gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()) + .review(ReviewInput.approve()); + gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()) + .submit(); + + assertThat(gApi.changes().id(result.getChangeId()).includedIn().branches) + .containsExactly("master"); + assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags) + .isEmpty(); + + grantTagPermissions(); + gApi.projects().name(project.get()).tag("test-tag").create(new TagInput()); + + assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags) + .containsExactly("test-tag"); + + createBranch(new Branch.NameKey(project.get(), "test-branch")); + + assertThat(gApi.changes().id(result.getChangeId()).includedIn().branches) + .containsExactly("master", "test-branch"); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java index 875725f..91fc3e7 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -84,7 +84,7 @@ AccessSection s = config.getAccessSection("refs/heads/*", true); Permission p = s.getPermission(LABEL + "Code-Review", true); PermissionRule rule = new PermissionRule(config - .resolve(SystemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER))); + .resolve(systemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER))); rule.setMin(-2); rule.setMax(+2); p.add(rule);
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..7c84d6a 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 @@ -137,7 +141,7 @@ assertThat(m.rcpt()).containsExactly(user.emailAddress); if (notesMigration.readChanges()) { assertThat(m.body()) - .contains(admin.fullName + " has uploaded a new change for review."); + .contains(admin.fullName + " has uploaded this change for review."); } else { assertThat(m.body()).contains("Hello " + user.fullName + ",\n"); assertThat(m.body()).contains("I'd like you to do a code review."); @@ -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/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java index d696af4..5db627b 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -28,6 +28,7 @@ import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.extensions.api.changes.ChangeApi; import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; @@ -42,6 +43,7 @@ import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.server.git.ChangeAlreadyMergedException; import com.google.gerrit.testutil.ConfigSuite; +import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.TestTimeUtil; import org.eclipse.jgit.lib.Config; @@ -55,6 +57,8 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.util.List; + public class CreateChangeIT extends AbstractDaemonTest { @ConfigSuite.Config public static Config allowDraftsDisabled() { @@ -101,6 +105,30 @@ } @Test + public void notificationsOnChangeCreation() throws Exception { + setApiUser(user); + watch(project.get(), null); + + // check that watcher is notified + setApiUser(admin); + assertCreateSucceeds(newChangeInput(ChangeStatus.NEW)); + + 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 this change for review."); + + // check that watcher is not notified if notify=NONE + sender.clear(); + ChangeInput input = newChangeInput(ChangeStatus.NEW); + input.notify = NotifyHandling.NONE; + assertCreateSucceeds(input); + assertThat(sender.getMessages()).isEmpty(); + } + + @Test public void createNewChangeSignedOffByFooter() throws Exception { assume().that(isAllowDrafts()).isTrue(); setSignedOffByFooter(); @@ -253,6 +281,7 @@ private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception { ChangeInfo out = gApi.changes().create(in).get(); + assertThat(out.project).isEqualTo(in.project); assertThat(out.branch).isEqualTo(in.branch); assertThat(out.subject).isEqualTo(in.subject); assertThat(out.topic).isEqualTo(in.topic);
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/DeleteVoteIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java new file mode 100644 index 0000000..fb25da1 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -0,0 +1,109 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.ChangeMessageInfo; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.testutil.FakeEmailSender; +import com.google.gson.reflect.TypeToken; + +import org.junit.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DeleteVoteIT extends AbstractDaemonTest { + @Test + public void deleteVoteOnChange() throws Exception { + deleteVote(false); + } + + @Test + public void deleteVoteOnRevision() throws Exception { + deleteVote(true); + } + + private void deleteVote(boolean onRevisionLevel) throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + PushOneCommit.Result r2 = amendChange(r.getChangeId()); + + setApiUser(user); + recommend(r.getChangeId()); + + sender.clear(); + String endPoint = "/changes/" + r.getChangeId() + + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "") + + "/reviewers/" + user.getId().toString() + + "/votes/Code-Review"; + + RestResponse response = adminRestSession.delete(endPoint); + response.assertNoContent(); + + List<FakeEmailSender.Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + FakeEmailSender.Message msg = messages.get(0); + assertThat(msg.rcpt()).containsExactly(user.emailAddress); + assertThat(msg.body()).contains( + admin.fullName + " has removed a vote on this change.\n"); + assertThat(msg.body()).contains("Removed Code-Review+1 by " + + user.fullName + " <" + user.email + ">" + "\n"); + + endPoint = "/changes/" + r.getChangeId() + + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "") + + "/reviewers/" + user.getId().toString() + + "/votes"; + + response = adminRestSession.get(endPoint); + response.assertOK(); + + Map<String, Short> m = newGson().fromJson(response.getReader(), + new TypeToken<Map<String, Short>>() {}.getType()); + + assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0)); + + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + + ChangeMessageInfo message = Iterables.getLast(c.messages); + assertThat(message.author._accountId).isEqualTo(admin.getId().get()); + assertThat(message.message).isEqualTo( + "Removed Code-Review+1 by User <user@example.com>\n"); + assertThat(getReviewers(c.reviewers.get(REVIEWER))) + .containsExactlyElementsIn( + ImmutableSet.of(admin.getId(), user.getId())); + } + + private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) { + return Iterables.transform(r, a -> new Account.Id(a._accountId)); + } +}
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/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java index b66358f..19563b8 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -33,7 +33,6 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.Util; import org.eclipse.jgit.junit.TestRepository; @@ -166,7 +165,7 @@ new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch"); createBranch(newBranch); block(Permission.PUSH, - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), "refs/for/" + newBranch.get()); exception.expect(AuthException.class); exception.expectMessage("Move not permitted"); @@ -181,7 +180,7 @@ new Branch.NameKey(r.getChange().change().getProject(), "moveTest"); createBranch(newBranch); block(Permission.ABANDON, - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), r.getChange().change().getDest().get()); setApiUser(user); exception.expect(AuthException.class); @@ -228,7 +227,7 @@ LabelType patchSetLock = Util.patchSetLock(); cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock); AccountGroup.UUID registeredUsers = - SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers, "refs/heads/*"); saveProjectConfig(cfg);
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..03921c3 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.ArrayList; 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 ArrayList<>(); + 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..bf3f05c 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 @@ -212,7 +219,7 @@ assertThat(reviewers).hasSize(1); reviewers = suggestReviewers(changeId, "first1 last2"); - assertThat(reviewers).hasSize(0); + assertThat(reviewers).isEmpty(); reviewers = suggestReviewers(changeId, name("user")); assertThat(reviewers).hasSize(6); @@ -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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK deleted file mode 100644 index d65b84a..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'rest_config', - srcs = glob(['*IT.java']), - labels = ['rest'] -)
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/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java index 13f7070..b4bfed6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -33,15 +33,14 @@ @Test public void flushAll() throws Exception { - RestResponse r = adminRestSession.get("/config/server/caches/project_list"); + RestResponse r = adminRestSession.getOK("/config/server/caches/project_list"); CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long) 0); - r = adminRestSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL)); - r.assertOK(); + r = adminRestSession.postOK("/config/server/caches/", new PostCaches.Input(FLUSH_ALL)); r.consume(); - r = adminRestSession.get("/config/server/caches/project_list"); + r = adminRestSession.getOK("/config/server/caches/project_list"); cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isNull(); } @@ -62,24 +61,23 @@ @Test public void flush() throws Exception { - RestResponse r = adminRestSession.get("/config/server/caches/project_list"); + RestResponse r = adminRestSession.getOK("/config/server/caches/project_list"); CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long)0); - r = adminRestSession.get("/config/server/caches/projects"); + r = adminRestSession.getOK("/config/server/caches/projects"); cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long)1); - r = adminRestSession.post("/config/server/caches/", + r = adminRestSession.postOK("/config/server/caches/", new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list"))); - r.assertOK(); r.consume(); - r = adminRestSession.get("/config/server/caches/project_list"); + r = adminRestSession.getOK("/config/server/caches/project_list"); cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isNull(); - r = adminRestSession.get("/config/server/caches/projects"); + r = adminRestSession.getOK("/config/server/caches/projects"); cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long)1); } @@ -102,7 +100,7 @@ @Test public void flush_UnprocessableEntity() throws Exception { - RestResponse r = adminRestSession.get("/config/server/caches/projects"); + RestResponse r = adminRestSession.getOK("/config/server/caches/projects"); CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long)0); @@ -111,7 +109,7 @@ r.assertUnprocessableEntity(); r.consume(); - r = adminRestSession.get("/config/server/caches/projects"); + r = adminRestSession.getOK("/config/server/caches/projects"); cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class); assertThat(cacheInfo.entries.mem).isGreaterThan((long)0); } @@ -121,9 +119,8 @@ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES); try { - RestResponse r = userRestSession.post("/config/server/caches/", + RestResponse r = userRestSession.postOK("/config/server/caches/", new PostCaches.Input(FLUSH, Arrays.asList("projects"))); - r.assertOK(); r.consume(); userRestSession
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..63f4919 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,67 @@ 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.acceptance.UseSsh; +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 +107,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,12 +124,13 @@ // 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 + @UseSsh @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true") public void serverConfigWithPlugin() throws Exception { Path plugins = tempSiteDir.newFolder("plugins").toPath(); @@ -134,7 +138,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 +146,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 +193,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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK deleted file mode 100644 index 1947148..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK +++ /dev/null
@@ -1,8 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'rest_group', - srcs = glob(['*IT.java']), - labels = ['rest'] -) -
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/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java deleted file mode 100644 index 31e7382..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java +++ /dev/null
@@ -1,75 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.acceptance.rest.group; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.extensions.api.groups.GroupInput; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.reviewdb.client.AccountGroup; - -import org.junit.Test; - -public class CreateGroupIT extends AbstractDaemonTest { - - @Test - public void createGroup() throws Exception { - GroupInput in = new GroupInput(); - in.name = name("group"); - gApi.groups().create(in); - AccountGroup accountGroup = - groupCache.get(new AccountGroup.NameKey(in.name)); - assertThat(accountGroup).isNotNull(); - assertThat(accountGroup.getName()).isEqualTo(in.name); - } - - @Test - public void createGroupAlreadyExists() throws Exception { - GroupInput in = new GroupInput(); - in.name = name("group"); - gApi.groups().create(in); - assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull(); - - exception.expect(ResourceConflictException.class); - exception.expectMessage("group '" + in.name + "' already exists"); - gApi.groups().create(in); - } - - @Test - public void createGroupWithDifferentCase() throws Exception { - GroupInput in = new GroupInput(); - in.name = name("group"); - gApi.groups().create(in); - assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull(); - - GroupInput inLowerCase = new GroupInput(); - inLowerCase.name = in.name.toUpperCase(); - gApi.groups().create(inLowerCase); - assertThat(groupCache.get(new AccountGroup.NameKey(inLowerCase.name))) - .isNotNull(); - } - - @Test - public void createSystemGroupWithDifferentCase() throws Exception { - String registeredUsers = "Registered Users"; - GroupInput in = new GroupInput(); - in.name = registeredUsers.toUpperCase(); - - exception.expect(ResourceConflictException.class); - exception.expectMessage("group '" + registeredUsers + "' already exists"); - gApi.groups().create(in); - } -}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsIT.java new file mode 100644 index 0000000..b3b0b27 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsIT.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package com.google.gerrit.acceptance.rest.group; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.RestResponse; + +import org.junit.Test; + +public class GroupsIT extends AbstractDaemonTest { + @Test + public void invalidQueryOptions() throws Exception { + RestResponse r = adminRestSession.put("/groups/?query=foo&query2=bar"); + r.assertBadRequest(); + assertThat(r.getEntityContent()) + .isEqualTo("\"query\" and \"query2\" options are mutually exclusive"); + } +}
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 deleted file mode 100644 index d53e69a..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK +++ /dev/null
@@ -1,37 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'rest_project', - srcs = glob(['*IT.java']), - deps = [ - ':branch', - ':project', - ], - labels = ['rest'], -) - -java_library( - name = 'branch', - srcs = [ - 'BranchAssert.java', - ], - deps = [ - '//lib:truth', - '//gerrit-extension-api:api', - '//gerrit-server:server', - ], -) - -java_library( - 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/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/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java deleted file mode 100644 index c860bf0..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java +++ /dev/null
@@ -1,65 +0,0 @@ -// Copyright (C) 2013 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.acceptance.rest.project; - -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 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 static void assertRefNames(Iterable<String> expectedRefs, - Iterable<BranchInfo> actualBranches) { - Iterable<String> actualNames = refs(actualBranches); - assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder(); - } - - public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) { - assertThat(actual.ref).isEqualTo(expected.ref); - if (expected.revision != null) { - assertThat(actual.revision).named("revision of " + actual.ref) - .isEqualTo(expected.revision); - } - assertThat(toBoolean(actual.canDelete)).named("can delete " + actual.ref) - .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 boolean toBoolean(Boolean b) { - if (b == null) { - return false; - } - return b.booleanValue(); - } -}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java new file mode 100644 index 0000000..dac922e --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java
@@ -0,0 +1,72 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.extensions.api.changes.IncludedInInfo; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.projects.TagInput; +import com.google.gerrit.reviewdb.client.Branch; + +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; + +public class CommitIncludedInIT extends AbstractDaemonTest { + @Test + public void includedInOpenChange() throws Exception { + Result result = createChange(); + assertThat(getIncludedIn(result.getCommit().getId()).branches).isEmpty(); + assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty(); + } + + @Test + public void includedInMergedChange() throws Exception { + Result result = createChange(); + gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()) + .review(ReviewInput.approve()); + gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()) + .submit(); + + assertThat(getIncludedIn(result.getCommit().getId()).branches) + .containsExactly("master"); + assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty(); + + grantTagPermissions(); + gApi.projects().name(result.getChange().project().get()).tag("test-tag") + .create(new TagInput()); + + assertThat(getIncludedIn(result.getCommit().getId()).tags) + .containsExactly("test-tag"); + + createBranch(new Branch.NameKey(project.get(), "test-branch")); + + assertThat(getIncludedIn(result.getCommit().getId()).branches) + .containsExactly("master", "test-branch"); + } + + private IncludedInInfo getIncludedIn(ObjectId id) throws Exception { + RestResponse r = userRestSession + .get("/projects/" + project.get() + "/commits/" + id.name() + "/in"); + IncludedInInfo result = + newGson().fromJson(r.getReader(), IncludedInInfo.class); + r.consume(); + return result; + } +}
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 d09eeec..48a8cd4 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
@@ -53,7 +53,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(); @@ -66,7 +66,7 @@ } @Test - public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict() + public void createProjectHttpWhenProjectAlreadyExists_Conflict() throws Exception { adminRestSession .put("/projects/" + allProjects.get()) @@ -74,7 +74,7 @@ } @Test - public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed() + public void createProjectHttpWhenProjectAlreadyExists_PreconditionFailed() throws Exception { adminRestSession .putWithHeader("/projects/" + allProjects.get(), @@ -84,7 +84,7 @@ @Test @UseLocalDisk - public void testCreateProjectHttpWithUnreasonableName_BadRequest() + public void createProjectHttpWithUnreasonableName_BadRequest() throws Exception { adminRestSession .put("/projects/" + Url.encode(name("invalid/../name"))) @@ -92,7 +92,7 @@ } @Test - public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception { + public void createProjectHttpWithNameMismatch_BadRequest() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("otherName"); adminRestSession @@ -101,7 +101,7 @@ } @Test - public void testCreateProjectHttpWithInvalidRefName_BadRequest() + public void createProjectHttpWithInvalidRefName_BadRequest() throws Exception { ProjectInput in = new ProjectInput(); in.branches = Collections.singletonList(name("invalid ref name")); @@ -111,7 +111,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); @@ -122,7 +122,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); @@ -133,7 +133,7 @@ } @Test - public void testCreateProjectWithProperties() throws Exception { + public void createProjectWithProperties() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -156,7 +156,7 @@ } @Test - public void testCreateChildProject() throws Exception { + public void createChildProject() throws Exception { String parentName = name("parent"); ProjectInput in = new ProjectInput(); in.name = parentName; @@ -172,7 +172,7 @@ } @Test - public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity() + public void createChildProjectUnderNonExistingParent_UnprocessableEntity() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("newProjectName"); @@ -181,7 +181,7 @@ } @Test - public void testCreateProjectWithOwner() throws Exception { + public void createProjectWithOwner() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -200,7 +200,7 @@ } @Test - public void testCreateProjectWithNonExistingOwner_UnprocessableEntity() + public void createProjectWithNonExistingOwner_UnprocessableEntity() throws Exception { ProjectInput in = new ProjectInput(); in.name = name("newProjectName"); @@ -209,7 +209,7 @@ } @Test - public void testCreatePermissionOnlyProject() throws Exception { + public void createPermissionOnlyProject() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -219,7 +219,7 @@ } @Test - public void testCreateProjectWithEmptyCommit() throws Exception { + public void createProjectWithEmptyCommit() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -229,7 +229,7 @@ } @Test - public void testCreateProjectWithBranches() throws Exception { + public void createProjectWithBranches() throws Exception { String newProjectName = name("newProject"); ProjectInput in = new ProjectInput(); in.name = newProjectName; @@ -245,7 +245,7 @@ } @Test - public void testCreateProjectWithCapability() throws Exception { + public void createProjectWithCapability() throws Exception { allowGlobalCapabilities(SystemGroupBackend.REGISTERED_USERS, GlobalCapability.CREATE_PROJECT); try { @@ -261,7 +261,7 @@ } @Test - public void testCreateProjectWithoutCapability_Forbidden() throws Exception { + public void createProjectWithoutCapability_Forbidden() throws Exception { setApiUser(user); ProjectInput in = new ProjectInput(); in.name = name("newProject"); @@ -269,7 +269,7 @@ } @Test - public void testCreateProjectWhenProjectAlreadyExists_Conflict() + public void createProjectWhenProjectAlreadyExists_Conflict() throws Exception { ProjectInput in = new ProjectInput(); in.name = allProjects.get(); @@ -277,7 +277,7 @@ } @Test - public void testCreateProjectWithCreateProjectCapabilityAndParentNotVisible() + public void createProjectWithCreateProjectCapabilityAndParentNotVisible() throws Exception { Project parent = projectCache.get(allProjects).getProject(); parent.setState(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
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/RefAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java new file mode 100644 index 0000000..0cbf79a --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/RefAssert.java
@@ -0,0 +1,59 @@ +// 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.project; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.Iterables; +import com.google.gerrit.extensions.api.projects.RefInfo; + +import java.util.List; + +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<? extends RefInfo> actualRefs) { + Iterable<String> actualNames = refs(actualRefs); + assertThat(actualNames).containsExactlyElementsIn(expectedRefs).inOrder(); + } + + 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) + .isEqualTo(expected.revision); + } + assertThat(toBoolean(actual.canDelete)).named("can delete " + actual.ref) + .isEqualTo(toBoolean(expected.canDelete)); + } + + private static Iterable<String> refs(Iterable<? extends RefInfo> infos) { + return Iterables.transform(infos, b -> b.ref); + } + + private static boolean toBoolean(Boolean b) { + if (b == null) { + return false; + } + return b.booleanValue(); + } +}
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..fcaf7ce 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"; @@ -336,12 +337,6 @@ } } - 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 + "*"); - } - private ListRefsRequest<TagInfo> getTags() throws Exception { return gApi.projects().name(project.get()).tags(); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK deleted file mode 100644 index 5384447..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'server_change', - srcs = glob(['*IT.java']), - labels = ['server'], -)
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..68b87da 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; @@ -132,7 +135,67 @@ String changeId = r.getChangeId(); String revId = r.getCommit().getName(); ReviewInput input = new ReviewInput(); - CommentInput comment = newComment(file, Side.REVISION, line, "comment 1"); + CommentInput comment = + newComment(file, Side.REVISION, line, "comment 1", false); + input.comments = new HashMap<>(); + input.comments.put(comment.path, Lists.newArrayList(comment)); + revision(r).review(input); + Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId); + assertThat(result).isNotEmpty(); + CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path)); + assertThat(comment).isEqualTo(infoToInput(file).apply(actual)); + assertThat(comment).isEqualTo(infoToInput(file).apply( + getPublishedComment(changeId, revId, actual.id))); + } + } + + @Test + public void postCommentWithReply() throws Exception { + for (Integer line : lines) { + String file = "file"; + String contents = "contents " + line; + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, + "first subject", file, contents); + PushOneCommit.Result r = push.to("refs/for/master"); + String changeId = r.getChangeId(); + String revId = r.getCommit().getName(); + ReviewInput input = new ReviewInput(); + CommentInput comment = + newComment(file, Side.REVISION, line, "comment 1", false); + input.comments = new HashMap<>(); + input.comments.put(comment.path, Lists.newArrayList(comment)); + revision(r).review(input); + Map<String, List<CommentInfo>> result = + getPublishedComments(changeId, revId); + CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path)); + + input = new ReviewInput(); + comment = newComment(file, Side.REVISION, line, "comment 1 reply", false); + comment.inReplyTo = actual.id; + input.comments = new HashMap<>(); + input.comments.put(comment.path, Lists.newArrayList(comment)); + revision(r).review(input); + result = getPublishedComments(changeId, revId); + actual = result.get(comment.path).get(1); + assertThat(comment).isEqualTo(infoToInput(file).apply(actual)); + assertThat(comment).isEqualTo(infoToInput(file).apply( + getPublishedComment(changeId, revId, actual.id))); + } + } + + @Test + public void postCommentWithUnresolved() throws Exception { + for (Integer line : lines) { + String file = "file"; + String contents = "contents " + line; + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, + "first subject", file, contents); + PushOneCommit.Result r = push.to("refs/for/master"); + String changeId = r.getChangeId(); + String revId = r.getCommit().getName(); + ReviewInput input = new ReviewInput(); + CommentInput comment = + newComment(file, Side.REVISION, line, "comment 1", true); input.comments = new HashMap<>(); input.comments.put(comment.path, Lists.newArrayList(comment)); revision(r).review(input); @@ -148,13 +211,14 @@ @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(); - CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1"); - CommentInput c2 = newComment(file, Side.PARENT, line, "auto-merge of ps-1"); + CommentInput c1 = newComment(file, Side.REVISION, line, "ps-1", false); + CommentInput c2 = + newComment(file, Side.PARENT, line, "auto-merge of ps-1", false); CommentInput c3 = newCommentOnParent(file, 1, line, "parent-1 of ps-1"); CommentInput c4 = newCommentOnParent(file, 2, line, "parent-2 of ps-1"); input.comments = new HashMap<>(); @@ -165,6 +229,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", false); + 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", false); + 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 @@ -180,7 +277,8 @@ List<CommentInput> expectedComments = new ArrayList<>(); for (Integer line : lines) { ReviewInput input = new ReviewInput(); - CommentInput comment = newComment(file, Side.REVISION, line, "comment " + line); + CommentInput comment = + newComment(file, Side.REVISION, line, "comment " + line, false); expectedComments.add(comment); input.comments = new HashMap<>(); input.comments.put(comment.path, Lists.newArrayList(comment)); @@ -290,7 +388,8 @@ Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn(); ReviewInput input = new ReviewInput(); - CommentInput comment = newComment(file, Side.REVISION, line, "comment 1"); + CommentInput comment = + newComment(file, Side.REVISION, line, "comment 1", false); comment.updated = timestamp; input.comments = new HashMap<>(); input.comments.put(comment.path, Lists.newArrayList(comment)); @@ -361,7 +460,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 +603,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 +617,12 @@ + url + "#/c/" + c + "/1/a.txt\n" + "File a.txt:\n" + "\n" + + url + "#/c/" + c + "/1/a.txt@a2\n" + "PS1, Line 2: \n" + "what happened to this?\n" + "\n" + "\n" + + url + "#/c/" + c + "/1/a.txt@1\n" + "PS1, Line 1: ew\n" + "nit: trailing whitespace\n" + "\n" @@ -530,20 +630,25 @@ + url + "#/c/" + c + "/2/a.txt\n" + "File a.txt:\n" + "\n" + + url + "#/c/" + c + "/2/a.txt@a1\n" + "PS2, Line 1: \n" + "comment 1 on base\n" + "\n" + "\n" + + url + "#/c/" + c + "/2/a.txt@a2\n" + "PS2, Line 2: \n" + "comment 2 on base\n" + "\n" + "\n" + + url + "#/c/" + c + "/2/a.txt@1\n" + "PS2, Line 1: ew\n" + "join lines\n" + "\n" + "\n" + + url + "#/c/" + c + "/2/a.txt@2\n" + "PS2, Line 2: nten\n" + "typo: content\n" + + "\n" + "\n"); } @@ -646,36 +751,39 @@ } private static CommentInput newComment(String path, Side side, int line, - String message) { + String message, Boolean unresolved) { CommentInput c = new CommentInput(); - return populate(c, path, side, null, line, message); + return populate(c, path, side, null, line, message, unresolved); } private static CommentInput newCommentOnParent(String path, int parent, int line, String message) { CommentInput c = new CommentInput(); - return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, message); + return populate(c, path, Side.PARENT, Integer.valueOf(parent), line, + message, false); } private DraftInput newDraft(String path, Side side, int line, String message) { DraftInput d = new DraftInput(); - return populate(d, path, side, null, line, message); + return populate(d, path, side, null, line, message, false); } private DraftInput newDraftOnParent(String path, int parent, int line, String message) { DraftInput d = new DraftInput(); - return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, message); + return populate(d, path, Side.PARENT, Integer.valueOf(parent), line, + message, false); } private static <C extends Comment> C populate(C c, String path, Side side, - Integer parent, int line, String message) { + Integer parent, int line, String message, Boolean unresolved) { c.path = path; c.side = side; c.parent = parent; c.line = line != 0 ? line : null; c.message = message; + c.unresolved = unresolved; if (line != 0) { Comment.Range range = new Comment.Range(); range.startLine = line; @@ -687,29 +795,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; }; } @@ -719,5 +819,7 @@ to.line = from.line; to.message = from.message; to.range = from.range; + to.unresolved = from.unresolved; + to.inReplyTo = from.inReplyTo; } }
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..fced72e 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
@@ -50,6 +50,7 @@ import com.google.gerrit.server.git.BatchUpdate.RepoContext; import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.notedb.ChangeNoteUtil; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.testutil.InMemoryRepositoryManager; import com.google.gerrit.testutil.TestChanges; @@ -297,8 +298,10 @@ String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; PatchSet ps = newPatchSet(psId, rev, adminId); - db.changes().insert(singleton(c)); - db.patchSets().insert(singleton(ps)); + if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) { + db.changes().insert(singleton(c)); + db.patchSets().insert(singleton(ps)); + } addNoteDbCommit( c.getId(), "Create change\n" @@ -796,7 +799,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); @@ -824,10 +827,12 @@ Change c = new Change(ctl.getChange()); PatchSet.Id psId = nextPatchSetId(ctl); c.setCurrentPatchSet(psId, subject, c.getOriginalSubject()); - PatchSet ps = newPatchSet(psId, rev, adminId); - db.patchSets().insert(singleton(ps)); - db.changes().update(singleton(c)); + + if (PrimaryStorage.of(c) == PrimaryStorage.REVIEW_DB) { + db.patchSets().insert(singleton(ps)); + db.changes().update(singleton(c)); + } addNoteDbCommit( c.getId(),
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..c577f65 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
@@ -16,22 +16,24 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; 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; +import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.change.ChangesCollection; import com.google.gerrit.server.change.GetRelated.ChangeAndCommit; import com.google.gerrit.server.change.GetRelated.RelatedInfo; -import com.google.gerrit.server.edit.ChangeEditModifier; -import com.google.gerrit.server.edit.ChangeEditUtil; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.query.change.ChangeData; @@ -46,6 +48,7 @@ import org.junit.Test; import java.util.List; +import java.util.Optional; public class GetRelatedIT extends AbstractDaemonTest { private String systemTimeZone; @@ -63,12 +66,6 @@ } @Inject - private ChangeEditUtil editUtil; - - @Inject - private ChangeEditModifier editModifier; - - @Inject private BatchUpdate.Factory updateFactory; @Inject @@ -577,11 +574,18 @@ pushHead(testRepo, "refs/for/master", false); Change ch2 = getChange(c2_1).change(); - editModifier.createEdit(ch2, getPatchSet(ch2.currentPatchSetId())); - editModifier.modifyFile(editUtil.byChange(ch2).get(), "a.txt", - RawInputUtil.create(new byte[] {'a'})); - ObjectId editRev = - ObjectId.fromString(editUtil.byChange(ch2).get().getRevision().get()); + String changeId2 = ch2.getKey().get(); + gApi.changes() + .id(changeId2) + .edit() + .create(); + gApi.changes() + .id(changeId2) + .edit() + .modifyFile("a.txt", RawInputUtil.create(new byte[] {'a'})); + Optional<EditInfo> edit = getEdit(changeId2); + assertThat(edit).isPresent(); + ObjectId editRev = ObjectId.fromString(edit.get().commit.commit); PatchSet.Id ps1_1 = getPatchSetId(c1_1); PatchSet.Id ps2_1 = getPatchSetId(c2_1); @@ -646,6 +650,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 +692,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..7dc8b98 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() @@ -144,7 +204,7 @@ assertThat(result.changes.get(0).changeId).isEqualTo(id1); assertThat(result.nonVisibleChanges).isEqualTo(1); } else { - assertThat(result.changes).hasSize(0); + assertThat(result.changes).isEmpty(); assertThat(result.nonVisibleChanges).isEqualTo(0); } } @@ -168,7 +228,7 @@ gApi.changes().id(id1).submittedTogether(); } else { List<ChangeInfo> result = gApi.changes().id(id1).submittedTogether(); - assertThat(result).hasSize(0); + assertThat(result).isEmpty(); } } @@ -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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK deleted file mode 100644 index 4fbc977..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'server_event', - srcs = glob(['*IT.java']), - labels = ['server'], -)
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/event/CommentAddedEventIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java index 40ec9da..8293bf9 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -33,7 +33,6 @@ import com.google.gerrit.extensions.registration.RegistrationHandle; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.Util; import com.google.inject.Inject; @@ -63,7 +62,7 @@ public void setUp() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); AccountGroup.UUID anonymousUsers = - SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); + systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers, "refs/heads/*"); Util.allow(cfg, Permission.forLabel(pLabel.getName()), 0, 1, anonymousUsers,
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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUCK deleted file mode 100644 index d9976e5..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'server_notedb', - srcs = glob(['*IT.java']), - labels = ['notedb', 'server'], -)
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..2ce3d61 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
@@ -15,25 +15,34 @@ package com.google.gerrit.acceptance.server.notedb; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; import static com.google.common.truth.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.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +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.common.CommentInfo; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Account; @@ -42,12 +51,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,23 +65,34 @@ 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.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; import com.google.gerrit.testutil.TestChanges; import com.google.gerrit.testutil.TestTimeUtil; import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.OrmRuntimeException; 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 +106,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 +116,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 +139,7 @@ private Provider<ReviewDb> dbProvider; @Inject - private PatchLineCommentsUtil plcUtil; + private CommentsUtil commentsUtil; @Inject private Provider<PostReview> postReview; @@ -123,10 +153,16 @@ @Inject private Sequences seq; + @Inject + private ChangeBundleReader bundleReader; + + @Inject + private PatchSetInfoFactory patchSetInfoFactory; + @Before public void setUp() throws Exception { assume().that(NoteDbMode.readWrite()).isFalse(); - TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS); + TestTimeUtil.resetWithClockStep(1, SECONDS); setNotesMigration(false, false); } @@ -171,7 +207,18 @@ public void publishedComment() throws Exception { PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putComment(user, id, 1, "comment"); + putComment(user, id, 1, "comment", null); + checker.rebuildAndCheckChanges(id); + } + + @Test + public void publishedCommentAndReply() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getPatchSetId().getParentKey(); + putComment(user, id, 1, "comment", null); + Map<String, List<CommentInfo>> comments = getPublishedComments(id); + String parentUuid = comments.get("a.txt").get(0).id; + putComment(user, id, 1, "comment", parentUuid); checker.rebuildAndCheckChanges(id); } @@ -196,7 +243,7 @@ public void draftComment() throws Exception { PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "comment"); + putDraft(user, id, 1, "comment", null); checker.rebuildAndCheckChanges(id); } @@ -204,8 +251,8 @@ public void draftAndPublishedComment() throws Exception { PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "draft comment"); - putComment(user, id, 1, "published comment"); + putDraft(user, id, 1, "draft comment", null); + putComment(user, id, 1, "published comment", null); checker.rebuildAndCheckChanges(id); } @@ -213,7 +260,7 @@ public void publishDraftComment() throws Exception { PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "draft comment"); + putDraft(user, id, 1, "draft comment", null); publishDrafts(user, id); checker.rebuildAndCheckChanges(id); } @@ -340,14 +387,14 @@ assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo( changeMetaId.name()); - putDraft(user, id, 1, "comment by user"); + putDraft(user, id, 1, "comment by user", null); ObjectId userDraftsId = getMetaRef( allUsers, refsDraftComments(id, user.getId())); assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo( changeMetaId.name() + "," + user.getId() + "=" + userDraftsId.name()); - putDraft(admin, id, 2, "comment by admin"); + putDraft(admin, id, 2, "comment by admin", null); ObjectId adminDraftsId = getMetaRef( allUsers, refsDraftComments(id, admin.getId())); assertThat(admin.getId().get()).isLessThan(user.getId().get()); @@ -356,7 +403,7 @@ + "," + admin.getId() + "=" + adminDraftsId.name() + "," + user.getId() + "=" + userDraftsId.name()); - putDraft(admin, id, 2, "revised comment by admin"); + putDraft(admin, id, 2, "revised comment by admin", null); adminDraftsId = getMetaRef( allUsers, refsDraftComments(id, admin.getId())); assertThat(getUnwrappedDb().changes().get(id).getNoteDbState()).isEqualTo( @@ -387,8 +434,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 +447,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 +464,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())) { @@ -420,7 +472,7 @@ public boolean updateChange(ChangeContext ctx) throws OrmException { PatchSet.Id psId = ctx.getChange().currentPatchSetId(); ChangeMessage cm = new ChangeMessage( - new ChangeMessage.Key(id, ChangeUtil.messageUUID(ctx.getDb())), + new ChangeMessage.Key(id, ChangeUtil.messageUuid()), ctx.getAccountId(), ctx.getWhen(), psId); cm.setMessage(msg); ctx.getDb().changeMessages().insert(Collections.singleton(cm)); @@ -428,29 +480,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 +533,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 +563,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); @@ -525,7 +581,7 @@ PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "comment by user"); + putDraft(user, id, 1, "comment by user", null); assertChangeUpToDate(true, id); ObjectId oldMetaId = @@ -533,7 +589,7 @@ // Add a draft behind NoteDb's back. setNotesMigration(false, false); - putDraft(user, id, 1, "second comment by user"); + putDraft(user, id, 1, "second comment by user", null); setInvalidNoteDbState(id); assertDraftsUpToDate(false, id, user); assertThat(getMetaRef(allUsers, refsDraftComments(id, user.getId()))) @@ -549,8 +605,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 @@ -568,7 +624,7 @@ PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "comment by user"); + putDraft(user, id, 1, "comment by user", null); assertChangeUpToDate(true, id); ObjectId oldMetaId = @@ -576,16 +632,21 @@ // Add a draft behind NoteDb's back. setNotesMigration(false, false); - putDraft(user, id, 1, "second comment by user"); + putDraft(user, id, 1, "second comment by user", null); 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))), + Optional.empty()); c.setNoteDbState(bogusState.toString()); db.changes().update(Collections.singleton(c)); @@ -604,8 +665,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 @@ -624,12 +685,12 @@ PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "comment"); + putDraft(user, id, 1, "comment", null); assertDraftsUpToDate(true, id, user); // Make a ReviewDb change behind NoteDb's back and ensure it's detected. setNotesMigration(false, false); - putDraft(user, id, 1, "comment"); + putDraft(user, id, 1, "comment", null); setInvalidNoteDbState(id); assertDraftsUpToDate(false, id, user); @@ -711,6 +772,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 +871,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, @@ -876,7 +917,7 @@ public void rebuildDeletesOldDraftRefs() throws Exception { PushOneCommit.Result r = createChange(); Change.Id id = r.getPatchSetId().getParentKey(); - putDraft(user, id, 1, "comment"); + putDraft(user, id, 1, "comment", null); Account.Id otherAccountId = new Account.Id(user.getId().get() + 1234); String otherDraftRef = refsDraftComments(id, otherAccountId); @@ -971,6 +1012,221 @@ 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); + } + + @Test + public void resolveCommentsInheritsValueFromParentWhenUnspecified() + throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getPatchSetId().getParentKey(); + putDraft(user, id, 1, "comment", true); + putDraft(user, id, 1, "newComment", null); + + Map<String, List<CommentInfo>> comments = + gApi.changes().id(id.get()).current().drafts(); + for (List<CommentInfo> cList: comments.values()) { + for (CommentInfo ci: cList) { + assertThat(ci.unresolved).isEqualTo(true); + } + } + } + + @Test + public void rebuilderRespectsReadOnlyInNoteDbChangeState() throws Exception { + TestTimeUtil.resetWithClockStep(1, SECONDS); + PushOneCommit.Result r = createChange(); + PatchSet.Id psId1 = r.getPatchSetId(); + Change.Id id = psId1.getParentKey(); + + checker.rebuildAndCheckChanges(id); + setNotesMigration(true, true); + + ReviewDb db = getUnwrappedDb(); + Change c = db.changes().get(id); + NoteDbChangeState state = NoteDbChangeState.parse(c); + Timestamp until = + new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS)); + state = state.withReadOnlyUntil(until); + c.setNoteDbState(state.toString()); + db.changes().update(Collections.singleton(c)); + + try { + rebuilderWrapper.rebuild(db, id); + assert_().fail("expected rebuild to fail"); + } catch (OrmRuntimeException e) { + assertThat(e.getMessage()).contains("read-only until"); + } + + TestTimeUtil.setClock( + new Timestamp(until.getTime() + MILLISECONDS.convert(1, SECONDS))); + rebuilderWrapper.rebuild(db, id); + } + private void assertChangesReadOnly(RestApiException e) throws Exception { Throwable cause = e.getCause(); assertThat(cause).isInstanceOf(UpdateException.class); @@ -1022,12 +1278,13 @@ } } - private void putDraft(TestAccount account, Change.Id id, int line, String msg) - throws Exception { + private void putDraft(TestAccount account, Change.Id id, int line, String msg, + Boolean unresolved) throws Exception { DraftInput in = new DraftInput(); in.line = line; in.message = msg; in.path = PushOneCommit.FILE_NAME; + in.unresolved = unresolved; AcceptanceTestRequestScope.Context old = setApiUser(account); try { gApi.changes().id(id.get()).current().createDraft(in); @@ -1036,11 +1293,12 @@ } } - private void putComment(TestAccount account, Change.Id id, int line, String msg) - throws Exception { + private void putComment(TestAccount account, Change.Id id, int line, + String msg, String inReplyTo) throws Exception { CommentInput in = new CommentInput(); in.line = line; in.message = msg; + in.inReplyTo = inReplyTo; ReviewInput rin = new ReviewInput(); rin.comments = new HashMap<>(); rin.comments.put(PushOneCommit.FILE_NAME, ImmutableList.of(in)); @@ -1068,7 +1326,7 @@ private ChangeMessage insertMessage(Change.Id id, PatchSet.Id psId, Account.Id author, Timestamp ts, String message) throws Exception { ChangeMessage msg = new ChangeMessage( - new ChangeMessage.Key(id, ChangeUtil.messageUUID(db)), + new ChangeMessage.Key(id, ChangeUtil.messageUuid()), author, ts, psId); msg.setMessage(message); db.changeMessages().insert(Collections.singleton(msg)); @@ -1086,4 +1344,23 @@ 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); + } + + private Map<String, List<CommentInfo>> getPublishedComments(Change.Id id) + throws Exception { + return gApi.changes().id(id.get()).current().comments(); + } }
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..fc39a97 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -0,0 +1,418 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.server.notedb; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; +import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.TimeUtil; +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.extensions.restapi.RestApiException; +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.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.PrimaryStorageMigrator; +import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper; +import com.google.gerrit.testutil.ConfigSuite; +import com.google.gerrit.testutil.NoteDbMode; +import com.google.gerrit.testutil.TestTimeUtil; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.OrmRuntimeException; +import com.google.inject.Inject; +import com.google.inject.util.Providers; + +import com.github.rholder.retry.Retryer; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Timestamp; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class NoteDbPrimaryIT extends AbstractDaemonTest { + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + cfg.setString("notedb", null, "concurrentWriterTimeout", "0s"); + cfg.setString("notedb", null, "primaryStorageMigrationTimeout", "1d"); + cfg.setBoolean("noteDb", null, "testRebuilderWrapper", true); + return cfg; + } + + @Inject + private AllUsersName allUsers; + + @Inject + private TestChangeRebuilderWrapper rebuilderWrapper; + + private PrimaryStorageMigrator migrator; + + @Before + public void setUp() throws Exception { + assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.READ_WRITE); + db = ReviewDbUtil.unwrapDb(db); + TestTimeUtil.resetWithClockStep(1, SECONDS); + migrator = newMigrator(null); + } + + private PrimaryStorageMigrator newMigrator( + @Nullable Retryer<NoteDbChangeState> ensureRebuiltRetryer) { + return new PrimaryStorageMigrator( + cfg, Providers.of(db), repoManager, allUsers, rebuilderWrapper, + ensureRebuiltRetryer); + } + + @After + public void tearDown() { + TestTimeUtil.useSystemTime(); + } + + @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(); + } + + @Test + public void readOnlyReviewDb() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + testReadOnly(id); + } + + @Test + public void readOnlyNoteDb() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + setNoteDbPrimary(id); + testReadOnly(id); + } + + private void testReadOnly(Change.Id id) throws Exception { + Timestamp before = TimeUtil.nowTs(); + Timestamp until = new Timestamp(before.getTime() + 1000 * 3600); + + // Set read-only. + Change c = db.changes().get(id); + assertThat(c).named("change " + id).isNotNull(); + NoteDbChangeState state = NoteDbChangeState.parse(c); + state = state.withReadOnlyUntil(until); + c.setNoteDbState(state.toString()); + db.changes().update(Collections.singleton(c)); + + assertThat(gApi.changes().id(id.get()).get().subject) + .isEqualTo(PushOneCommit.SUBJECT); + assertThat(gApi.changes().id(id.get()).get().topic).isNull(); + try { + gApi.changes().id(id.get()).topic("a-topic"); + assert_().fail("expected read-only exception"); + } catch (RestApiException e) { + Optional<Throwable> oe = Throwables.getCausalChain(e).stream() + .filter(x -> x instanceof OrmRuntimeException).findFirst(); + assertThat(oe.isPresent()) + .named("OrmRuntimeException in causal chain of " + e) + .isTrue(); + assertThat(oe.get().getMessage()).contains("read-only"); + } + assertThat(gApi.changes().id(id.get()).get().topic).isNull(); + + TestTimeUtil.setClock(new Timestamp(until.getTime() + 1000)); + assertThat(gApi.changes().id(id.get()).get().subject) + .isEqualTo(PushOneCommit.SUBJECT); + gApi.changes().id(id.get()).topic("a-topic"); + assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic"); + } + + @Test + public void migrateToNoteDb() throws Exception { + testMigrateToNoteDb(createChange().getChange().getId()); + } + + @Test + public void migrateToNoteDbWithRebuildingFirst() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + Change c = db.changes().get(id); + c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + db.changes().update(Collections.singleton(c)); + testMigrateToNoteDb(id); + } + + private void testMigrateToNoteDb(Change.Id id) throws Exception { + assertThat(PrimaryStorage.of(db.changes().get(id))) + .isEqualTo(PrimaryStorage.REVIEW_DB); + migrator.migrateToNoteDbPrimary(id); + assertNoteDbPrimary(id); + + gApi.changes().id(id.get()).topic("a-topic"); + assertThat(gApi.changes().id(id.get()).get().topic).isEqualTo("a-topic"); + assertThat(db.changes().get(id).getTopic()).isNull(); + } + + @Test + public void migrateToNoteDbFailsRebuildingOnceAndRetries() throws Exception { + Change.Id id = createChange().getChange().getId(); + + Change c = db.changes().get(id); + c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + db.changes().update(Collections.singleton(c)); + rebuilderWrapper.failNextUpdate(); + + migrator = newMigrator( + RetryerBuilder.<NoteDbChangeState> newBuilder() + .retryIfException() + .withStopStrategy(StopStrategies.neverStop()) + .build()); + migrator.migrateToNoteDbPrimary(id); + assertNoteDbPrimary(id); + } + + @Test + public void migrateToNoteDbFailsRebuildingAndStops() throws Exception { + Change.Id id = createChange().getChange().getId(); + + Change c = db.changes().get(id); + c.setNoteDbState("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + db.changes().update(Collections.singleton(c)); + rebuilderWrapper.failNextUpdate(); + + migrator = newMigrator( + RetryerBuilder.<NoteDbChangeState> newBuilder() + .retryIfException() + .withStopStrategy(StopStrategies.stopAfterAttempt(1)) + .build()); + exception.expect(OrmException.class); + exception.expectMessage("Retrying failed"); + migrator.migrateToNoteDbPrimary(id); + } + + @Test + public void migrateToNoteDbMissingOldState() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + Change c = db.changes().get(id); + c.setNoteDbState(null); + db.changes().update(Collections.singleton(c)); + + exception.expect(OrmRuntimeException.class); + exception.expectMessage("no note_db_state"); + migrator.migrateToNoteDbPrimary(id); + } + + @Test + public void migrateToNoteDbLeaseExpires() throws Exception { + TestTimeUtil.resetWithClockStep(2, DAYS); + exception.expect(OrmRuntimeException.class); + exception.expectMessage("read-only lease"); + migrator.migrateToNoteDbPrimary(createChange().getChange().getId()); + } + + @Test + public void migrateToNoteDbAlreadyReadOnly() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + Change c = db.changes().get(id); + NoteDbChangeState state = NoteDbChangeState.parse(c); + Timestamp until = + new Timestamp(TimeUtil.nowMs() + MILLISECONDS.convert(1, DAYS)); + state = state.withReadOnlyUntil(until); + c.setNoteDbState(state.toString()); + db.changes().update(Collections.singleton(c)); + + exception.expect(OrmRuntimeException.class); + exception.expectMessage("read-only until " + until); + migrator.migrateToNoteDbPrimary(id); + } + + @Test + public void migrateToNoteDbAlreadyMigrated() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + assertThat(PrimaryStorage.of(db.changes().get(id))) + .isEqualTo(PrimaryStorage.REVIEW_DB); + migrator.migrateToNoteDbPrimary(id); + assertNoteDbPrimary(id); + + migrator.migrateToNoteDbPrimary(id); + assertNoteDbPrimary(id); + } + + 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 void assertNoteDbPrimary(Change.Id id) throws Exception { + assertThat(PrimaryStorage.of(db.changes().get(id))) + .isEqualTo(PrimaryStorage.NOTE_DB); + } + + 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/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK deleted file mode 100644 index 013115d..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/BUCK +++ /dev/null
@@ -1,7 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'server_project', - srcs = glob(['*IT.java']), - labels = ['server'], -)
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..d6019cf 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,14 +26,15 @@ 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; import com.google.gerrit.server.project.Util; import com.google.inject.Inject; @@ -63,7 +64,7 @@ public void setUp() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); AccountGroup.UUID anonymousUsers = - SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); + systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers, "refs/heads/*"); Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers, @@ -175,6 +176,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..43f6679 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,11 +19,10 @@ 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; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.git.NotifyConfig; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.mail.Address; @@ -33,11 +32,11 @@ import org.eclipse.jgit.junit.TestRepository; import org.junit.Test; -import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @NoHttpd +@Sandboxed public class ProjectWatchIT extends AbstractDaemonTest { @Test public void newPatchSetsNotifyConfig() throws Exception { @@ -161,16 +160,153 @@ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); } - private void watch(String project, String filter) - throws RestApiException { - List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); - ProjectWatchInfo pwi = new ProjectWatchInfo(); - pwi.project = project; - pwi.filter = filter; - pwi.notifyAbandonedChanges = true; - pwi.notifyNewChanges = true; - pwi.notifyAllComments = true; - projectsToWatch.add(pwi); - gApi.accounts().self().setWatchedProjects(projectsToWatch); + @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()).isEmpty(); + } + + @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()).isEmpty(); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java index 56a56ee..09e03b6a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
@@ -21,6 +21,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; @@ -31,6 +32,7 @@ import java.util.Locale; @NoHttpd +@UseSsh public class AbandonRestoreIT extends AbstractDaemonTest { @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK deleted file mode 100644 index 0729b68..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK +++ /dev/null
@@ -1,8 +0,0 @@ -include_defs('//gerrit-acceptance-tests/tests.defs') - -acceptance_tests( - group = 'ssh', - srcs = glob(['*IT.java']), - deps = ['//lib/commons:compress'], - labels = ['ssh'], -)
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/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java index 025fcfa..f786bba 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
@@ -21,6 +21,7 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.UseSsh; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.RemoteRefUpdate; @@ -29,6 +30,7 @@ import java.util.Locale; @NoHttpd +@UseSsh public class BanCommitIT extends AbstractDaemonTest { @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java index 85d460e..a45cb0e 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/CreateProjectIT.java
@@ -18,11 +18,13 @@ import static com.google.common.truth.Truth.assert_; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.project.ProjectState; import org.junit.Test; +@UseSsh public class CreateProjectIT extends AbstractDaemonTest { @Test
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..7f003bf 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
@@ -21,6 +21,7 @@ import com.google.gerrit.acceptance.GcAssert; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.common.data.GarbageCollectionResult; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.GarbageCollection; @@ -34,6 +35,7 @@ import java.util.Locale; @NoHttpd +@UseSsh public class GarbageCollectionIT extends AbstractDaemonTest { @Inject @@ -78,7 +80,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..11595d0 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
@@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.SshSession; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.Side; @@ -36,12 +37,13 @@ import java.util.List; @NoHttpd +@UseSsh public class QueryIT extends AbstractDaemonTest { 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 +70,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 +85,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 +102,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 +112,7 @@ } @Test - public void testCurrentPatchSetOptionJSON() throws Exception { + public void currentPatchSetOptionJSON() throws Exception { String changeId = createChange().getChangeId(); amendChange(changeId); @@ -121,7 +123,7 @@ changes = executeSuccessfulQuery("--current-patch-set " + changeId); assertThat(changes.size()).isEqualTo(1); assertThat(changes.get(0).currentPatchSet).isNotNull(); - assertThat(changes.get(0).currentPatchSet.number).isEqualTo("2"); + assertThat(changes.get(0).currentPatchSet.number).isEqualTo(2); gApi.changes().id(changeId).current().review(ReviewInput.approve()); changes = executeSuccessfulQuery("--current-patch-set " + changeId); @@ -133,7 +135,7 @@ } @Test - public void testPatchSetsOptionJSON() throws Exception { + public void patchSetsOptionJSON() throws Exception { String changeId = createChange().getChangeId(); amendChange(changeId); amendChange(changeId); @@ -159,7 +161,7 @@ } @Test - public void testFileOptionJSON() throws Exception { + public void fileOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = @@ -185,7 +187,7 @@ } @Test - public void testCommentOptionJSON() throws Exception { + public void commentOptionJSON() throws Exception { String changeId = createChange().getChangeId(); List<ChangeAttribute> changes = executeSuccessfulQuery(changeId); @@ -199,7 +201,7 @@ } @Test - public void testCommentOptionsInCurrentPatchSetJSON() throws Exception { + public void commentOptionsInCurrentPatchSetJSON() throws Exception { String changeId = createChange().getChangeId(); ReviewInput review = new ReviewInput(); @@ -224,7 +226,7 @@ } @Test - public void testCommentOptionInPatchSetsJSON() throws Exception { + public void commentOptionInPatchSetsJSON() throws Exception { String changeId = createChange().getChangeId(); ReviewInput review = new ReviewInput(); @@ -268,7 +270,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 +292,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 +305,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/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java index 32a0175..2f0dcd3 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.UseSsh; import com.google.gerrit.testutil.NoteDbMode; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -40,6 +41,7 @@ import java.util.TreeSet; @NoHttpd +@UseSsh public class UploadArchiveIT extends AbstractDaemonTest { @Before
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 deleted file mode 100644 index 85cc78b..0000000 --- a/gerrit-acceptance-tests/tests.defs +++ /dev/null
@@ -1,33 +0,0 @@ -BOUNCYCASTLE = [ - '//lib/bouncycastle:bcpkix', - '//lib/bouncycastle:bcpg', -] - -def acceptance_tests( - group, - srcs, - deps = [], - labels = [], - source_under_test = [], - vm_args = ['-Xmx256m']): - from os import path - if path.exists('/dev/urandom'): - vm_args = vm_args + ['-Djava.security.egd=file:/dev/./urandom'] - - java_test( - name = group, - srcs = srcs, - 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', - ], - vm_args = vm_args, - )
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK deleted file mode 100644 index e858a72..0000000 --- a/gerrit-antlr/BUCK +++ /dev/null
@@ -1,36 +0,0 @@ -PARSER_DEPS = [ - ':query_exception', - '//lib/antlr:java_runtime', -] - -java_library( - name = 'query_exception', - srcs = ['src/main/java/com/google/gerrit/server/query/QueryParseException.java'], - visibility = ['PUBLIC'], -) - -genantlr( - name = 'query_antlr', - srcs = ['src/main/antlr3/com/google/gerrit/server/query/Query.g'], - out = 'query_antlr.src.zip', -) - -java_library( - name = 'lib', - srcs = [':query_antlr'], - deps = PARSER_DEPS, -) - -# Hack necessary to expose ANTLR generated code as JAR to Eclipse. -genrule( - name = 'query_link', - cmd = 'ln -s $(location :lib) $OUT', - out = 'query_parser.jar', -) - -prebuilt_jar( - name = 'query_parser', - binary_jar = ':query_link', - deps = PARSER_DEPS, - visibility = ['PUBLIC'], -)
diff --git a/gerrit-antlr/BUILD b/gerrit-antlr/BUILD index c955ab1..f4ce4c7 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/BUCK b/gerrit-cache-h2/BUCK deleted file mode 100644 index 0bc1cb12..0000000 --- a/gerrit-cache-h2/BUCK +++ /dev/null
@@ -1,28 +0,0 @@ -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 = ['PUBLIC'], -) - -java_test( - name = 'tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':cache-h2', - '//gerrit-server:server', - '//lib:guava', - '//lib:h2', - '//lib/guice:guice', - '//lib:junit', - ], -)
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 deleted file mode 100644 index 847fd25..0000000 --- a/gerrit-common/BUCK +++ /dev/null
@@ -1,75 +0,0 @@ -SRC = 'src/main/java/com/google/gerrit/' - -ANNOTATIONS = [ - SRC + x for x in [ - 'common/Nullable.java', - 'common/audit/Audit.java', - 'common/auth/SignInRequired.java', - ] -] - -java_library( - name = 'annotations', - srcs = ANNOTATIONS, - 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/jgit/org.eclipse.jgit:jgit', - '//lib/joda:joda-time', - '//lib/log:api', - ], - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'server', - srcs = glob([SRC + 'common/**/*.java'], excludes = ANNOTATIONS), - deps = [ - ':annotations', - '//gerrit-extension-api:api', - '//gerrit-patch-jgit:server', - '//gerrit-prettify:server', - '//gerrit-reviewdb:server', - '//lib:guava', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/joda:joda-time', - '//lib/log:api', - ], - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -TEST = 'src/test/java/com/google/gerrit/common/' -AUTO_VALUE_TEST_SRCS = [TEST + 'AutoValueTest.java'] - -java_test( - name = 'client_tests', - srcs = glob(['src/test/java/**/*.java'], excludes = AUTO_VALUE_TEST_SRCS), - deps = [ - ':client', - '//lib:guava', - '//lib:junit', - ], - source_under_test = [':client'], -) - -java_test( - name = 'auto_value_tests', - srcs = AUTO_VALUE_TEST_SRCS, - deps = [ - '//lib:truth', - '//lib/auto:auto-value', - ], -)
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/AccountInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java deleted file mode 100644 index d7803c1..0000000 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java +++ /dev/null
@@ -1,79 +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.reviewdb.client.Account; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** In-memory table of {@link AccountInfo}, indexed by {@code Account.Id}. */ -public class AccountInfoCache { - private static final AccountInfoCache EMPTY; - static { - EMPTY = new AccountInfoCache(); - EMPTY.accounts = Collections.emptyMap(); - } - - /** Obtain an empty cache singleton. */ - public static AccountInfoCache empty() { - return EMPTY; - } - - protected Map<Account.Id, AccountInfo> accounts; - - protected AccountInfoCache() { - } - - public AccountInfoCache(final Iterable<AccountInfo> list) { - accounts = new HashMap<>(); - for (final AccountInfo ai : list) { - accounts.put(ai.getId(), ai); - } - } - - /** - * Lookup the account summary - * <p> - * The return value can take on one of three forms: - * <ul> - * <li>{@code null}, if {@code id == null}.</li> - * <li>a valid info block, if {@code id} was loaded.</li> - * <li>an anonymous info block, if {@code id} was not loaded.</li> - * </ul> - * - * @param id the id desired. - * @return info block for the account. - */ - public AccountInfo get(final Account.Id id) { - if (id == null) { - return null; - } - - AccountInfo r = accounts.get(id); - if (r == null) { - r = new AccountInfo(id); - accounts.put(id, r); - } - return r; - } - - /** Merge the information from another cache into this one. */ - public void merge(final AccountInfoCache other) { - assert this != EMPTY; - accounts.putAll(other.accounts); - } -}
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..fa282ca 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,13 @@ import java.util.Map; public class CommentDetail { - protected List<PatchLineComment> a; - protected List<PatchLineComment> b; - protected AccountInfoCache accounts; + protected List<Comment> a; + protected List<Comment> b; 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,44 +43,26 @@ protected CommentDetail() { } - public boolean include(final PatchLineComment p) { - final PatchSet.Id psId = p.getKey().getParentKey().getParentKey(); - switch (p.getSide()) { - case 0: - if (idA == null && idB.equals(psId)) { - a.add(p); - return true; - } - break; - - case 1: - if (idA != null && idA.equals(psId)) { - a.add(p); - return true; - } - - if (idB.equals(psId)) { - b.add(p); - return true; - } - break; + public void include(Change.Id changeId, Comment p) { + PatchSet.Id psId = new PatchSet.Id(changeId, p.key.patchSetId); + if (p.side == 0) { + if (idA == null && idB.equals(psId)) { + a.add(p); + } + } else if (p.side == 1) { + if (idA != null && idA.equals(psId)) { + a.add(p); + } else if (idB.equals(psId)) { + b.add(p); + } } - return false; } - public void setAccountInfoCache(final AccountInfoCache a) { - accounts = a; - } - - public AccountInfoCache getAccounts() { - return accounts; - } - - public List<PatchLineComment> getCommentsA() { + public List<Comment> getCommentsA() { return a; } - public List<PatchLineComment> getCommentsB() { + public List<Comment> getCommentsB() { return b; } @@ -88,24 +70,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 +97,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 +124,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 +133,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/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java index c890812..2ba0937 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -21,20 +21,13 @@ import java.util.List; public class GroupDetail { - public AccountInfoCache accounts; public AccountGroup group; public List<AccountGroupMember> members; public List<AccountGroupById> includes; - public GroupReference ownerGroup; - public boolean canModify; public GroupDetail() { } - public void setAccounts(AccountInfoCache c) { - accounts = c; - } - public void setGroup(AccountGroup g) { group = g; } @@ -46,12 +39,4 @@ public void setIncludes(List<AccountGroupById> i) { includes = i; } - - public void setOwnerGroup(GroupReference g) { - ownerGroup = g; - } - - public void setCanModify(final boolean canModify) { - this.canModify = canModify; - } }
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/BUILD b/gerrit-elasticsearch/BUILD new file mode 100644 index 0000000..8c94830 --- /dev/null +++ b/gerrit-elasticsearch/BUILD
@@ -0,0 +1,70 @@ +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-reviewdb:client", + "//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..f5cc273 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -0,0 +1,228 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.elasticsearch; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; +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.Gson; +import com.google.gson.GsonBuilder; +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; + protected final Gson gson; + protected final ElasticQueryBuilder queryBuilder; + + AbstractElasticIndex(@GerritServerConfig Config cfg, + FillArgs fillArgs, + SitePaths sitePaths, + Schema<V> schema, + String indexName) { + this.fillArgs = fillArgs; + this.sitePaths = sitePaths; + this.schema = schema; + this.gson = new GsonBuilder() + .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create(); + this.queryBuilder = new ElasticQueryBuilder(); + 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..3e91fb4 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -0,0 +1,222 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF 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 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.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.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.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.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; + +public 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 AccountMapping mapping; + private final Provider<AccountCache> accountCache; + + @AssistedInject + ElasticAccountIndex( + @GerritServerConfig Config cfg, + SitePaths sitePaths, + Provider<AccountCache> accountCache, + @Assisted Schema<AccountState> schema) { + // No parts of FillArgs are currently required, just use null. + super(cfg, null, sitePaths, schema, ACCOUNTS_PREFIX); + this.accountCache = accountCache; + this.mapping = new AccountMapping(schema); + } + + @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, Account.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(toAccountState(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 toAccountState(JsonElement json) { + JsonElement source = json.getAsJsonObject().get("_source"); + if (source == null) { + source = json.getAsJsonObject().get("fields"); + } + + Account.Id id = new Account.Id( + source.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().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..96cd1c4 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -0,0 +1,389 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.elasticsearch; + +import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES; +import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES; +import static 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.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 ChangeMapping mapping; + private final Provider<ReviewDb> db; + 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); + } + + @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/ElasticGroupIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java new file mode 100644 index 0000000..f9c96d1 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -0,0 +1,218 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.elasticsearch; + +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.AccountGroup; +import com.google.gerrit.server.account.GroupCache; +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.group.GroupField; +import com.google.gerrit.server.index.group.GroupIndex; +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.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.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; + +public class ElasticGroupIndex + extends AbstractElasticIndex<AccountGroup.UUID, AccountGroup> + implements GroupIndex { + static class GroupMapping { + MappingProperties groups; + + GroupMapping(Schema<AccountGroup> schema) { + this.groups = ElasticMapping.createMapping(schema); + } + } + + static final String GROUPS = "groups"; + static final String GROUPS_PREFIX = GROUPS + "_"; + + private static final Logger log = + LoggerFactory.getLogger(ElasticGroupIndex.class); + + private final GroupMapping mapping; + private final Provider<GroupCache> groupCache; + + @AssistedInject + ElasticGroupIndex( + @GerritServerConfig Config cfg, + SitePaths sitePaths, + Provider<GroupCache> groupCache, + @Assisted Schema<AccountGroup> schema) { + // No parts of FillArgs are currently required, just use null. + super(cfg, null, sitePaths, schema, GROUPS_PREFIX); + this.groupCache = groupCache; + this.mapping = new GroupMapping(schema); + } + + @Override + public void replace(AccountGroup group) throws IOException { + Bulk bulk = new Bulk.Builder() + .defaultIndex(indexName) + .defaultType(GROUPS) + .addAction(insert(GROUPS, group)) + .refresh(refresh) + .build(); + JestResult result = client.execute(bulk); + if (!result.isSucceeded()) { + throw new IOException( + String.format("Failed to replace group %s in index %s: %s", + group.getGroupUUID().get(), indexName, result.getErrorMessage())); + } + } + + @Override + public DataSource<AccountGroup> getSource(Predicate<AccountGroup> p, + QueryOptions opts) throws QueryParseException { + return new QuerySource(p, opts); + } + + @Override + protected Builder addActions(Builder builder, AccountGroup.UUID c) { + return builder.addAction(delete(GROUPS, c)); + } + + @Override + protected String getMappings() { + ImmutableMap<String, GroupMapping> mappings = + ImmutableMap.of("mappings", mapping); + return gson.toJson(mappings); + } + + @Override + protected String getId(AccountGroup group) { + return group.getGroupUUID().get(); + } + + private class QuerySource implements DataSource<AccountGroup> { + private final Search search; + private final Set<String> fields; + + QuerySource(Predicate<AccountGroup> p, QueryOptions opts) + throws QueryParseException { + QueryBuilder qb = queryBuilder.toQueryBuilder(p); + fields = IndexUtils.groupFields(opts); + SearchSourceBuilder searchSource = new SearchSourceBuilder() + .query(qb) + .from(opts.start()) + .size(opts.limit()) + .fields(Lists.newArrayList(fields)); + + Sort sort = new Sort(GroupField.UUID.getName(), Sorting.ASC); + sort.setIgnoreUnmapped(); + + search = new Search.Builder(searchSource.toString()) + .addType(GROUPS) + .addIndex(indexName) + .addSort(ImmutableList.of(sort)) + .build(); + } + + @Override + public int getCardinality() { + return 10; + } + + @Override + public ResultSet<AccountGroup> read() throws OrmException { + try { + List<AccountGroup> 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(toAccountGroup(json.get(i))); + } + } + } else { + log.error(result.getErrorMessage()); + } + final List<AccountGroup> r = Collections.unmodifiableList(results); + return new ResultSet<AccountGroup>() { + @Override + public Iterator<AccountGroup> iterator() { + return r.iterator(); + } + + @Override + public List<AccountGroup> toList() { + return r; + } + + @Override + public void close() { + // Do nothing. + } + }; + } catch (IOException e) { + throw new OrmException(e); + } + } + + @Override + public String toString() { + return search.toString(); + } + + private AccountGroup toAccountGroup(JsonElement json) { + JsonElement source = json.getAsJsonObject().get("_source"); + if (source == null) { + source = json.getAsJsonObject().get("fields"); + } + + AccountGroup.UUID uuid = new AccountGroup.UUID( + source.getAsJsonObject().get(GroupField.UUID.getName()).getAsString()); + // Use the GroupCache rather than depending on any stored fields in the + // document (of which there shouldn't be any). + return groupCache.get().get(uuid); + } + } +}
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..49b8f53 --- /dev/null +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -0,0 +1,75 @@ +// 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.gerrit.server.index.group.GroupIndex; +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(AccountIndex.class, ElasticAccountIndex.class) + .build(AccountIndex.Factory.class)); + install( + new FactoryModuleBuilder() + .implement(ChangeIndex.class, ElasticChangeIndex.class) + .build(ChangeIndex.Factory.class)); + install( + new FactoryModuleBuilder() + .implement(GroupIndex.class, ElasticGroupIndex.class) + .build(GroupIndex.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/ElasticQueryGroupsTest.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java new file mode 100644 index 0000000..ebb5635 --- /dev/null +++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
@@ -0,0 +1,69 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.elasticsearch; + +import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo; +import com.google.gerrit.server.query.group.AbstractQueryGroupsTest; +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 ElasticQueryGroupsTest extends AbstractQueryGroupsTest { + 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/ElasticTestUtils.java b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java new file mode 100644 index 0000000..959fd3c --- /dev/null +++ b/gerrit-elasticsearch/src/test/java/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -0,0 +1,206 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF 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 static com.google.gerrit.elasticsearch.ElasticGroupIndex.GROUPS_PREFIX; + +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.elasticsearch.ElasticGroupIndex.GroupMapping; +import com.google.gerrit.reviewdb.client.AccountGroup; +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.index.group.GroupSchemaDefinitions; +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(); + + Schema<AccountGroup> groupSchema = + GroupSchemaDefinitions.INSTANCE.getLatest(); + GroupMapping groupMapping = new GroupMapping(groupSchema); + nodeInfo.node + .client() + .admin() + .indices() + .prepareCreate( + String.format( + "%s%04d", GROUPS_PREFIX, groupSchema.getVersion())) + .addMapping(ElasticGroupIndex.GROUPS, gson.toJson(groupMapping)) + .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 deleted file mode 100644 index 61cd406..0000000 --- a/gerrit-extension-api/BUCK +++ /dev/null
@@ -1,89 +0,0 @@ -include_defs('//lib/JGIT_VERSION') -include_defs('//lib/GUAVA_VERSION') - -SRC = 'src/main/java/com/google/gerrit/extensions/' -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 = ['PUBLIC'], -) - -java_library( - name = 'client-lib', - srcs = EXT_API_SRCS, - resources = EXT_API_SRCS + glob([SRC + 'Extensions.gwt.xml']), - visibility = ['PUBLIC'], -) - -java_binary( - name = 'extension-api', - deps = [':lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'lib', - exported_deps = [ - ':api', - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib:servlet-api-3_1', - ], - visibility = ['PUBLIC'], -) - -java_library( - name = 'api', - srcs = glob([SRC + '**/*.java']), - deps = [ - '//gerrit-common:annotations', - ], - provided_deps = [ - '//lib:guava', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - ], - visibility = ['PUBLIC'], -) - -java_sources( - name = 'extension-api-src', - srcs = SRCS, - visibility = ['PUBLIC'], -) - -java_test( - name = 'api_tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':api', - '//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'], - srcs = SRCS, - deps = [ - '//lib:guava', - '//lib/guice:javax-inject', - '//lib/guice:guice_library', - '//lib/guice:guice-assistedinject', - '//lib/jgit/org.eclipse.jgit:jgit', - '//gerrit-common:annotations', - ], - visibility = ['PUBLIC'], - external_docs = [JGIT_DOC_URL, GUAVA_DOC_URL], -)
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 8e8931a..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.5</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..767fce6 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
@@ -19,6 +19,7 @@ import com.google.gerrit.extensions.client.EditPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.ProjectWatchInfo; +import com.google.gerrit.extensions.common.AccountExternalIdInfo; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.AgreementInfo; import com.google.gerrit.extensions.common.ChangeInfo; @@ -34,6 +35,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; @@ -62,6 +66,8 @@ void addEmail(EmailInput input) throws RestApiException; + void setStatus(String status) throws RestApiException; + List<SshKeyInfo> listSshKeys() throws RestApiException; SshKeyInfo addSshKey(String key) throws RestApiException; void deleteSshKey(int seq) throws RestApiException; @@ -76,146 +82,168 @@ void index() throws RestApiException; + List<AccountExternalIdInfo> getExternalIds() throws RestApiException; + void deleteExternalIds(List<String> externalIds) throws RestApiException; + /** * A default implementation which allows source compatibility * when adding new methods to the interface. **/ class NotImplemented implements AccountApi { @Override - public AccountInfo get() throws RestApiException { + public AccountInfo get() { throw new NotImplementedException(); } @Override - public String getAvatarUrl(int size) throws RestApiException { + public boolean getActive() { throw new NotImplementedException(); } @Override - public GeneralPreferencesInfo getPreferences() throws RestApiException { + public void setActive(boolean active) { throw new NotImplementedException(); } @Override - public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) - throws RestApiException { + public String getAvatarUrl(int size) { throw new NotImplementedException(); } @Override - public DiffPreferencesInfo getDiffPreferences() throws RestApiException { + public GeneralPreferencesInfo getPreferences() { throw new NotImplementedException(); } @Override - public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) - throws RestApiException { + public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) { throw new NotImplementedException(); } @Override - public EditPreferencesInfo getEditPreferences() throws RestApiException { + public DiffPreferencesInfo getDiffPreferences() { throw new NotImplementedException(); } @Override - public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) - throws RestApiException { + public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) { throw new NotImplementedException(); } @Override - public List<ProjectWatchInfo> getWatchedProjects() - throws RestApiException { + public EditPreferencesInfo getEditPreferences() { + throw new NotImplementedException(); + } + + @Override + public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) { + throw new NotImplementedException(); + } + + @Override + public List<ProjectWatchInfo> getWatchedProjects() { throw new NotImplementedException(); } @Override public List<ProjectWatchInfo> setWatchedProjects( - List<ProjectWatchInfo> in) throws RestApiException { + List<ProjectWatchInfo> in) { throw new NotImplementedException(); } @Override - public void deleteWatchedProjects(List<ProjectWatchInfo> in) - throws RestApiException { + public void deleteWatchedProjects(List<ProjectWatchInfo> in) { throw new NotImplementedException(); } @Override - public void starChange(String changeId) throws RestApiException { + public void starChange(String changeId) { throw new NotImplementedException(); } @Override - public void unstarChange(String changeId) throws RestApiException { + public void unstarChange(String changeId) { throw new NotImplementedException(); } @Override - public void setStars(String changeId, StarsInput input) - throws RestApiException { + public void setStars(String changeId, StarsInput input) { throw new NotImplementedException(); } @Override - public SortedSet<String> getStars(String changeId) throws RestApiException { + public SortedSet<String> getStars(String changeId) { throw new NotImplementedException(); } @Override - public List<ChangeInfo> getStarredChanges() throws RestApiException { + public List<ChangeInfo> getStarredChanges() { throw new NotImplementedException(); } @Override - public void addEmail(EmailInput input) throws RestApiException { + public void addEmail(EmailInput input) { throw new NotImplementedException(); } @Override - public List<SshKeyInfo> listSshKeys() throws RestApiException { + public void setStatus(String status) { throw new NotImplementedException(); } @Override - public SshKeyInfo addSshKey(String key) throws RestApiException { + public List<SshKeyInfo> listSshKeys() { throw new NotImplementedException(); } @Override - public void deleteSshKey(int seq) throws RestApiException { + public SshKeyInfo addSshKey(String key) { + throw new NotImplementedException(); + } + + @Override + public void deleteSshKey(int seq) { throw new NotImplementedException(); } @Override public Map<String, GpgKeyInfo> putGpgKeys(List<String> add, - List<String> remove) throws RestApiException { + List<String> remove) { throw new NotImplementedException(); } @Override - public GpgKeyApi gpgKey(String id) throws RestApiException { + public GpgKeyApi gpgKey(String id) { throw new NotImplementedException(); } @Override - public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException { + public Map<String, GpgKeyInfo> listGpgKeys() { throw new NotImplementedException(); } @Override - public List<AgreementInfo> listAgreements() throws RestApiException { + public List<AgreementInfo> listAgreements() { throw new NotImplementedException(); } @Override - public void signAgreement(String agreementName) throws RestApiException { + public void signAgreement(String agreementName) { throw new NotImplementedException(); } @Override - public void index() throws RestApiException { + public void index() { + throw new NotImplementedException(); + } + + @Override + public List<AccountExternalIdInfo> getExternalIds() { + throw new NotImplementedException(); + } + + @Override + public void deleteExternalIds(List<String> externalIds) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java index a697091..58f1b9b 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -79,7 +79,7 @@ throws RestApiException; /** - * Queries users. + * Query users. * <p> * Example code: * {@code query().withQuery("name:John email:example.com").withLimit(5).get()} @@ -89,7 +89,7 @@ QueryRequest query() throws RestApiException; /** - * Queries users. + * Query users. * <p> * Shortcut API for {@code query().withQuery(String)}. * @@ -108,7 +108,7 @@ private int limit; /** - * Executes query and returns a list of accounts. + * Execute query and return a list of accounts. */ public abstract List<AccountInfo> get() throws RestApiException; @@ -154,7 +154,7 @@ EnumSet.noneOf(ListAccountsOption.class); /** - * Executes query and returns a list of accounts. + * Execute query and return a list of accounts. */ public abstract List<AccountInfo> get() throws RestApiException; @@ -224,48 +224,47 @@ **/ class NotImplemented implements Accounts { @Override - public AccountApi id(String id) throws RestApiException { + public AccountApi id(String id) { throw new NotImplementedException(); } @Override - public AccountApi id(int id) throws RestApiException { + public AccountApi id(int id) { throw new NotImplementedException(); } @Override - public AccountApi self() throws RestApiException { + public AccountApi self() { throw new NotImplementedException(); } @Override - public AccountApi create(String username) throws RestApiException { + public AccountApi create(String username) { throw new NotImplementedException(); } @Override - public AccountApi create(AccountInput input) throws RestApiException { + public AccountApi create(AccountInput input) { throw new NotImplementedException(); } @Override - public SuggestAccountsRequest suggestAccounts() throws RestApiException { + public SuggestAccountsRequest suggestAccounts() { throw new NotImplementedException(); } @Override - public SuggestAccountsRequest suggestAccounts(String query) - throws RestApiException { + public SuggestAccountsRequest suggestAccounts(String query) { throw new NotImplementedException(); } @Override - public QueryRequest query() throws RestApiException { + public QueryRequest query() { throw new NotImplementedException(); } @Override - public QueryRequest query(String query) throws RestApiException { + public QueryRequest query(String query) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java index 6f87e8b..9bea6f9 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -28,12 +28,12 @@ */ class NotImplemented implements GpgKeyApi { @Override - public GpgKeyInfo get() throws RestApiException { + public GpgKeyInfo get() { throw new NotImplementedException(); } @Override - public void delete() throws RestApiException { + public void delete() { 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-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java new file mode 100644 index 0000000..61b5b85 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AssigneeInput.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.changes; + +import com.google.gerrit.extensions.restapi.DefaultInput; + +public class AssigneeInput { + @DefaultInput + public String assignee; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index f656c2d..efdf764 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,12 @@ 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.RobotCommentInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -94,9 +97,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,13 +113,15 @@ void publish() throws RestApiException; /** - * Deletes a draft change. + * Deletes a change. */ void delete() throws RestApiException; String topic() throws RestApiException; void topic(String topic) throws RestApiException; + IncludedInInfo includedIn() throws RestApiException; + void addReviewer(AddReviewerInput in) throws RestApiException; void addReviewer(String in) throws RestApiException; @@ -123,10 +134,25 @@ ChangeInfo get() throws RestApiException; /** {@code get} with {@link ListChangesOption} set to none. */ ChangeInfo info() throws RestApiException; - /** Retrieve change edit when exists. */ + + /** + * Retrieve change edit when exists. + * + * @deprecated Replaced by {@link ChangeApi#edit()} in combination with + * {@link ChangeEditApi#get()}. + */ + @Deprecated EditInfo getEdit() throws RestApiException; /** + * Provides access to an API regarding the change edit of this change. + * + * @return a {@code ChangeEditApi} for the change edit of this change + * @throws RestApiException if the API isn't accessible + */ + ChangeEditApi edit() throws RestApiException; + + /** * Set hashtags on a change **/ void setHashtags(HashtagsInput input) throws RestApiException; @@ -139,6 +165,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} @@ -148,6 +196,16 @@ Map<String, List<CommentInfo>> comments() throws RestApiException; /** + * Get all robot comments on a change. + * + * @return robot comments in a map keyed by path; robot comments have the + * {@code revision} field set to indicate their patch set. + * + * @throws RestApiException + */ + Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException; + + /** * Get all draft comments for the current user on a change. * * @return drafts in a map keyed by path; comments have the {@code revision} @@ -196,168 +254,215 @@ } @Override - public RevisionApi current() throws RestApiException { + public RevisionApi current() { throw new NotImplementedException(); } @Override - public RevisionApi revision(int id) throws RestApiException { + public RevisionApi revision(int id) { throw new NotImplementedException(); } @Override - public ReviewerApi reviewer(String id) throws RestApiException { + public ReviewerApi reviewer(String id) { throw new NotImplementedException(); } @Override - public RevisionApi revision(String id) throws RestApiException { + public RevisionApi revision(String id) { throw new NotImplementedException(); } @Override - public void abandon() throws RestApiException { + public void abandon() { throw new NotImplementedException(); } @Override - public void abandon(AbandonInput in) throws RestApiException { + public void abandon(AbandonInput in) { throw new NotImplementedException(); } @Override - public void restore() throws RestApiException { + public void restore() { throw new NotImplementedException(); } @Override - public void restore(RestoreInput in) throws RestApiException { + public void restore(RestoreInput in) { throw new NotImplementedException(); } @Override - public void move(String destination) throws RestApiException { + public void move(String destination) { throw new NotImplementedException(); } @Override - public void move(MoveInput in) throws RestApiException { + public void move(MoveInput in) { throw new NotImplementedException(); } @Override - public ChangeApi revert() throws RestApiException { + public ChangeApi revert() { throw new NotImplementedException(); } @Override - public ChangeApi revert(RevertInput in) throws RestApiException { + public ChangeApi revert(RevertInput in) { throw new NotImplementedException(); } @Override - public void publish() throws RestApiException { + public void publish() { throw new NotImplementedException(); } @Override - public void delete() throws RestApiException { + public void delete() { throw new NotImplementedException(); } @Override - public String topic() throws RestApiException { + public String topic() { throw new NotImplementedException(); } @Override - public void topic(String topic) throws RestApiException { + public void topic(String topic) { throw new NotImplementedException(); } @Override - public void addReviewer(AddReviewerInput in) throws RestApiException { + public IncludedInInfo includedIn() { throw new NotImplementedException(); } @Override - public void addReviewer(String in) throws RestApiException { + public void addReviewer(AddReviewerInput in) { throw new NotImplementedException(); } @Override - public SuggestedReviewersRequest suggestReviewers() throws RestApiException { + public void addReviewer(String in) { throw new NotImplementedException(); } @Override - public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException { + public SuggestedReviewersRequest suggestReviewers() { throw new NotImplementedException(); } @Override - public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException { + public SuggestedReviewersRequest suggestReviewers(String query) { throw new NotImplementedException(); } @Override - public ChangeInfo get() throws RestApiException { + public ChangeInfo get(EnumSet<ListChangesOption> options) { throw new NotImplementedException(); } @Override - public ChangeInfo info() throws RestApiException { + public ChangeInfo get() { throw new NotImplementedException(); } @Override - public EditInfo getEdit() throws RestApiException { + public ChangeInfo info() { throw new NotImplementedException(); } @Override - public void setHashtags(HashtagsInput input) throws RestApiException { + public EditInfo getEdit() { throw new NotImplementedException(); } @Override - public Set<String> getHashtags() throws RestApiException { + public ChangeEditApi edit() { throw new NotImplementedException(); } @Override - public Map<String, List<CommentInfo>> comments() throws RestApiException { + public void setHashtags(HashtagsInput input) { throw new NotImplementedException(); } @Override - public Map<String, List<CommentInfo>> drafts() throws RestApiException { + public Set<String> getHashtags() { throw new NotImplementedException(); } @Override - public ChangeInfo check() throws RestApiException { + public AccountInfo setAssignee(AssigneeInput input) { throw new NotImplementedException(); } @Override - public ChangeInfo check(FixInput fix) throws RestApiException { + public AccountInfo getAssignee() { throw new NotImplementedException(); } @Override - public void index() throws RestApiException { + public List<AccountInfo> getPastAssignees() { throw new NotImplementedException(); } @Override - public List<ChangeInfo> submittedTogether() throws RestApiException { + public AccountInfo deleteAssignee() { + throw new NotImplementedException(); + } + + @Override + public Map<String, List<CommentInfo>> comments() { + throw new NotImplementedException(); + } + + @Override + public Map<String, List<RobotCommentInfo>> robotComments() { + throw new NotImplementedException(); + } + + @Override + public Map<String, List<CommentInfo>> drafts() { + throw new NotImplementedException(); + } + + @Override + public ChangeInfo check() { + throw new NotImplementedException(); + } + + @Override + public ChangeInfo check(FixInput fix) { + throw new NotImplementedException(); + } + + @Override + public void index() { + throw new NotImplementedException(); + } + + @Override + public List<ChangeInfo> submittedTogether() { throw new NotImplementedException(); } @Override public SubmittedTogetherInfo submittedTogether( - EnumSet<SubmittedTogetherOption> options) throws RestApiException { + EnumSet<SubmittedTogetherOption> options) { + throw new NotImplementedException(); + } + + @Override + public SubmittedTogetherInfo submittedTogether( + EnumSet<ListChangesOption> a, + EnumSet<SubmittedTogetherOption> b) { + throw new NotImplementedException(); + } + + @Override + public ChangeInfo createMergePatchSet(MergePatchSetInput in) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java new file mode 100644 index 0000000..da3c1fc --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -0,0 +1,237 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.changes; + +import com.google.gerrit.extensions.common.EditInfo; +import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.NotImplementedException; +import com.google.gerrit.extensions.restapi.RawInput; +import com.google.gerrit.extensions.restapi.RestApiException; + +import java.util.Optional; + +/** + * An API for the change edit of a change. A change edit is similar to a patch + * set and will become one if it is published + * (by {@link #publish(PublishChangeEditInput)}). Whenever the descriptions + * below refer to files of a change edit, they actually refer to the files of + * the Git tree which is represented by the change edit. A change can have at + * most one change edit at each point in time. + */ +public interface ChangeEditApi { + + /** + * Retrieves details regarding the change edit. + * + * @return an {@code Optional} containing details about the change edit if it + * exists, or {@code Optional.empty()} + * @throws RestApiException if the change edit couldn't be retrieved + */ + Optional<EditInfo> get() throws RestApiException; + + /** + * Creates a new change edit. It has exactly the same Git tree as the current + * patch set of the change. + * + * @throws RestApiException if the change edit couldn't be created or a change + * edit already exists + */ + void create() throws RestApiException; + + /** + * Deletes the change edit. + * + * @throws RestApiException if the change edit couldn't be deleted or a change + * edit wasn't present + */ + void delete() throws RestApiException; + + /** + * Rebases the change edit on top of the latest patch set of this change. + * + * @throws RestApiException if the change edit couldn't be rebased or a change + * edit wasn't present + */ + void rebase() throws RestApiException; + + /** + * Publishes the change edit using default settings. See + * {@link #publish(PublishChangeEditInput)} for more details. + * + * @throws RestApiException if the change edit couldn't be published or a + * change edit wasn't present + */ + void publish() throws RestApiException; + + /** + * Publishes the change edit. Publishing means that the change edit is turned + * into a regular patch set of the change. + * + * @param publishChangeEditInput a {@code PublishChangeEditInput} specifying + * the options which should be applied + * @throws RestApiException if the change edit couldn't be published or a + * change edit wasn't present + */ + void publish(PublishChangeEditInput publishChangeEditInput) + throws RestApiException; + + /** + * Retrieves the contents of the specified file from the change edit. + * + * @param filePath the path of the file + * @return an {@code Optional} containing the contents of the file as a + * {@code BinaryResult} if the file exists within the change edit, or + * {@code Optional.empty()} + * @throws RestApiException if the contents of the file couldn't be retrieved + * or a change edit wasn't present + */ + Optional<BinaryResult> getFile(String filePath) throws RestApiException; + + /** + * Renames a file of the change edit or moves the file to another directory. + * If the change edit doesn't exist, it will be created based on the current + * patch set of the change. + * + * @param oldFilePath the current file path + * @param newFilePath the desired file path + * @throws RestApiException if the file couldn't be renamed + */ + void renameFile(String oldFilePath, String newFilePath) + throws RestApiException; + + /** + * Restores a file of the change edit to the state in which it was before the + * patch set on which the change edit is based. This includes the file content + * as well as the existence or non-existence of the file. If the change edit + * doesn't exist, it will be created based on the current patch set of the + * change. + * + * @param filePath the path of the file + * @throws RestApiException if the file couldn't be restored to its previous + * state + */ + void restoreFile(String filePath) throws RestApiException; + + /** + * Modify the contents of the specified file of the change edit. If no content + * is provided, the content of the file is erased but the file isn't deleted. + * If the change edit doesn't exist, it will be created based on the current + * patch set of the change. + * + * @param filePath the path of the file which should be modified + * @param newContent the desired content of the file + * @throws RestApiException if the content of the file couldn't be modified + */ + void modifyFile(String filePath, RawInput newContent) throws RestApiException; + + /** + * Deletes the specified file from the change edit. If the change edit doesn't + * exist, it will be created based on the current patch set of the change. + * + * @param filePath the path fo the file which should be deleted + * @throws RestApiException if the file couldn't be deleted + */ + void deleteFile(String filePath) throws RestApiException; + + /** + * Retrieves the commit message of the change edit. + * + * @return the commit message of the change edit + * @throws RestApiException if the commit message couldn't be retrieved or a + * change edit wasn't present + */ + String getCommitMessage() throws RestApiException; + + /** + * Modifies the commit message of the change edit. If the change edit doesn't + * exist, it will be created based on the current patch set of the change. + * + * @param newCommitMessage the desired commit message + * @throws RestApiException if the commit message couldn't be modified + */ + void modifyCommitMessage(String newCommitMessage) throws RestApiException; + + /** + * A default implementation which allows source compatibility + * when adding new methods to the interface. + **/ + class NotImplemented implements ChangeEditApi { + @Override + public Optional<EditInfo> get() { + throw new NotImplementedException(); + } + + @Override + public void create() { + throw new NotImplementedException(); + } + + @Override + public void delete() { + throw new NotImplementedException(); + } + + @Override + public void rebase() { + throw new NotImplementedException(); + } + + @Override + public void publish() { + throw new NotImplementedException(); + } + + @Override + public void publish(PublishChangeEditInput publishChangeEditInput) { + throw new NotImplementedException(); + } + + @Override + public Optional<BinaryResult> getFile(String filePath) { + throw new NotImplementedException(); + } + + @Override + public void renameFile(String oldFilePath, String newFilePath) { + throw new NotImplementedException(); + } + + @Override + public void restoreFile(String filePath) { + throw new NotImplementedException(); + } + + @Override + public void modifyFile(String filePath, RawInput newContent) { + throw new NotImplementedException(); + } + + @Override + public void deleteFile(String filePath) { + throw new NotImplementedException(); + } + + @Override + public String getCommitMessage() { + throw new NotImplementedException(); + } + + @Override + public void modifyCommitMessage(String newCommitMessage) { + throw new NotImplementedException(); + } + } + +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java index aa67473..f0d3bfa 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -142,22 +142,22 @@ **/ class NotImplemented implements Changes { @Override - public ChangeApi id(int id) throws RestApiException { + public ChangeApi id(int id) { throw new NotImplementedException(); } @Override - public ChangeApi id(String triplet) throws RestApiException { + public ChangeApi id(String triplet) { throw new NotImplementedException(); } @Override - public ChangeApi id(String project, String branch, String id) throws RestApiException { + public ChangeApi id(String project, String branch, String id) { throw new NotImplementedException(); } @Override - public ChangeApi create(ChangeInput in) throws RestApiException { + public ChangeApi create(ChangeInput in) { 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/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java index adac284..e4c2781 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -27,7 +27,7 @@ **/ class NotImplemented implements CommentApi { @Override - public CommentInfo get() throws RestApiException { + public CommentInfo get() { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java new file mode 100644 index 0000000..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-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java index 50335db..f4c79bb 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -29,12 +29,12 @@ class NotImplemented extends CommentApi.NotImplemented implements DraftApi { @Override - public CommentInfo update(DraftInput in) throws RestApiException { + public CommentInfo update(DraftInput in) { throw new NotImplementedException(); } @Override - public void delete() throws RestApiException { + public void delete() { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java index 2536c46..a0ca4d0 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -96,27 +96,27 @@ **/ class NotImplemented implements FileApi { @Override - public BinaryResult content() throws RestApiException { + public BinaryResult content() { throw new NotImplementedException(); } @Override - public DiffInfo diff() throws RestApiException { + public DiffInfo diff() { throw new NotImplementedException(); } @Override - public DiffInfo diff(String base) throws RestApiException { + public DiffInfo diff(String base) { throw new NotImplementedException(); } @Override - public DiffInfo diff(int parent) throws RestApiException { + public DiffInfo diff(int parent) { throw new NotImplementedException(); } @Override - public DiffRequest diffRequest() throws RestApiException { + public DiffRequest diffRequest() { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java new file mode 100644 index 0000000..f1fc3ac --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/IncludedInInfo.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.gerrit.extensions.api.changes; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class IncludedInInfo { + public List<String> branches; + public List<String> tags; + public Map<String, Collection<String>> external; + + public IncludedInInfo(List<String> branches, + List<String> tags, + Map<String, Collection<String>> external) { + this.branches = branches; + this.tags = tags; + this.external = external; + } +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.java new file mode 100644 index 0000000..ef49651 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/NotifyInfo.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.api.changes; + +import java.util.List; + +/** 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-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java new file mode 100644 index 0000000..e3b8a53 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RecipientType.java
@@ -0,0 +1,19 @@ +// 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.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..10f5bc7 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 @@ -32,22 +33,27 @@ **/ class NotImplemented implements ReviewerApi { @Override - public Map<String, Short> votes() throws RestApiException { + public Map<String, Short> votes() { throw new NotImplementedException(); } @Override - public void deleteVote(String label) throws RestApiException { + public void deleteVote(String label) { throw new NotImplementedException(); } @Override - public void deleteVote(DeleteVoteInput input) throws RestApiException { + public void deleteVote(DeleteVoteInput input) { throw new NotImplementedException(); } @Override - public void remove() throws RestApiException { + public void remove() { + throw new NotImplementedException(); + } + + @Override + public void remove(DeleteReviewerInput input) { 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..a763b77 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,16 +33,22 @@ 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; ChangeApi rebase(RebaseInput in) throws RestApiException; boolean canRebase() throws RestApiException; + RevisionReviewerApi reviewer(String id) throws RestApiException; void setReviewed(String path, boolean reviewed) throws RestApiException; Set<String> reviewed() throws RestApiException; @@ -52,68 +60,101 @@ 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. **/ class NotImplemented implements RevisionApi { @Override - public void delete() throws RestApiException { + public void delete() { throw new NotImplementedException(); } @Override - public void review(ReviewInput in) throws RestApiException { + public void review(ReviewInput in) { throw new NotImplementedException(); } @Override - public void submit() throws RestApiException { + public void submit() { throw new NotImplementedException(); } @Override - public void submit(SubmitInput in) throws RestApiException { + public void submit(SubmitInput in) { throw new NotImplementedException(); } @Override - public void publish() throws RestApiException { + public void publish() { throw new NotImplementedException(); } @Override - public ChangeApi cherryPick(CherryPickInput in) throws RestApiException { + public ChangeApi cherryPick(CherryPickInput in) { throw new NotImplementedException(); } @Override - public ChangeApi rebase() throws RestApiException { + public ChangeApi rebase() { throw new NotImplementedException(); } @Override - public ChangeApi rebase(RebaseInput in) throws RestApiException { + public ChangeApi rebase(RebaseInput in) { throw new NotImplementedException(); } @@ -123,37 +164,42 @@ } @Override - public void setReviewed(String path, boolean reviewed) throws RestApiException { + public RevisionReviewerApi reviewer(String id) { throw new NotImplementedException(); } @Override - public Set<String> reviewed() throws RestApiException { + public void setReviewed(String path, boolean reviewed) { throw new NotImplementedException(); } @Override - public MergeableInfo mergeable() throws RestApiException { + public Set<String> reviewed() { throw new NotImplementedException(); } @Override - public MergeableInfo mergeableOtherBranches() throws RestApiException { + public MergeableInfo mergeable() { throw new NotImplementedException(); } @Override - public Map<String, FileInfo> files(String base) throws RestApiException { + public MergeableInfo mergeableOtherBranches() { throw new NotImplementedException(); } @Override - public Map<String, FileInfo> files(int parentNum) throws RestApiException { + public Map<String, FileInfo> files(String base) { throw new NotImplementedException(); } @Override - public Map<String, FileInfo> files() throws RestApiException { + public Map<String, FileInfo> files(int parentNum) { + throw new NotImplementedException(); + } + + @Override + public Map<String, FileInfo> files() { throw new NotImplementedException(); } @@ -163,58 +209,107 @@ } @Override - public Map<String, List<CommentInfo>> comments() throws RestApiException { + public Map<String, List<CommentInfo>> comments() { throw new NotImplementedException(); } @Override - public List<CommentInfo> commentsAsList() throws RestApiException { + public Map<String, List<RobotCommentInfo>> robotComments() { throw new NotImplementedException(); } @Override - public List<CommentInfo> draftsAsList() throws RestApiException { + public List<CommentInfo> commentsAsList() { throw new NotImplementedException(); } @Override - public Map<String, List<CommentInfo>> drafts() throws RestApiException { + public List<CommentInfo> draftsAsList() { throw new NotImplementedException(); } @Override - public DraftApi createDraft(DraftInput in) throws RestApiException { + public List<RobotCommentInfo> robotCommentsAsList() { throw new NotImplementedException(); } @Override - public DraftApi draft(String id) throws RestApiException { + public Map<String, List<CommentInfo>> drafts() { throw new NotImplementedException(); } @Override - public CommentApi comment(String id) throws RestApiException { + public DraftApi createDraft(DraftInput in) { throw new NotImplementedException(); } @Override - public BinaryResult patch() throws RestApiException { + public DraftApi draft(String id) { throw new NotImplementedException(); } @Override - public Map<String, ActionInfo> actions() throws RestApiException { + public CommentApi comment(String id) { throw new NotImplementedException(); } @Override - public SubmitType submitType() throws RestApiException { + public RobotCommentApi robotComment(String id) { throw new NotImplementedException(); } @Override - public SubmitType testSubmitType(TestSubmitRuleInput in) - throws RestApiException { + public BinaryResult patch() { + throw new NotImplementedException(); + } + + @Override + public BinaryResult patch(String path) { + throw new NotImplementedException(); + } + + @Override + public Map<String, ActionInfo> actions() { + throw new NotImplementedException(); + } + + @Override + public SubmitType submitType() { + throw new NotImplementedException(); + } + + @Override + public BinaryResult submitPreview() { + throw new NotImplementedException(); + } + + @Override + public BinaryResult submitPreview(String format) { + throw new NotImplementedException(); + } + + @Override + public SubmitType testSubmitType(TestSubmitRuleInput in) { + throw new NotImplementedException(); + } + + @Override + public MergeListRequest getMergeList() { + throw new NotImplementedException(); + } + + @Override + public void description(String description) { + throw new NotImplementedException(); + } + + @Override + public String description() { + throw new NotImplementedException(); + } + + @Override + public String etag() { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java new file mode 100644 index 0000000..4e13f90 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
@@ -0,0 +1,49 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.changes; + +import com.google.gerrit.extensions.restapi.NotImplementedException; +import com.google.gerrit.extensions.restapi.RestApiException; + +import java.util.Map; + +public interface RevisionReviewerApi { + Map<String, Short> votes() throws RestApiException; + + void deleteVote(String label) throws RestApiException; + + void deleteVote(DeleteVoteInput input) throws RestApiException; + + /** + * A default implementation which allows source compatibility + * when adding new methods to the interface. + **/ + class NotImplemented implements RevisionReviewerApi { + @Override + public Map<String, Short> votes() { + throw new NotImplementedException(); + } + + @Override + public void deleteVote(String label) { + throw new NotImplementedException(); + } + + @Override + public void deleteVote(DeleteVoteInput input) { + 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..38d9f95 --- /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() { + 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..aa7ccdd 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; @@ -38,31 +41,34 @@ **/ class NotImplemented implements Server { @Override - public String getVersion() throws RestApiException { + public String getVersion() { throw new NotImplementedException(); } @Override - public GeneralPreferencesInfo getDefaultPreferences() - throws RestApiException { + public ServerInfo getInfo() { + throw new NotImplementedException(); + } + + @Override + public GeneralPreferencesInfo getDefaultPreferences() { throw new NotImplementedException(); } @Override public GeneralPreferencesInfo setDefaultPreferences( - GeneralPreferencesInfo in) throws RestApiException { + GeneralPreferencesInfo in) { throw new NotImplementedException(); } @Override - public DiffPreferencesInfo getDefaultDiffPreferences() - throws RestApiException { + public DiffPreferencesInfo getDefaultDiffPreferences() { throw new NotImplementedException(); } @Override - public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) - throws RestApiException { + public DiffPreferencesInfo setDefaultDiffPreferences( + DiffPreferencesInfo in) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java index d3f4463..e651069 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -144,62 +144,71 @@ List<? extends GroupAuditEventInfo> auditLog() throws RestApiException; /** + * Reindexes the group. + * + * Only supported for internal groups. + * + * @throws RestApiException + */ + void index() throws RestApiException; + + /** * A default implementation which allows source compatibility * when adding new methods to the interface. **/ class NotImplemented implements GroupApi { @Override - public GroupInfo get() throws RestApiException { + public GroupInfo get() { throw new NotImplementedException(); } @Override - public GroupInfo detail() throws RestApiException { + public GroupInfo detail() { throw new NotImplementedException(); } @Override - public String name() throws RestApiException { + public String name() { throw new NotImplementedException(); } @Override - public void name(String name) throws RestApiException { + public void name(String name) { throw new NotImplementedException(); } @Override - public GroupInfo owner() throws RestApiException { + public GroupInfo owner() { throw new NotImplementedException(); } @Override - public void owner(String owner) throws RestApiException { + public void owner(String owner) { throw new NotImplementedException(); } @Override - public String description() throws RestApiException { + public String description() { throw new NotImplementedException(); } @Override - public void description(String description) throws RestApiException { + public void description(String description) { throw new NotImplementedException(); } @Override - public GroupOptionsInfo options() throws RestApiException { + public GroupOptionsInfo options() { throw new NotImplementedException(); } @Override - public void options(GroupOptionsInfo options) throws RestApiException { + public void options(GroupOptionsInfo options) { throw new NotImplementedException(); } @Override - public List<AccountInfo> members() throws RestApiException { + public List<AccountInfo> members() { throw new NotImplementedException(); } @@ -210,27 +219,27 @@ } @Override - public void addMembers(String... members) throws RestApiException { + public void addMembers(String... members) { throw new NotImplementedException(); } @Override - public void removeMembers(String... members) throws RestApiException { + public void removeMembers(String... members) { throw new NotImplementedException(); } @Override - public List<GroupInfo> includedGroups() throws RestApiException { + public List<GroupInfo> includedGroups() { throw new NotImplementedException(); } @Override - public void addGroups(String... groups) throws RestApiException { + public void addGroups(String... groups) { throw new NotImplementedException(); } @Override - public void removeGroups(String... groups) throws RestApiException { + public void removeGroups(String... groups) { throw new NotImplementedException(); } @@ -239,5 +248,10 @@ throws RestApiException { throw new NotImplementedException(); } + + @Override + public void index() { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java index b58009d..e5491b5 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -52,6 +52,25 @@ /** @return new request for listing groups. */ ListRequest list(); + /** + * Query groups. + * <p> + * Example code: + * {@code query().withQuery("inname:test").withLimit(10).get()} + * + * @return API for setting parameters and getting result. + */ + QueryRequest query(); + + /** + * Query groups. + * <p> + * Shortcut API for {@code query().withQuery(String)}. + * + * @see #query() + */ + QueryRequest query(String query); + abstract class ListRequest { private final EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class); @@ -182,22 +201,100 @@ } /** + * API for setting parameters and getting result. + * Used for {@code query()}. + * + * @see #query() + */ + abstract class QueryRequest { + private String query; + private int limit; + private int start; + private EnumSet<ListGroupsOption> options = + EnumSet.noneOf(ListGroupsOption.class); + + /** + * Execute query and returns the matched groups as list. + */ + public abstract List<GroupInfo> get() throws RestApiException; + + /** + * Set query. + * + * @param query needs to be in human-readable form. + */ + public QueryRequest withQuery(String query) { + this.query = query; + return this; + } + + /** + * Set limit for returned list of groups. + * Optional; server-default is used when not provided. + */ + public QueryRequest withLimit(int limit) { + this.limit = limit; + return this; + } + + /** + * Set number of groups to skip. + * Optional; no groups are skipped when not provided. + */ + public QueryRequest withStart(int start) { + this.start = start; + return this; + } + + public QueryRequest withOption(ListGroupsOption options) { + this.options.add(options); + return this; + } + + public QueryRequest withOptions(ListGroupsOption... options) { + this.options.addAll(Arrays.asList(options)); + return this; + } + + public QueryRequest withOptions(EnumSet<ListGroupsOption> options) { + this.options = options; + return this; + } + + public String getQuery() { + return query; + } + + public int getLimit() { + return limit; + } + + public int getStart() { + return start; + } + + public EnumSet<ListGroupsOption> getOptions() { + return options; + } + } + + /** * A default implementation which allows source compatibility * when adding new methods to the interface. **/ class NotImplemented implements Groups { @Override - public GroupApi id(String id) throws RestApiException { + public GroupApi id(String id) { throw new NotImplementedException(); } @Override - public GroupApi create(String name) throws RestApiException { + public GroupApi create(String name) { throw new NotImplementedException(); } @Override - public GroupApi create(GroupInput input) throws RestApiException { + public GroupApi create(GroupInput input) { throw new NotImplementedException(); } @@ -205,5 +302,15 @@ public ListRequest list() { throw new NotImplementedException(); } + + @Override + public QueryRequest query() { + throw new NotImplementedException(); + } + + @Override + public QueryRequest query(String query) { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java index 222248d..5c31437 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -36,22 +36,22 @@ **/ class NotImplemented implements BranchApi { @Override - public BranchApi create(BranchInput in) throws RestApiException { + public BranchApi create(BranchInput in) { throw new NotImplementedException(); } @Override - public BranchInfo get() throws RestApiException { + public BranchInfo get() { throw new NotImplementedException(); } @Override - public void delete() throws RestApiException { + public void delete() { throw new NotImplementedException(); } @Override - public BinaryResult file(String path) throws RestApiException { + public BinaryResult file(String path) { 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-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java index 3bffac0..574cb3a 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -28,12 +28,12 @@ **/ class NotImplemented implements ChildProjectApi { @Override - public ProjectInfo get() throws RestApiException { + public ProjectInfo get() { throw new NotImplementedException(); } @Override - public ProjectInfo get(boolean recursive) throws RestApiException { + public ProjectInfo get(boolean recursive) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.java new file mode 100644 index 0000000..b933624 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/DeleteTagsInput.java
@@ -0,0 +1,21 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.api.projects; + +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..dd3d61e 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; @@ -120,49 +121,47 @@ **/ class NotImplemented implements ProjectApi { @Override - public ProjectApi create() throws RestApiException { + public ProjectApi create() { throw new NotImplementedException(); } @Override - public ProjectApi create(ProjectInput in) throws RestApiException { + public ProjectApi create(ProjectInput in) { throw new NotImplementedException(); } @Override - public ProjectInfo get() throws RestApiException { + public ProjectInfo get() { throw new NotImplementedException(); } @Override - public String description() throws RestApiException { + public String description() { throw new NotImplementedException(); } @Override - public ProjectAccessInfo access() throws RestApiException { + public ProjectAccessInfo access() { throw new NotImplementedException(); } @Override - public ConfigInfo config() throws RestApiException { + public ConfigInfo config() { throw new NotImplementedException(); } @Override - public ConfigInfo config(ConfigInput in) throws RestApiException { + public ConfigInfo config(ConfigInput in) { throw new NotImplementedException(); } @Override - public ProjectAccessInfo access(ProjectAccessInput p) - throws RestApiException { + public ProjectAccessInfo access(ProjectAccessInput p) { throw new NotImplementedException(); } @Override - public void description(DescriptionInput in) - throws RestApiException { + public void description(DescriptionInput in) { throw new NotImplementedException(); } @@ -182,27 +181,32 @@ } @Override - public List<ProjectInfo> children(boolean recursive) throws RestApiException { + public List<ProjectInfo> children(boolean recursive) { throw new NotImplementedException(); } @Override - public ChildProjectApi child(String name) throws RestApiException { + public ChildProjectApi child(String name) { throw new NotImplementedException(); } @Override - public BranchApi branch(String ref) throws RestApiException { + public BranchApi branch(String ref) { throw new NotImplementedException(); } @Override - public TagApi tag(String ref) throws RestApiException { + public TagApi tag(String ref) { throw new NotImplementedException(); } @Override - public void deleteBranches(DeleteBranchesInput in) throws RestApiException { + public void deleteBranches(DeleteBranchesInput in) { + throw new NotImplementedException(); + } + + @Override + public void deleteTags(DeleteTagsInput in) { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java index fdbadb2..7992348 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -177,17 +177,17 @@ **/ class NotImplemented implements Projects { @Override - public ProjectApi name(String name) throws RestApiException { + public ProjectApi name(String name) { throw new NotImplementedException(); } @Override - public ProjectApi create(ProjectInput in) throws RestApiException { + public ProjectApi create(ProjectInput in) { throw new NotImplementedException(); } @Override - public ProjectApi create(String name) throws RestApiException { + public ProjectApi create(String name) { 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..97dbf15 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,18 +22,25 @@ TagInfo get() throws RestApiException; + void delete() throws RestApiException; + /** * A default implementation which allows source compatibility * when adding new methods to the interface. **/ class NotImplemented implements TagApi { @Override - public TagApi create(TagInput input) throws RestApiException { + public TagApi create(TagInput input) { throw new NotImplementedException(); } @Override - public TagInfo get() throws RestApiException { + public TagInfo get() { + throw new NotImplementedException(); + } + + @Override + public void delete() { throw new NotImplementedException(); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java new file mode 100644 index 0000000..07d9f37 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.client; + +public enum AccountFieldName { + FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java new file mode 100644 index 0000000..2056e25 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -0,0 +1,87 @@ +// 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.extensions.client; + +public enum AuthType { + /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> */ + OPENID, + + /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> in Single Sign On mode */ + OPENID_SSO, + + /** + * Login relies upon the container/web server security. + * <p> + * The container or web server must populate an HTTP header with a unique name + * for the current user. Gerrit will implicitly trust the value of this header + * to supply the unique identity. + */ + HTTP, + + /** + * Login relies upon the container/web server security, but also uses LDAP. + * <p> + * Like {@link #HTTP}, the container or web server must populate an HTTP + * header with a unique name for the current user. Gerrit will implicitly + * trust the value of this header to supply the unique identity. + * <p> + * In addition to trusting the HTTP headers, Gerrit will obtain basic user + * registration (name and email) from LDAP, and some group memberships. + */ + HTTP_LDAP, + + /** + * Login via client SSL certificate. + * <p> + * This authentication type is actually kind of SSO. Gerrit will configure + * 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. + * <p> + * After the authentication is done Gerrit will obtain basic user + * registration (name and email) from LDAP, and some group memberships. + * Therefore, the "_LDAP" suffix in the name of this authentication type. + */ + CLIENT_SSL_CERT_LDAP, + + /** + * Login collects username and password through a web form, and binds to LDAP. + * <p> + * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and + * makes the connection to the LDAP server on their behalf. + */ + LDAP, + + /** + * Login collects username and password through a web form, and binds to LDAP. + * <p> + * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and + * makes the connection to the LDAP server on their behalf. + * <p> + * Unlike the more generic {@link #LDAP} mode, Gerrit can only query the + * directory via an actual authenticated user account. + */ + LDAP_BIND, + + /** Login is managed by additional, unspecified code. */ + CUSTOM_EXTENSION, + + /** Development mode to enable becoming anyone you want. */ + DEVELOPMENT_BECOME_ANY_ACCOUNT, + + /** Generic OAuth provider over HTTP. */ + OAUTH +}
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..0bf116a 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-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/Theme.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java index 6408f9d..d7a5b80 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
@@ -18,6 +18,7 @@ // Light themes DEFAULT, DAY_3024, + DUOTONE_LIGHT, BASE16_LIGHT, ECLIPSE, ELEGANT, @@ -40,6 +41,7 @@ COBALT, COLORFORTH, DRACULA, + DUOTONE_DARK, ERLANG_DARK, HOPSCOTCH, ICECODER, @@ -66,7 +68,6 @@ public boolean isDark() { switch (this) { - case NIGHT_3024: case ABCDEF: case AMBIANCE: case BASE16_DARK: @@ -75,6 +76,7 @@ case COBALT: case COLORFORTH: case DRACULA: + case DUOTONE_DARK: case ERLANG_DARK: case HOPSCOTCH: case ICECODER: @@ -86,6 +88,7 @@ case MIDNIGHT: case MONOKAI: case NIGHT: + case NIGHT_3024: case PARAISO_DARK: case PASTEL_ON_DARK: case RAILSCASTS: @@ -99,9 +102,10 @@ case XQ_DARK: case ZENBURN: return true; + case BASE16_LIGHT: case DEFAULT: case DAY_3024: - case BASE16_LIGHT: + case DUOTONE_LIGHT: case ECLIPSE: case ELEGANT: case MDN_LIKE:
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/AccountExternalIdInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java new file mode 100644 index 0000000..3bcf387 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
@@ -0,0 +1,52 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import com.google.common.collect.ComparisonChain; + +import java.util.Objects; + +public class AccountExternalIdInfo + implements Comparable<AccountExternalIdInfo> { + public String identity; + public String emailAddress; + public Boolean trusted; + public Boolean canDelete; + + @Override + public int compareTo(AccountExternalIdInfo a) { + return ComparisonChain.start() + .compare(a.identity, identity) + .compare(a.emailAddress, emailAddress) + .result(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof AccountExternalIdInfo) { + AccountExternalIdInfo a = (AccountExternalIdInfo) o; + return (Objects.equals(a.identity, identity)) + && (Objects.equals(a.emailAddress, emailAddress)) + && (Objects.equals(a.trusted, trusted)) + && (Objects.equals(a.canDelete, canDelete)); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(identity, emailAddress, trusted, canDelete); + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java index 2c35d5e..2fb32d7 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -24,6 +24,7 @@ public String username; public List<AvatarInfo> avatars; public Boolean _moreAccounts; + public String status; public AccountInfo(Integer id) { this._accountId = id;
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-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java index 88c3ea8..b033190 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -14,8 +14,13 @@ package com.google.gerrit.extensions.common; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.NotifyInfo; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.client.ChangeStatus; +import java.util.Map; + public class ChangeInput { public String project; public String branch; @@ -26,4 +31,8 @@ public String baseChange; public Boolean newBranch; public MergeInput merge; + + /** Who to send email notifications to after change is created. */ + public NotifyHandling notify = NotifyHandling.ALL; + public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java new file mode 100644 index 0000000..180e2d2 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; +import java.util.Map; + +public class DownloadInfo { + public Map<String, DownloadSchemeInfo> schemes; + public List<String> archives; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java new file mode 100644 index 0000000..0e8ad65 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.Map; + +public class DownloadSchemeInfo { + public String url; + public Boolean isAuthRequired; + public Boolean isAuthSupported; + public Map<String, String> commands; + public Map<String, String> cloneCommands; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.java new file mode 100644 index 0000000..9e5890e --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixReplacementInfo.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import com.google.gerrit.extensions.client.Comment; + +public class FixReplacementInfo { + public String path; + public Comment.Range range; + public String replacement; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java new file mode 100644 index 0000000..7ba7fcc --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FixSuggestionInfo.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; + +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-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java index f956a03..55fb92a 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -25,6 +25,7 @@ public Integer groupId; public String owner; public String ownerId; + public Boolean _moreGroups; // These fields are only supplied for internal groups, and only if requested. public List<AccountInfo> members;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java new file mode 100644 index 0000000..263b6c4 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergePatchSetInput.java
@@ -0,0 +1,21 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class MergePatchSetInput { + public String subject; + public boolean inheritParent; + public MergeInput merge; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java new file mode 100644 index 0000000..845f7cb7 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import java.util.List; + +public class PluginConfigInfo { + public Boolean hasAvatars; + public List<String> jsResourcePaths; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java new file mode 100644 index 0000000..e66c242 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class ReceiveInfo { + public Boolean enableSignedPush; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/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-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java new file mode 100644 index 0000000..8d8731f --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.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; + +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-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java new file mode 100644 index 0000000..98d650c --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
@@ -0,0 +1,18 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class SshdInfo { +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java new file mode 100644 index 0000000..5b0dcbe --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class SuggestInfo { + public int from; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java new file mode 100644 index 0000000..5010689 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class UserConfigInfo { + public String anonymousCowardName; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java new file mode 100644 index 0000000..5c35a49 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/VotingRangeInfo.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +public class 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/config/ExternalIncludedIn.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java index d78fa63..9e12c8a 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
@@ -14,7 +14,7 @@ package com.google.gerrit.extensions.config; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.annotations.ExtensionPoint; import java.util.Collection; @@ -37,6 +37,6 @@ * @param branches the branches that include the commit * @return additional entries for IncludedInInfo */ - Multimap<String, String> getIncludedIn(String project, String commit, + ListMultimap<String, String> getIncludedIn(String project, String commit, Collection<String> tags, Collection<String> branches); }
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/main/java/com/google/gerrit/extensions/restapi/NeedsParams.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NeedsParams.java new file mode 100644 index 0000000..28dcc99 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/NeedsParams.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.restapi; + +import com.google.common.collect.ListMultimap; + +/** + * Optional interface for {@link RestCollection}. + * <p> + * Collections that implement this interface can get to know about the request + * parameters. + */ +public interface NeedsParams { + /** + * Sets the request parameter. + * + * @param params the request parameter + */ + void setParams(ListMultimap<String, String> params) throws RestApiException; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java index b9004f2..1ff4960 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -34,5 +34,5 @@ * @return WebLinkInfo that links to patch set in external service, * null if there should be no link. */ - WebLinkInfo getPatchSetWebLink(final String projectName, final String commit); + WebLinkInfo getPatchSetWebLink(String projectName, String commit); }
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 deleted file mode 100644 index 73d9f04..0000000 --- a/gerrit-gpg/BUCK +++ /dev/null
@@ -1,57 +0,0 @@ -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', -] - -java_library( - name = 'gpg', - srcs = glob(['src/main/java/**/*.java']), - provided_deps = DEPS + [ - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcprov', - ], - visibility = ['PUBLIC'], -) - -TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java']) - -java_library( - name = 'testutil', - srcs = TESTUTIL_SRCS, - deps = DEPS + [ - ':gpg', - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcprov', - ], - visibility = ['PUBLIC'], -) - -java_test( - name = 'gpg_tests', - srcs = glob( - ['src/test/java/**/*.java'], - excludes = TESTUTIL_SRCS, - ), - deps = DEPS + [ - ':gpg', - ':testutil', - '//gerrit-cache-h2:cache-h2', - '//gerrit-lucene:lucene', - '//gerrit-server:testutil', - '//lib:truth', - '//lib/bouncycastle:bcpg', - '//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..be929ed 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,20 +18,15 @@ 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; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -66,8 +61,6 @@ @Singleton public static class Factory { - private final Provider<ReviewDb> db; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; private final String webUrl; private final IdentifiedUser.GenericFactory userFactory; @@ -76,13 +69,9 @@ @Inject Factory(@GerritServerConfig Config cfg, - Provider<ReviewDb> db, - AccountIndexCollection accountIndexes, Provider<InternalAccountQuery> accountQueryProvider, IdentifiedUser.GenericFactory userFactory, @CanonicalWebUrl String webUrl) { - this.db = db; - this.accountIndexes = accountIndexes; this.accountQueryProvider = accountQueryProvider; this.webUrl = webUrl; this.userFactory = userFactory; @@ -116,8 +105,6 @@ } } - private final Provider<ReviewDb> db; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; private final String webUrl; private final IdentifiedUser.GenericFactory userFactory; @@ -125,8 +112,6 @@ private IdentifiedUser expectedUser; private GerritPublicKeyChecker(Factory factory) { - this.db = factory.db; - this.accountIndexes = factory.accountIndexes; this.accountQueryProvider = factory.accountQueryProvider; this.webUrl = factory.webUrl; this.userFactory = factory.userFactory; @@ -177,25 +162,15 @@ private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException { - IdentifiedUser user; - if (accountIndexes.getSearchIndex() != null) { - List<AccountState> accountStates = - accountQueryProvider.get().byExternalId(toExtIdKey(key).get()); - if (accountStates.isEmpty()) { - return CheckResult.bad("Key is not associated with any users"); - } - if (accountStates.size() > 1) { - return CheckResult.bad("Key is associated with multiple users"); - } - user = userFactory.create(accountStates.get(0)); - } else { - AccountExternalId extId = db.get().accountExternalIds().get( - toExtIdKey(key)); - if (extId == null) { - return CheckResult.bad("Key is not associated with any users"); - } - user = userFactory.create(extId.getAccountId()); + List<AccountState> accountStates = + accountQueryProvider.get().byExternalId(toExtIdKey(key).get()); + if (accountStates.isEmpty()) { + return CheckResult.bad("Key is not associated with any users"); } + if (accountStates.size() > 1) { + return CheckResult.bad("Key is associated with multiple users"); + } + IdentifiedUser user = userFactory.create(accountStates.get(0)); Set<String> allowedUserIds = getAllowedUserIds(user); if (allowedUserIds.isEmpty()) { @@ -227,12 +202,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 +249,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..20db31f 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; @@ -48,8 +47,7 @@ import com.google.gerrit.server.account.AccountCache; 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; @@ -91,7 +89,6 @@ private final GerritPublicKeyChecker.Factory checkerFactory; private final AddKeySender.Factory addKeyFactory; private final AccountCache accountCache; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; @Inject @@ -102,7 +99,6 @@ GerritPublicKeyChecker.Factory checkerFactory, AddKeySender.Factory addKeyFactory, AccountCache accountCache, - AccountIndexCollection accountIndexes, Provider<InternalAccountQuery> accountQueryProvider) { this.serverIdent = serverIdent; this.db = db; @@ -111,7 +107,6 @@ this.checkerFactory = checkerFactory; this.addKeyFactory = addKeyFactory; this.accountCache = accountCache; - this.accountIndexes = accountIndexes; this.accountQueryProvider = accountQueryProvider; } @@ -132,28 +127,15 @@ for (PGPPublicKeyRing keyRing : newKeys) { PGPPublicKey key = keyRing.getPublicKey(); AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint()); - if (accountIndexes.getSearchIndex() != null) { - Account account = getAccountByExternalId(extIdKey.get()); - if (account != null) { - if (!account.getId().equals(rsrc.getUser().getAccountId())) { - throw new ResourceConflictException( - "GPG key already associated with another account"); - } - } else { - newExtIds.add( - new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey)); + Account account = getAccountByExternalId(extIdKey.get()); + if (account != null) { + if (!account.getId().equals(rsrc.getUser().getAccountId())) { + throw new ResourceConflictException( + "GPG key already associated with another account"); } } else { - AccountExternalId existing = db.get().accountExternalIds().get(extIdKey); - if (existing != null) { - if (!existing.getAccountId().equals(rsrc.getUser().getAccountId())) { - throw new ResourceConflictException( - "GPG key already associated with another account"); - } - } else { - newExtIds.add( - new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey)); - } + newExtIds.add( + new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey)); } } @@ -161,13 +143,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/BUCK b/gerrit-gwtdebug/BUCK deleted file mode 100644 index 3670916..0000000 --- a/gerrit-gwtdebug/BUCK +++ /dev/null
@@ -1,17 +0,0 @@ -java_library( - name = 'gwtdebug', - srcs = glob(['src/main/java/**/*.java']), - 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', - ], - visibility = ['//tools/eclipse:classpath'], -)
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 deleted file mode 100644 index 79a97a9..0000000 --- a/gerrit-gwtexpui/BUCK +++ /dev/null
@@ -1,114 +0,0 @@ -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', - ], - provided_deps = ['//lib/gwt:user'], - deps = [ - ':SafeHtml', - ':UserAgent', - '//lib:LICENSE-clippy', - '//lib:LICENSE-silk_icons', - ], - visibility = ['PUBLIC'], -) - -java_library( - name = 'CSS', - srcs = glob([SRC + 'css/rebind/*.java']), - resources = [SRC + 'css/CSS.gwt.xml'], - provided_deps = ['//lib/gwt:dev'], - visibility = ['PUBLIC'], -) - -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', - ], - provided_deps = ['//lib/gwt:user'], - deps = [ - ':SafeHtml', - ':UserAgent', - ], - visibility = ['PUBLIC'], -) - -java_library( - name = 'linker_server', - srcs = glob([SRC + 'linker/server/*.java']), - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -gwt_module( - name = 'Progress', - srcs = glob([SRC + 'progress/client/*.java']), - gwt_xml = SRC + 'progress/Progress.gwt.xml', - resources = [SRC + 'progress/client/progress.css'], - provided_deps = ['//lib/gwt:user'], - visibility = ['PUBLIC'], -) - -gwt_module( - name = 'SafeHtml', - srcs = glob([SRC + 'safehtml/client/*.java']), - gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml', - resources = [SRC + 'safehtml/client/safehtml.css'], - provided_deps = ['//lib/gwt:user'], - visibility = ['PUBLIC'], -) - -java_test( - name = 'SafeHtml_tests', - srcs = glob([ - 'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java', - ]), - deps = [ - ':SafeHtml', - '//lib:truth', - '//lib/gwt:user', - '//lib/gwt:dev', - ], - source_under_test = [':SafeHtml'], -) - -gwt_module( - name = 'UserAgent', - srcs = glob([SRC + 'user/client/*.java']), - gwt_xml = SRC + 'user/User.gwt.xml', - resources = [SRC + 'user/client/tooltip.css'], - provided_deps = ['//lib/gwt:user'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'server', - srcs = glob([SRC + 'server/*.java']), - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'client-src-lib', - srcs = [], - resources = glob( - [SRC + n for n in [ - 'clippy/**/*', - 'globalkey/**/*', - 'safehtml/**/*', - 'user/**/*', - ]] - ), - visibility = ['PUBLIC'], -)
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 deleted file mode 100644 index ef78d98..0000000 --- a/gerrit-gwtui-common/BUCK +++ /dev/null
@@ -1,72 +0,0 @@ -EXPORTED_DEPS = [ - '//gerrit-common:client', - '//gerrit-gwtexpui:Clippy', - '//gerrit-gwtexpui:GlobalKey', - '//gerrit-gwtexpui:Progress', - '//gerrit-gwtexpui:SafeHtml', - '//gerrit-gwtexpui:UserAgent', -] -DEPS = ['//lib/gwt:user'] -SRC = 'src/main/java/com/google/gerrit/' -DIFFY = glob(['src/main/resources/com/google/gerrit/client/diffy*.png']) - -gwt_module( - name = 'client', - srcs = glob([SRC + 'client/**/*.java']), - gwt_xml = SRC + 'GerritGwtUICommon.gwt.xml', - resources = glob(['src/main/**/*']), - exported_deps = EXPORTED_DEPS, - provided_deps = DEPS, - visibility = ['PUBLIC'], -) - -java_library( - name = 'client-lib', - srcs = glob(['src/main/**/*.java']), - resources = glob(['src/main/**/*']), - exported_deps = EXPORTED_DEPS, - provided_deps = DEPS, - visibility = ['PUBLIC'], -) - -java_library( - name = 'client-src-lib', - srcs = [], - resources = glob(['src/main/**/*']), - visibility = ['PUBLIC'], -) - -prebuilt_jar( - name = 'diffy_logo', - binary_jar = ':diffy_image_files_ln', - deps = [ - '//lib:LICENSE-diffy', - '//lib:LICENSE-CC-BY3.0-unported', - ], - 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']), - deps = [ - ':client', - '//lib:junit', - '//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 345e1e3..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,11 +15,11 @@ package com.google.gerrit.client.info; import com.google.gerrit.client.rpc.Natives; +import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.GitBasicAuthPolicy; -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.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import java.util.ArrayList; @@ -53,22 +53,30 @@ return authType() == AuthType.CUSTOM_EXTENSION; } - public final boolean canEdit(Account.FieldName f) { + public final boolean canEdit(AccountFieldName f) { return editableAccountFields().contains(f); } - public final List<Account.FieldName> editableAccountFields() { - List<Account.FieldName> fields = new ArrayList<>(); + public final List<AccountFieldName> editableAccountFields() { + List<AccountFieldName> fields = new ArrayList<>(); for (String f : Natives.asList(_editableAccountFields())) { - fields.add(Account.FieldName.valueOf(f)); + fields.add(AccountFieldName.valueOf(f)); } return fields; } + public final List<AgreementInfo> contributorAgreements() { + List<AgreementInfo> agreements = new ArrayList<>(); + for (AgreementInfo a : Natives.asList(_contributorAgreements())) { + agreements.add(a); + } + return agreements; + } + public final boolean siteHasUsernames() { if (isCustomExtension() && httpPasswordUrl() != null - && !canEdit(FieldName.USER_NAME)) { + && !canEdit(AccountFieldName.USER_NAME)) { return false; } return true; @@ -100,6 +108,8 @@ private native String authTypeRaw() /*-{ return this.auth_type; }-*/; private native JsArrayString _editableAccountFields() /*-{ return this.editable_account_fields; }-*/; + private native JsArray<AgreementInfo> _contributorAgreements() + /*-{ return this.contributor_agreements; }-*/; protected AuthInfo() { }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/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-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java new file mode 100644 index 0000000..deed44d --- /dev/null +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
@@ -0,0 +1,31 @@ +// 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.client.info; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.http.client.URL; + +public class GroupBaseInfo extends JavaScriptObject { + public final AccountGroup.UUID getGroupUUID() { + return new AccountGroup.UUID(URL.decodeQueryString(id())); + } + + public final native String id() /*-{ return this.id; }-*/; + public final native String name() /*-{ return this.name; }-*/; + + protected GroupBaseInfo() { + } +}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java new file mode 100644 index 0000000..fa051a1 --- /dev/null +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
@@ -0,0 +1,60 @@ +// 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.client.info; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.http.client.URL; + +public class GroupInfo extends GroupBaseInfo { + public final AccountGroup.Id getGroupId() { + return new AccountGroup.Id(group_id()); + } + + public final native GroupOptionsInfo options() /*-{ return this.options; }-*/; + public final native String description() /*-{ return this.description; }-*/; + public final native String url() /*-{ return this.url; }-*/; + public final native String owner() /*-{ return this.owner; }-*/; + public final native void owner(String o) /*-{ if(o)this.owner=o; }-*/; + public final native JsArray<AccountInfo> members() /*-{ return this.members; }-*/; + public final native JsArray<GroupInfo> includes() /*-{ return this.includes; }-*/; + + private native int group_id() /*-{ return this.group_id; }-*/; + private native String owner_id() /*-{ return this.owner_id; }-*/; + private native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/; + + public final AccountGroup.UUID getOwnerUUID() { + String owner = owner_id(); + if (owner != null) { + return new AccountGroup.UUID(URL.decodeQueryString(owner)); + } + return null; + } + + public final void setOwnerUUID(AccountGroup.UUID uuid) { + owner_id(URL.encodeQueryString(uuid.get())); + } + + protected GroupInfo() { + } + + public static class GroupOptionsInfo extends JavaScriptObject { + public final native boolean isVisibleToAll() /*-{ return this['visible_to_all'] ? true : false; }-*/; + + protected GroupOptionsInfo() { + } + } +}
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 deleted file mode 100644 index 1e39831..0000000 --- a/gerrit-gwtui/BUCK +++ /dev/null
@@ -1,67 +0,0 @@ -include_defs('//gerrit-gwtui/gwt.defs') -include_defs('//tools/gwt-constants.defs') - -DEPS = GWT_TRANSITIVE_DEPS + [ - '//gerrit-gwtexpui:CSS', - '//lib:gwtjsonrpc', - '//lib/gwt:dev', -] - -gwt_genrule(MODULE, DEPS) -gwt_genrule(MODULE, DEPS, '_r') - -gwt_user_agent_permutations( - name = 'ui', - module_name = 'gerrit_ui', - modules = [MODULE], - module_deps = [':ui_module'], - deps = DEPS, - visibility = ['//:'], -) - -def gen_ui_module(name, suffix = ""): - gwt_module( - name = name + suffix, - srcs = glob(['src/main/java/**/*.java']), - gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'), - resources = glob(['src/main/java/**/*']), - deps = [ - ':silk_icons', - '//gerrit-gwtui-common:diffy_logo', - '//gerrit-gwtui-common:client', - '//gerrit-gwtexpui:CSS', - '//lib/codemirror:codemirror' + suffix, - '//lib/gwt:user', - ], - visibility = [ - '//tools/eclipse:classpath', - '//Documentation:licenses.txt', - '//Documentation:js_licenses.txt', - ], - ) - -gen_ui_module(name = 'ui_module') -gen_ui_module(name = 'ui_module', suffix = '_r') - -java_library( - name = 'silk_icons', - deps = [ - '//lib:LICENSE-silk_icons', - ], -) - -java_test( - name = 'ui_tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':ui_module', - '//gerrit-common:client', - '//gerrit-extension-api:client', - '//lib:junit', - '//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 deleted file mode 100644 index cd8fa74..0000000 --- a/gerrit-gwtui/gwt.defs +++ /dev/null
@@ -1,142 +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. -from multiprocessing import cpu_count - -BROWSERS = [ - 'chrome', - 'firefox', - 'gecko1_8', - 'safari', - 'msie', 'ie8', 'ie9', 'ie10', 'ie11', - 'edge', -] -ALIASES = { - 'chrome': 'safari', - 'firefox': 'gecko1_8', - 'msie': 'ie11', - 'edge': 'edge', -} -MODULE = 'com.google.gerrit.GerritGwtUI' -CPU_COUNT = cpu_count() - -def gwt_genrule(module, deps, suffix = ""): - dbg = 'ui_dbg' + suffix - opt = 'ui_opt' + suffix - soyc = 'ui_soyc' + suffix - module_dep = ':ui_module' + suffix - args = GWT_COMPILER_ARGS_RELEASE_MODE if suffix == "_r" else GWT_COMPILER_ARGS - - genrule( - name = 'ui_optdbg' + suffix, - cmd = 'cd $TMP;' + - 'unzip -q $(location :%s);' % dbg + - 'mv' + - ' gerrit_ui/gerrit_ui.nocache.js' + - ' gerrit_ui/dbg_gerrit_ui.nocache.js;' + - 'unzip -qo $(location :%s);' % opt + - 'mkdir -p \$(dirname $OUT);' + - 'zip -qr $OUT .', - out = 'ui_optdbg' + suffix + '.zip', - visibility = ['PUBLIC'], - ) - - gwt_binary( - name = opt, - modules = [module], - module_deps = [module_dep], - deps = deps + ([':' + dbg] if CPU_COUNT < 8 else []), - local_workers = CPU_COUNT, - strict = True, - experimental_args = args, - vm_args = GWT_JVM_ARGS, - ) - - gwt_binary( - name = dbg, - modules = [module], - style = 'PRETTY', - optimize = 0, - module_deps = [module_dep], - deps = deps, - local_workers = CPU_COUNT, - strict = True, - experimental_args = args, - vm_args = GWT_JVM_ARGS, - visibility = ['PUBLIC'], - ) - - gwt_binary( - name = soyc, - modules = [module], - module_deps = [module_dep], - deps = deps + [':' + dbg], - local_workers = CPU_COUNT, - strict = True, - experimental_args = args + ['-compileReport'], - vm_args = GWT_JVM_ARGS, - ) - -def gwt_user_agent_permutations( - name, - module_name, - modules, - style = 'PRETTY', - optimize = 0, - draft_compile = True, - module_deps = [], - deps = [], - browsers = BROWSERS, - visibility = []): - for ua in browsers: - impl = ua - if ua in ALIASES: - impl = ALIASES[ua] - xml = ''.join([ - "<module rename-to='%s'>" % module_name, - "<inherits name='%s'/>" % modules[0], - "<set-property name='user.agent' value='%s'/>" % impl, - "<set-property name='locale' value='default'/>", - "</module>", - ]) - gwt = '%s_%s.gwt.xml' % (modules[0].replace('.', '/'), ua) - gwt_name = '%s_%s' % (name, ua) - jar = '%s.gwtxml.jar' % (gwt_name) - - genrule( - name = '%s_gwtxml_gen' % gwt_name, - cmd = 'cd $TMP;' + - ('mkdir -p \$(dirname %s);' % gwt) + - ('echo "%s">%s;' % (xml, gwt)) + - 'zip -qr $OUT .', - out = jar, - ) - 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, - modules = [modules[0] + '_' + ua], - style = style, - optimize = optimize, - draft_compile = draft_compile, - module_deps = module_deps + [':%s_gwtxml_lib' % gwt_name], - deps = deps, - local_workers = CPU_COUNT, - strict = True, - experimental_args = GWT_COMPILER_ARGS, - vm_args = GWT_JVM_ARGS, - visibility = visibility, - )
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 ba4b202..79876e7 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..68b8ec8 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; @@ -136,6 +138,7 @@ private static ViewSite<Screen> body; private static String lastChangeListToken; private static String lastViewToken; + private static Anchor uiSwitcherLink; static { SYSTEM_SVC = GWT.create(SystemInfoService.class); @@ -173,6 +176,7 @@ public static void display(final String token) { if (body.getView() == null || !body.getView().displayToken(token)) { dispatcher.display(token); + updateUiLink(token); } } @@ -199,6 +203,7 @@ LocalComments.saveInlineComments(); } body.setView(view); + updateUiLink(token); } } @@ -286,6 +291,7 @@ } /** @return access token to prove user identity during REST API calls. */ + @Nullable public static String getXGerritAuth() { return xGerritAuth; } @@ -529,6 +535,23 @@ ApiGlue.fireEvent("history", token); } + private static String getUiSwitcherUrl(String token) { + UrlBuilder builder = new UrlBuilder(); + builder.setProtocol(Location.getProtocol()); + builder.setHost(Location.getHost()); + String port = Location.getPort(); + if (port != null && !port.isEmpty()) { + builder.setPort(Integer.parseInt(port)); + } + String[] tokens = token.split("@", 2); + builder.setPath(tokens[0]); + if (tokens.length == 2) { + builder.setHash(tokens[1]); + } + builder.setParameter("polygerrit", "1"); + return builder.buildString(); + } + private static void populateBottomMenu(RootPanel btmmenu, HostPageData hpd) { String vs = hpd.version; if (vs == null || vs.isEmpty()) { @@ -537,6 +560,14 @@ btmmenu.add(new InlineHTML(M.poweredBy(vs))); + if (info().gerrit().webUis().contains(UiType.POLYGERRIT)) { + btmmenu.add(new InlineLabel(" | ")); + uiSwitcherLink = new Anchor(C.newUi(), + getUiSwitcherUrl(History.getToken())); + uiSwitcherLink.setStyleName(""); + btmmenu.add(uiSwitcherLink); + } + String reportBugUrl = info().gerrit().reportBugUrl(); if (reportBugUrl != null) { String reportBugText = info().gerrit().reportBugText(); @@ -552,6 +583,10 @@ btmmenu.add(new InlineLabel(C.keyHelp())); } + private static void updateUiLink(String token) { + uiSwitcherLink.setHref(getUiSwitcherUrl(token)); + } + private void onModuleLoad2(HostPageData hpd) { RESOURCES.gwt_override().ensureInjected(); RESOURCES.css().ensureInjected();
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..b144863 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
@@ -42,6 +42,8 @@ String branchDeletionDialogTitle(); String branchDeletionConfirmationMessage(); + String newUi(); + String notSignedInTitle(); String notSignedInBody();
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..b4665b2 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
@@ -23,6 +23,8 @@ branchDeletionDialogTitle = Branch Deletion branchDeletionConfirmationMessage = Do you really want to delete the following branches? +newUi = New UI + notSignedInTitle = Code Review - Session Expired notSignedInBody = <b>Session Expired</b>\ <p>You are no longer signed in to Gerrit Code Review.</p>\
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..0f27f57 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; @@ -70,7 +70,7 @@ }); } - protected abstract void display(final GroupInfo group, final boolean canModify); + protected abstract void display(GroupInfo group, boolean canModify); protected AccountGroup.UUID getGroupUUID() { return group.getGroupUUID();
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 0f05ff2..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()); } } @@ -571,36 +597,38 @@ private void initEditMode(ChangeInfo info, String revision) { if (Gerrit.isSignedIn()) { RevisionInfo rev = info.revision(revision); - boolean isOpen = info.status().isOpen(); - if (isOpen && 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 (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 { + editMode.setVisible(false); + addFile.setVisible(false); + reviewMode.setVisible(false); + } - if (rev.isEdit()) { - if (isOpen) { + 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); } } @@ -619,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); } } @@ -887,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); + } } } @@ -916,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); @@ -932,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; @@ -954,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) { @@ -970,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()) @@ -993,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, @@ -1015,6 +1107,7 @@ @Override public void onFailure(Throwable caught) { + files.showError(caught); } })); } @@ -1227,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 { @@ -1401,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; } } @@ -1414,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/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java index 5257ae0..eed0f9b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -88,7 +88,7 @@ } } - abstract Runnable diffChunkNav(final CodeMirror cm, final Direction dir); + abstract Runnable diffChunkNav(CodeMirror cm, Direction dir); void diffChunkNavHelper(List<? extends DiffChunkInfo> chunks, DiffScreen host, int res, Direction dir) {
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..7419dd5 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/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java index b7910f5..861b84c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -23,7 +23,7 @@ import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; public class DiffInfo extends JavaScriptObject { @@ -43,7 +43,7 @@ } private List<WebLinkInfo> filterWebLinks(DiffView diffView) { - List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>(); + List<WebLinkInfo> filteredDiffWebLinks = new ArrayList<>(); List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks()); if (allDiffWebLinks != null) { for (DiffWebLinkInfo webLink : allDiffWebLinks) {
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..1cb5995 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(); } @@ -777,7 +778,7 @@ this.prefsAction = prefsAction; } - abstract void operation(final Runnable apply); + abstract void operation(Runnable apply); private Runnable upToChange(final boolean openReplyBox) { return new Runnable() { @@ -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/GroupBaseInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java deleted file mode 100644 index 4811e59..0000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java +++ /dev/null
@@ -1,31 +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.client.groups; - -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.http.client.URL; - -public class GroupBaseInfo extends JavaScriptObject { - public final AccountGroup.UUID getGroupUUID() { - return new AccountGroup.UUID(URL.decodeQueryString(id())); - } - - public final native String id() /*-{ return this.id; }-*/; - public final native String name() /*-{ return this.name; }-*/; - - protected GroupBaseInfo() { - } -}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java deleted file mode 100644 index c3fd4ed..0000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java +++ /dev/null
@@ -1,61 +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.client.groups; - -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; -import com.google.gwt.http.client.URL; - -public class GroupInfo extends GroupBaseInfo { - public final AccountGroup.Id getGroupId() { - return new AccountGroup.Id(group_id()); - } - - public final native GroupOptionsInfo options() /*-{ return this.options; }-*/; - public final native String description() /*-{ return this.description; }-*/; - public final native String url() /*-{ return this.url; }-*/; - public final native String owner() /*-{ return this.owner; }-*/; - public final native void owner(String o) /*-{ if(o)this.owner=o; }-*/; - public final native JsArray<AccountInfo> members() /*-{ return this.members; }-*/; - public final native JsArray<GroupInfo> includes() /*-{ return this.includes; }-*/; - - private native int group_id() /*-{ return this.group_id; }-*/; - private native String owner_id() /*-{ return this.owner_id; }-*/; - private native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/; - - public final AccountGroup.UUID getOwnerUUID() { - String owner = owner_id(); - if (owner != null) { - return new AccountGroup.UUID(URL.decodeQueryString(owner)); - } - return null; - } - - public final void setOwnerUUID(AccountGroup.UUID uuid) { - owner_id(URL.encodeQueryString(uuid.get())); - } - - protected GroupInfo() { - } - - public static class GroupOptionsInfo extends JavaScriptObject { - public final native boolean isVisibleToAll() /*-{ return this['visible_to_all'] ? true : false; }-*/; - - protected GroupOptionsInfo() { - } - } -}
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..e0a7d0c 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; @@ -58,7 +59,7 @@ } public static void myOwned(String groupName, AsyncCallback<GroupMap> cb) { - myOwnedGroups().addParameter("q", groupName).get( + myOwnedGroups().addParameter("g", groupName).get( NativeMap.copyKeysIntoChildren(cb)); }
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/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java index 4e6f500..5577772 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -28,7 +28,7 @@ import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; public abstract class CherryPickDialog extends TextAreaActionDialog { @@ -49,7 +49,7 @@ newBranch = new SuggestBox(new HighlightSuggestOracle() { @Override protected void onRequestSuggestions(Request request, Callback done) { - LinkedList<BranchSuggestion> suggestions = new LinkedList<>(); + List<BranchSuggestion> suggestions = new ArrayList<>(); for (final BranchInfo b : branches) { if (b.ref().contains(request.getQuery())) { suggestions.add(new BranchSuggestion(b));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/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/main/java/net/codemirror/theme/Themes.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java index 80304a3..dc95b4a 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
@@ -32,6 +32,8 @@ @Source("cobalt.css") ExternalTextResource cobalt(); @Source("colorforth.css") ExternalTextResource colorforth(); @Source("dracula.css") ExternalTextResource dracula(); + @Source("duotone-dark.css") ExternalTextResource duotone_dark(); + @Source("duotone-light.css") ExternalTextResource duotone_light(); @Source("eclipse.css") ExternalTextResource eclipse(); @Source("elegant.css") ExternalTextResource elegant(); @Source("erlang-dark.css") ExternalTextResource erlang_dark();
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 deleted file mode 100644 index d52963a..0000000 --- a/gerrit-httpd/BUCK +++ /dev/null
@@ -1,79 +0,0 @@ -SRCS = glob( - ['src/main/java/**/*.java'], -) -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/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', - ], - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -java_sources( - name = 'httpd-src', - srcs = SRCS + RESOURCES, - visibility = ['PUBLIC'], -) - -java_test( - 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', - '//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', - ], - 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/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 4b66023..cb9e616 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; @@ -613,45 +619,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/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java index 8ae0e5c..8392f84 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -17,7 +17,7 @@ import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation; import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.annotations.Export; import com.google.gerrit.server.plugins.InvalidPluginException; import com.google.gerrit.server.plugins.ModuleGenerator; @@ -35,7 +35,8 @@ class HttpAutoRegisterModuleGenerator extends ServletModule implements ModuleGenerator { private final Map<String, Class<HttpServlet>> serve = new HashMap<>(); - private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create(); + private final ListMultimap<TypeLiteral<?>, Class<?>> listeners = + LinkedListMultimap.create(); @Override protected void configureServlets() {
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..966a2fb --- /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 bazel = firstNonNull(properties.getProperty("bazel"), "bazel"); + ProcessBuilder proc = new ProcessBuilder(bazel, "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 deleted file mode 100644 index 0b4a02e..0000000 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java +++ /dev/null
@@ -1,118 +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 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 { - 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); - String buck = firstNonNull(properties.getProperty("buck"), "buck"); - ProcessBuilder proc = new ProcessBuilder(buck, "build", target) - .directory(root.toFile()) - .redirectErrorStream(true); - 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)); - } - - 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; - } - - 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>"); - } - } - - static class BuildFailureException extends Exception { - private static final long serialVersionUID = 1L; - - final byte[] why; - - BuildFailureException(byte[] why) { - this.why = why; - } - } -}
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..0571ee6 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,40 @@ .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 = getSourceRootOrNull(); + builder = new BazelBuild(sourceRoot); + } + + private static Path getSourceRootOrNull() { + try { + return GerritLauncher.resolveInSourceRoot("."); + } catch (FileNotFoundException e) { + return null; + } } private FileSystem getDistributionArchive(File war) throws IOException { @@ -355,14 +402,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 +432,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/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java index f80cc49..782055c 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -23,7 +23,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.Url; @@ -59,7 +59,7 @@ } <T> boolean parse(T param, - Multimap<String, String> in, + ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res) throws IOException { @@ -90,8 +90,8 @@ } static void splitQueryString(String queryString, - Multimap<String, String> config, - Multimap<String, String> params) { + ListMultimap<String, String> config, + ListMultimap<String, String> params) { if (!Strings.isNullOrEmpty(queryString)) { for (String kvPair : Splitter.on('&').split(queryString)) { Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator();
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..cc2f7d7 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,15 +42,16 @@ 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.ImmutableMultimap; +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.LinkedHashMultimap; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.io.BaseEncoding; import com.google.common.io.CountingOutputStream; import com.google.common.math.IntMath; @@ -64,6 +74,7 @@ import com.google.gerrit.extensions.restapi.ETagView; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.NeedsParams; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.PreconditionFailedException; import com.google.gerrit.extensions.restapi.RawInput; @@ -85,6 +96,7 @@ import com.google.gerrit.server.OptionUtil; import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.account.CapabilityUtils; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.util.http.RequestUtil; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; @@ -103,6 +115,7 @@ import com.google.inject.Provider; import com.google.inject.util.Providers; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.Heap; import org.slf4j.Logger; @@ -131,6 +144,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 +165,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 +192,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; } } @@ -216,14 +245,24 @@ int status = SC_OK; long responseBytes = -1; Object result = null; - Multimap<String, String> params = LinkedHashMultimap.create(); + ListMultimap<String, String> params = + MultimapBuilder.hashKeys().arrayListValues().build(); + ListMultimap<String, String> config = + MultimapBuilder.hashKeys().arrayListValues().build(); Object inputRequestBody = null; RestResource rsrc = TopLevelResource.INSTANCE; ViewData viewData = null; try { + if (isCorsPreflight(req)) { + doCorsPreflight(req, res); + return; + } + checkCors(req, res); checkUserSession(req); + ParameterParser.splitQueryString(req.getQueryString(), config, params); + List<IdString> path = splitPath(req); RestCollection<RestResource, RestResource> rc = members.get(); CapabilityUtils.checkRequiresCapability(globals.currentUser, @@ -232,7 +271,11 @@ viewData = new ViewData(null, null); if (path.isEmpty()) { - if (isGetOrHead(req)) { + if (rc instanceof NeedsParams) { + ((NeedsParams)rc).setParams(params); + } + + if (isRead(req)) { viewData = new ViewData(null, rc.list()); } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) { @SuppressWarnings("unchecked") @@ -273,7 +316,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") @@ -324,13 +367,11 @@ return; } - Multimap<String, String> config = LinkedHashMultimap.create(); - ParameterParser.splitQueryString(req.getQueryString(), config, params); if (!globals.paramParser.get().parse(viewData.view, params, req, res)) { 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") @@ -421,13 +462,81 @@ metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get() - .getSessionId(), globals.currentUser.get(), req, - auditStartTs, params, inputRequestBody, status, + globals.auditService.dispatch(new ExtendedHttpAuditEvent( + globals.webSession.get().getSessionId(), globals.currentUser.get(), + req, auditStartTs, params, inputRequestBody, status, result, rsrc, viewData == null ? null : viewData.view)); } } + 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 +547,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 +578,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: @@ -670,7 +779,7 @@ public static long replyJson(@Nullable HttpServletRequest req, HttpServletResponse res, - Multimap<String, String> config, + ListMultimap<String, String> config, Object result) throws IOException { TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE); @@ -689,7 +798,7 @@ .setCharacterEncoding(UTF_8)); } - private static Gson newGson(Multimap<String, String> config, + private static Gson newGson(ListMultimap<String, String> config, @Nullable HttpServletRequest req) { GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder(); @@ -700,7 +809,7 @@ } private static void enablePrettyPrint(GsonBuilder gb, - Multimap<String, String> config, + ListMultimap<String, String> config, @Nullable HttpServletRequest req) { String pp = Iterables.getFirst(config.get("pp"), null); if (pp == null) { @@ -715,7 +824,7 @@ } private static void enablePartialGetFields(GsonBuilder gb, - Multimap<String, String> config) { + ListMultimap<String, String> config) { final Set<String> want = new HashSet<>(); for (String p : config.get("fields")) { Iterables.addAll(want, OptionUtil.splitOptionValue(p)); @@ -935,16 +1044,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 +1077,22 @@ 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); + user.setLastLoginExternalIdKey( + globals.webSession.get().getLastLoginExternalId()); + } 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,8 +1131,9 @@ static long replyText(@Nullable HttpServletRequest req, HttpServletResponse res, String text) throws IOException { - if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) { - return replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text)); + if ((req == null || isRead(req)) && isMaybeHTML(text)) { + return replyJson(req, res, ImmutableListMultimap.of("pp", "0"), + new JsonPrimitive(text)); } if (!text.endsWith("\n")) { text += "\n";
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java index 319907b..e0b48dc 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -14,8 +14,8 @@ package com.google.gerrit.httpd.rpc; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.audit.AuditService; import com.google.gerrit.audit.RpcAuditEvent; import com.google.gerrit.common.TimeUtil; @@ -133,25 +133,29 @@ } Audit note = method.getAnnotation(Audit.class); if (note != null) { - final String sid = call.getWebSession().getSessionId(); - final CurrentUser username = call.getWebSession().getUser(); - final Multimap<String, ?> args = - extractParams(note, call); - final String what = extractWhat(note, call); - final Object result = call.getResult(); + String sid = call.getWebSession().getSessionId(); + CurrentUser username = call.getWebSession().getUser(); + ListMultimap<String, ?> args = extractParams(note, call); + String what = extractWhat(note, call); + Object result = call.getResult(); - audit.dispatch(new RpcAuditEvent(sid, username, what, call.getWhen(), - args, call.getHttpServletRequest().getMethod(), call.getHttpServletRequest().getMethod(), - ((AuditedHttpServletResponse) (call.getHttpServletResponse())) - .getStatus(), result)); + audit.dispatch( + new RpcAuditEvent( + sid, username, what, call.getWhen(), args, + call.getHttpServletRequest().getMethod(), + call.getHttpServletRequest().getMethod(), + ((AuditedHttpServletResponse) (call.getHttpServletResponse())) + .getStatus(), + result)); } } catch (Throwable all) { log.error("Unable to log the call", all); } } - private Multimap<String, ?> extractParams(final Audit note, final GerritCall call) { - Multimap<String, Object> args = ArrayListMultimap.create(); + private ListMultimap<String, ?> extractParams(Audit note, GerritCall call) { + ListMultimap<String, Object> args = + MultimapBuilder.hashKeys().arrayListValues().build(); Object[] params = call.getParams(); for (int i = 0; i < params.length; i++) {
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/doc/QueryDocumentationFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java index 973adbe..3458ffc 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java
@@ -15,8 +15,7 @@ package com.google.gerrit.httpd.rpc.doc; import com.google.common.base.Strings; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ImmutableListMultimap; import com.google.gerrit.httpd.restapi.RestApiServlet; import com.google.gerrit.server.documentation.QueryDocumentationExecutor; import com.google.gerrit.server.documentation.QueryDocumentationExecutor.DocQueryException; @@ -68,8 +67,7 @@ HttpServletResponse rsp = (HttpServletResponse) response; try { List<DocResult> result = searcher.doQuery(request.getParameter("q")); - Multimap<String, String> config = LinkedHashMultimap.create(); - RestApiServlet.replyJson(req, rsp, config, result); + RestApiServlet.replyJson(req, rsp, ImmutableListMultimap.of(), result); } catch (DocQueryException e) { log.error("Doc search failed:", e); rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
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/BUCK b/gerrit-launcher/BUCK deleted file mode 100644 index 5be25fa..0000000 --- a/gerrit-launcher/BUCK +++ /dev/null
@@ -1,13 +0,0 @@ -# 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 = [ - '//gerrit-acceptance-framework/...', - '//gerrit-acceptance-tests/...', - '//gerrit-httpd:', - '//gerrit-main:main_lib', - '//gerrit-pgm:', - ], -)
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 7954146..b0eabea 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)); } @@ -104,6 +108,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); } @@ -577,25 +619,47 @@ /** * 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"); } + static String SOURCE_ROOT_RESOURCE = "/gerrit-launcher/workspace-root.txt"; + /** - * Locate the path of the {@code buck-out} directory in a source tree. + * 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 getDeveloperBuckOut() throws FileNotFoundException { - return resolveInSourceRoot("buck-out"); - } - - private static Path resolveInSourceRoot(String name) + 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()); @@ -616,7 +680,7 @@ // 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("WORKSPACE"))) { Path parent = dir.getParent(); if (parent == null) { throw new FileNotFoundException("Cannot find source root from " + u); @@ -634,7 +698,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/BUCK b/gerrit-lucene/BUCK deleted file mode 100644 index 771a021..0000000 --- a/gerrit-lucene/BUCK +++ /dev/null
@@ -1,41 +0,0 @@ -QUERY_BUILDER = [ - '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 = ['PUBLIC'], -) - -java_library( - name = 'lucene', - srcs = glob(['src/main/java/**/*.java'], excludes = 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 = ['PUBLIC'], -)
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/ChangeSubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java index 9bec978..6f0df0f 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -97,8 +97,4 @@ } super.add(doc, values); } - - @Override - public void stop() { - } }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java deleted file mode 100644 index f43e385..0000000 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/GerritIndexStatus.java +++ /dev/null
@@ -1,78 +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.lucene; - -import com.google.common.primitives.Ints; -import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; - -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; - -import java.io.IOException; - -class GerritIndexStatus { - private static final String SECTION = "index"; - private static final String KEY_READY = "ready"; - - private final FileBasedConfig cfg; - - GerritIndexStatus(SitePaths sitePaths) - throws ConfigInvalidException, IOException { - cfg = new FileBasedConfig( - sitePaths.index_dir.resolve("gerrit_index.config").toFile(), - FS.detect()); - cfg.load(); - convertLegacyConfig(); - } - - void setReady(String indexName, int version, boolean ready) { - cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready); - } - - boolean getReady(String indexName, int version) { - return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY, - false); - } - - void save() throws IOException { - cfg.save(); - } - - private void convertLegacyConfig() throws IOException { - boolean dirty = false; - // Convert legacy [index "25"] to modern [index "changes_0025"]. - for (String subsection : cfg.getSubsections(SECTION)) { - Integer v = Ints.tryParse(subsection); - if (v != null) { - String ready = cfg.getString(SECTION, subsection, KEY_READY); - if (ready != null) { - dirty = false; - cfg.unset(SECTION, subsection, KEY_READY); - cfg.setString(SECTION, - indexDirName(ChangeSchemaDefinitions.NAME, v), KEY_READY, ready); - } - } - } - if (dirty) { - cfg.save(); - } - } - - private static String indexDirName(String indexName, int version) { - return String.format("%s_%04d", indexName, version); - } -}
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 59980e7..cfcf6dc 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; @@ -32,6 +31,7 @@ import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import org.apache.lucene.document.Document; @@ -56,7 +56,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 @@ -79,7 +78,7 @@ private final GerritIndexWriterConfig indexWriterConfig; private final QueryBuilder<AccountState> queryBuilder; - private final AccountCache accountCache; + private final Provider<AccountCache> accountCache; private static Directory dir(Schema<AccountState> schema, Config cfg, SitePaths sitePaths) throws IOException { @@ -95,7 +94,7 @@ LuceneAccountIndex( @GerritServerConfig Config cfg, SitePaths sitePaths, - AccountCache accountCache, + Provider<AccountCache> accountCache, @Assisted Schema<AccountState> schema) throws IOException { super(schema, sitePaths, dir(schema, cfg, sitePaths), ACCOUNTS, null, new GerritIndexWriterConfig(cfg, ACCOUNTS), new SearcherFactory()); @@ -164,7 +163,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,24 +197,13 @@ } } - 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()); // 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 + // 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); - } - - @Override - public void stop() { + return accountCache.get().get(id); } }
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 530566c..688d4e7 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,21 +16,19 @@ 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.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; @@ -45,6 +43,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; @@ -53,6 +52,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()); @@ -317,7 +320,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 @@ -394,7 +397,7 @@ close(); throw new OrmRuntimeException(e); } catch (ExecutionException e) { - Throwables.propagateIfPossible(e.getCause()); + Throwables.throwIfUnchecked(e.getCause()); throw new OrmRuntimeException(e.getCause()); } } @@ -405,35 +408,10 @@ } } - 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, + private static ListMultimap<String, IndexableField> fields(Document doc, Set<String> fields) { - Multimap<String, IndexableField> stored = - ArrayListMultimap.create(fields.size(), 4); + ListMultimap<String, IndexableField> stored = + MultimapBuilder.hashKeys(fields.size()).arrayListValues(4).build(); for (IndexableField f : doc) { String name = f.name(); if (fields.contains(name)) { @@ -443,7 +421,7 @@ return stored; } - private ChangeData toChangeData(Multimap<String, IndexableField> doc, + private ChangeData toChangeData(ListMultimap<String, IndexableField> doc, Set<String> fields, String idFieldName) { ChangeData cd; // Either change or the ID field was guaranteed to be included in the call @@ -485,19 +463,27 @@ 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; } - private void decodePatchSets(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodePatchSets(ListMultimap<String, IndexableField> doc, + ChangeData cd) { List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC); if (!patchSets.isEmpty()) { @@ -507,12 +493,14 @@ } } - private void decodeApprovals(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeApprovals(ListMultimap<String, IndexableField> doc, + ChangeData cd) { cd.setCurrentApprovals( decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC)); } - private void decodeChangedLines(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeChangedLines(ListMultimap<String, IndexableField> doc, + ChangeData cd) { IndexableField added = Iterables.getFirst(doc.get(ADDED_FIELD), null); IndexableField deleted = Iterables.getFirst(doc.get(DELETED_FIELD), null); if (added != null && deleted != null) { @@ -528,7 +516,8 @@ } } - private void decodeMergeable(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeMergeable(ListMultimap<String, IndexableField> doc, + ChangeData cd) { IndexableField f = Iterables.getFirst(doc.get(MERGEABLE_FIELD), null); if (f != null) { String mergeable = f.stringValue(); @@ -540,7 +529,8 @@ } } - private void decodeReviewedBy(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeReviewedBy(ListMultimap<String, IndexableField> doc, + ChangeData cd) { Collection<IndexableField> reviewedBy = doc.get(REVIEWEDBY_FIELD); if (reviewedBy.size() > 0) { Set<Account.Id> accounts = @@ -556,7 +546,8 @@ } } - private void decodeHashtags(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeHashtags(ListMultimap<String, IndexableField> doc, + ChangeData cd) { Collection<IndexableField> hashtag = doc.get(HASHTAG_FIELD); Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtag.size()); for (IndexableField r : hashtag) { @@ -565,20 +556,11 @@ 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) { + private void decodeStar(ListMultimap<String, IndexableField> doc, + ChangeData cd) { Collection<IndexableField> star = doc.get(STAR_FIELD); - Multimap<Account.Id, String> stars = ArrayListMultimap.create(); + ListMultimap<Account.Id, String> stars = + MultimapBuilder.hashKeys().arrayListValues().build(); for (IndexableField r : star) { StarredChangesUtil.StarField starField = StarredChangesUtil.StarField.parse(r.stringValue()); @@ -589,20 +571,34 @@ cd.setStars(stars); } - private void decodeReviewers(Multimap<String, IndexableField> doc, ChangeData cd) { + private void decodeReviewers(ListMultimap<String, IndexableField> doc, + ChangeData cd) { cd.setReviewers( ChangeField.parseReviewerFieldValues( FluentIterable.from(doc.get(REVIEWER_FIELD)) - .transform( - new Function<IndexableField, String>() { - @Override - public String apply(IndexableField in) { - return in.stringValue(); - } - }))); + .transform(IndexableField::stringValue))); } - private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc, + private void decodeSubmitRecords(ListMultimap<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(ListMultimap<String, IndexableField> doc, + ChangeData cd) { + cd.setRefStates(copyAsBytes(doc.get(REF_STATE_FIELD))); + } + + private void decodeRefStatePatterns(ListMultimap<String, IndexableField> doc, + ChangeData cd) { + cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD))); + } + + private static <T> List<T> decodeProtos( + ListMultimap<String, IndexableField> doc, String fieldName, ProtobufCodec<T> codec) { Collection<IndexableField> fields = doc.get(fieldName); if (fields.isEmpty()) { @@ -616,4 +612,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/LuceneGroupIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java new file mode 100644 index 0000000..aef485c --- /dev/null +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -0,0 +1,199 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.lucene; + +import static com.google.gerrit.server.index.group.GroupField.UUID; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.account.GroupCache; +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.group.GroupIndex; +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 com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.SearcherFactory; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TopFieldDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.RAMDirectory; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class LuceneGroupIndex extends + AbstractLuceneIndex<AccountGroup.UUID, AccountGroup> implements GroupIndex { + private static final Logger log = + LoggerFactory.getLogger(LuceneGroupIndex.class); + + private static final String GROUPS = "groups"; + + private static final String UUID_SORT_FIELD = sortFieldName(UUID); + + private static Term idTerm(AccountGroup group) { + return idTerm(group.getGroupUUID()); + } + + private static Term idTerm(AccountGroup.UUID uuid) { + return QueryBuilder.stringTerm(UUID.getName(), uuid.get()); + } + + private final GerritIndexWriterConfig indexWriterConfig; + private final QueryBuilder<AccountGroup> queryBuilder; + private final Provider<GroupCache> groupCache; + + private static Directory dir(Schema<AccountGroup> schema, Config cfg, + SitePaths sitePaths) throws IOException { + if (LuceneIndexModule.isInMemoryTest(cfg)) { + return new RAMDirectory(); + } + Path indexDir = + LuceneVersionManager.getDir(sitePaths, GROUPS + "_", schema); + return FSDirectory.open(indexDir); + } + + @Inject + LuceneGroupIndex( + @GerritServerConfig Config cfg, + SitePaths sitePaths, + Provider<GroupCache> groupCache, + @Assisted Schema<AccountGroup> schema) throws IOException { + super(schema, sitePaths, dir(schema, cfg, sitePaths), GROUPS, null, + new GerritIndexWriterConfig(cfg, GROUPS), new SearcherFactory()); + this.groupCache = groupCache; + + indexWriterConfig = + new GerritIndexWriterConfig(cfg, GROUPS); + queryBuilder = new QueryBuilder<>(schema, indexWriterConfig.getAnalyzer()); + } + + @Override + public void replace(AccountGroup group) throws IOException { + try { + // No parts of FillArgs are currently required, just use null. + replace(idTerm(group), toDocument(group, null)).get(); + } catch (ExecutionException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void delete(AccountGroup.UUID key) throws IOException { + try { + delete(idTerm(key)).get(); + } catch (ExecutionException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public DataSource<AccountGroup> getSource(Predicate<AccountGroup> p, + QueryOptions opts) throws QueryParseException { + return new QuerySource(opts, queryBuilder.toQuery(p), + new Sort(new SortField(UUID_SORT_FIELD, SortField.Type.STRING, false))); + } + + private class QuerySource implements DataSource<AccountGroup> { + private final QueryOptions opts; + private final Query query; + private final Sort sort; + + private QuerySource(QueryOptions opts, Query query, Sort sort) { + this.opts = opts; + this.query = query; + this.sort = sort; + } + + @Override + public int getCardinality() { + return 10; + } + + @Override + public ResultSet<AccountGroup> read() throws OrmException { + IndexSearcher searcher = null; + try { + searcher = acquire(); + int realLimit = opts.start() + opts.limit(); + TopFieldDocs docs = searcher.search(query, realLimit, sort); + List<AccountGroup> 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, IndexUtils.groupFields(opts)); + result.add(toAccountGroup(doc)); + } + final List<AccountGroup> r = Collections.unmodifiableList(result); + return new ResultSet<AccountGroup>() { + @Override + public Iterator<AccountGroup> iterator() { + return r.iterator(); + } + + @Override + public List<AccountGroup> toList() { + return r; + } + + @Override + public void close() { + // Do nothing. + } + }; + } catch (IOException e) { + throw new OrmException(e); + } finally { + if (searcher != null) { + try { + release(searcher); + } catch (IOException e) { + log.warn("cannot release Lucene searcher", e); + } + } + } + } + } + + private AccountGroup toAccountGroup(Document doc) { + AccountGroup.UUID uuid = + new AccountGroup.UUID(doc.getField(UUID.getName()).stringValue()); + // Use the GroupCache rather than depending on any stored fields in the + // document (of which there shouldn't be any). + return groupCache.get().get(uuid); + } +}
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..1d1ca95 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,24 @@ 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.gerrit.server.index.group.GroupIndex; 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); } @@ -75,18 +62,22 @@ protected void configure() { install( new FactoryModuleBuilder() + .implement(AccountIndex.class, LuceneAccountIndex.class) + .build(AccountIndex.Factory.class)); + install( + new FactoryModuleBuilder() .implement(ChangeIndex.class, LuceneChangeIndex.class) .build(ChangeIndex.Factory.class)); install( new FactoryModuleBuilder() - .implement(AccountIndex.class, LuceneAccountIndex.class) - .build(AccountIndex.Factory.class)); + .implement(GroupIndex.class, LuceneGroupIndex.class) + .build(GroupIndex.Factory.class)); install(new IndexModule(threads)); if (singleVersions == null) { install(new MultiVersionModule()); } else { - install(new SingleVersionModule()); + install(new SingleVersionModule(singleVersions)); } } @@ -104,66 +95,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-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java index a993b49..cfec648 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -15,6 +15,7 @@ package com.google.gerrit.lucene; import static com.google.common.base.Preconditions.checkArgument; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.lucene.search.BooleanClause.Occur.MUST; import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT; import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; @@ -54,6 +55,12 @@ return new Term(name, builder.get()); } + static Term stringTerm(String name, String value) { + BytesRefBuilder builder = new BytesRefBuilder(); + builder.append(value.getBytes(UTF_8), 0, value.length()); + return new Term(name, builder.get()); + } + private final Schema<V> schema; private final org.apache.lucene.util.QueryBuilder queryBuilder;
diff --git a/gerrit-main/BUCK b/gerrit-main/BUCK deleted file mode 100644 index 388126e..0000000 --- a/gerrit-main/BUCK +++ /dev/null
@@ -1,15 +0,0 @@ -java_binary( - name = 'main_bin', - main_class = 'Main', - deps = [':main_lib'], - visibility = ['PUBLIC'], -) - -java_library( - 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/BUCK b/gerrit-oauth/BUCK deleted file mode 100644 index fa5a8e2..0000000 --- a/gerrit-oauth/BUCK +++ /dev/null
@@ -1,26 +0,0 @@ -SRCS = glob( - ['src/main/java/**/*.java'], -) -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', - ], - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -)
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-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java index 35f79c9..9deec44 100644 --- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java +++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -44,6 +44,7 @@ import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.Optional; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; @@ -165,10 +166,10 @@ private boolean authenticateWithIdentityClaimedDuringHandshake( AuthRequest req, HttpServletResponse rsp, String claimedIdentifier) throws AccountException, IOException { - Account.Id claimedId = accountManager.lookup(claimedIdentifier); - Account.Id actualId = accountManager.lookup(user.getExternalId()); - if (claimedId != null && actualId != null) { - if (claimedId.equals(actualId)) { + Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier); + Optional<Account.Id> actualId = accountManager.lookup(user.getExternalId()); + if (claimedId.isPresent() && actualId.isPresent()) { + if (claimedId.get().equals(actualId.get())) { // Both link to the same account, that's what we expected. log.debug("OAuth2: claimed identity equals current id"); } else { @@ -176,23 +177,23 @@ // for what might be the same user. // log.error("OAuth accounts disagree over user identity:\n" - + " Claimed ID: " + claimedId + " is " + claimedIdentifier - + "\n" + " Delgate ID: " + actualId + " is " + + " Claimed ID: " + claimedId.get() + " is " + claimedIdentifier + + "\n" + " Delgate ID: " + actualId.get() + " is " + user.getExternalId()); rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } - } else if (claimedId != null && actualId == null) { + } else if (claimedId.isPresent() && !actualId.isPresent()) { // Claimed account already exists: link to it. // log.info("OAuth2: linking claimed identity to {}", - claimedId.toString()); + claimedId.get().toString()); try { - accountManager.link(claimedId, req); + accountManager.link(claimedId.get(), req); } catch (OrmException e) { log.error("Cannot link: " + user.getExternalId() + " to user identity:\n" - + " Claimed ID: " + claimedId + " is " + claimedIdentifier); + + " Claimed ID: " + claimedId.get() + " is " + claimedIdentifier); rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return false; }
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK deleted file mode 100644 index 5eace7b..0000000 --- a/gerrit-openid/BUCK +++ /dev/null
@@ -1,26 +0,0 @@ -java_library( - name = 'openid', - srcs = glob(['src/main/java/**/*.java']), - resources = glob(['src/main/resources/**/*']), - deps = [ - '//lib/openid:consumer', - ], - 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', - ], - visibility = ['PUBLIC'], -)
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-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java index 67ac895..dccb6f7 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -43,6 +43,7 @@ import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.Optional; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; @@ -122,8 +123,9 @@ AuthResult arsp = null; try { String claimedIdentifier = user.getClaimedIdentity(); - Account.Id actualId = accountManager.lookup(user.getExternalId()); - Account.Id claimedId = null; + Optional<Account.Id> actualId = + accountManager.lookup(user.getExternalId()); + Optional<Account.Id> claimedId = Optional.empty(); // We try to retrieve claimed identity. // For some reason, for example staging instance @@ -133,17 +135,17 @@ // That why we query it here, not to lose linking mode. if (!Strings.isNullOrEmpty(claimedIdentifier)) { claimedId = accountManager.lookup(claimedIdentifier); - if (claimedId == null) { + if (!claimedId.isPresent()) { log.debug("Claimed identity is unknown"); } } // Use case 1: claimed identity was provided during handshake phase // and user account exists for this identity - if (claimedId != null) { + if (claimedId.isPresent()) { log.debug("Claimed identity is set and is known"); - if (actualId != null) { - if (claimedId.equals(actualId)) { + if (actualId.isPresent()) { + if (claimedId.get().equals(actualId.get())) { // Both link to the same account, that's what we expected. log.debug("Both link to the same account. All is fine."); } else { @@ -151,8 +153,8 @@ // for what might be the same user. The admin would have to // link the accounts manually. log.error("OAuth accounts disagree over user identity:\n" - + " Claimed ID: " + claimedId + " is " + claimedIdentifier - + "\n" + " Delgate ID: " + actualId + " is " + + " Claimed ID: " + claimedId.get() + " is " + claimedIdentifier + + "\n" + " Delgate ID: " + actualId.get() + " is " + user.getExternalId()); rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return; @@ -161,11 +163,11 @@ // Claimed account already exists: link to it. log.debug("Claimed account already exists: link to it."); try { - accountManager.link(claimedId, areq); + accountManager.link(claimedId.get(), areq); } catch (OrmException e) { log.error("Cannot link: " + user.getExternalId() + " to user identity:\n" - + " Claimed ID: " + claimedId + " is " + claimedIdentifier); + + " Claimed ID: " + claimedId.get() + " is " + claimedIdentifier); rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return; }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java index 36947a9..f3130e7 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -62,6 +62,7 @@ import java.io.IOException; import java.net.URL; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.servlet.http.Cookie; @@ -364,25 +365,26 @@ // identity we have in our AuthRequest above. We still should have a // link between the two, so set one up if not present. // - Account.Id claimedId = accountManager.lookup(claimedIdentifier); - Account.Id actualId = accountManager.lookup(areq.getExternalId()); + Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier); + Optional<Account.Id> actualId = + accountManager.lookup(areq.getExternalId()); - if (claimedId != null && actualId != null) { - if (claimedId.equals(actualId)) { + if (claimedId.isPresent() && actualId.isPresent()) { + if (claimedId.get().equals(actualId.get())) { // Both link to the same account, that's what we expected. } else { // This is (for now) a fatal error. There are two records // for what might be the same user. // log.error("OpenID accounts disagree over user identity:\n" - + " Claimed ID: " + claimedId + " is " + claimedIdentifier - + "\n" + " Delgate ID: " + actualId + " is " + + " Claimed ID: " + claimedId.get() + " is " + claimedIdentifier + + "\n" + " Delgate ID: " + actualId.get() + " is " + areq.getExternalId()); cancelWithError(req, rsp, "Contact site administrator"); return; } - } else if (claimedId == null && actualId != null) { + } else if (!claimedId.isPresent() && actualId.isPresent()) { // Older account, the actual was already created but the claimed // was missing due to a bug in Gerrit. Link the claimed. // @@ -390,14 +392,14 @@ new com.google.gerrit.server.account.AuthRequest(claimedIdentifier); linkReq.setDisplayName(areq.getDisplayName()); linkReq.setEmailAddress(areq.getEmailAddress()); - accountManager.link(actualId, linkReq); + accountManager.link(actualId.get(), linkReq); - } else if (claimedId != null && actualId == null) { + } else if (claimedId.isPresent() && !actualId.isPresent()) { // Claimed account already exists, but it smells like the user has // changed their delegate to point to a different provider. Link // the new provider. // - accountManager.link(claimedId, areq); + accountManager.link(claimedId.get(), areq); } else { // Both are null, we are going to create a new account below.
diff --git a/gerrit-patch-commonsnet/BUCK b/gerrit-patch-commonsnet/BUCK deleted file mode 100644 index 53b382f..0000000 --- a/gerrit-patch-commonsnet/BUCK +++ /dev/null
@@ -1,11 +0,0 @@ -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 = ['PUBLIC'], -)
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 deleted file mode 100644 index 09ccf9c..0000000 --- a/gerrit-patch-jgit/BUCK +++ /dev/null
@@ -1,66 +0,0 @@ -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 = [ - '//lib:gwtjsonrpc', - ':Edit', - ], - provided_deps = ['//lib/gwt:user'], - visibility = ['PUBLIC'], -) - -gwt_module( - name = 'Edit', - srcs = [':jgit_edit_src'], - deps = [':edit_src'], - visibility = ['PUBLIC'], -) - -prebuilt_jar( - name = 'edit_src', - binary_jar = ':jgit_edit_src', -) - -genrule( - name = 'jgit_edit_src', - cmd = 'unzip -qd $TMP $(location //lib/jgit/org.eclipse.jgit:jgit_src) ' + - 'org/eclipse/jgit/diff/Edit.java;' + - 'cd $TMP;' + - 'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java', - out = 'edit.src.zip', -) - -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 = ['PUBLIC'], -) - -java_test( - name = 'jgit_patch_tests', - srcs = glob(['src/test/java/**/*.java']), - deps = [ - ':server', - '//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 deleted file mode 100644 index 4be941c..0000000 --- a/gerrit-pgm/BUCK +++ /dev/null
@@ -1,184 +0,0 @@ -SRCS = 'src/main/java/com/google/gerrit/pgm/' -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/joda:joda-time', - '//lib/log:api', - '//lib/log:log4j', -] - -DEPS = BASE_JETTY_DEPS + [ - '//gerrit-reviewdb:server', - '//lib/log:jsonevent-layout', -] - -java_library( - name = 'init-api', - srcs = INIT_API_SRCS, - deps = DEPS + ['//gerrit-common:annotations'], - visibility = ['PUBLIC'], -) - -java_sources( - name = 'init-api-src', - srcs = INIT_API_SRCS, - visibility = ['PUBLIC'], -) - -java_library( - name = 'init', - srcs = glob([SRCS + 'init/*.java']), - resources = glob([RSRCS + 'init/*']), - deps = DEPS + [ - ':init-api', - ':util', - '//gerrit-common:annotations', - '//gerrit-lucene:lucene', - '//lib:args4j', - '//lib:derby', - '//lib:gwtjsonrpc', - '//lib:gwtorm', - '//lib:h2', - '//lib/commons:validator', - '//lib/mina:sshd', - ], - provided_deps = ['//gerrit-launcher:launcher'], - visibility = [ - '//gerrit-acceptance-framework/...', - '//gerrit-acceptance-tests/...', - '//gerrit-war:', - ], -) - -REST_UTIL_DEPS = [ - '//gerrit-cache-h2:cache-h2', - '//gerrit-util-cli:cli', - '//lib:args4j', - '//lib:gwtorm', - '//lib/commons:dbcp', -] - -java_library( - name = 'util', - deps = DEPS + REST_UTIL_DEPS, - exported_deps = [':util-nodep'], - visibility = [ - '//gerrit-acceptance-tests/...', - '//gerrit-gwtdebug:gwtdebug', - '//gerrit-war:', - ], -) - -java_library( - name = 'util-nodep', - srcs = glob([SRCS + 'util/*.java']), - provided_deps = DEPS + REST_UTIL_DEPS, - visibility = ['//gerrit-acceptance-framework/...'], -) - -JETTY_DEPS = [ - '//lib/jetty:jmx', - '//lib/jetty:server', - '//lib/jetty:servlet', -] - -java_library( - name = 'http', - deps = DEPS + JETTY_DEPS, - exported_deps = [':http-jetty'], - visibility = ['//gerrit-war:'], -) - -java_library( - name = 'http-jetty', - srcs = glob([SRCS + 'http/jetty/*.java']), - provided_deps = JETTY_DEPS + BASE_JETTY_DEPS + [ - '//gerrit-launcher:launcher', - '//gerrit-reviewdb:client', - '//lib:servlet-api-3_1', - ], - visibility = ['//gerrit-acceptance-framework/...'], -) - -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', - '//lib/auto:auto-value', - '//lib/prolog:cafeteria', - '//lib/prolog:compiler', - '//lib/prolog:runtime', -] - -java_library( - name = 'pgm', - resources = glob([RSRCS + '*']), - deps = DEPS + REST_PGM_DEPS + [ - ':daemon', - ], - visibility = [ - '//:', - '//gerrit-acceptance-tests/...', - '//gerrit-gwtdebug:gwtdebug', - '//tools/eclipse:classpath', - '//Documentation:licenses.txt', - ], -) - -# no transitive deps, used for gerrit-acceptance-framework -java_library( - name = 'daemon', - srcs = glob([SRCS + '*.java', SRCS + 'rules/*.java']), - resources = glob([RSRCS + '*']), - deps = ['//lib/auto:auto-value'], - provided_deps = DEPS + REST_PGM_DEPS + [ - '//gerrit-launcher:launcher', - ], - visibility = [ - '//gerrit-acceptance-framework/...', - '//gerrit-gwtdebug:gwtdebug', - ], -) - -java_test( - 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', - ], - source_under_test = [':pgm'], -)
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD index 59b371a..1eaaacb 100644 --- a/gerrit-pgm/BUILD +++ b/gerrit-pgm/BUILD
@@ -1,161 +1,176 @@ -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-elasticsearch:elasticsearch', + "//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 09fe5a3..ec00a63 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,7 @@ 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.StartupChecks; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -56,6 +57,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 +69,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; @@ -126,7 +129,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; } @@ -152,6 +155,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; @@ -172,16 +178,26 @@ } @VisibleForTesting - public Daemon(Runnable serverStarted) { + public Daemon(Runnable serverStarted, Path sitePath) { + super (sitePath); this.serverStarted = serverStarted; } + @VisibleForTesting + public void setEnableSshd(boolean enable) { + sshd = enable; + } + public void setEnableHttpd(boolean enable) { httpd = enable; } @Override public int run() throws Exception { + if (stopOnly) { + RuntimeShutdown.manualShutdown(); + return 0; + } if (doInit) { try { new Init(getSitePath()).run(); @@ -215,14 +231,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(); } }); @@ -314,6 +323,13 @@ @VisibleForTesting public void stop() { + if (runId != null) { + try { + Files.delete(runFile); + } catch (IOException err) { + log.warn("failed to delete " + runFile, err); + } + } manager.stop(); } @@ -354,6 +370,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 { @@ -363,6 +380,7 @@ modules.add(new PluginRestApiModule()); modules.add(new RestCacheAdminModule()); modules.add(new GpgModule(config)); + modules.add(new StartupChecks.Module()); if (MoreObjects.firstNonNull(httpd, true)) { modules.add(new CanonicalWebUrlModule() { @Override @@ -402,15 +420,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); } @@ -420,6 +441,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..52ce6b2 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,21 +14,22 @@ 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; -import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Ordering; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.common.FormatUtil; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; import com.google.gerrit.extensions.registration.DynamicSet; @@ -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(); @@ -138,7 +153,7 @@ ListeningExecutorService executor = newExecutor(); System.out.println("Rebuilding the NoteDb"); - final ImmutableMultimap<Project.NameKey, Change.Id> changesByProject = + ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject(); boolean ok; Stopwatch sw = Stopwatch.createStarted(); @@ -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); @@ -226,21 +241,16 @@ return MoreExecutors.newDirectExecutorService(); } - private ImmutableMultimap<Project.NameKey, Change.Id> getChangesByProject() + private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject() throws OrmException { // Memorize all changes so we can close the db connection and allow // rebuilder threads to use the full connection pool. - Multimap<Project.NameKey, Change.Id> changesByProject = - ArrayListMultimap.create(); + ListMultimap<Project.NameKey, Change.Id> changesByProject = + MultimapBuilder.hashKeys().arrayListValues().build(); try (ReviewDb db = schemaFactory.open()) { if (projects.isEmpty() && !changes.isEmpty()) { - Iterable<Change> todo = unwrapDb(db).changes().get( - Iterables.transform(changes, 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()); } @@ -260,7 +270,40 @@ } } } - return ImmutableMultimap.copyOf(changesByProject); + return ImmutableListMultimap.copyOf(changesByProject); } } + + private boolean rebuildProject(ReviewDb db, + ImmutableListMultimap<Project.NameKey, Change.Id> allChanges, + Project.NameKey project, Repository allUsersRepo) + throws IOException, OrmException { + checkArgument(allChanges.containsKey(project)); + boolean ok = true; + ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(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/SwitchSecureStore.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java index ac84e82..280795a2 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -163,20 +163,21 @@ private String getNewSecureStoreClassName(Path secureStore) throws IOException { - JarScanner scanner = new JarScanner(secureStore); - List<String> newSecureStores = - scanner.findSubClassesOf(SecureStore.class); - if (newSecureStores.isEmpty()) { - throw new RuntimeException(String.format( - "Cannot find implementation of SecureStore interface in %s", - secureStore.toAbsolutePath())); + try (JarScanner scanner = new JarScanner(secureStore)) { + List<String> newSecureStores = + scanner.findSubClassesOf(SecureStore.class); + if (newSecureStores.isEmpty()) { + throw new RuntimeException(String.format( + "Cannot find implementation of SecureStore interface in %s", + secureStore.toAbsolutePath())); + } + if (newSecureStores.size() > 1) { + throw new RuntimeException(String.format( + "Found too many implementations of SecureStore:\n%s\nin %s", Joiner + .on("\n").join(newSecureStores), secureStore.toAbsolutePath())); + } + return Iterables.getOnlyElement(newSecureStores); } - if (newSecureStores.size() > 1) { - throw new RuntimeException(String.format( - "Found too many implementations of SecureStore:\n%s\nin %s", Joiner - .on("\n").join(newSecureStores), secureStore.toAbsolutePath())); - } - return Iterables.getOnlyElement(newSecureStores); } private String getCurrentSecureStoreClassName(SitePaths sitePaths) {
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/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java index f625f75..ad1f844 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -30,12 +30,16 @@ import com.google.gerrit.pgm.init.api.InstallAllPlugins; import com.google.gerrit.pgm.init.api.InstallPlugins; import com.google.gerrit.pgm.init.api.LibraryDownload; +import com.google.gerrit.pgm.init.index.IndexManagerOnInit; +import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit; +import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit; import com.google.gerrit.pgm.util.SiteProgram; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.config.GerritServerConfigModule; import com.google.gerrit.server.config.SitePath; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.IndexModule; import com.google.gerrit.server.plugins.JarScanner; import com.google.gerrit.server.schema.SchemaUpdater; import com.google.gerrit.server.schema.UpdateUI; @@ -129,10 +133,18 @@ init.initializer.run(); init.flags.deleteOnFailure = false; - run = createSiteRun(init); - run.upgradeSchema(); + Injector sysInjector = createSysInjector(init); + IndexManagerOnInit indexManager = + sysInjector.getInstance(IndexManagerOnInit.class); + try { + indexManager.start(); + run = createSiteRun(init); + run.upgradeSchema(); - init.initializer.postRun(createSysInjector(init)); + init.initializer.postRun(sysInjector); + } finally { + indexManager.stop(); + } } catch (Exception | Error failure) { if (init.flags.deleteOnFailure) { recursiveDelete(getSitePath()); @@ -314,13 +326,12 @@ return null; } - try { - Path secureStoreLib = Paths.get(secureStore); - if (!Files.exists(secureStoreLib)) { - throw new InvalidSecureStoreException(String.format( - "File %s doesn't exist", secureStore)); - } - JarScanner scanner = new JarScanner(secureStoreLib); + Path secureStoreLib = Paths.get(secureStore); + if (!Files.exists(secureStoreLib)) { + throw new InvalidSecureStoreException(String.format( + "File %s doesn't exist", secureStore)); + } + try (JarScanner scanner = new JarScanner(secureStoreLib)) { List<String> secureStores = scanner.findSubClassesOf(SecureStore.class); if (secureStores.isEmpty()) { @@ -350,10 +361,12 @@ final GitRepositoryManager repositoryManager; @Inject - SiteRun(final ConsoleUI ui, final SitePaths site, final InitFlags flags, - final SchemaUpdater schemaUpdater, - final SchemaFactory<ReviewDb> schema, - final GitRepositoryManager repositoryManager) { + SiteRun(ConsoleUI ui, + SitePaths site, + InitFlags flags, + SchemaUpdater schemaUpdater, + SchemaFactory<ReviewDb> schema, + GitRepositoryManager repositoryManager) { this.ui = ui; this.site = site; this.flags = flags; @@ -431,7 +444,18 @@ bind(InitFlags.class).toInstance(init.flags); } }); - sysInjector = createDbInjector(SINGLE_USER).createChildInjector(modules); + Injector dbInjector = createDbInjector(SINGLE_USER); + switch (IndexModule.getIndexType(dbInjector)) { + case LUCENE: + modules.add(new LuceneIndexModuleOnInit()); + break; + case ELASTICSEARCH: + modules.add(new ElasticIndexModuleOnInit()); + break; + default: + throw new IllegalStateException("unsupported index.type"); + } + sysInjector = dbInjector.createChildInjector(modules); } return sysInjector; }
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..43056f9 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,8 +28,10 @@ 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.gerrit.server.account.AccountState; +import com.google.gerrit.server.index.account.AccountIndex; +import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; @@ -38,13 +41,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.List; public class InitAdminUser implements InitStep { private final ConsoleUI ui; private final InitFlags flags; private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory; private SchemaFactory<ReviewDb> dbFactory; + private AccountIndexCollection indexCollection; @Inject InitAdminUser( @@ -65,6 +72,11 @@ this.dbFactory = dbFactory; } + @Inject(optional = true) + void set(AccountIndexCollection indexCollection) { + this.indexCollection = indexCollection; + } + @Override public void postRun() throws Exception { AuthType authType = @@ -84,12 +96,14 @@ AccountSshKey sshKey = readSshKey(id); String email = readEmail(sshKey); + List<AccountExternalId> extIds = new ArrayList<>(2); AccountExternalId extUser = new AccountExternalId(id, new AccountExternalId.Key( AccountExternalId.SCHEME_USERNAME, username)); if (!Strings.isNullOrEmpty(httpPassword)) { extUser.setPassword(httpPassword); } + extIds.add(extUser); db.accountExternalIds().insert(Collections.singleton(extUser)); if (email != null) { @@ -97,6 +111,7 @@ new AccountExternalId(id, new AccountExternalId.Key( AccountExternalId.SCHEME_MAILTO, email)); extMailto.setEmailAddress(email); + extIds.add(extMailto); db.accountExternalIds().insert(Collections.singleton(extMailto)); } @@ -105,11 +120,11 @@ a.setPreferredEmail(email); db.accounts().insert(Collections.singleton(a)); - AccountGroupName adminGroup = db.accountGroupNames().get( + AccountGroupName adminGroupName = db.accountGroupNames().get( new AccountGroup.NameKey("Administrators")); AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, - adminGroup.getId())); + adminGroupName.getId())); db.accountGroupMembers().insert(Collections.singleton(m)); if (sshKey != null) { @@ -118,6 +133,15 @@ authorizedKeys.addKey(sshKey.getSshPublicKey()); authorizedKeys.save("Added SSH key for initial admin user\n"); } + + AccountGroup adminGroup = + db.accountGroups().get(adminGroupName.getId()); + AccountState as = new AccountState(a, + Collections.singleton(adminGroup.getGroupUUID()), extIds, + new HashMap<>()); + for (AccountIndex accountIndex : indexCollection.getWriteIndexes()) { + accountIndex.replace(as); + } } } }
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/init/index/IndexManagerOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/IndexManagerOnInit.java new file mode 100644 index 0000000..813a7ac --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/IndexManagerOnInit.java
@@ -0,0 +1,55 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.pgm.init.index; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.server.index.IndexDefinition; +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import java.util.Collection; + +/** + * This class starts/stops the indexes from the init program so that init can + * write updates the indexes. + */ +public class IndexManagerOnInit { + private final LifecycleListener indexManager; + private final Collection<IndexDefinition<?, ?, ?>> defs; + + @Inject + IndexManagerOnInit( + @Named(IndexModuleOnInit.INDEX_MANAGER) LifecycleListener indexManager, + Collection<IndexDefinition<?, ?, ?>> defs) { + this.indexManager = indexManager; + this.defs = defs; + } + + public void start() { + indexManager.start(); + + for (IndexDefinition<?, ?, ?> def : defs) { + def.getIndexCollection().start(); + } + } + + public void stop() { + indexManager.stop(); + + for (IndexDefinition<?, ?, ?> def : defs) { + def.getIndexCollection().stop(); + } + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java new file mode 100644 index 0000000..7b1d2eb --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/IndexModuleOnInit.java
@@ -0,0 +1,111 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.pgm.init.index; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.index.IndexDefinition; +import com.google.gerrit.server.index.SchemaDefinitions; +import com.google.gerrit.server.index.SingleVersionModule; +import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener; +import com.google.gerrit.server.index.account.AccountIndexCollection; +import com.google.gerrit.server.index.account.AccountIndexDefinition; +import com.google.gerrit.server.index.account.AccountSchemaDefinitions; +import com.google.gerrit.server.index.account.AllAccountsIndexer; +import com.google.gerrit.server.index.group.AllGroupsIndexer; +import com.google.gerrit.server.index.group.GroupIndexCollection; +import com.google.gerrit.server.index.group.GroupIndexDefinition; +import com.google.gerrit.server.index.group.GroupSchemaDefinitions; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.ProvisionException; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import com.google.inject.util.Providers; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class IndexModuleOnInit extends AbstractModule { + static final String INDEX_MANAGER = "IndexModuleOnInit/IndexManager"; + + private static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS = + ImmutableList.<SchemaDefinitions<?>> of( + AccountSchemaDefinitions.INSTANCE, + GroupSchemaDefinitions.INSTANCE); + + @Override + protected void configure() { + // The AccountIndex implementations (LuceneAccountIndex and + // ElasticAccountIndex) need AccountCache only for reading from the index. + // On init we only want to write to the index, hence we don't need the + // account cache. + bind(AccountCache.class).toProvider(Providers.of(null)); + + // AccountIndexDefinition wants to have AllAccountsIndexer but it is only + // used by the Reindex program and the OnlineReindexer which are both not + // used during init, hence we don't need AllAccountsIndexer. + bind(AllAccountsIndexer.class).toProvider(Providers.of(null)); + + bind(AccountIndexCollection.class); + + // The GroupIndex implementations (LuceneGroupIndex and ElasticGroupIndex) + // need GroupCache only for reading from the index. On init we only want to + // write to the index, hence we don't need the group cache. + bind(GroupCache.class).toProvider(Providers.of(null)); + + // GroupIndexDefinition wants to have AllGroupsIndexer but it is only used + // by the Reindex program and the OnlineReindexer which are both not used + // during init, hence we don't need AllGroupsIndexer. + bind(AllGroupsIndexer.class).toProvider(Providers.of(null)); + + bind(GroupIndexCollection.class); + + bind(new TypeLiteral<Map<String, Integer>>() {}) + .annotatedWith(Names.named(SingleVersionModule.SINGLE_VERSIONS)) + .toInstance(ImmutableMap.<String, Integer> of()); + bind(LifecycleListener.class).annotatedWith(Names.named(INDEX_MANAGER)) + .to(SingleVersionListener.class); + } + + @Provides + Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions( + AccountIndexDefinition accounts, + GroupIndexDefinition groups) { + Collection<IndexDefinition<?, ?, ?>> result = + ImmutableList.<IndexDefinition<?, ?, ?>> of( + accounts, + groups); + Set<String> expected = + FluentIterable.from(ALL_SCHEMA_DEFS) + .transform(SchemaDefinitions::getName) + .toSet(); + Set<String> actual = FluentIterable.from(result) + .transform(IndexDefinition::getName) + .toSet(); + if (!expected.equals(actual)) { + throw new ProvisionException( + "need index definitions for all schemas: " + + expected + " != " + actual); + } + return result; + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/elasticsearch/ElasticIndexModuleOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/elasticsearch/ElasticIndexModuleOnInit.java new file mode 100644 index 0000000..7f74c2b --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/elasticsearch/ElasticIndexModuleOnInit.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.pgm.init.index.elasticsearch; + +import com.google.gerrit.elasticsearch.ElasticAccountIndex; +import com.google.gerrit.elasticsearch.ElasticGroupIndex; +import com.google.gerrit.pgm.init.index.IndexModuleOnInit; +import com.google.gerrit.server.index.account.AccountIndex; +import com.google.gerrit.server.index.group.GroupIndex; +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; + +public class ElasticIndexModuleOnInit extends AbstractModule { + + @Override + protected void configure() { + install( + new FactoryModuleBuilder() + .implement(AccountIndex.class, ElasticAccountIndex.class) + .build(AccountIndex.Factory.class)); + + install( + new FactoryModuleBuilder() + .implement(GroupIndex.class, ElasticGroupIndex.class) + .build(GroupIndex.Factory.class)); + + install(new IndexModuleOnInit()); + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java new file mode 100644 index 0000000..12a44dc --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/index/lucene/LuceneIndexModuleOnInit.java
@@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.pgm.init.index.lucene; + +import com.google.gerrit.lucene.LuceneAccountIndex; +import com.google.gerrit.lucene.LuceneGroupIndex; +import com.google.gerrit.pgm.init.index.IndexModuleOnInit; +import com.google.gerrit.server.index.account.AccountIndex; +import com.google.gerrit.server.index.group.GroupIndex; +import com.google.inject.AbstractModule; +import com.google.inject.assistedinject.FactoryModuleBuilder; + +public class LuceneIndexModuleOnInit extends AbstractModule { + @Override + protected void configure() { + install( + new FactoryModuleBuilder() + .implement(AccountIndex.class, LuceneAccountIndex.class) + .build(AccountIndex.Factory.class)); + + install( + new FactoryModuleBuilder() + .implement(GroupIndex.class, LuceneGroupIndex.class) + .build(GroupIndex.Factory.class)); + + install(new IndexModuleOnInit()); + } +}
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/ProxyUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ProxyUtil.java index c2a52ec..7a12d38 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ProxyUtil.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ProxyUtil.java
@@ -37,6 +37,8 @@ package com.google.gerrit.pgm.util; +import com.google.common.base.Strings; + import org.eclipse.jgit.util.CachedAuthenticator; import java.net.MalformedURLException; @@ -57,7 +59,7 @@ */ static void configureHttpProxy() throws MalformedURLException { final String s = System.getenv("http_proxy"); - if (s == null || s.equals("")) { + if (Strings.isNullOrEmpty(s)) { return; }
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..3a0d7e5 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,31 +15,31 @@ # 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 v156 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.56/bcprov-jdk15on-1.56.jar + sha1 = a153c6f9744a3e9dd6feab5e210e1c9861362ec7 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 v156 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk15on/1.56/bcpkix-jdk15on-1.56.jar + sha1 = 4648af70268b6fdb24674fb1fd7c1fcc73db1231 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 v156 + url = https://repo1.maven.org/maven2/org/bouncycastle/bcpg-jdk15on/1.56/bcpg-jdk15on-1.56.jar + sha1 = 9c3f2e7072c8cc1152079b5c25291a9f462631f1 needs = bouncyCastleProvider remove = bcpg-.*[.]jar [library "mysqlDriver"] - name = MySQL Connector/J 5.1.21 - url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar - sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9 + name = MySQL Connector/J 5.1.40 + url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.40/mysql-connector-java-5.1.40.jar + sha1 = ef2a2ceab1735eaaae0b5d1cccf574fb7c6e1c52 remove = mysql-connector-java-.*[.]jar [library "oracleDriver"]
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 deleted file mode 100644 index 8cbf1a1..0000000 --- a/gerrit-plugin-api/BUCK +++ /dev/null
@@ -1,77 +0,0 @@ -SRCS = [ - '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', -] - -java_binary( - name = 'plugin-api', - deps = [':lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'lib', - exported_deps = 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:gson', - '//lib:guava', - '//lib:gwtorm', - '//lib:jsch', - '//lib:mime-util', - '//lib:servlet-api-3_1', - '//lib:velocity', - '//lib/commons:lang', - '//lib/dropwizard:dropwizard-core', - '//lib/guice:guice', - '//lib/guice:guice-assistedinject', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet', - '//lib/joda:joda-time', - '//lib/log:api', - '//lib/mina:sshd', - '//lib/prolog:compiler', - '//lib/prolog:runtime', - ], - visibility = ['PUBLIC'], -) - -java_binary( - name = 'plugin-api-src', - deps = [ - '//gerrit-extension-api:extension-api-src', - ] + [d + '-src' for d in PLUGIN_API], - visibility = ['PUBLIC'], -) - -java_doc( - name = 'plugin-api-javadoc', - title = 'Gerrit Review Plugin API Documentation', - pkgs = ['com.google.gerrit'], - paths = [n for n in SRCS], - srcs = glob([n + '**/*.java' for n in SRCS]), - deps = [ - ':plugin-api', - '//lib/bouncycastle:bcprov', - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcpkix', - ], - visibility = ['PUBLIC'], - do_it_wrong = True, -)
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD index 2c18ca6..ebc5619 100644 --- a/gerrit-plugin-api/BUILD +++ b/gerrit-plugin-api/BUILD
@@ -1,51 +1,110 @@ 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/commons:lang3", + "//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 2e8a39d..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.5</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 f06e1da..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.5</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/HttpModule.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java deleted file mode 100644 index a0fed9e..0000000 --- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.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.servlet.ServletModule; - -class HttpModule extends ServletModule { - @Override - protected void configureServlets() { - // TODO - } -}
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 bec11c1..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.5</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 deleted file mode 100644 index 2ee0e19..0000000 --- a/gerrit-plugin-gwtui/BUCK +++ /dev/null
@@ -1,65 +0,0 @@ -COMMON = ['gerrit-gwtui-common/src/main/java/'] -GWTEXPUI = ['gerrit-gwtexpui/src/main/java/'] -SRC = 'src/main/java/com/google/gerrit/' -SRCS = glob([SRC + '**/*.java']) - -DEPS = ['//lib/gwt:user'] - -java_binary( - name = 'gwtui-api', - deps = [ - ':gwtui-api-lib', - '//gerrit-gwtui-common:client-lib', - ], - visibility = ['PUBLIC'], -) - -java_library( - name = 'gwtui-api-lib', - srcs = SRCS, - resources = glob(['src/main/**/*']), - exported_deps = ['//gerrit-gwtui-common:client-lib'], - provided_deps = DEPS + ['//lib/gwt:dev'], - visibility = ['PUBLIC'], -) - -java_binary( - name = 'gwtui-api-src', - deps = [ - ':gwtui-api-src-lib', - '//gerrit-gwtexpui:client-src-lib', - '//gerrit-gwtui-common:client-src-lib', - ], - visibility = ['PUBLIC'], -) - -java_library( - name = 'gwtui-api-src-lib', - srcs = [], - resources = glob(['src/main/**/*']), - visibility = ['PUBLIC'], -) - -java_doc( - name = 'gwtui-api-javadoc', - title = 'Gerrit Review GWT Extension API Documentation', - pkgs = [ - 'com.google.gerrit', - 'com.google.gwtexpui.clippy', - 'com.google.gwtexpui.globalkey', - 'com.google.gwtexpui.safehtml', - 'com.google.gwtexpui.user', - ], - paths = COMMON + GWTEXPUI, - srcs = SRCS, - deps = DEPS + [ - '//lib:gwtjsonrpc', - '//lib:gwtorm_client', - '//lib/gwt:dev', - '//gerrit-gwtui-common:client-lib', - '//gerrit-common:client', - '//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 d0b43a2..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.5</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 515a5d0..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.5</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/BUCK b/gerrit-prettify/BUCK deleted file mode 100644 index bf2e02a..0000000 --- a/gerrit-prettify/BUCK +++ /dev/null
@@ -1,47 +0,0 @@ -SRC = 'src/main/java/com/google/gerrit/prettify/' - -gwt_module( - name = 'client', - srcs = glob([ - SRC + 'common/**/*.java', - ]), - gwt_xml = SRC + 'PrettyFormatter.gwt.xml', - deps = [ - '//gerrit-gwtexpui:SafeHtml', - ], - exported_deps = [ - '//gerrit-extension-api:client', - '//gerrit-patch-jgit:client', - '//gerrit-patch-jgit:Edit', - '//gerrit-reviewdb:client', - '//lib:gwtjsonrpc', - '//lib:gwtjsonrpc_src', - ], - provided_deps = ['//lib/gwt:user'], - visibility = ['PUBLIC'], -) - -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 = ['PUBLIC'], -) - -export_file( - name = 'prettify.min.js', - src = 'src/main/resources/com/google/gerrit/prettify/client/prettify.js', - visibility = ['//Documentation:'], -) - -export_file( - name = 'prettify.min.css', - src = 'src/main/resources/com/google/gerrit/prettify/client/prettify.css', - visibility = ['//Documentation:'], -)
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 deleted file mode 100644 index 82e0135..0000000 --- a/gerrit-reviewdb/BUCK +++ /dev/null
@@ -1,38 +0,0 @@ -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 = ['PUBLIC'], -) - -java_library( - name = 'server', - srcs = glob([SRC + '**/*.java']), - resources = glob(['src/main/resources/**/*']), - deps = [ - '//gerrit-extension-api:api', - '//lib:guava', - '//lib:gwtorm', - ], - visibility = ['PUBLIC'], -) - -java_test( - name = 'client_tests', - srcs = glob([TESTS + 'client/**/*.java']), - deps = [ - ':client', - '//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..39a59b9 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
@@ -42,9 +42,6 @@ * managed {@link AccountGroup}. Multiple records can exist when the user is a * member of more than one group.</li> * - * <li>{@link AccountProjectWatch}: user's email settings related to a specific - * {@link Project}. One record per project the user is interested in tracking.</li> - * * <li>{@link AccountSshKey}: user's public SSH keys, for authentication through * the internal SSH daemon. One record per SSH key uploaded by the user, keys * are checked in random order until a match is found.</li> @@ -55,10 +52,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]"; @@ -84,7 +77,7 @@ protected Id() { } - public Id(final int id) { + public Id(int id) { this.id = id; } @@ -99,8 +92,8 @@ } /** Parse an Account.Id out of a string representation. */ - public static Id parse(final String str) { - final Id r = new Id(); + public static Id parse(String str) { + Id r = new Id(); r.fromString(str); return r; } @@ -161,10 +154,14 @@ // DELETED: id = 6 (generalPreferences) - /** Is this user active */ + /** Is this user inactive? */ @Column(id = 7) protected boolean inactive; + /** The user-settable status of this account (e.g. busy, OOO, available) */ + @Column(id = 8, notNull = false) + protected String status; + /** <i>computed</i> the username selected from the identities. */ protected String userName; @@ -197,7 +194,7 @@ } /** Set the full name of the user ("Given-name Surname" style). */ - public void setFullName(final String name) { + public void setFullName(String name) { if (name != null && !name.trim().isEmpty()) { fullName = name.trim(); } else { @@ -211,7 +208,7 @@ } /** Set the email address the user prefers to be contacted through. */ - public void setPreferredEmail(final String addr) { + public void setPreferredEmail(String addr) { preferredEmail = addr; } @@ -279,13 +276,21 @@ inactive = ! active; } + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + /** @return the computed user name for this account */ public String getUserName() { return userName; } /** Update the computed user name property. */ - public void setUserName(final String userName) { + public void setUserName(String userName) { this.userName = userName; }
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/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java deleted file mode 100644 index a6796e7..0000000 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java +++ /dev/null
@@ -1,202 +0,0 @@ -// Copyright (C) 2008 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.reviewdb.client; - -import com.google.gwtorm.client.Column; -import com.google.gwtorm.client.CompoundKey; -import com.google.gwtorm.client.StringKey; - -/** An {@link Account} interested in a {@link Project}. */ -public final class AccountProjectWatch { - - public enum NotifyType { - // sort by name, except 'ALL' which should stay last - ABANDONED_CHANGES, - ALL_COMMENTS, - NEW_CHANGES, - NEW_PATCHSETS, - SUBMITTED_CHANGES, - - ALL - } - - public static final String FILTER_ALL = "*"; - - public static class Key extends CompoundKey<Account.Id> { - private static final long serialVersionUID = 1L; - - @Column(id = 1) - protected Account.Id accountId; - - @Column(id = 2) - protected Project.NameKey projectName; - - @Column(id = 3) - protected Filter filter; - - protected Key() { - accountId = new Account.Id(); - projectName = new Project.NameKey(); - filter = new Filter(); - } - - public Key(Account.Id a, Project.NameKey g, String f) { - accountId = a; - projectName = g; - filter = new Filter(f); - } - - @Override - public Account.Id getParentKey() { - return accountId; - } - - public Project.NameKey getProjectName() { - return projectName; - } - - public Filter getFilter() { - return filter; - } - - @Override - public com.google.gwtorm.client.Key<?>[] members() { - return new com.google.gwtorm.client.Key<?>[] {projectName, filter}; - } - } - - public static class Filter extends StringKey<com.google.gwtorm.client.Key<?>> { - private static final long serialVersionUID = 1L; - - @Column(id = 1) - protected String filter; - - protected Filter() { - } - - public Filter(String f) { - filter = f != null && !f.isEmpty() ? f : FILTER_ALL; - } - - @Override - public String get() { - return filter; - } - - @Override - protected void set(String newValue) { - filter = newValue; - } - } - - @Column(id = 1, name = Column.NONE) - protected Key key; - - /** Automatically send email notifications of new changes? */ - @Column(id = 2) - protected boolean notifyNewChanges; - - /** Automatically receive comments published to this project */ - @Column(id = 3) - protected boolean notifyAllComments; - - /** Automatically receive changes submitted to this project */ - @Column(id = 4) - protected boolean notifySubmittedChanges; - - @Column(id = 5) - protected boolean notifyNewPatchSets; - - @Column(id = 6) - protected boolean notifyAbandonedChanges; - - protected AccountProjectWatch() { - } - - public AccountProjectWatch(final AccountProjectWatch.Key k) { - key = k; - } - - public AccountProjectWatch.Key getKey() { - return key; - } - - public Account.Id getAccountId() { - return key.accountId; - } - - public Project.NameKey getProjectNameKey() { - return key.projectName; - } - - public String getFilter() { - return FILTER_ALL.equals(key.filter.get()) ? null : key.filter.get(); - } - - public boolean isNotify(final NotifyType type) { - switch (type) { - case NEW_CHANGES: - return notifyNewChanges; - - case NEW_PATCHSETS: - return notifyNewPatchSets; - - case ALL_COMMENTS: - return notifyAllComments; - - case SUBMITTED_CHANGES: - return notifySubmittedChanges; - - case ABANDONED_CHANGES: - return notifyAbandonedChanges; - - case ALL: - break; - } - return false; - } - - public void setNotify(final NotifyType type, final boolean v) { - switch (type) { - case NEW_CHANGES: - notifyNewChanges = v; - break; - - case NEW_PATCHSETS: - notifyNewPatchSets = v; - break; - - case ALL_COMMENTS: - notifyAllComments = v; - break; - - case SUBMITTED_CHANGES: - notifySubmittedChanges = v; - break; - - case ABANDONED_CHANGES: - notifyAbandonedChanges = v; - break; - - case ALL: - notifyNewChanges = v; - notifyNewPatchSets = v; - notifyAllComments = v; - notifySubmittedChanges = v; - notifyAbandonedChanges = v; - break; - } - } -}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java deleted file mode 100644 index 38a78ba..0000000 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java +++ /dev/null
@@ -1,87 +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.reviewdb.client; - -public enum AuthType { - /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */ - OPENID, - - /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */ - OPENID_SSO, - - /** - * Login relies upon the container/web server security. - * <p> - * The container or web server must populate an HTTP header with a unique name - * for the current user. Gerrit will implicitly trust the value of this header - * to supply the unique identity. - */ - HTTP, - - /** - * Login relies upon the container/web server security, but also uses LDAP. - * <p> - * Like {@link #HTTP}, the container or web server must populate an HTTP - * header with a unique name for the current user. Gerrit will implicitly - * trust the value of this header to supply the unique identity. - * <p> - * In addition to trusting the HTTP headers, Gerrit will obtain basic user - * registration (name and email) from LDAP, and some group memberships. - */ - HTTP_LDAP, - - /** - * Login via client SSL certificate. - * <p> - * This authentication type is actually kind of SSO. Gerrit will configure - * 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. - * <p> - * After the authentication is done Gerrit will obtain basic user - * registration (name and email) from LDAP, and some group memberships. - * Therefore, the "_LDAP" suffix in the name of this authentication type. - */ - CLIENT_SSL_CERT_LDAP, - - /** - * Login collects username and password through a web form, and binds to LDAP. - * <p> - * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and - * makes the connection to the LDAP server on their behalf. - */ - LDAP, - - /** - * Login collects username and password through a web form, and binds to LDAP. - * <p> - * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and - * makes the connection to the LDAP server on their behalf. - * <p> - * Unlike the more generic {@link #LDAP} mode, Gerrit can only query the - * directory via an actual authenticated user account. - */ - LDAP_BIND, - - /** Login is managed by additional, unspecified code. */ - CUSTOM_EXTENSION, - - /** Development mode to enable becoming anyone you want. */ - DEVELOPMENT_BECOME_ANY_ACCOUNT, - - /** Generic OAuth provider over HTTP. */ - OAUTH -}
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..d5edb75 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,30 @@ } } + 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()); + plc.setUnresolved(c.unresolved); + return plc; + } + @Column(id = 1, name = Column.NONE) protected Key key; @@ -126,6 +169,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 +205,7 @@ key = o.key; lineNbr = o.lineNbr; author = o.author; + realAuthor = o.realAuthor; writtenOn = o.writtenOn; status = o.status; side = o.side; @@ -186,6 +241,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 +324,26 @@ return tag; } + public void setUnresolved(Boolean unresolved) { + this.unresolved = unresolved; + } + + public Boolean getUnresolved() { + return unresolved; + } + + 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) { @@ -274,7 +358,8 @@ && Objects.equals(parentUuid, c.getParentUuid()) && Objects.equals(range, c.getRange()) && Objects.equals(revId, c.getRevId()) - && Objects.equals(tag, c.getTag()); + && Objects.equals(tag, c.getTag()) + && Objects.equals(unresolved, c.getUnresolved()); } return false; } @@ -291,6 +376,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 +387,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/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java index 12bd80f..b930356 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -28,24 +28,9 @@ @PrimaryKey("key") AccountExternalId get(AccountExternalId.Key key) throws OrmException; - @Query("WHERE key >= ? AND key <= ? ORDER BY key LIMIT ?") - ResultSet<AccountExternalId> suggestByKey(AccountExternalId.Key keyA, - AccountExternalId.Key keyB, int limit) throws OrmException; - @Query("WHERE accountId = ?") ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException; - @Query("WHERE accountId = ? AND emailAddress = ?") - ResultSet<AccountExternalId> byAccountEmail(Account.Id id, String email) - throws OrmException; - - @Query("WHERE emailAddress = ?") - ResultSet<AccountExternalId> byEmailAddress(String email) throws OrmException; - - @Query("WHERE emailAddress >= ? AND emailAddress <= ? ORDER BY emailAddress LIMIT ?") - ResultSet<AccountExternalId> suggestByEmailAddress(String emailA, - String emailB, int limit) throws OrmException; - @Query ResultSet<AccountExternalId> all() throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java deleted file mode 100644 index c6f4775..0000000 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java +++ /dev/null
@@ -1,37 +0,0 @@ -// Copyright (C) 2008 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.reviewdb.server; - -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountProjectWatch; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gwtorm.server.Access; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.PrimaryKey; -import com.google.gwtorm.server.Query; -import com.google.gwtorm.server.ResultSet; - -public interface AccountProjectWatchAccess extends - Access<AccountProjectWatch, AccountProjectWatch.Key> { - @Override - @PrimaryKey("key") - AccountProjectWatch get(AccountProjectWatch.Key key) throws OrmException; - - @Query("WHERE key.accountId = ?") - ResultSet<AccountProjectWatch> byAccount(Account.Id id) throws OrmException; - - @Query("WHERE key.projectName = ?") - ResultSet<AccountProjectWatch> byProject(Project.NameKey name) throws OrmException; -}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java index 81f3e57..cff34fa 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -65,4 +65,7 @@ + "' AND author = ?") ResultSet<PatchLineComment> draftByAuthor(Account.Id author) throws OrmException; + + @Query + ResultSet<PatchLineComment> all() throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java index f7452c5..bf3c9e4 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -29,4 +29,7 @@ @Query("WHERE id.changeId = ? ORDER BY id.patchSetId") ResultSet<PatchSet> byChange(Change.Id id) throws OrmException; + + @Query + ResultSet<PatchSet> all() throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java index cddee73..66e6a3b 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -39,4 +39,7 @@ @Query("WHERE key.patchSetId = ? AND key.accountId = ?") ResultSet<PatchSetApproval> byPatchSetUser(PatchSet.Id patchSet, Account.Id account) throws OrmException; + + @Query + ResultSet<PatchSetApproval> all() throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java index c585ca5..dafca81 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -17,7 +17,6 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.SystemConfig; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.Relation; @@ -71,8 +70,7 @@ // Deleted @Relation(id = 18) - @Relation(id = 19) - AccountProjectWatchAccess accountProjectWatches(); + // Deleted @Relation(id = 19) // Deleted @Relation(id = 20) @@ -119,12 +117,4 @@ @Sequence(startWith = FIRST_CHANGE_ID) @Deprecated int nextChangeId() throws OrmException; - - /** - * Next id for a block of {@link ChangeMessage} records. - * - * @see com.google.gerrit.server.ChangeUtil#messageUUID(ReviewDb) - */ - @Sequence - int nextChangeMessageId() throws OrmException; }
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/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java index 6b25378..50f22ce 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -109,11 +109,6 @@ } @Override - public AccountProjectWatchAccess accountProjectWatches() { - return delegate.accountProjectWatches(); - } - - @Override public ChangeAccess changes() { return delegate.changes(); } @@ -164,11 +159,6 @@ return delegate.nextChangeId(); } - @Override - public int nextChangeMessageId() throws OrmException { - return delegate.nextChangeMessageId(); - } - public static class ChangeAccessWrapper implements ChangeAccess { protected final ChangeAccess delegate; @@ -367,6 +357,11 @@ Account.Id account) throws OrmException { return delegate.byPatchSetUser(patchSet, account); } + + @Override + public ResultSet<PatchSetApproval> all() throws OrmException { + return delegate.all(); + } } public static class ChangeMessageAccessWrapper @@ -564,6 +559,10 @@ return delegate.byChange(id); } + @Override + public ResultSet<PatchSet> all() throws OrmException { + return delegate.all(); + } } public static class PatchLineCommentAccessWrapper @@ -700,5 +699,10 @@ throws OrmException { return delegate.draftByAuthor(author); } + + @Override + public ResultSet<PatchLineComment> all() throws OrmException { + return delegate.all(); + } } }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql index 2110295..deceab9 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -21,10 +21,6 @@ CREATE INDEX account_external_ids_byAccount ON account_external_ids (account_id); --- covers: byEmailAddress, suggestByEmailAddress -CREATE INDEX account_external_ids_byEmail -ON account_external_ids (email_address); - -- ********************************************************************* -- AccountGroupMemberAccess @@ -39,13 +35,6 @@ CREATE INDEX account_group_id_byInclude ON account_group_by_id (include_uuid); --- ********************************************************************* --- AccountProjectWatchAccess --- @PrimaryKey covers: byAccount --- covers: byProject -CREATE INDEX account_project_watches_byP -ON account_project_watches (project_name); - -- ********************************************************************* -- ApprovalCategoryAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql index 334b6c4..1ec8ea6 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -25,11 +25,6 @@ ON account_external_ids (account_id) # --- covers: byEmailAddress, suggestByEmailAddress -CREATE INDEX account_external_ids_byEmail -ON account_external_ids (email_address) -# - -- ********************************************************************* -- AccountGroupMemberAccess @@ -47,14 +42,6 @@ -- ********************************************************************* --- AccountProjectWatchAccess --- @PrimaryKey covers: byAccount --- covers: byProject -CREATE INDEX acc_project_watches_byProject -ON account_project_watches (project_name) -# - --- ********************************************************************* -- ApprovalCategoryAccess -- too small to bother indexing
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql index bdceb7b..a11c86b 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -68,10 +68,6 @@ CREATE INDEX account_external_ids_byAccount ON account_external_ids (account_id); --- covers: byEmailAddress, suggestByEmailAddress -CREATE INDEX account_external_ids_byEmail -ON account_external_ids (email_address); - -- ********************************************************************* -- AccountGroupMemberAccess @@ -86,13 +82,6 @@ CREATE INDEX account_group_id_byInclude ON account_group_by_id (include_uuid); --- ********************************************************************* --- AccountProjectWatchAccess --- @PrimaryKey covers: byAccount --- covers: byProject -CREATE INDEX account_project_watches_byP -ON account_project_watches (project_name); - -- ********************************************************************* -- ApprovalCategoryAccess
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 deleted file mode 100644 index 4fc578c..0000000 --- a/gerrit-server/BUCK +++ /dev/null
@@ -1,213 +0,0 @@ -CONSTANTS_SRC = [ - 'src/main/java/com/google/gerrit/server/documentation/Constants.java', -] - -SRCS = glob( - ['src/main/java/**/*.java'], - excludes = CONSTANTS_SRC, -) -RESOURCES = glob(['src/main/resources/**/*']) - -java_library( - name = 'constants', - srcs = CONSTANTS_SRC, - visibility = ['PUBLIC'], -) - -# TODO(sop) break up gerrit-server java_library(), its too big -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: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', - ], - provided_deps = [ - '//lib:servlet-api-3_1', - ], - visibility = ['PUBLIC'], -) - -java_sources( - name = 'server-src', - srcs = SRCS + RESOURCES, - visibility = ['PUBLIC'], -) - -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', -] - -TESTUTIL = glob([ - '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 = [ - '//lib/auto:auto-value', - ], - provided_deps = TESTUTIL_DEPS, - exported_deps = [ - '//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 = ['PUBLIC'], -) - -PROLOG_TEST_CASE = [ - 'src/test/java/com/google/gerrit/rules/PrologTestCase.java', -] -PROLOG_TESTS = glob( - ['src/test/java/com/google/gerrit/rules/**/*.java'], - excludes = 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', - ], -) - -java_test( - 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'], -) - -java_test( - 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', - ], - source_under_test = [':server'], -) - -java_test( - name = 'server_tests', - labels = ['server'], - srcs = glob( - ['src/test/java/**/*.java'], - excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS - ), - resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']), - deps = TESTUTIL_DEPS + [ - ':testutil', - '//gerrit-antlr:query_exception', - '//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/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..17496e8 100644 --- a/gerrit-server/BUILD +++ b/gerrit-server/BUILD
@@ -1,208 +1,244 @@ -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 + [ + "//gerrit-pgm:init", + "//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 = "large", + 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 = "large", + 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..3184b15 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,21 +17,22 @@ 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.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; 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 ListMultimap<String, ?> EMPTY_PARAMS = + ImmutableListMultimap.of(); public final String sessionId; public final CurrentUser who; public final long when; public final String what; - public final Multimap<String, ?> params; + public final ListMultimap<String, ?> params; public final Object result; public final long timeAtStart; public final long elapsed; @@ -58,7 +59,7 @@ * @param result result of the event */ public AuditEvent(String sessionId, CurrentUser who, String what, long when, - Multimap<String, ?> params, Object result) { + ListMultimap<String, ?> params, Object result) { Preconditions.checkNotNull(what, "what is a mandatory not null param !"); this.sessionId = MoreObjects.firstNonNull(sessionId, UNKNOWN_SESSION_ID);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java index 90cddee..6bd7deb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java
@@ -15,7 +15,7 @@ package com.google.gerrit.audit; import com.google.common.base.Preconditions; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.server.CurrentUser; @@ -45,11 +45,11 @@ * @param view view rendering object */ public ExtendedHttpAuditEvent(String sessionId, CurrentUser who, - HttpServletRequest httpRequest, long when, Multimap<String, ?> params, + HttpServletRequest httpRequest, long when, ListMultimap<String, ?> params, Object input, int status, Object result, RestResource resource, RestView<RestResource> view) { - super(sessionId, who, httpRequest.getRequestURI(), when, params, httpRequest.getMethod(), - input, status, result); + super(sessionId, who, httpRequest.getRequestURI(), when, params, + httpRequest.getMethod(), input, status, result); this.httpRequest = Preconditions.checkNotNull(httpRequest); this.resource = resource; this.view = view;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java index 805e050..300d760 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java
@@ -13,7 +13,7 @@ // limitations under the License. package com.google.gerrit.audit; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.server.CurrentUser; public class HttpAuditEvent extends AuditEvent { @@ -34,8 +34,9 @@ * @param status HTTP status * @param result result of the event */ - public HttpAuditEvent(String sessionId, CurrentUser who, String what, long when, - Multimap<String, ?> params, String httpMethod, Object input, int status, Object result) { + public HttpAuditEvent(String sessionId, CurrentUser who, String what, + long when, ListMultimap<String, ?> params, String httpMethod, + Object input, int status, Object result) { super(sessionId, who, what, when, params, result); this.httpMethod = httpMethod; this.input = input;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java index 157b72d..cefc3a2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java
@@ -11,9 +11,10 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package com.google.gerrit.audit; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.server.CurrentUser; public class RpcAuditEvent extends HttpAuditEvent { @@ -32,8 +33,8 @@ * @param result result of the event */ public RpcAuditEvent(String sessionId, CurrentUser who, String what, - long when, Multimap<String, ?> params, String httpMethod, Object input, - int status, Object result) { + long when, ListMultimap<String, ?> params, String httpMethod, + Object input, int status, Object result) { super(sessionId, who, what, when, params, httpMethod, input, status, result); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java index 58864c8..6823de3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java
@@ -11,15 +11,16 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package com.google.gerrit.audit; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.server.CurrentUser; public class SshAuditEvent extends AuditEvent { public SshAuditEvent(String sessionId, CurrentUser who, String what, - long when, Multimap<String, ?> params, Object result) { + long when, ListMultimap<String, ?> params, Object result) { super(sessionId, who, what, when, params, result); } }
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/rules/PredicateClassLoader.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java index eb2d264..52a9363 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java
@@ -11,10 +11,11 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package com.google.gerrit.rules; import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; import com.google.gerrit.extensions.registration.DynamicSet; import java.util.Collection; @@ -24,7 +25,7 @@ */ public class PredicateClassLoader extends ClassLoader { - private final Multimap<String, ClassLoader> packageClassLoaderMap = + private final SetMultimap<String, ClassLoader> packageClassLoaderMap = LinkedHashMultimap.create(); public PredicateClassLoader(
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..c61f613 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; @@ -48,7 +49,9 @@ 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 +76,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 +178,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 +203,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 +218,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 +239,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 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 +279,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..7dda538 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 { @@ -82,7 +82,7 @@ } public ChangeControl findOne(Change.Id id, CurrentUser user) - throws OrmException, NoSuchChangeException { + throws OrmException { List<ChangeControl> ctls = find(id, user); if (ctls.size() != 1) { throw new NoSuchChangeException(id);
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..e49b617 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) { + return newMessage( + ctx.getChange().currentPatchSetId(), + ctx.getUser(), ctx.getWhen(), body, tag); + } + + public static ChangeMessage newMessage( + PatchSet.Id psId, CurrentUser user, Timestamp when, + String body, @Nullable String tag) { + checkNotNull(psId); + Account.Id accountId = user.isInternalUser() ? null : user.getAccountId(); + ChangeMessage m = new ChangeMessage( + new ChangeMessage.Key(psId.getParentKey(), ChangeUtil.messageUuid()), + 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..fbf6d03 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,12 +14,11 @@ 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.common.io.BaseEncoding; import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.util.IdGenerator; -import com.google.gwtorm.server.OrmException; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Ref; @@ -27,52 +26,29 @@ import org.eclipse.jgit.lib.Repository; import java.io.IOException; +import java.security.SecureRandom; import java.util.Map; +import java.util.Random; @Singleton public class ChangeUtil { - private static final Object uuidLock = new Object(); - private static final int SEED = 0x2418e6f9; - private static int uuidPrefix; - private static int uuidSeq; + private static final Random UUID_RANDOM = new SecureRandom(); + private static final BaseEncoding UUID_ENCODING = + BaseEncoding.base16().lowerCase(); private static final int SUBJECT_MAX_LENGTH = 80; 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.from(comparingInt(PatchSet::getPatchSetId)); - public static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural() - .onResultOf(TO_PS_ID); - - /** - * Generate a new unique identifier for change message entities. - * - * @param db the database connection, used to increment the change message - * allocation sequence. - * @return the new unique identifier. - * @throws OrmException the database couldn't be incremented. - */ - public static String messageUUID(ReviewDb db) throws OrmException { - int p; - int s; - synchronized (uuidLock) { - if (uuidSeq == 0) { - uuidPrefix = db.nextChangeMessageId(); - uuidSeq = Integer.MAX_VALUE; - } - p = uuidPrefix; - s = uuidSeq--; - } - String u = IdGenerator.format(IdGenerator.mix(SEED, p)); - String l = IdGenerator.format(IdGenerator.mix(p, s)); - return u + '_' + l; + /** @return a new unique identifier for change message entities. */ + public static String messageUuid() { + byte[] buf = new byte[8]; + UUID_RANDOM.nextBytes(buf); + return UUID_ENCODING.encode(buf, 0, 4) + '_' + + UUID_ENCODING.encode(buf, 4, 4); } public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
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..a9af7e0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -0,0 +1,509 @@ +// Copyright (C) 2014 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toList; + +import com.google.common.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.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.reviewdb.client.PatchLineComment; +import com.google.gerrit.reviewdb.client.PatchLineComment.Status; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.client.RobotComment; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.GerritServerId; +import com.google.gerrit.server.git.BatchUpdate.ChangeContext; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.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, + @Nullable Boolean unresolved, + @Nullable String parentUuid) + throws OrmException, UnprocessableEntityException { + if (unresolved == null) { + if (parentUuid == null) { + // Default to false if comment is not descended from another. + unresolved = false; + } else { + // Inherit unresolved value from inReplyTo comment if not specified. + Comment.Key key = new Comment.Key(parentUuid, path, psId.patchSetId); + Optional<Comment> parent = get(ctx.getDb(), ctx.getNotes(), key); + if (!parent.isPresent()) { + throw new UnprocessableEntityException( + "Invalid parentUuid supplied for comment"); + } + unresolved = parent.get().unresolved; + } + } + Comment c = new Comment( + new Comment.Key(ChangeUtil.messageUuid(), path, psId.get()), + ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId, + unresolved); + c.parentUuid = parentUuid; + 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) { + RobotComment c = new RobotComment( + new Comment.Key(ChangeUtil.messageUuid(), 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); + } + 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); + } + } + } + + /** + * Get NoteDb draft refs for a change. + * <p> + * Works if NoteDb is not enabled, but the results are not meaningful. + * <p> + * This is just a simple ref scan, so the results may potentially include refs + * for zombie draft comments. A zombie draft is one which has been published + * but the write to delete the draft ref from All-Users failed. + * + * @param changeId change ID. + * @return raw refs from All-Users repo. + */ + public Collection<Ref> getDraftRefs(Change.Id changeId) + 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..b62283a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -16,10 +16,13 @@ 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.account.CapabilityControl; 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> @@ -43,6 +46,8 @@ private AccessPath accessPath = AccessPath.UNKNOWN; private CapabilityControl capabilities; + private PropertyKey<AccountExternalId.Key> lastLoginExternalIdPropertyKey = + PropertyKey.create(); protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) { this.capabilityControlFactory = capabilityControlFactory; @@ -72,6 +77,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 @@ -138,4 +153,12 @@ */ public <T> void put(PropertyKey<T> key, @Nullable T value) { } + + public void setLastLoginExternalIdKey(AccountExternalId.Key externalIdKey) { + put(lastLoginExternalIdPropertyKey, externalIdKey); + } + + public AccountExternalId.Key getLastLoginExternalIdKey() { + return get(lastLoginExternalIdPropertyKey); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java index 168dbf7..313a3e3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; @@ -31,7 +32,9 @@ import com.google.gerrit.server.config.DisableReverseDnsLookup; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.inject.Inject; +import com.google.inject.OutOfScopeException; import com.google.inject.Provider; +import com.google.inject.ProvisionException; import com.google.inject.Singleton; import com.google.inject.util.Providers; @@ -323,18 +326,7 @@ } user = user + "|" + "account-" + ua.getId().toString(); - String host = null; - SocketAddress remotePeer = remotePeerProvider.get(); - if (remotePeer instanceof InetSocketAddress) { - InetSocketAddress sa = (InetSocketAddress) remotePeer; - InetAddress in = sa.getAddress(); - host = in != null ? getHost(in) : sa.getHostName(); - } - if (host == null || host.isEmpty()) { - host = "unknown"; - } - - return new PersonIdent(name, user + "@" + host, when, tz); + return new PersonIdent(name, user + "@" + guessHost(), when, tz); } public PersonIdent newCommitterIdent(final Date when, final TimeZone tz) { @@ -424,6 +416,57 @@ } } + /** + * Returns a materialized copy of the user with all dependencies. + * + * Invoke all providers and factories of dependent objects and store the + * references to a copy of the current identified user. + * + * @return copy of the identified user + */ + public IdentifiedUser materializedCopy() { + CapabilityControl capabilities = getCapabilities(); + Provider<SocketAddress> remotePeer; + try { + remotePeer = Providers.of(remotePeerProvider.get()); + } catch (OutOfScopeException | ProvisionException e) { + remotePeer = new Provider<SocketAddress>() { + @Override + public SocketAddress get() { + throw e; + } + }; + } + return new IdentifiedUser(new CapabilityControl.Factory() { + + @Override + public CapabilityControl create(CurrentUser user) { + return capabilities; + } + }, authConfig, realm, anonymousCowardName, + Providers.of(canonicalUrl.get()), accountCache, groupBackend, + disableReverseDnsLookup, remotePeer, state, realUser); + } + + private String guessHost() { + String host = null; + SocketAddress remotePeer = null; + try { + remotePeer = remotePeerProvider.get(); + } catch (OutOfScopeException | ProvisionException e) { + // Leave null. + } + if (remotePeer instanceof InetSocketAddress) { + InetSocketAddress sa = (InetSocketAddress) remotePeer; + InetAddress in = sa.getAddress(); + host = in != null ? getHost(in) : sa.getHostName(); + } + if (Strings.isNullOrEmpty(host)) { + return "unknown"; + } + return host; + } + private String getHost(final InetAddress in) { if (Boolean.FALSE.equals(disableReverseDnsLookup)) { return in.getCanonicalHostName();
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..fdc1feb 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
@@ -89,7 +89,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 +101,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);
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..a848c6c 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,27 +14,23 @@ 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; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountState; @@ -42,8 +38,7 @@ import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.change.PostReviewers; 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,226 +48,206 @@ 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 { - 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); - } - }); + @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)); + } + } + + // 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 AccountLoader accountLoader; - 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 AccountQueryBuilder accountQueryBuilder; + private final AccountQueryProcessor accountQueryProcessor; private final GroupBackend groupBackend; private final GroupMembers.Factory groupMembersFactory; private final Provider<CurrentUser> currentUser; + private final ReviewerRecommender reviewerRecommender; + private final Metrics metrics; @Inject ReviewersUtil(AccountLoader.Factory accountLoaderFactory, - AccountCache accountCache, - AccountIndexCollection indexes, - AccountQueryBuilder queryBuilder, - AccountQueryProcessor queryProcessor, - AccountControl.Factory accountControlFactory, - Provider<ReviewDb> dbProvider, + AccountQueryBuilder accountQueryBuilder, + AccountQueryProcessor accountQueryProcessor, GroupBackend groupBackend, GroupMembers.Factory groupMembersFactory, - Provider<CurrentUser> currentUser) { + Provider<CurrentUser> currentUser, + 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.dbProvider = dbProvider; + this.accountQueryBuilder = accountQueryBuilder; + this.accountQueryProcessor = accountQueryProcessor; + this.currentUser = currentUser; 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; + 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())); + } + + if (suggestedReviewer.size() <= limit) { + return suggestedReviewer; + } + return suggestedReviewer.subList(0, limit); + } + + private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers, + VisibilityControl visibilityControl) throws OrmException { + try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) { + try { + 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.add(id); + } } - reviewer.add(suggestedReviewerInfo); + return new ArrayList<>(matches); + } catch (QueryParseException e) { + return ImmutableList.of(); } } + } - reviewer = ORDERING.immutableSortedCopy(reviewer); - if (reviewer.size() <= limit) { + 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; } - return reviewer.subList(0, limit); } - private Collection<AccountInfo> suggestAccounts(SuggestReviewers suggestReviewers, - VisibilityControl visibilityControl) - throws OrmException { - AccountIndex searchIndex = indexes.getSearchIndex(); - if (searchIndex != null) { - return suggestAccountsFromIndex(suggestReviewers, visibilityControl); - } - return suggestAccountsFromDb(suggestReviewers, visibilityControl); - } - - private Collection<AccountInfo> suggestAccountsFromIndex( - SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) - throws OrmException { - try { - Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>(); - QueryResult<AccountState> result = queryProcessor - .setLimit(suggestReviewers.getLimit()) - .query(queryBuilder.defaultQuery(suggestReviewers.getQuery())); - for (AccountState accountState : result.entities()) { - Account.Id id = accountState.getAccount().getId(); - if (visibilityControl.isVisibleTo(id)) { - matches.put(id, accountLoader.get(id)); - } - } - - accountLoader.fill(); - - return matches.values(); - } catch (QueryParseException e) { - return ImmutableList.of(); - } - } - - private Collection<AccountInfo> suggestAccountsFromDb( - SuggestReviewers suggestReviewers, VisibilityControl visibilityControl) - throws OrmException { - String query = suggestReviewers.getQuery(); - int limit = suggestReviewers.getLimit(); - - String a = query; - String b = a + MAX_SUFFIX; - - Map<Account.Id, AccountInfo> r = new LinkedHashMap<>(); - Map<Account.Id, String> queryEmail = new HashMap<>(); - - for (Account p : dbProvider.get().accounts() - .suggestByFullName(a, b, limit)) { - if (p.isActive()) { - addSuggestion(r, p.getId(), visibilityControl); - } - } - - if (r.size() < limit) { - for (Account p : dbProvider.get().accounts() - .suggestByPreferredEmail(a, b, limit - r.size())) { - if (p.isActive()) { - addSuggestion(r, p.getId(), visibilityControl); - } - } - } - - if (r.size() < limit) { - for (AccountExternalId e : dbProvider.get().accountExternalIds() - .suggestByEmailAddress(a, b, limit - r.size())) { - if (!r.containsKey(e.getAccountId())) { - Account p = accountCache.get(e.getAccountId()).getAccount(); - if (p.isActive()) { - if (addSuggestion(r, p.getId(), visibilityControl)) { - queryEmail.put(e.getAccountId(), e.getEmailAddress()); - } + 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; } - - 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()); } - private boolean addSuggestion(Map<Account.Id, AccountInfo> map, - Account.Id account, VisibilityControl visibilityControl) - throws OrmException { - if (!map.containsKey(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)); - return true; - } - return false; - } - - private List<GroupReference> suggestAccountGroup( + private List<GroupReference> suggestAccountGroups( SuggestReviewers suggestReviewers, ProjectControl ctl) { return Lists.newArrayList( Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl), @@ -285,7 +260,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..5d9fbd6 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.ImmutableMultimap; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; 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); @@ -196,7 +218,7 @@ } public void unstarAll(Project.NameKey project, Change.Id changeId) - throws OrmException, NoSuchChangeException { + throws OrmException { try (Repository repo = repoManager.openRepository(allUsers); RevWalk rw = new RevWalk(repo)) { BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate(); @@ -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(); @@ -248,30 +270,12 @@ 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(); + try (Repository repo = repoManager.openRepository(allUsers)) { + return getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId)) + .stream() + .map(Account.Id::parse) + .filter(accountId -> hasStar(repo, changeId, accountId, label)) + .collect(toSet()); } catch (IOException e) { throw new OrmException( String.format("Get accounts that starred change %d failed", @@ -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,8 +300,22 @@ } } - public ImmutableMultimap<Account.Id, String> byChangeFromIndex( - Change.Id changeId) throws OrmException, NoSuchChangeException { + private boolean hasStar(Repository repo, Change.Id changeId, + Account.Id accountId, String label) { + try { + return readLabels(repo, + RefNames.refsStarredChanges(changeId, accountId)).labels() + .contains(label); + } catch (IOException e) { + log.error(String.format( + "Cannot query stars by account %d on change %d", + accountId.get(), changeId.get()), e); + return false; + } + } + + public ImmutableListMultimap<Account.Id, String> byChangeFromIndex( + Change.Id changeId) throws OrmException { Set<String> fields = ImmutableSet.of( ChangeField.ID.getName(), ChangeField.STAR.getName()); @@ -341,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/StartupCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/StartupCheck.java new file mode 100644 index 0000000..2a7e4c9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StartupCheck.java
@@ -0,0 +1,33 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import com.google.gerrit.extensions.events.LifecycleListener; + +/** + * Check executed on Gerrit startup. + */ +public interface StartupCheck { + /** + * Performs Gerrit startup check, can abort startup by throwing + * {@link StartupException}. + * <p> + * Called on Gerrit startup after all {@link LifecycleListener} have been + * invoked. + * + * @throws StartupException thrown if Gerrit startup should be aborted + */ + void check() throws StartupException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StartupChecks.java b/gerrit-server/src/main/java/com/google/gerrit/server/StartupChecks.java new file mode 100644 index 0000000..0a208fe --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StartupChecks.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.account.UniversalGroupBackend; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class StartupChecks implements LifecycleListener { + public static class Module extends LifecycleModule { + @Override + protected void configure() { + DynamicSet.setOf(binder(), StartupCheck.class); + listener().to(StartupChecks.class); + DynamicSet.bind(binder(), StartupCheck.class) + .to(UniversalGroupBackend.ConfigCheck.class); + DynamicSet.bind(binder(), StartupCheck.class) + .to(SystemGroupBackend.NameCheck.class); + } + } + + private final DynamicSet<StartupCheck> startupChecks; + + @Inject + StartupChecks(DynamicSet<StartupCheck> startupChecks) { + this.startupChecks = startupChecks; + } + + @Override + public void start() throws StartupException { + for (StartupCheck startupCheck : startupChecks) { + startupCheck.check(); + } + } + + @Override + public void stop() { + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StartupException.java b/gerrit-server/src/main/java/com/google/gerrit/server/StartupException.java new file mode 100644 index 0000000..f84594b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StartupException.java
@@ -0,0 +1,27 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +public class StartupException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public StartupException(String message) { + super(message); + } + + public StartupException(String message, Throwable cause) { + super(message, cause); + } +}
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/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java index 0856616..2ddea85d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -21,7 +21,6 @@ import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.cache.CacheModule; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; @@ -87,15 +86,12 @@ static class Loader extends CacheLoader<String, Set<Account.Id>> { private final SchemaFactory<ReviewDb> schema; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; @Inject Loader(SchemaFactory<ReviewDb> schema, - AccountIndexCollection accountIndexes, Provider<InternalAccountQuery> accountQueryProvider) { this.schema = schema; - this.accountIndexes = accountIndexes; this.accountQueryProvider = accountQueryProvider; } @@ -106,18 +102,11 @@ for (Account a : db.accounts().byPreferredEmail(email)) { r.add(a.getId()); } - if (accountIndexes.getSearchIndex() != null) { - for (AccountState accountState : accountQueryProvider.get() - .byExternalId( - (new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, - email)).get())) { - r.add(accountState.getAccount().getId()); - } - } else { - for (AccountExternalId a : db.accountExternalIds() - .byEmailAddress(email)) { - r.add(a.getAccountId()); - } + for (AccountState accountState : accountQueryProvider.get() + .byExternalId( + (new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, + email)).get())) { + r.add(accountState.getAccount().getId()); } return ImmutableSet.copyOf(r); }
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..0ba84f3 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; @@ -24,12 +23,10 @@ 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.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import com.google.gerrit.server.cache.CacheModule; -import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.index.account.AccountIndexer; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; @@ -42,7 +39,6 @@ import com.google.inject.name.Named; import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +47,7 @@ import java.util.Collections; 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; @@ -158,7 +154,6 @@ private final GroupCache groupCache; private final GeneralPreferencesLoader loader; private final LoadingCache<String, Optional<Account.Id>> byName; - private final boolean readFromGit; private final Provider<WatchConfig.Accessor> watchConfig; @Inject @@ -167,14 +162,11 @@ GeneralPreferencesLoader loader, @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername, - @GerritServerConfig Config cfg, Provider<WatchConfig.Accessor> watchConfig) { this.schema = sf; this.groupCache = groupCache; this.loader = loader; this.byName = byUsername; - this.readFromGit = - cfg.getBoolean("user", null, "readProjectWatchesFromGit", false); this.watchConfig = watchConfig; } @@ -220,50 +212,28 @@ account.setGeneralPreferences(GeneralPreferencesInfo.defaults()); } - Map<ProjectWatchKey, Set<NotifyType>> projectWatches = - readFromGit - ? watchConfig.get().getProjectWatches(who) - : GetWatchedProjects.readProjectWatchesFromDb(db, who); - return new AccountState(account, internalGroups, externalIds, - projectWatches); + watchConfig.get().getProjectWatches(who)); } } static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> { - private final SchemaFactory<ReviewDb> schema; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; @Inject - ByNameLoader(SchemaFactory<ReviewDb> sf, - AccountIndexCollection accountIndexes, - Provider<InternalAccountQuery> accountQueryProvider) { - this.schema = sf; - this.accountIndexes = accountIndexes; + ByNameLoader(Provider<InternalAccountQuery> accountQueryProvider) { this.accountQueryProvider = accountQueryProvider; } @Override public Optional<Account.Id> load(String username) throws Exception { - AccountExternalId.Key key = new AccountExternalId.Key( // - AccountExternalId.SCHEME_USERNAME, // - username); - if (accountIndexes.getSearchIndex() != null) { - AccountState accountState = - accountQueryProvider.get().oneByExternalId(key.get()); - return accountState != null - ? Optional.of(accountState.getAccount().getId()) - : Optional.<Account.Id>absent(); - } - - try (ReviewDb db = schema.open()) { - AccountExternalId id = db.accountExternalIds().get(key); - if (id != null) { - return Optional.of(id.getAccountId()); - } - return Optional.absent(); - } + AccountExternalId.Key key = new AccountExternalId.Key( // + AccountExternalId.SCHEME_USERNAME, // + username); + AccountState accountState = + accountQueryProvider.get().oneByExternalId(key.get()); + return Optional.ofNullable(accountState) + .map(s -> s.getAccount().getId()); } } }
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..eebf868 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
@@ -42,7 +42,10 @@ USERNAME, /** Numeric account ID, may be deprecated. */ - ID + ID, + + /** The user-settable status of this account (e.g. busy, OOO, available) */ + STATUS } public abstract void fillAccountInfo( @@ -52,10 +55,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/AccountInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java deleted file mode 100644 index 32781f0..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java +++ /dev/null
@@ -1,75 +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.account; - -import com.google.gerrit.common.data.AccountInfo; -import com.google.gerrit.common.data.AccountInfoCache; -import com.google.gerrit.reviewdb.client.Account; -import com.google.inject.Inject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Efficiently builds an {@link AccountInfoCache}. */ -public class AccountInfoCacheFactory { - public interface Factory { - AccountInfoCacheFactory create(); - } - - private final AccountCache accountCache; - private final Map<Account.Id, Account> out; - - @Inject - AccountInfoCacheFactory(final AccountCache accountCache) { - this.accountCache = accountCache; - this.out = new HashMap<>(); - } - - /** - * Indicate an account will be needed later on. - * - * @param id identity that will be needed in the future; may be null. - */ - public void want(final Account.Id id) { - if (id != null && !out.containsKey(id)) { - out.put(id, accountCache.get(id).getAccount()); - } - } - - /** Indicate one or more accounts will be needed later on. */ - public void want(final Iterable<Account.Id> ids) { - for (final Account.Id id : ids) { - want(id); - } - } - - public Account get(final Account.Id id) { - want(id); - return out.get(id); - } - - /** - * Create an AccountInfoCache with the currently loaded Account entities. - * */ - public AccountInfoCache create() { - final List<AccountInfo> r = new ArrayList<>(out.size()); - for (final Account a : out.values()) { - r.add(new AccountInfo(a)); - } - return new AccountInfoCache(r); - } -}
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..1ddc762 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
@@ -42,6 +42,7 @@ FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME, + FillOptions.STATUS, FillOptions.AVATARS)); public interface Factory { @@ -94,7 +95,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); } } @@ -106,4 +107,10 @@ } fill(); } + + public AccountInfo fillOne(Account.Id id) throws OrmException { + AccountInfo info = get(id); + fill(); + return info; + } }
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..7236604 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,13 +21,13 @@ 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; import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; /** Tracks authentication related details for user accounts. */ @@ -61,7 +62,6 @@ private final ProjectCache projectCache; private final AtomicBoolean awaitsFirstAccountCheck; private final AuditService auditService; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; @Inject @@ -73,7 +73,6 @@ ChangeUserName.Factory changeUserNameFactory, ProjectCache projectCache, AuditService auditService, - AccountIndexCollection accountIndexes, Provider<InternalAccountQuery> accountQueryProvider) { this.schema = schema; this.byIdCache = byIdCache; @@ -84,28 +83,20 @@ this.projectCache = projectCache; this.awaitsFirstAccountCheck = new AtomicBoolean(true); this.auditService = auditService; - this.accountIndexes = accountIndexes; this.accountQueryProvider = accountQueryProvider; } /** - * @return user identified by this external identity string, or null. + * @return user identified by this external identity string */ - public Account.Id lookup(String externalId) throws AccountException { + public Optional<Account.Id> lookup(String externalId) + throws AccountException { try { - if (accountIndexes.getSearchIndex() != null) { - AccountState accountState = - accountQueryProvider.get().oneByExternalId(externalId); - return accountState != null - ? accountState.getAccount().getId() - : null; - } - - try (ReviewDb db = schema.open()) { - AccountExternalId ext = - db.accountExternalIds().get(new AccountExternalId.Key(externalId)); - return ext != null ? ext.getAccountId() : null; - } + AccountState accountState = + accountQueryProvider.get().oneByExternalId(externalId); + return accountState != null + ? Optional.of(accountState.getAccount().getId()) + : Optional.empty(); } catch (OrmException e) { throw new AccountException("Cannot lookup account " + externalId, e); } @@ -125,7 +116,7 @@ try { try (ReviewDb db = schema.open()) { AccountExternalId.Key key = id(who); - AccountExternalId id = getAccountExternalId(db, key); + AccountExternalId id = getAccountExternalId(key); if (id == null) { // New account, automatically create and return. // @@ -147,37 +138,18 @@ } } - private AccountExternalId getAccountExternalId(ReviewDb db, - AccountExternalId.Key key) throws OrmException { - if (accountIndexes.getSearchIndex() != null) { - AccountState accountState = - accountQueryProvider.get().oneByExternalId(key.get()); - if (accountState != null) { - for (AccountExternalId extId : accountState.getExternalIds()) { - if (extId.getKey().equals(key)) { - return extId; - } - } - } - return null; - } - - // We don't have at the moment an account_by_external_id cache - // but by using the accounts cache we get the list of external_ids - // without having to query the DB every time - if (key.getScheme().equals(AccountExternalId.SCHEME_GERRIT) - || key.getScheme().equals(AccountExternalId.SCHEME_USERNAME)) { - AccountState state = byIdCache.getByUsername( - key.get().substring(key.getScheme().length())); - if (state != null) { - for (AccountExternalId accountExternalId : state.getExternalIds()) { - if (accountExternalId.getKey().equals(key)) { - return accountExternalId; - } + private AccountExternalId getAccountExternalId(AccountExternalId.Key key) + throws OrmException { + AccountState accountState = + accountQueryProvider.get().oneByExternalId(key.get()); + if (accountState != null) { + for (AccountExternalId extId : accountState.getExternalIds()) { + if (extId.getKey().equals(key)) { + return extId; } } } - return db.accountExternalIds().get(key); + return null; } private void update(ReviewDb db, AuthRequest who, AccountExternalId extId) @@ -201,14 +173,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 +324,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 @@ -385,7 +357,7 @@ throws AccountException, OrmException, IOException { try (ReviewDb db = schema.open()) { AccountExternalId.Key key = id(who); - AccountExternalId extId = getAccountExternalId(db, key); + AccountExternalId extId = getAccountExternalId(key); if (extId != null) { if (!extId.getAccountId().equals(to)) { throw new AccountException("Identity in use by another account"); @@ -406,8 +378,8 @@ if (who.getEmailAddress() != null) { byEmailCache.evict(who.getEmailAddress()); - byIdCache.evict(to); } + byIdCache.evict(to); } return new AuthResult(to, key, false); @@ -469,7 +441,7 @@ throws AccountException, OrmException, IOException { try (ReviewDb db = schema.open()) { AccountExternalId.Key key = id(who); - AccountExternalId extId = getAccountExternalId(db, key); + AccountExternalId extId = getAccountExternalId(key); if (extId != null) { if (!extId.getAccountId().equals(from)) { throw new AccountException(
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..f3356e5 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,12 +14,10 @@ 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; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -38,19 +36,16 @@ private final Realm realm; private final AccountByEmailCache byEmail; private final AccountCache byId; - private final AccountIndexCollection accountIndexes; private final Provider<InternalAccountQuery> accountQueryProvider; @Inject AccountResolver(Realm realm, AccountByEmailCache byEmail, AccountCache byId, - AccountIndexCollection accountIndexes, Provider<InternalAccountQuery> accountQueryProvider) { this.realm = realm; this.byEmail = byEmail; this.byId = byId; - this.accountIndexes = accountIndexes; this.accountQueryProvider = accountQueryProvider; } @@ -183,47 +178,15 @@ return Collections.singleton(id); } - if (accountIndexes.getSearchIndex() != null) { - List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail); - if (m.size() == 1) { - return Collections.singleton(m.get(0).getAccount().getId()); - } - - // 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(); - } - - List<Account> m = db.accounts().byFullName(nameOrEmail).toList(); + List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail); if (m.size() == 1) { - return Collections.singleton(m.get(0).getId()); + return Collections.singleton(m.get(0).getAccount().getId()); } // At this point we have no clue. Just perform a whole bunch of suggestions // and pray we come up with a reasonable result list. - Set<Account.Id> result = new HashSet<>(); - String a = nameOrEmail; - String b = nameOrEmail + "\u9fa5"; - for (Account act : db.accounts().suggestByFullName(a, b, 10)) { - result.add(act.getId()); - } - for (AccountExternalId extId : db.accountExternalIds() - .suggestByKey( - new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, a), - new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, b), 10)) { - result.add(extId.getAccountId()); - } - for (AccountExternalId extId : db.accountExternalIds() - .suggestByEmailAddress(a, b, 10)) { - result.add(extId.getAccountId()); - } - return result; + return accountQueryProvider.get().byDefault(nameOrEmail).stream() + .map(a -> a.getAccount().getId()) + .collect(toSet()); } }
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..827bdee 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
@@ -24,9 +24,9 @@ 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.CurrentUser.PropertyKey; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import java.util.Collection; @@ -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/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java index 4bf4214..6d245a3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -42,6 +42,7 @@ CapabilityCollection create(@Nullable AccessSection section); } + private final SystemGroupBackend systemGroupBackend; private final ImmutableMap<String, ImmutableList<PermissionRule>> permissions; public final ImmutableList<PermissionRule> administrateServer; @@ -52,8 +53,11 @@ @Inject CapabilityCollection( + SystemGroupBackend systemGroupBackend, @AdministrateServerGroups ImmutableSet<GroupReference> admins, @Assisted @Nullable AccessSection section) { + this.systemGroupBackend = systemGroupBackend; + if (section == null) { section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES); } @@ -119,12 +123,10 @@ return r != null ? r : ImmutableList.<PermissionRule> of(); } - private static final GroupReference anonymous = SystemGroupBackend - .getGroup(SystemGroupBackend.ANONYMOUS_USERS); - - private static void configureDefaults(Map<String, List<PermissionRule>> out, + private void configureDefaults(Map<String, List<PermissionRule>> out, AccessSection section) { - configureDefault(out, section, GlobalCapability.QUERY_LIMIT, anonymous); + configureDefault(out, section, GlobalCapability.QUERY_LIMIT, + systemGroupBackend.getGroup(SystemGroupBackend.ANONYMOUS_USERS)); } private static void configureDefault(Map<String, List<PermissionRule>> out,
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..b0dea00 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; @@ -49,9 +49,9 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -145,7 +145,7 @@ } } - LinkedList<AccountExternalId> externalIds = new LinkedList<>(); + List<AccountExternalId> externalIds = new ArrayList<>(); externalIds.add(extUser); for (AccountExternalIdCreator c : externalIdCreators) { externalIds.addAll(c.create(id, username, input.email));
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/DeleteExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java new file mode 100644 index 0000000..cadb3f1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
@@ -0,0 +1,111 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.account; + +import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; + +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Singleton +public class DeleteExternalIds implements + RestModifyView<AccountResource, List<String>> { + private final Provider<ReviewDb> db; + private final AccountByEmailCache accountByEmailCache; + private final AccountCache accountCache; + private final Provider<CurrentUser> self; + private final Provider<ReviewDb> dbProvider; + + @Inject + DeleteExternalIds( + Provider<ReviewDb> db, + AccountByEmailCache accountByEmailCache, + AccountCache accountCache, + Provider<CurrentUser> self, + Provider<ReviewDb> dbProvider) { + this.db = db; + this.accountByEmailCache = accountByEmailCache; + this.accountCache = accountCache; + this.self = self; + this.dbProvider = dbProvider; + } + + @Override + public Response<?> apply(AccountResource resource, List<String> externalIds) + throws RestApiException, IOException, OrmException { + if (self.get() != resource.getUser()) { + throw new AuthException("not allowed to delete external IDs"); + } + + if (externalIds == null || externalIds.size() == 0) { + throw new BadRequestException("external IDs are required"); + } + + Account.Id accountId = resource.getUser().getAccountId(); + Map<AccountExternalId.Key, AccountExternalId> externalIdMap = + db.get().accountExternalIds().byAccount( + resource.getUser().getAccountId()).toList() + .stream().collect(Collectors.toMap(i -> i.getKey(), i -> i)); + + List<AccountExternalId> toDelete = new ArrayList<>(); + AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey(); + for (String externalIdStr : externalIds) { + AccountExternalId id = externalIdMap.get( + new AccountExternalId.Key(externalIdStr)); + + if (id == null) { + throw new UnprocessableEntityException(String.format( + "External id %s does not exist", externalIdStr)); + } + + if ((!id.isScheme(SCHEME_USERNAME)) + && ((last == null) || (!last.get().equals(id.getExternalId())))) { + toDelete.add(id); + } else { + throw new ResourceConflictException(String.format( + "External id %s cannot be deleted", externalIdStr)); + } + } + + if (!toDelete.isEmpty()) { + dbProvider.get().accountExternalIds().delete(toDelete); + accountCache.evict(accountId); + for (AccountExternalId e : toDelete) { + accountByEmailCache.evict(e.getEmailAddress()); + } + } + + return Response.none(); + } +}
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..990a563 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,21 +14,18 @@ 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; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountProjectWatch; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.ResultSet; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -36,24 +33,19 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import java.io.IOException; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; @Singleton public class DeleteWatchedProjects implements RestModifyView<AccountResource, List<ProjectWatchInfo>> { - private final Provider<ReviewDb> dbProvider; private final Provider<IdentifiedUser> self; private final AccountCache accountCache; private final WatchConfig.Accessor watchConfig; @Inject - DeleteWatchedProjects(Provider<ReviewDb> dbProvider, - Provider<IdentifiedUser> self, + DeleteWatchedProjects(Provider<IdentifiedUser> self, AccountCache accountCache, WatchConfig.Accessor watchConfig) { - this.dbProvider = dbProvider; this.self = self; this.accountCache = accountCache; this.watchConfig = watchConfig; @@ -73,45 +65,12 @@ } Account.Id accountId = rsrc.getUser().getAccountId(); - deleteFromDb(accountId, input); - deleteFromGit(accountId, input); + watchConfig.deleteProjectWatches( + accountId, + input.stream().map(w -> ProjectWatchKey.create( + new Project.NameKey(w.project), w.filter)) + .collect(toList())); accountCache.evict(accountId); return Response.none(); } - - private void deleteFromDb(Account.Id accountId, List<ProjectWatchInfo> input) - throws OrmException, IOException { - ResultSet<AccountProjectWatch> watchedProjects = - dbProvider.get().accountProjectWatches().byAccount(accountId); - HashMap<AccountProjectWatch.Key, AccountProjectWatch> watchedProjectsMap = - new HashMap<>(); - for (AccountProjectWatch watchedProject : watchedProjects) { - watchedProjectsMap.put(watchedProject.getKey(), watchedProject); - } - - List<AccountProjectWatch> watchesToDelete = new LinkedList<>(); - for (ProjectWatchInfo projectInfo : input) { - AccountProjectWatch.Key key = new AccountProjectWatch.Key(accountId, - new Project.NameKey(projectInfo.project), projectInfo.filter); - if (watchedProjectsMap.containsKey(key)) { - watchesToDelete.add(watchedProjectsMap.get(key)); - } - } - if (!watchesToDelete.isEmpty()) { - dbProvider.get().accountProjectWatches().delete(watchesToDelete); - accountCache.evict(accountId); - } - } - - 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); - } - })); - } }
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/GetExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java new file mode 100644 index 0000000..0b3674c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
@@ -0,0 +1,91 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.account; + +import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.gerrit.extensions.common.AccountExternalIdInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Singleton +public class GetExternalIds implements RestReadView<AccountResource> { + private final Provider<ReviewDb> db; + private final Provider<CurrentUser> self; + private final AuthConfig authConfig; + + @Inject + GetExternalIds(Provider<ReviewDb> db, + Provider<CurrentUser> self, + AuthConfig authConfig) { + this.db = db; + this.self = self; + this.authConfig = authConfig; + } + + @Override + public List<AccountExternalIdInfo> apply(AccountResource resource) + throws RestApiException, OrmException { + if (self.get() != resource.getUser()) { + throw new AuthException("not allowed to get external IDs"); + } + + Collection<AccountExternalId> ids = db.get().accountExternalIds() + .byAccount(resource.getUser().getAccountId()).toList(); + if (ids.isEmpty()) { + return ImmutableList.of(); + } + List<AccountExternalIdInfo> result = + Lists.newArrayListWithCapacity(ids.size()); + for (AccountExternalId id : ids) { + AccountExternalIdInfo info = new AccountExternalIdInfo(); + info.identity = id.getExternalId(); + info.emailAddress = id.getEmailAddress(); + info.trusted = + toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id))); + // The identity can be deleted only if its not the one used to + // establish this web session, and if only if an identity was + // actually used to establish this web session. + if (!id.isScheme(SCHEME_USERNAME)) { + AccountExternalId.Key last = resource.getUser() + .getLastLoginExternalIdKey(); + info.canDelete = + toBoolean(last == null || !last.get().equals(info.identity)); + } + result.add(info); + } + return result; + } + + private static Boolean toBoolean(boolean v) { + return v ? v : null; + } +} +
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/GetStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetStatus.java new file mode 100644 index 0000000..5d57c4c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetStatus.java
@@ -0,0 +1,27 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.account; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.inject.Singleton; + +@Singleton +public class GetStatus implements RestReadView<AccountResource> { + @Override + public String apply(AccountResource rsrc) { + return Strings.nullToEmpty(rsrc.getUser().getAccount().getStatus()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java index 3748e17..61600f4e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -20,26 +20,20 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountProjectWatch; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; -import com.google.gerrit.server.config.GerritServerConfig; 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.ConfigInvalidException; -import org.eclipse.jgit.lib.Config; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,20 +41,13 @@ @Singleton public class GetWatchedProjects implements RestReadView<AccountResource> { - private final Provider<ReviewDb> dbProvider; private final Provider<IdentifiedUser> self; - private final boolean readFromGit; private final WatchConfig.Accessor watchConfig; @Inject - public GetWatchedProjects(Provider<ReviewDb> dbProvider, - Provider<IdentifiedUser> self, - @GerritServerConfig Config cfg, + public GetWatchedProjects(Provider<IdentifiedUser> self, WatchConfig.Accessor watchConfig) { - this.dbProvider = dbProvider; this.self = self; - this.readFromGit = - cfg.getBoolean("user", null, "readProjectWatchesFromGit", false); this.watchConfig = watchConfig; } @@ -73,14 +60,9 @@ + "of other users"); } Account.Id accountId = rsrc.getUser().getAccountId(); - Map<ProjectWatchKey, Set<NotifyType>> projectWatches = - readFromGit - ? watchConfig.getProjectWatches(accountId) - : readProjectWatchesFromDb(dbProvider.get(), accountId); - - List<ProjectWatchInfo> projectWatchInfos = new LinkedList<>(); - for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches - .entrySet()) { + List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>(); + for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : watchConfig + .getProjectWatches(accountId).entrySet()) { ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.filter = e.getKey().filter(); pwi.project = e.getKey().project().get(); @@ -112,22 +94,4 @@ private static Boolean toBoolean(boolean value) { return value ? true : null; } - - public static Map<ProjectWatchKey, Set<NotifyType>> readProjectWatchesFromDb( - ReviewDb db, Account.Id who) throws OrmException { - Map<ProjectWatchKey, Set<NotifyType>> projectWatches = - new HashMap<>(); - for (AccountProjectWatch apw : db.accountProjectWatches().byAccount(who)) { - ProjectWatchKey key = - ProjectWatchKey.create(apw.getProjectNameKey(), apw.getFilter()); - Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class); - for (NotifyType notifyType : NotifyType.values()) { - if (apw.isNotify(notifyType)) { - notifyValues.add(notifyType); - } - } - projectWatches.put(key, notifyValues); - } - return projectWatches; - } }
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..3214b35 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,9 +14,12 @@ 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; +import java.io.IOException; + /** Tracks group objects in memory for efficient access. */ public interface GroupCache { AccountGroup get(AccountGroup.Id groupId); @@ -31,14 +34,14 @@ @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); + void onCreateGroup(AccountGroup.NameKey newGroupName) throws IOException; - void evict(AccountGroup group); + void evict(AccountGroup group) throws IOException; - void evictAfterRename(final AccountGroup.NameKey oldName, - final AccountGroup.NameKey newName); + void evictAfterRename(AccountGroup.NameKey oldName, + AccountGroup.NameKey newName) throws IOException; }
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..a920c22 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,18 +14,20 @@ 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; import com.google.gerrit.server.cache.CacheModule; +import com.google.gerrit.server.index.group.GroupIndexer; import com.google.gwtorm.server.OrmDuplicateKeyException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Module; +import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.name.Named; @@ -33,8 +35,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collections; +import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; /** Tracks group objects in memory for efficient access. */ @@ -76,17 +79,20 @@ private final LoadingCache<String, Optional<AccountGroup>> byName; private final LoadingCache<String, Optional<AccountGroup>> byUUID; private final SchemaFactory<ReviewDb> schema; + private final Provider<GroupIndexer> indexer; @Inject GroupCacheImpl( @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId, @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName, @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID, - SchemaFactory<ReviewDb> schema) { + SchemaFactory<ReviewDb> schema, + Provider<GroupIndexer> indexer) { this.byId = byId; this.byName = byName; this.byUUID = byUUID; this.schema = schema; + this.indexer = indexer; } @Override @@ -101,7 +107,7 @@ } @Override - public void evict(final AccountGroup group) { + public void evict(final AccountGroup group) throws IOException { if (group.getId() != null) { byId.invalidate(group.getId()); } @@ -111,17 +117,19 @@ if (group.getGroupUUID() != null) { byUUID.invalidate(group.getGroupUUID().get()); } + indexer.get().index(group.getGroupUUID()); } @Override public void evictAfterRename(final AccountGroup.NameKey oldName, - final AccountGroup.NameKey newName) { + final AccountGroup.NameKey newName) throws IOException { if (oldName != null) { byName.invalidate(oldName.get()); } if (newName != null) { byName.invalidate(newName.get()); } + indexer.get().index(get(newName).getGroupUUID()); } @Override @@ -130,7 +138,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 +151,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,18 +159,20 @@ } @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(); } } @Override - public void onCreateGroup(AccountGroup.NameKey newGroupName) { + public void onCreateGroup(AccountGroup.NameKey newGroupName) + throws IOException { byName.invalidate(newGroupName.get()); + indexer.get().index(get(newGroupName).getGroupUUID()); } private static AccountGroup missing(AccountGroup.Id key) { @@ -183,7 +193,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 +213,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 +238,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/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java index 94feb7d..5f1840f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,23 +14,17 @@ package com.google.gerrit.server.account; -import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDetail; -import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.errors.NoSuchGroupException; -import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupById; import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.group.GroupInfoCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; @@ -42,9 +36,6 @@ private final ReviewDb db; private final GroupControl.Factory groupControl; private final GroupCache groupCache; - private final GroupBackend groupBackend; - private final AccountInfoCacheFactory aic; - private final GroupInfoCache gic; private final AccountGroup.Id groupId; private GroupControl control; @@ -53,16 +44,10 @@ GroupDetailFactory(ReviewDb db, GroupControl.Factory groupControl, GroupCache groupCache, - GroupBackend groupBackend, - AccountInfoCacheFactory.Factory accountInfoCacheFactory, - GroupInfoCache.Factory groupInfoCacheFactory, @Assisted AccountGroup.Id groupId) { this.db = db; this.groupControl = groupControl; this.groupCache = groupCache; - this.groupBackend = groupBackend; - this.aic = accountInfoCacheFactory.create(); - this.gic = groupInfoCacheFactory.create(); this.groupId = groupId; } @@ -73,14 +58,8 @@ AccountGroup group = groupCache.get(groupId); GroupDetail detail = new GroupDetail(); detail.setGroup(group); - GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID()); - if (ownerGroup != null) { - detail.setOwnerGroup(GroupReference.forGroup(ownerGroup)); - } detail.setMembers(loadMembers()); detail.setIncludes(loadIncludes()); - detail.setAccounts(aic.create()); - detail.setCanModify(control.isOwner()); return detail; } @@ -88,33 +67,9 @@ List<AccountGroupMember> members = new ArrayList<>(); for (AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) { if (control.canSeeMember(m.getAccountId())) { - aic.want(m.getAccountId()); members.add(m); } } - - Collections.sort(members, new Comparator<AccountGroupMember>() { - @Override - public int compare(AccountGroupMember o1, AccountGroupMember o2) { - Account a = aic.get(o1.getAccountId()); - Account b = aic.get(o2.getAccountId()); - return n(a).compareTo(n(b)); - } - - private String n(final Account a) { - String n = a.getFullName(); - if (n != null && n.length() > 0) { - return n; - } - - n = a.getPreferredEmail(); - if (n != null && n.length() > 0) { - return n; - } - - return a.getId().toString(); - } - }); return members; } @@ -123,32 +78,10 @@ for (AccountGroupById m : db.accountGroupById().byGroup(groupId)) { if (control.canSeeGroup()) { - gic.want(m.getIncludeUUID()); groups.add(m); } } - Collections.sort(groups, new Comparator<AccountGroupById>() { - @Override - public int compare(AccountGroupById o1, AccountGroupById o2) { - GroupDescription.Basic a = gic.get(o1.getIncludeUUID()); - GroupDescription.Basic b = gic.get(o2.getIncludeUUID()); - return n(a).compareTo(n(b)); - } - - private String n (GroupDescription.Basic a) { - if (a == null) { - return ""; - } - - String n = a.getName(); - if (n != null && n.length() > 0) { - return n; - } - return a.getGroupUUID().get(); - } - }); - return groups; } }
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/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java index b8cdf76..53b0467 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java
@@ -19,7 +19,6 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.Index.Input; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -43,7 +42,7 @@ @Override public Response<?> apply(AccountResource rsrc, Input input) - throws IOException, AuthException, OrmException { + throws IOException, AuthException { if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { throw new AuthException("not allowed to index account");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java index 78a801e..d18babf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -103,6 +103,11 @@ ? AccountState.getUserName(externalIds) : null; } + + if (options.contains(FillOptions.STATUS)) { + info.status = account.getStatus(); + } + if (options.contains(FillOptions.AVATARS)) { AvatarProvider ap = avatar.get(); if (ap != null) {
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/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java index 8c5228f..c78008d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -44,6 +44,8 @@ get(ACCOUNT_KIND, "name").to(GetName.class); put(ACCOUNT_KIND, "name").to(PutName.class); delete(ACCOUNT_KIND, "name").to(PutName.class); + get(ACCOUNT_KIND, "status").to(GetStatus.class); + put(ACCOUNT_KIND, "status").to(PutStatus.class); get(ACCOUNT_KIND, "username").to(GetUsername.class); put(ACCOUNT_KIND, "username").to(PutUsername.class); get(ACCOUNT_KIND, "active").to(GetActive.class); @@ -95,6 +97,9 @@ get(STAR_KIND).to(Stars.Get.class); post(STAR_KIND).to(Stars.Post.class); + get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class); + post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class); + factory(CreateAccount.Factory.class); factory(CreateEmail.Factory.class); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java index d54ec50..92fe837 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
@@ -21,11 +21,8 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountProjectWatch; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import com.google.gerrit.server.project.ProjectsCollection; import com.google.gwtorm.server.OrmException; @@ -38,8 +35,6 @@ import java.io.IOException; import java.util.EnumSet; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,7 +42,6 @@ @Singleton public class PostWatchedProjects implements RestModifyView<AccountResource, List<ProjectWatchInfo>> { - private final Provider<ReviewDb> dbProvider; private final Provider<IdentifiedUser> self; private final GetWatchedProjects getWatchedProjects; private final ProjectsCollection projectsCollection; @@ -55,13 +49,11 @@ private final WatchConfig.Accessor watchConfig; @Inject - public PostWatchedProjects(Provider<ReviewDb> dbProvider, - Provider<IdentifiedUser> self, + public PostWatchedProjects(Provider<IdentifiedUser> self, GetWatchedProjects getWatchedProjects, ProjectsCollection projectsCollection, AccountCache accountCache, WatchConfig.Accessor watchConfig) { - this.dbProvider = dbProvider; this.self = self; this.getWatchedProjects = getWatchedProjects; this.projectsCollection = projectsCollection; @@ -78,52 +70,11 @@ throw new AuthException("not allowed to edit project watches"); } Account.Id accountId = rsrc.getUser().getAccountId(); - updateInDb(accountId, input); - updateInGit(accountId, input); + watchConfig.upsertProjectWatches(accountId, asMap(input)); accountCache.evict(accountId); return getWatchedProjects.apply(rsrc); } - private void updateInDb(Account.Id accountId, List<ProjectWatchInfo> input) - throws BadRequestException, UnprocessableEntityException, IOException, - OrmException { - Set<AccountProjectWatch.Key> keys = new HashSet<>(); - List<AccountProjectWatch> watchedProjects = new LinkedList<>(); - for (ProjectWatchInfo a : input) { - if (a.project == null) { - throw new BadRequestException("project name must be specified"); - } - - Project.NameKey projectKey = - projectsCollection.parse(a.project).getNameKey(); - AccountProjectWatch.Key key = - new AccountProjectWatch.Key(accountId, projectKey, a.filter); - if (!keys.add(key)) { - throw new BadRequestException("duplicate entry for project " - + format(key.getProjectName().get(), key.getFilter().get())); - } - AccountProjectWatch apw = new AccountProjectWatch(key); - apw.setNotify(AccountProjectWatch.NotifyType.ABANDONED_CHANGES, - toBoolean(a.notifyAbandonedChanges)); - apw.setNotify(AccountProjectWatch.NotifyType.ALL_COMMENTS, - toBoolean(a.notifyAllComments)); - apw.setNotify(AccountProjectWatch.NotifyType.NEW_CHANGES, - toBoolean(a.notifyNewChanges)); - apw.setNotify(AccountProjectWatch.NotifyType.NEW_PATCHSETS, - toBoolean(a.notifyNewPatchSets)); - apw.setNotify(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, - toBoolean(a.notifySubmittedChanges)); - watchedProjects.add(apw); - } - dbProvider.get().accountProjectWatches().upsert(watchedProjects); - } - - private void updateInGit(Account.Id accountId, List<ProjectWatchInfo> input) - throws BadRequestException, UnprocessableEntityException, IOException, - ConfigInvalidException { - watchConfig.upsertProjectWatches(accountId, asMap(input)); - } - private Map<ProjectWatchKey, Set<NotifyType>> asMap( List<ProjectWatchInfo> input) throws BadRequestException, UnprocessableEntityException, IOException { @@ -168,7 +119,7 @@ private static String format(String project, String filter) { return project - + (filter != null && !AccountProjectWatch.FILTER_ALL.equals(filter) + + (filter != null && !WatchConfig.FILTER_ALL.equals(filter) ? " and filter " + filter : ""); }
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/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java new file mode 100644 index 0000000..c61c356 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -0,0 +1,91 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.account; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.DefaultInput; +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; +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.PutStatus.Input; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.io.IOException; +import java.util.Collections; + +@Singleton +public class PutStatus implements RestModifyView<AccountResource, Input> { + public static class Input { + @DefaultInput + String status; + + public Input(String status) { + this.status = status; + } + + public Input() { + } + } + + private final Provider<CurrentUser> self; + private final Provider<ReviewDb> dbProvider; + private final AccountCache byIdCache; + + @Inject + PutStatus(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, + AccountCache byIdCache) { + this.self = self; + this.dbProvider = dbProvider; + this.byIdCache = byIdCache; + } + + @Override + public Response<String> apply(AccountResource rsrc, Input input) + throws AuthException, + ResourceNotFoundException, OrmException, IOException { + if (self.get() != rsrc.getUser() + && !self.get().getCapabilities().canModifyAccount()) { + throw new AuthException("not allowed to set status"); + } + return apply(rsrc.getUser(), input); + } + + public Response<String> apply(IdentifiedUser user, Input input) + throws ResourceNotFoundException, OrmException, + IOException { + if (input == null) { + input = new Input(); + } + + Account a = dbProvider.get().accounts().get(user.getAccountId()); + if (a == null) { + throw new ResourceNotFoundException("account not found"); + } + a.setStatus(Strings.nullToEmpty(input.status)); + dbProvider.get().accounts().update(Collections.singleton(a)); + byIdCache.evict(a.getId()); + return Strings.isNullOrEmpty(a.getStatus()) + ? Response.none() + : Response.ok(a.getStatus()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java index 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/QueryAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java index 000637a..cef829f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/QueryAccounts.java
@@ -23,13 +23,9 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountDirectory.FillOptions; import com.google.gerrit.server.api.accounts.AccountInfoComparator; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.index.account.AccountIndex; -import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.QueryResult; @@ -43,7 +39,6 @@ import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -51,15 +46,10 @@ public class QueryAccounts implements RestReadView<TopLevelResource> { private static final int MAX_SUGGEST_RESULTS = 100; - private static final String MAX_SUFFIX = "\u9fa5"; - private final AccountControl accountControl; private final AccountLoader.Factory accountLoaderFactory; - private final AccountCache accountCache; - private final AccountIndexCollection indexes; private final AccountQueryBuilder queryBuilder; private final AccountQueryProcessor queryProcessor; - private final ReviewDb db; private final boolean suggestConfig; private final int suggestFrom; @@ -110,21 +100,13 @@ } @Inject - QueryAccounts(AccountControl.Factory accountControlFactory, - AccountLoader.Factory accountLoaderFactory, - AccountCache accountCache, - AccountIndexCollection indexes, + QueryAccounts(AccountLoader.Factory accountLoaderFactory, AccountQueryBuilder queryBuilder, AccountQueryProcessor queryProcessor, - ReviewDb db, @GerritServerConfig Config cfg) { - this.accountControl = accountControlFactory.get(); this.accountLoaderFactory = accountLoaderFactory; - this.accountCache = accountCache; - this.indexes = indexes; this.queryBuilder = queryBuilder; this.queryProcessor = queryProcessor; - this.db = db; this.suggestFrom = cfg.getInt("suggest", null, "from", 0); this.options = EnumSet.noneOf(ListAccountsOption.class); @@ -169,22 +151,6 @@ } accountLoader = accountLoaderFactory.create(fillOptions); - AccountIndex searchIndex = indexes.getSearchIndex(); - if (searchIndex != null) { - return queryFromIndex(); - } - - if (!suggest) { - throw new MethodNotAllowedException(); - } - if (start != null) { - throw new MethodNotAllowedException("option start not allowed"); - } - return queryFromDb(); - } - - public List<AccountInfo> queryFromIndex() - throws BadRequestException, MethodNotAllowedException, OrmException { if (queryProcessor.isDisabled()) { throw new MethodNotAllowedException("query disabled"); } @@ -223,57 +189,4 @@ throw new BadRequestException(e.getMessage()); } } - - public List<AccountInfo> queryFromDb() throws OrmException { - String a = query; - String b = a + MAX_SUFFIX; - - Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>(); - Map<Account.Id, String> queryEmail = new HashMap<>(); - - for (Account p : db.accounts().suggestByFullName(a, b, suggestLimit)) { - addSuggestion(matches, p); - } - if (matches.size() < suggestLimit) { - for (Account p : db.accounts() - .suggestByPreferredEmail(a, b, suggestLimit - matches.size())) { - addSuggestion(matches, p); - } - } - if (matches.size() < suggestLimit) { - for (AccountExternalId e : db.accountExternalIds() - .suggestByEmailAddress(a, b, suggestLimit - matches.size())) { - if (addSuggestion(matches, e.getAccountId())) { - queryEmail.put(e.getAccountId(), e.getEmailAddress()); - } - } - } - - accountLoader.fill(); - for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) { - AccountInfo info = matches.get(p.getKey()); - if (info != null) { - info.email = p.getValue(); - } - } - - return AccountInfoComparator.ORDER_NULLS_LAST.sortedCopy(matches.values()); - } - - private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account a) { - if (!a.isActive()) { - return false; - } - Account.Id id = a.getId(); - if (!map.containsKey(id) && accountControl.canSee(id)) { - map.put(id, accountLoader.get(id)); - return true; - } - return false; - } - - private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) { - Account a = accountCache.get(id).getAccount(); - return addSuggestion(map, a); - } }
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..31a3f22 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
@@ -15,11 +15,12 @@ package com.google.gerrit.server.account; import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR; +import static java.util.stream.Collectors.joining; -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.ListMultimap; +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; @@ -27,10 +28,14 @@ import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StartupCheck; +import com.google.gerrit.server.StartupException; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.ProjectControl; import com.google.inject.Inject; import com.google.inject.Singleton; +import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -138,8 +143,8 @@ @Override public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) { - Multimap<GroupMembership, AccountGroup.UUID> lookups = - ArrayListMultimap.create(); + ListMultimap<GroupMembership, AccountGroup.UUID> lookups = + MultimapBuilder.hashKeys().arrayListValues().build(); for (AccountGroup.UUID uuid : uuids) { if (uuid == null) { continue; @@ -168,8 +173,8 @@ @Override public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> uuids) { - Multimap<GroupMembership, AccountGroup.UUID> lookups = - ArrayListMultimap.create(); + ListMultimap<GroupMembership, AccountGroup.UUID> lookups = + MultimapBuilder.hashKeys().arrayListValues().build(); for (AccountGroup.UUID uuid : uuids) { if (uuid == null) { continue; @@ -208,4 +213,38 @@ } return false; } + + public static class ConfigCheck implements StartupCheck { + private final Config cfg; + private final UniversalGroupBackend universalGroupBackend; + + @Inject + ConfigCheck(@GerritServerConfig Config cfg, + UniversalGroupBackend groupBackend) { + this.cfg = cfg; + this.universalGroupBackend = groupBackend; + } + + @Override + public void check() throws StartupException { + String invalid = cfg.getSubsections("groups").stream() + .filter( + sub -> { + AccountGroup.UUID uuid = new AccountGroup.UUID(sub); + GroupBackend groupBackend = universalGroupBackend.backend(uuid); + return groupBackend == null || groupBackend.get(uuid) == null; + }) + .map(u -> "'" + u + "'") + .collect(joining(",")); + + if (!invalid.isEmpty()) { + throw new StartupException(String.format( + "Subsections for 'groups' in gerrit.config must be valid group" + + " UUIDs. The following group UUIDs could not be resolved: " + + invalid + + " Please remove/fix these 'groups' subsections in" + + " gerrit.config.")); + } + } + } }
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..2976ab5 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,18 +21,15 @@ 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.ListMultimap; +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; -import com.google.gerrit.reviewdb.client.AccountProjectWatch; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.IdentifiedUser; @@ -148,6 +145,19 @@ } } + public synchronized void deleteAllProjectWatches(Account.Id accountId) + throws IOException, ConfigInvalidException { + WatchConfig watchConfig = read(accountId); + boolean commit = false; + if (!watchConfig.getProjectWatches().isEmpty()) { + watchConfig.getProjectWatches().clear(); + commit = true; + } + if (commit) { + commit(watchConfig); + } + } + private WatchConfig read(Account.Id accountId) throws IOException, ConfigInvalidException { try (Repository git = repoManager.openRepository(allUsersName)) { @@ -178,6 +188,19 @@ public abstract @Nullable String filter(); } + public enum NotifyType { + // sort by name, except 'ALL' which should stay last + ABANDONED_CHANGES, + ALL_COMMENTS, + NEW_CHANGES, + NEW_PATCHSETS, + SUBMITTED_CHANGES, + + ALL + } + + public static final String FILTER_ALL = "*"; + public static final String WATCH_CONFIG = "watch.config"; public static final String PROJECT = "project"; public static final String KEY_NOTIFY = "notify"; @@ -239,6 +262,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 +279,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(); + ListMultimap<String, String> notifyValuesByProject = + MultimapBuilder.hashKeys().arrayListValues().build(); for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches .entrySet()) { NotifyValue notifyValue = @@ -311,7 +340,7 @@ return null; } String filter = notifyValue.substring(0, i).trim(); - if (filter.isEmpty() || AccountProjectWatch.FILTER_ALL.equals(filter)) { + if (filter.isEmpty() || FILTER_ALL.equals(filter)) { filter = null; } @@ -319,9 +348,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 +358,7 @@ nt, accountId.get(), project, notifyValue))); continue; } - notifyTypes.add(notifyType.get()); + notifyTypes.add(notifyType); } } return create(filter, notifyTypes); @@ -348,7 +377,7 @@ public String toString() { List<NotifyType> notifyTypes = new ArrayList<>(notifyTypes()); StringBuilder notifyValue = new StringBuilder(); - notifyValue.append(firstNonNull(filter(), AccountProjectWatch.FILTER_ALL)) + notifyValue.append(firstNonNull(filter(), FILTER_ALL)) .append(" ["); Joiner.on(", ").appendTo(notifyValue, notifyTypes); notifyValue.append("]");
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..3c20719 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; @@ -24,6 +26,7 @@ import com.google.gerrit.extensions.client.EditPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.ProjectWatchInfo; +import com.google.gerrit.extensions.common.AccountExternalIdInfo; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.AgreementInfo; import com.google.gerrit.extensions.common.AgreementInput; @@ -31,6 +34,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,18 +42,24 @@ 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.DeleteExternalIds; 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; import com.google.gerrit.server.account.GetEditPreferences; +import com.google.gerrit.server.account.GetExternalIds; import com.google.gerrit.server.account.GetPreferences; import com.google.gerrit.server.account.GetSshKeys; 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.PutStatus; import com.google.gerrit.server.account.SetDiffPreferences; import com.google.gerrit.server.account.SetEditPreferences; import com.google.gerrit.server.account.SetPreferences; @@ -100,7 +110,13 @@ 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; + private final GetExternalIds getExternalIds; + private final DeleteExternalIds deleteExternalIds; + private final PutStatus putStatus; @Inject AccountApiImpl(AccountLoader.Factory ailf, @@ -128,7 +144,13 @@ SshKeys sshKeys, GetAgreements getAgreements, PutAgreement putAgreement, + GetActive getActive, + PutActive putActive, + DeleteActive deleteActive, Index index, + GetExternalIds getExternalIds, + DeleteExternalIds deleteExternalIds, + PutStatus putStatus, @Assisted AccountResource account) { this.account = account; this.accountLoaderFactory = ailf; @@ -156,7 +178,13 @@ this.gpgApiAdapter = gpgApiAdapter; this.getAgreements = getAgreements; this.putAgreement = putAgreement; + this.getActive = getActive; + this.putActive = putActive; + this.deleteActive = deleteActive; this.index = index; + this.getExternalIds = getExternalIds; + this.deleteExternalIds = deleteExternalIds; + this.putStatus = putStatus; } @Override @@ -173,6 +201,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(); @@ -331,6 +378,16 @@ } @Override + public void setStatus(String status) throws RestApiException { + PutStatus.Input in = new PutStatus.Input(status); + try { + putStatus.apply(account, in); + } catch (OrmException | IOException e) { + throw new RestApiException("Cannot set status", e); + } + } + + @Override public List<SshKeyInfo> listSshKeys() throws RestApiException { try { return getSshKeys.apply(account); @@ -409,8 +466,27 @@ public void index() throws RestApiException { try { index.apply(account, new Index.Input()); - } catch (IOException | OrmException e) { + } catch (IOException e) { throw new RestApiException("Cannot index account", e); } } + + @Override + public List<AccountExternalIdInfo> getExternalIds() throws RestApiException { + try { + return getExternalIds.apply(account); + } catch (OrmException e) { + throw new RestApiException("Cannot get external IDs", e); + } + } + + @Override + public void deleteExternalIds(List<String> externalIds) + throws RestApiException { + try { + deleteExternalIds.apply(account, externalIds); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot delete external IDs", e); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index 9bfb342..2a4b5ca 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,10 +16,13 @@ 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.ChangeEditApi; import com.google.gerrit.extensions.api.changes.Changes; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.api.changes.HashtagsInput; +import com.google.gerrit.extensions.api.changes.IncludedInInfo; import com.google.gerrit.extensions.api.changes.MoveInput; import com.google.gerrit.extensions.api.changes.RestoreInput; import com.google.gerrit.extensions.api.changes.RevertInput; @@ -28,28 +31,37 @@ 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.RobotCommentInfo; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; 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.server.change.Abandon; -import com.google.gerrit.server.change.ChangeEdits; +import com.google.gerrit.server.change.ChangeIncludedIn; 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; import com.google.gerrit.server.change.ListChangeDrafts; +import com.google.gerrit.server.change.ListChangeRobotComments; import com.google.gerrit.server.change.Move; import com.google.gerrit.server.change.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 +70,10 @@ 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.NoSuchChangeException; +import com.google.gerrit.server.project.InvalidChangeOperationException; 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,21 +97,28 @@ 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 ChangeIncludedIn includedIn; 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 ListChangeRobotComments listChangeRobotComments; private final ListChangeDrafts listDrafts; + private final ChangeEditApiImpl.Factory changeEditApi; private final Check check; private final Index index; - private final ChangeEdits.Detail editDetail; private final Move move; @Inject @@ -111,20 +131,27 @@ Abandon abandon, Revert revert, Restore restore, - SubmittedTogether submittedTogether, + CreateMergePatchSet updateByMerge, + Provider<SubmittedTogether> submittedTogether, PublishDraftPatchSet.CurrentRevision publishDraftChange, - DeleteDraftChange deleteDraftChange, + DeleteChange deleteChange, GetTopic getTopic, PutTopic putTopic, + ChangeIncludedIn includedIn, PostReviewers postReviewers, ChangeJson.Factory changeJson, PostHashtags postHashtags, GetHashtags getHashtags, + PutAssignee putAssignee, + GetAssignee getAssignee, + GetPastAssignees getPastAssignees, + DeleteAssignee deleteAssignee, ListChangeComments listComments, + ListChangeRobotComments listChangeRobotComments, ListChangeDrafts listDrafts, + ChangeEditApiImpl.Factory changeEditApi, Check check, Index index, - ChangeEdits.Detail editDetail, Move move, @Assisted ChangeResource change) { this.changeApi = changeApi; @@ -136,20 +163,27 @@ 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.includedIn = includedIn; 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.listChangeRobotComments = listChangeRobotComments; this.listDrafts = listDrafts; + this.changeEditApi = changeEditApi; this.check = check; this.index = index; - this.editDetail = editDetail; this.move = move; this.change = change; } @@ -242,27 +276,45 @@ public ChangeApi revert(RevertInput in) throws RestApiException { try { return changeApi.id(revert.apply(change, in)._number); - } catch (OrmException | IOException | UpdateException - | NoSuchChangeException e) { + } catch (OrmException | IOException | UpdateException e) { throw new RestApiException("Cannot revert change", e); } } - @SuppressWarnings("unchecked") + @Override + public ChangeInfo createMergePatchSet(MergePatchSetInput in) + throws RestApiException { + try { + return updateByMerge.apply(change, in).value(); + } catch (IOException | UpdateException | InvalidChangeOperationException + | OrmException e) { + throw new RestApiException("Cannot update change by merge", e); + } + } + @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 +332,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); } @@ -303,6 +355,15 @@ } @Override + public IncludedInInfo includedIn() throws RestApiException { + try { + return includedIn.apply(change); + } catch (OrmException | IOException e) { + throw new RestApiException("Could not extract IncludedIn data", e); + } + } + + @Override public void addReviewer(String reviewer) throws RestApiException { AddReviewerInput in = new AddReviewerInput(); in.reviewer = reviewer; @@ -362,12 +423,12 @@ @Override public EditInfo getEdit() throws RestApiException { - try { - Response<EditInfo> edit = editDetail.apply(change); - return edit.isNone() ? null : edit.value(); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot retrieve change edit", e); - } + return edit().get().orElse(null); + } + + @Override + public ChangeEditApi edit() throws RestApiException { + return changeEditApi.create(change); } @Override @@ -394,6 +455,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 | OrmException e) { + throw new RestApiException("Cannot delete assignee", e); + } + } + + @Override public Map<String, List<CommentInfo>> comments() throws RestApiException { try { return listComments.apply(change); @@ -403,6 +503,16 @@ } @Override + public Map<String, List<RobotCommentInfo>> robotComments() + throws RestApiException { + try { + return listChangeRobotComments.apply(change); + } catch (OrmException e) { + throw new RestApiException("Cannot get robot comments", e); + } + } + + @Override public Map<String, List<CommentInfo>> drafts() throws RestApiException { try { return listDrafts.apply(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java new file mode 100644 index 0000000..b186767 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -0,0 +1,226 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.api.changes; + +import com.google.gerrit.extensions.api.changes.ChangeEditApi; +import com.google.gerrit.extensions.api.changes.PublishChangeEditInput; +import com.google.gerrit.extensions.common.EditInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.RawInput; +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.server.change.ChangeEditResource; +import com.google.gerrit.server.change.ChangeEdits; +import com.google.gerrit.server.change.ChangeResource; +import com.google.gerrit.server.change.DeleteChangeEdit; +import com.google.gerrit.server.change.PublishChangeEdit; +import com.google.gerrit.server.change.RebaseChangeEdit; +import com.google.gerrit.server.git.UpdateException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import java.io.IOException; +import java.util.Optional; + +public class ChangeEditApiImpl implements ChangeEditApi { + interface Factory { + ChangeEditApiImpl create(ChangeResource changeResource); + } + + private final ChangeEdits.Detail editDetail; + private final ChangeEdits.Post changeEditsPost; + private final DeleteChangeEdit deleteChangeEdit; + private final RebaseChangeEdit.Rebase rebaseChangeEdit; + private final PublishChangeEdit.Publish publishChangeEdit; + private final ChangeEdits.Get changeEditsGet; + private final ChangeEdits.Put changeEditsPut; + private final ChangeEdits.DeleteContent changeEditDeleteContent; + private final ChangeEdits.GetMessage getChangeEditCommitMessage; + private final ChangeEdits.EditMessage modifyChangeEditCommitMessage; + private final ChangeEdits changeEdits; + private final ChangeResource changeResource; + + @Inject + public ChangeEditApiImpl(ChangeEdits.Detail editDetail, + ChangeEdits.Post changeEditsPost, + DeleteChangeEdit deleteChangeEdit, + RebaseChangeEdit.Rebase rebaseChangeEdit, + PublishChangeEdit.Publish publishChangeEdit, + ChangeEdits.Get changeEditsGet, + ChangeEdits.Put changeEditsPut, + ChangeEdits.DeleteContent changeEditDeleteContent, + ChangeEdits.GetMessage getChangeEditCommitMessage, + ChangeEdits.EditMessage modifyChangeEditCommitMessage, + ChangeEdits changeEdits, + @Assisted ChangeResource changeResource) { + this.editDetail = editDetail; + this.changeEditsPost = changeEditsPost; + this.deleteChangeEdit = deleteChangeEdit; + this.rebaseChangeEdit = rebaseChangeEdit; + this.publishChangeEdit = publishChangeEdit; + this.changeEditsGet = changeEditsGet; + this.changeEditsPut = changeEditsPut; + this.changeEditDeleteContent = changeEditDeleteContent; + this.getChangeEditCommitMessage = getChangeEditCommitMessage; + this.modifyChangeEditCommitMessage = modifyChangeEditCommitMessage; + this.changeEdits = changeEdits; + this.changeResource = changeResource; + } + + @Override + public Optional<EditInfo> get() throws RestApiException { + try { + Response<EditInfo> edit = editDetail.apply(changeResource); + return edit.isNone() ? Optional.empty() : Optional.of(edit.value()); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot retrieve change edit", e); + } + } + + @Override + public void create() throws RestApiException { + try { + changeEditsPost.apply(changeResource, null); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot create change edit", e); + } + } + + @Override + public void delete() throws RestApiException { + try { + deleteChangeEdit.apply(changeResource, new DeleteChangeEdit.Input()); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot delete change edit", e); + } + } + + @Override + public void rebase() throws RestApiException { + try { + rebaseChangeEdit.apply(changeResource, null); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot rebase change edit", e); + } + } + + @Override + public void publish() throws RestApiException { + publish(null); + } + + @Override + public void publish(PublishChangeEditInput publishChangeEditInput) + throws RestApiException { + try { + publishChangeEdit.apply(changeResource, publishChangeEditInput); + } catch (IOException | OrmException | UpdateException e) { + throw new RestApiException("Cannot publish change edit", e); + } + } + + @Override + public Optional<BinaryResult> getFile(String filePath) + throws RestApiException { + try { + ChangeEditResource changeEditResource = getChangeEditResource(filePath); + Response<BinaryResult> fileResponse = + changeEditsGet.apply(changeEditResource); + return fileResponse.isNone() + ? Optional.empty() + : Optional.of(fileResponse.value()); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot retrieve file of change edit", e); + } + } + + @Override + public void renameFile(String oldFilePath, String newFilePath) + throws RestApiException { + try { + ChangeEdits.Post.Input renameInput = new ChangeEdits.Post.Input(); + renameInput.oldPath = oldFilePath; + renameInput.newPath = newFilePath; + changeEditsPost.apply(changeResource, renameInput); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot rename file of change edit", e); + } + } + + @Override + public void restoreFile(String filePath) throws RestApiException { + try { + ChangeEdits.Post.Input restoreInput = new ChangeEdits.Post.Input(); + restoreInput.restorePath = filePath; + changeEditsPost.apply(changeResource, restoreInput); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot restore file of change edit", e); + } + } + + @Override + public void modifyFile(String filePath, RawInput newContent) + throws RestApiException { + try { + changeEditsPut.apply(changeResource.getControl(), filePath, newContent); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot modify file of change edit", e); + } + } + + @Override + public void deleteFile(String filePath) throws RestApiException { + try { + changeEditDeleteContent.apply(changeResource.getControl(), filePath); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot delete file of change edit", e); + } + } + + @Override + public String getCommitMessage() throws RestApiException { + try { + try (BinaryResult binaryResult = + getChangeEditCommitMessage.apply(changeResource)) { + return binaryResult.asString(); + } + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot get commit message of change edit", e); + } + } + + @Override + public void modifyCommitMessage(String newCommitMessage) + throws RestApiException { + ChangeEdits.EditMessage.Input input = new ChangeEdits.EditMessage.Input(); + input.message = newCommitMessage; + try { + modifyChangeEditCommitMessage.apply(changeResource, input); + } catch (IOException | OrmException e) { + throw new RestApiException("Cannot modify commit message of change edit", + e); + } + } + + private ChangeEditResource getChangeEditResource(String filePath) + throws ResourceNotFoundException, AuthException, IOException, + OrmException { + return changeEdits.parse(changeResource, IdString.fromDecoded(filePath)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java index e6ca18df..2aa0f3d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -22,7 +22,6 @@ import com.google.gerrit.server.change.GetContent; import com.google.gerrit.server.change.GetDiff; 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.assistedinject.Assisted; @@ -51,7 +50,7 @@ public BinaryResult content() throws RestApiException { try { return getContent.apply(file); - } catch (NoSuchChangeException | IOException | OrmException e) { + } catch (IOException | OrmException e) { throw new RestApiException("Cannot retrieve file content", e); } }
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..e91d64a 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,9 +24,12 @@ factory(ChangeApiImpl.Factory.class); factory(CommentApiImpl.Factory.class); + factory(RobotCommentApiImpl.Factory.class); factory(DraftApiImpl.Factory.class); factory(RevisionApiImpl.Factory.class); factory(FileApiImpl.Factory.class); factory(ReviewerApiImpl.Factory.class); + factory(RevisionReviewerApiImpl.Factory.class); + factory(ChangeEditApiImpl.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..a7aa2d0 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,16 @@ 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.RevisionReviewerApi; +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 +48,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.RevisionReviewers; +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.project.NoSuchChangeException; +import com.google.gerrit.server.patch.PatchListNotAvailableException; 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; @@ -79,11 +91,14 @@ private final GitRepositoryManager repoManager; private final Changes changes; + private final RevisionReviewers revisionReviewers; + private final RevisionReviewerApiImpl.Factory revisionReviewerApi; private final CherryPick cherryPick; private final DeleteDraftPatchSet deleteDraft; 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,24 +110,33 @@ 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, Changes changes, + RevisionReviewers revisionReviewers, + RevisionReviewerApiImpl.Factory revisionReviewerApi, CherryPick cherryPick, DeleteDraftPatchSet deleteDraft, Rebase rebase, RebaseUtil rebaseUtil, Submit submit, + PreviewSubmit submitPreview, PublishDraftPatchSet publish, Reviewed.PutReviewed putReviewed, Reviewed.DeleteReviewed deleteReviewed, @@ -123,24 +147,33 @@ 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; + this.revisionReviewers = revisionReviewers; + this.revisionReviewerApi = revisionReviewerApi; this.cherryPick = cherryPick; this.deleteDraft = deleteDraft; this.rebase = rebase; this.rebaseUtil = rebaseUtil; this.review = review; this.submit = submit; + this.submitPreview = submitPreview; this.publish = publish; this.files = files; this.putReviewed = putReviewed; @@ -150,15 +183,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 +226,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()); @@ -214,8 +264,7 @@ public ChangeApi rebase(RebaseInput in) throws RestApiException { try { return changes.id(rebase.apply(revision, in)._number); - } catch (OrmException | EmailException | UpdateException | IOException - | NoSuchChangeException e) { + } catch (OrmException | EmailException | UpdateException | IOException e) { throw new RestApiException("Cannot rebase ps", e); } } @@ -241,6 +290,16 @@ } @Override + public RevisionReviewerApi reviewer(String id) throws RestApiException { + try { + return revisionReviewerApi.create( + revisionReviewers.parse(revision, IdString.fromDecoded(id))); + } catch (OrmException e) { + throw new RestApiException("Cannot parse reviewer", e); + } + } + + @Override public void setReviewed(String path, boolean reviewed) throws RestApiException { try { RestModifyView<FileResource, Reviewed.Input> view; @@ -264,7 +323,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 +352,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 +363,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 +374,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 +395,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 +422,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 +473,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 +492,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 +527,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/RevisionReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java new file mode 100644 index 0000000..82740c4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
@@ -0,0 +1,75 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.api.changes; + +import com.google.gerrit.extensions.api.changes.DeleteVoteInput; +import com.google.gerrit.extensions.api.changes.RevisionReviewerApi; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.change.DeleteVote; +import com.google.gerrit.server.change.ReviewerResource; +import com.google.gerrit.server.change.VoteResource; +import com.google.gerrit.server.change.Votes; +import com.google.gerrit.server.git.UpdateException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import java.util.Map; + +public class RevisionReviewerApiImpl implements RevisionReviewerApi { + interface Factory { + RevisionReviewerApiImpl create(ReviewerResource r); + } + + private final ReviewerResource reviewer; + private final Votes.List listVotes; + private final DeleteVote deleteVote; + + @Inject + RevisionReviewerApiImpl(Votes.List listVotes, + DeleteVote deleteVote, + @Assisted ReviewerResource reviewer) { + this.listVotes = listVotes; + this.deleteVote = deleteVote; + this.reviewer = reviewer; + } + + @Override + public Map<String, Short> votes() throws RestApiException { + try { + return listVotes.apply(reviewer); + } catch (OrmException e) { + throw new RestApiException("Cannot list votes", e); + } + } + + @Override + public void deleteVote(String label) throws RestApiException { + try { + deleteVote.apply(new VoteResource(reviewer, label), null); + } catch (UpdateException e) { + throw new RestApiException("Cannot delete vote", e); + } + } + + @Override + public void deleteVote(DeleteVoteInput input) throws RestApiException { + try { + deleteVote.apply(new VoteResource(reviewer, input.label), input); + } catch (UpdateException e) { + throw new RestApiException("Cannot delete vote", e); + } + } +}
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/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java index 5660176..b939a3b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -34,6 +34,7 @@ import com.google.gerrit.server.group.GetOptions; import com.google.gerrit.server.group.GetOwner; import com.google.gerrit.server.group.GroupResource; +import com.google.gerrit.server.group.Index; import com.google.gerrit.server.group.ListIncludedGroups; import com.google.gerrit.server.group.ListMembers; import com.google.gerrit.server.group.PutDescription; @@ -71,6 +72,7 @@ private final DeleteIncludedGroups deleteGroups; private final GetAuditLog getAuditLog; private final GroupResource rsrc; + private final Index index; @AssistedInject GroupApiImpl( @@ -91,6 +93,7 @@ AddIncludedGroups addGroups, DeleteIncludedGroups deleteGroups, GetAuditLog getAuditLog, + Index index, @Assisted GroupResource rsrc) { this.getGroup = getGroup; this.getDetail = getDetail; @@ -109,6 +112,7 @@ this.addGroups = addGroups; this.deleteGroups = deleteGroups; this.getAuditLog = getAuditLog; + this.index = index; this.rsrc = rsrc; } @@ -143,7 +147,7 @@ putName.apply(rsrc, in); } catch (NoSuchGroupException e) { throw new ResourceNotFoundException(name, e); - } catch (OrmException e) { + } catch (OrmException | IOException e) { throw new RestApiException("Cannot put group name", e); } } @@ -163,7 +167,7 @@ in.owner = owner; try { putOwner.apply(rsrc, in); - } catch (OrmException e) { + } catch (OrmException | IOException e) { throw new RestApiException("Cannot put group owner", e); } } @@ -179,7 +183,7 @@ in.description = description; try { putDescription.apply(rsrc, in); - } catch (OrmException e) { + } catch (OrmException | IOException e) { throw new RestApiException("Cannot put group description", e); } } @@ -193,7 +197,7 @@ public void options(GroupOptionsInfo options) throws RestApiException { try { putOptions.apply(rsrc, options); - } catch (OrmException e) { + } catch (OrmException | IOException e) { throw new RestApiException("Cannot put group options", e); } } @@ -270,4 +274,13 @@ throw new RestApiException("Cannot get audit log", e); } } + + @Override + public void index() throws RestApiException { + try { + index.apply(rsrc, new Index.Input()); + } catch (IOException e) { + throw new RestApiException("Cannot index group", e); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java index b509c55..dcd8761 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -20,6 +20,7 @@ import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.api.groups.Groups; +import com.google.gerrit.extensions.client.ListGroupsOption; import com.google.gerrit.extensions.common.GroupInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; @@ -30,6 +31,7 @@ import com.google.gerrit.server.group.CreateGroup; import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.group.ListGroups; +import com.google.gerrit.server.group.QueryGroups; import com.google.gerrit.server.project.ProjectsCollection; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -37,6 +39,7 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.List; import java.util.SortedMap; @Singleton @@ -45,6 +48,7 @@ private final GroupsCollection groups; private final ProjectsCollection projects; private final Provider<ListGroups> listGroups; + private final Provider<QueryGroups> queryGroups; private final Provider<CurrentUser> user; private final CreateGroup.Factory createGroup; private final GroupApiImpl.Factory api; @@ -55,6 +59,7 @@ GroupsCollection groups, ProjectsCollection projects, Provider<ListGroups> listGroups, + Provider<QueryGroups> queryGroups, Provider<CurrentUser> user, CreateGroup.Factory createGroup, GroupApiImpl.Factory api) { @@ -62,6 +67,7 @@ this.groups = groups; this.projects = projects; this.listGroups = listGroups; + this.queryGroups = queryGroups; this.user = user; this.createGroup = createGroup; this.api = api; @@ -145,4 +151,35 @@ throw new RestApiException("Cannot list groups", e); } } + + @Override + public QueryRequest query() { + return new QueryRequest() { + @Override + public List<GroupInfo> get() throws RestApiException { + return GroupsImpl.this.query(this); + } + }; + } + + @Override + public QueryRequest query(String query) { + return query().withQuery(query); + } + + private List<GroupInfo> query(QueryRequest r) + throws RestApiException { + try { + QueryGroups myQueryGroups = queryGroups.get(); + myQueryGroups.setQuery(r.getQuery()); + myQueryGroups.setLimit(r.getLimit()); + myQueryGroups.setStart(r.getStart()); + for (ListGroupsOption option : r.getOptions()) { + myQueryGroups.addOption(option); + } + return myQueryGroups.apply(TopLevelResource.INSTANCE); + } catch (OrmException e) { + throw new RestApiException("Cannot query groups", e); + } + } }
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..1e45387 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 { @@ -276,7 +276,8 @@ private void recursivelyExpandGroups(final Set<String> groupDNs, final LdapSchema schema, final DirContext ctx, final String groupDN) { - if (groupDNs.add(groupDN) && schema.accountMemberField != null) { + if (groupDNs.add(groupDN) && schema.accountMemberField != null + && schema.accountMemberExpandGroups) { ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN); if (cachedParentsDNs == null) { // Recursively identify the groups it is a member of. @@ -319,6 +320,7 @@ final ParameterizedString accountEmailAddress; final ParameterizedString accountSshUserName; final String accountMemberField; + final boolean accountMemberExpandGroups; final String[] accountMemberFieldArray; final List<LdapQuery> accountQueryList; final List<LdapQuery> accountWithMemberOfQueryList; @@ -390,6 +392,9 @@ } else { accountMemberFieldArray = null; } + accountMemberExpandGroups = + LdapRealm.optional(config, "accountMemberExpandGroups", + type.accountMemberExpandGroups()); final SearchScope accountScope = LdapRealm.scope(config, "accountScope"); final String accountPattern =
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/ldap/LdapType.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java index 3c1b0d2..4e68653 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java
@@ -30,6 +30,11 @@ return new ActiveDirectory(); } + supported = rootAtts.get("supportedExtension"); + if (supported != null && supported.contains("2.16.840.1.113730.3.8.10.1")) { + return new FreeIPA(); + } + return RFC_2307; } @@ -47,6 +52,8 @@ abstract String accountMemberField(); + abstract boolean accountMemberExpandGroups(); + abstract String accountPattern(); private static class Rfc2307 extends LdapType { @@ -89,6 +96,11 @@ String accountPattern() { return "(uid=${username})"; } + + @Override + boolean accountMemberExpandGroups() { + return true; + } } private static class ActiveDirectory extends LdapType { @@ -131,5 +143,58 @@ String accountPattern() { return "(&(objectClass=user)(sAMAccountName=${username}))"; } + + @Override + boolean accountMemberExpandGroups() { + return true; + } + } + + private static class FreeIPA extends LdapType { + + @Override + String groupPattern() { + return "(cn=${groupname})"; + } + + @Override + String groupName() { + return "cn"; + } + + @Override + String groupMemberPattern() { + return null; // FreeIPA uses memberOf in the account + } + + @Override + String accountFullName() { + return "displayName"; + } + + @Override + String accountEmailAddress() { + return "mail"; + } + + @Override + String accountSshUserName() { + return "uid"; + } + + @Override + String accountMemberField() { + return "memberOf"; + } + + @Override + String accountPattern() { + return "(uid=${username})"; + } + + @Override + boolean accountMemberExpandGroups() { + return false; + } } }
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..54c217d 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.ListMultimap; 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, + ListMultimap<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, + ListMultimap<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..566e9fa 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.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +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(); + ImmutableListMultimap.Builder<Project.NameKey, ChangeControl> builder = + ImmutableListMultimap.builder(); for (ChangeData cd : changesToAbandon) { + ChangeControl control = cd.changeControl(internalUser); + builder.put(control.getProject().getNameKey(), control); + } + + int count = 0; + ListMultimap<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/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java index c57f5a0..9deb864 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -17,7 +17,6 @@ import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.project.ChangeControl; @@ -60,10 +59,6 @@ return getChangeResource().getControl(); } - public Change getChange() { - return edit.getChange(); - } - public ChangeEdit getChangeEdit() { return edit; }
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..86527b2 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,10 +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; import com.google.gerrit.extensions.registration.DynamicMap; @@ -40,8 +37,7 @@ import com.google.gerrit.extensions.restapi.RestView; 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.server.PatchSetUtil; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.WebLinks; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditJson; @@ -50,6 +46,7 @@ import com.google.gerrit.server.edit.UnchangedCommitMessageException; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -65,6 +62,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; @Singleton public class ChangeEdits implements @@ -107,7 +105,7 @@ @Override public ChangeEditResource parse(ChangeResource rsrc, IdString id) throws ResourceNotFoundException, AuthException, IOException, - InvalidChangeOperationException, OrmException { + OrmException { Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange()); if (!edit.isPresent()) { throw new ResourceNotFoundException(id); @@ -119,7 +117,7 @@ @Override public Create create(ChangeResource parent, IdString id) throws RestApiException { - return createFactory.create(parent.getChange(), id.get()); + return createFactory.create(id.get()); } @@ -149,58 +147,25 @@ RestModifyView<ChangeResource, Put.Input> { interface Factory { - Create create(Change change, String path); + Create create(String path); } - private final Provider<ReviewDb> db; - private final ChangeEditUtil editUtil; - private final ChangeEditModifier editModifier; - private final PatchSetUtil psUtil; private final Put putEdit; - private final Change change; private final String path; @Inject - Create(Provider<ReviewDb> db, - ChangeEditUtil editUtil, - ChangeEditModifier editModifier, - PatchSetUtil psUtil, - Put putEdit, - @Assisted Change change, - @Assisted @Nullable String path) { - this.db = db; - this.editUtil = editUtil; - this.editModifier = editModifier; - this.psUtil = psUtil; + Create(Put putEdit, + @Assisted String path) { this.putEdit = putEdit; - this.change = change; this.path = path; } @Override public Response<?> apply(ChangeResource resource, Put.Input input) - throws AuthException, IOException, ResourceConflictException, - OrmException, InvalidChangeOperationException { - Optional<ChangeEdit> edit = editUtil.byChange(change); - if (edit.isPresent()) { - throw new ResourceConflictException(String.format( - "edit already exists for the change %s", - resource.getId())); - } - edit = createEdit(resource); - if (!Strings.isNullOrEmpty(path)) { - putEdit.apply(new ChangeEditResource(resource, edit.get(), path), - input); - } - return Response.none(); - } - - private Optional<ChangeEdit> createEdit(ChangeResource resource) - throws AuthException, IOException, ResourceConflictException, + throws AuthException, ResourceConflictException, IOException, OrmException { - editModifier.createEdit(change, - psUtil.current(db.get(), resource.getNotes())); - return editUtil.byChange(change); + putEdit.apply(resource.getControl(), path, input.content); + return Response.none(); } } @@ -213,44 +178,21 @@ DeleteFile create(String path); } - private final ChangeEditUtil editUtil; - private final ChangeEditModifier editModifier; - private final PatchSetUtil psUtil; - private final Provider<ReviewDb> db; + private final DeleteContent deleteContent; private final String path; @Inject - DeleteFile(ChangeEditUtil editUtil, - ChangeEditModifier editModifier, - PatchSetUtil psUtil, - Provider<ReviewDb> db, + DeleteFile(DeleteContent deleteContent, @Assisted String path) { - this.editUtil = editUtil; - this.editModifier = editModifier; - this.psUtil = psUtil; - this.db = db; + this.deleteContent = deleteContent; this.path = path; } @Override public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in) throws IOException, AuthException, ResourceConflictException, - OrmException, InvalidChangeOperationException, BadRequestException { - Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange()); - if (edit.isPresent()) { - // Edit is wiped out - editUtil.delete(edit.get()); - } else { - // Edit is created on top of current patch set by deleting path. - // Even if the latest patch set changed since the user triggered - // the operation, deleting the whole file is probably still what - // they intended. - editModifier.createEdit(rsrc.getChange(), - psUtil.current(db.get(), rsrc.getNotes())); - edit = editUtil.byChange(rsrc.getChange()); - editModifier.deleteFile(edit.get(), path); - } - return Response.none(); + OrmException { + return deleteContent.apply(rsrc.getControl(), path); } } @@ -330,48 +272,46 @@ public String newPath; } - private final Provider<ReviewDb> db; - private final ChangeEditUtil editUtil; private final ChangeEditModifier editModifier; - private final PatchSetUtil psUtil; + private final GitRepositoryManager repositoryManager; @Inject - Post(Provider<ReviewDb> db, - ChangeEditUtil editUtil, - ChangeEditModifier editModifier, - PatchSetUtil psUtil) { - this.db = db; - this.editUtil = editUtil; + Post(ChangeEditModifier editModifier, + GitRepositoryManager repositoryManager) { this.editModifier = editModifier; - this.psUtil = psUtil; + this.repositoryManager = repositoryManager; } @Override public Response<?> apply(ChangeResource resource, Post.Input input) - throws AuthException, InvalidChangeOperationException, IOException, - ResourceConflictException, OrmException { - Optional<ChangeEdit> edit = editUtil.byChange(resource.getChange()); - if (!edit.isPresent()) { - edit = createEdit(resource); - } - - if (input != null) { - if (!Strings.isNullOrEmpty(input.restorePath)) { - editModifier.restoreFile(edit.get(), input.restorePath); - } else if (!Strings.isNullOrEmpty(input.oldPath) - && !Strings.isNullOrEmpty(input.newPath)) { - editModifier.renameFile(edit.get(), input.oldPath, input.newPath); + throws AuthException, IOException, ResourceConflictException, + OrmException { + Project.NameKey project = resource.getProject(); + try (Repository repository = repositoryManager.openRepository(project)) { + ChangeControl changeControl = resource.getControl(); + if (isRestoreFile(input)) { + editModifier.restoreFile(repository, changeControl, + input.restorePath); + } else if (isRenameFile(input)) { + editModifier.renameFile(repository, changeControl, input.oldPath, + input.newPath); + } else { + editModifier.createEdit(repository, changeControl); } + } catch (InvalidChangeOperationException e) { + throw new ResourceConflictException(e.getMessage()); } return Response.none(); } - private Optional<ChangeEdit> createEdit(ChangeResource resource) - throws AuthException, IOException, ResourceConflictException, - OrmException { - editModifier.createEdit(resource.getChange(), - psUtil.current(db.get(), resource.getNotes())); - return editUtil.byChange(resource.getChange()); + private static boolean isRestoreFile(Input input) { + return input != null && !Strings.isNullOrEmpty(input.restorePath); + } + + private static boolean isRenameFile(Input input) { + return input != null + && !Strings.isNullOrEmpty(input.oldPath) + && !Strings.isNullOrEmpty(input.newPath); } } @@ -388,26 +328,33 @@ } private final ChangeEditModifier editModifier; + private final GitRepositoryManager repositoryManager; @Inject - Put(ChangeEditModifier editModifier) { + Put(ChangeEditModifier editModifier, + GitRepositoryManager repositoryManager) { this.editModifier = editModifier; + this.repositoryManager = repositoryManager; } @Override public Response<?> apply(ChangeEditResource rsrc, Input input) - throws AuthException, ResourceConflictException { - String path = rsrc.getPath(); + throws AuthException, ResourceConflictException, IOException, + OrmException { + return apply(rsrc.getControl(), rsrc.getPath(), input.content); + } + + public Response<?> apply(ChangeControl changeControl, String path, + RawInput newContent) throws ResourceConflictException, AuthException, + IOException, OrmException { if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') { throw new ResourceConflictException("Invalid path: " + path); } - try { - editModifier.modifyFile( - rsrc.getChangeEdit(), - rsrc.getPath(), - input.content); - } catch (InvalidChangeOperationException | IOException e) { + Project.NameKey project = changeControl.getChange().getProject(); + try (Repository repository = repositoryManager.openRepository(project)) { + editModifier.modifyFile(repository, changeControl, path, newContent); + } catch (InvalidChangeOperationException e) { throw new ResourceConflictException(e.getMessage()); } return Response.none(); @@ -427,18 +374,29 @@ } private final ChangeEditModifier editModifier; + private final GitRepositoryManager repositoryManager; @Inject - DeleteContent(ChangeEditModifier editModifier) { + DeleteContent(ChangeEditModifier editModifier, + GitRepositoryManager repositoryManager) { this.editModifier = editModifier; + this.repositoryManager = repositoryManager; } @Override public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input) - throws AuthException, ResourceConflictException { - try { - editModifier.deleteFile(rsrc.getChangeEdit(), rsrc.getPath()); - } catch (InvalidChangeOperationException | IOException e) { + throws AuthException, ResourceConflictException, OrmException, + IOException { + return apply(rsrc.getControl(), rsrc.getPath()); + } + + public Response<?> apply(ChangeControl changeControl, String filePath) + throws AuthException, IOException, OrmException, + ResourceConflictException { + Project.NameKey project = changeControl.getChange().getProject(); + try (Repository repository = repositoryManager.openRepository(project)) { + editModifier.deleteFile(repository, changeControl, filePath); + } catch (InvalidChangeOperationException e) { throw new ResourceConflictException(e.getMessage()); } return Response.none(); @@ -459,7 +417,7 @@ } @Override - public Response<?> apply(ChangeEditResource rsrc) + public Response<BinaryResult> apply(ChangeEditResource rsrc) throws IOException { try { ChangeEdit edit = rsrc.getChangeEdit(); @@ -490,7 +448,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 +457,7 @@ 0, edit.getRefName(), rsrc.getPath()); - r.webLinks = links.isEmpty() ? null : links.toList(); + r.webLinks = links.isEmpty() ? null : links; return r; } @@ -516,41 +474,30 @@ public String message; } - private final Provider<ReviewDb> db; private final ChangeEditModifier editModifier; - private final ChangeEditUtil editUtil; - private final PatchSetUtil psUtil; + private final GitRepositoryManager repositoryManager; @Inject - EditMessage(Provider<ReviewDb> db, - ChangeEditModifier editModifier, - ChangeEditUtil editUtil, - PatchSetUtil psUtil) { - this.db = db; + EditMessage(ChangeEditModifier editModifier, + GitRepositoryManager repositoryManager) { this.editModifier = editModifier; - this.editUtil = editUtil; - this.psUtil = psUtil; + this.repositoryManager = repositoryManager; } @Override public Object apply(ChangeResource rsrc, Input input) throws AuthException, - IOException, InvalidChangeOperationException, BadRequestException, - ResourceConflictException, OrmException { - Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange()); - if (!edit.isPresent()) { - editModifier.createEdit(rsrc.getChange(), - psUtil.current(db.get(), rsrc.getNotes())); - edit = editUtil.byChange(rsrc.getChange()); - } - + IOException, BadRequestException, ResourceConflictException, + OrmException { if (input == null || Strings.isNullOrEmpty(input.message)) { throw new BadRequestException("commit message must be provided"); } - try { - editModifier.modifyMessage(edit.get(), input.message); - } catch (UnchangedCommitMessageException ucm) { - throw new ResourceConflictException(ucm.getMessage()); + Project.NameKey project = rsrc.getProject(); + try (Repository repository = repositoryManager.openRepository(project)) { + ChangeControl changeControl = rsrc.getControl(); + editModifier.modifyMessage(repository, changeControl, input.message); + } catch (UnchangedCommitMessageException e) { + throw new ResourceConflictException(e.getMessage()); } return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.java new file mode 100644 index 0000000..77e6942 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeIncludedIn.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.change; + +import com.google.gerrit.extensions.api.changes.IncludedInInfo; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestReadView; +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.PatchSetUtil; +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.io.IOException; + +@Singleton +public class ChangeIncludedIn implements RestReadView<ChangeResource> { + private Provider<ReviewDb> db; + private PatchSetUtil psUtil; + private IncludedIn includedIn; + + @Inject + ChangeIncludedIn(Provider<ReviewDb> db, + PatchSetUtil psUtil, + IncludedIn includedIn) { + this.db = db; + this.psUtil = psUtil; + this.includedIn = includedIn; + } + + @Override + public IncludedInInfo apply(ChangeResource rsrc) + throws RestApiException, OrmException, IOException { + ChangeControl ctl = rsrc.getControl(); + PatchSet ps = psUtil.current(db.get(), rsrc.getNotes()); + Project.NameKey project = ctl.getProject().getNameKey(); + return includedIn.apply(project, ps.getRevision().get()); + } +}
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..1956643 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.ListMultimap; 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,11 +50,11 @@ 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; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.RefControl; @@ -62,7 +65,6 @@ import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.ChangeIdUtil; @@ -86,6 +88,7 @@ LoggerFactory.getLogger(ChangeInserter.class); private final ProjectControl.GenericFactory projectControlFactory; + private final IdentifiedUser.GenericFactory userFactory; private final ChangeControl.GenericFactory changeControlFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; @@ -106,10 +109,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 ListMultimap<RecipientType, Account.Id> accountsToNotify = + ImmutableListMultimap.of(); private Set<Account.Id> reviewers; private Set<Account.Id> extraCC; private Map<String, Short> approvals; @@ -128,6 +134,7 @@ @Inject ChangeInserter(ProjectControl.GenericFactory projectControlFactory, + IdentifiedUser.GenericFactory userFactory, ChangeControl.GenericFactory changeControlFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, @@ -142,6 +149,7 @@ @Assisted RevCommit commit, @Assisted String refName) { this.projectControlFactory = projectControlFactory; + this.userFactory = userFactory; this.changeControlFactory = changeControlFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; @@ -216,6 +224,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 +239,12 @@ return this; } + public ChangeInserter setAccountsToNotify( + ListMultimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); + return this; + } + public ChangeInserter setReviewers(Set<Account.Id> reviewers) { this.reviewers = reviewers; return this; @@ -333,6 +352,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 +360,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 +373,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( + 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 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) { + public void postUpdate(Context ctx) throws OrmException { + if (sendMail + && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) { Runnable sender = new Runnable() { @Override public void run() { @@ -380,6 +422,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 +483,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 +493,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..f4d7f8a 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,28 @@ 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 +65,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 +77,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 +92,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 +106,14 @@ 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 +123,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 +141,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 +195,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 +225,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 +249,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; @@ -236,11 +275,11 @@ } public ChangeInfo format(Project.NameKey project, Change.Id id) - throws OrmException, NoSuchChangeException { + throws OrmException { ChangeNotes notes; try { notes = notesFactory.createChecked(db.get(), project, id); - } catch (OrmException | NoSuchChangeException e) { + } catch (OrmException e) { if (!has(CHECK)) { throw e; } @@ -250,7 +289,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 +306,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 +321,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 +350,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 +379,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 +459,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 +511,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 +523,8 @@ out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet())); } + + out.removableReviewers = removableReviewers(ctl, out); } if (has(REVIEWER_UPDATES)) { @@ -495,8 +546,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 +582,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 +605,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 +703,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 +725,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 +735,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 +750,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 +760,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 +819,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(); + SetMultimap<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 +833,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 +893,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 +910,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 +944,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 +958,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 +991,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 +1030,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 +1057,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 +1086,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 +1150,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 +1172,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 +1187,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 +1203,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 +1241,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 +1252,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 +1330,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 1d1b27b..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); @@ -247,6 +255,10 @@ // it was a rework. } return ChangeKind.REWORK; + } finally { + if (close) { + repo.close(); + } } } @@ -304,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 @@ -312,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; } } @@ -337,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) { @@ -375,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); @@ -394,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; @@ -403,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..f09e268 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, false); 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( + 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..4b8d695 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.common.collect.SetMultimap; 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; @@ -56,7 +54,6 @@ import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; import com.google.gerrit.server.project.ChangeControl; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -129,7 +126,7 @@ private RevWalk rw; private RevCommit tip; - private Multimap<ObjectId, PatchSet> patchSetsBySha; + private SetMultimap<ObjectId, PatchSet> patchSetsBySha; private PatchSet currPs; private RevCommit currPsCommit; @@ -254,13 +251,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 +312,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))); } } @@ -414,7 +408,7 @@ if (!c.getDest().equals(change().getDest())) { continue; } - } catch (OrmException | NoSuchChangeException e) { + } catch (OrmException e) { warn(e); // Include this patch set; should cause an error below, which is good. } @@ -530,7 +524,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")); @@ -541,8 +535,8 @@ db.get(), inserter.getChange(), ctl.getUser()); insertPatchSetProblem.status = Status.FIXED; insertPatchSetProblem.outcome = "Inserted as patch set " + psId.get(); - } catch (OrmException | IOException | NoSuchChangeException - | UpdateException | RestApiException e) { + } catch (OrmException | IOException | UpdateException + | RestApiException e) { warn(e); for (ProblemInfo pi : currProblems) { pi.status = Status.FIX_FAILED; @@ -663,7 +657,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/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java index f5ddfe5..62d9b53 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -106,6 +106,7 @@ private final boolean allowDrafts; private final MergeUtil.Factory mergeUtilFactory; private final SubmitType submitType; + private final NotifyUtil notifyUtil; @Inject CreateChange(@AnonymousCowardName String anonymousCowardName, @@ -122,7 +123,8 @@ BatchUpdate.Factory updateFactory, PatchSetUtil psUtil, @GerritServerConfig Config config, - MergeUtil.Factory mergeUtilFactory) { + MergeUtil.Factory mergeUtilFactory, + NotifyUtil notifyUtil) { this.anonymousCowardName = anonymousCowardName; this.db = db; this.gitManager = gitManager; @@ -140,6 +142,7 @@ this.submitType = config .getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY); this.mergeUtilFactory = mergeUtilFactory; + this.notifyUtil = notifyUtil; } @Override @@ -269,6 +272,8 @@ ins.setTopic(topic); ins.setDraft(input.status == ChangeStatus.DRAFT); ins.setGroups(groups); + ins.setNotify(input.notify); + ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); try (BatchUpdate bu = updateFactory.create( db.get(), project, me, now)) { bu.setRepository(git, rw, oi);
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..5a7b756 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; @@ -26,13 +25,13 @@ 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.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 +49,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 +57,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 +86,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 +95,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; @@ -105,28 +104,25 @@ @Override public boolean updateChange(ChangeContext ctx) - throws ResourceNotFoundException, OrmException { + throws ResourceNotFoundException, OrmException, + UnprocessableEntityException { PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); 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); + String parentUuid = Url.decode(in.inReplyTo); + + comment = commentsUtil.newComment( + ctx, in.path, ps.getId(), in.side(), in.message.trim(), + in.unresolved, parentUuid); + 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..ceebce9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -0,0 +1,211 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 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..4d5b739 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -0,0 +1,131 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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.IdentifiedUser; +import com.google.gerrit.server.account.AccountLoader; +import com.google.gerrit.server.change.DeleteAssignee.Input; +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 AssigneeChanged assigneeChanged; + private final IdentifiedUser.GenericFactory userFactory; + private final AccountLoader.Factory accountLoaderFactory; + + @Inject + DeleteAssignee(BatchUpdate.Factory batchUpdateFactory, + ChangeMessagesUtil cmUtil, + Provider<ReviewDb> db, + AssigneeChanged assigneeChanged, + IdentifiedUser.GenericFactory userFactory, + AccountLoader.Factory accountLoaderFactory) { + this.batchUpdateFactory = batchUpdateFactory; + this.cmUtil = cmUtil; + this.db = db; + this.assigneeChanged = assigneeChanged; + this.userFactory = userFactory; + this.accountLoaderFactory = accountLoaderFactory; + } + + @Override + public Response<AccountInfo> apply(ChangeResource rsrc, Input input) + throws RestApiException, UpdateException, OrmException { + 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.Id deletedAssignee = op.getDeletedAssignee(); + return deletedAssignee == null + ? Response.none() + : Response.ok(accountLoaderFactory.create(true) + .fillOne(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; + } + IdentifiedUser deletedAssigneeUser = + userFactory.create(currentAssigneeId); + deletedAssignee = deletedAssigneeUser.getAccount(); + // noteDb + update.removeAssignee(); + // reviewDb + change.setAssignee(null); + addMessage(ctx, update, deletedAssigneeUser); + return true; + } + + public Account.Id getDeletedAssignee() { + return deletedAssignee != null ? deletedAssignee.getId() : null; + } + + private void addMessage(BatchUpdate.ChangeContext ctx, ChangeUpdate update, + IdentifiedUser deletedAssignee) throws OrmException { + ChangeMessage cmsg = ChangeMessagesUtil.newMessage( + ctx, "Assignee deleted: " + deletedAssignee.getNameEmail(), + 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/DeleteChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java new file mode 100644 index 0000000..18d7074 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -0,0 +1,92 @@ +// 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.change; + +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.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; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.Config; + +@Singleton +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<DeleteChangeOp> opProvider; + private final boolean allowDrafts; + + @Inject + public DeleteChange(Provider<ReviewDb> db, + BatchUpdate.Factory updateFactory, + Provider<DeleteChangeOp> opProvider, + @GerritServerConfig Config cfg) { + this.db = db; + this.updateFactory = updateFactory; + this.opProvider = opProvider; + this.allowDrafts = DeleteChangeOp.allowDrafts(cfg); + } + + @Override + public Response<?> apply(ChangeResource rsrc, Input input) + throws RestApiException, UpdateException { + try (BatchUpdate bu = updateFactory.create( + db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { + Change.Id id = rsrc.getChange().getId(); + bu.setOrder(BatchUpdate.Order.DB_BEFORE_REPO); + bu.addOp(id, opProvider.get()); + bu.execute(); + } + return Response.none(); + } + + @Override + public UiAction.Description getDescription(ChangeResource rsrc) { + try { + Change.Status status = rsrc.getChange().getStatus(); + ChangeControl changeControl = rsrc.getControl(); + boolean visible = isActionAllowed(changeControl, status) + && changeControl.canDelete(db.get(), status); + return new UiAction.Description() + .setLabel("Delete") + .setTitle("Delete change " + rsrc.getId()) + .setVisible(visible); + } catch (OrmException e) { + throw new IllegalStateException(e); + } + } + + 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..d1f7cac --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -0,0 +1,188 @@ +// 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.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.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 { + // 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/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java deleted file mode 100644 index a125272..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java +++ /dev/null
@@ -1,84 +0,0 @@ -// Copyright (C) 2013 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.change; - -import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.extensions.restapi.Response; -import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.extensions.webui.UiAction; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Change.Status; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.change.DeleteDraftChange.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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; - -import org.eclipse.jgit.lib.Config; - -@Singleton -public class DeleteDraftChange 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 boolean allowDrafts; - - @Inject - public DeleteDraftChange(Provider<ReviewDb> db, - BatchUpdate.Factory updateFactory, - Provider<DeleteDraftChangeOp> opProvider, - @GerritServerConfig Config cfg) { - this.db = db; - this.updateFactory = updateFactory; - this.opProvider = opProvider; - this.allowDrafts = DeleteDraftChangeOp.allowDrafts(cfg); - } - - @Override - public Response<?> apply(ChangeResource rsrc, Input input) - throws RestApiException, UpdateException { - try (BatchUpdate bu = updateFactory.create( - db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { - Change.Id id = rsrc.getChange().getId(); - bu.setOrder(BatchUpdate.Order.DB_BEFORE_REPO); - bu.addOp(id, opProvider.get()); - bu.execute(); - } - return Response.none(); - } - - @Override - public UiAction.Description getDescription(ChangeResource rsrc) { - try { - return new UiAction.Description() - .setLabel("Delete") - .setTitle("Delete draft change " + rsrc.getId()) - .setVisible(allowDrafts - && rsrc.getChange().getStatus() == Status.DRAFT - && rsrc.getControl().canDeleteDraft(db.get())); - } catch (OrmException e) { - throw new IllegalStateException(e); - } - } -}
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..e473e39 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
@@ -59,7 +59,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 +68,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 +97,7 @@ private Collection<PatchSet> patchSetsBeforeDeletion; private PatchSet patchSet; - private DeleteDraftChangeOp deleteChangeOp; + private DeleteChangeOp deleteChangeOp; private Op(PatchSet.Id psId) { this.psId = psId; @@ -116,7 +116,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,8 +146,8 @@ 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()); + // 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)); @@ -195,7 +195,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..3c5aeb5 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,12 @@ package com.google.gerrit.server.change; -import com.google.common.base.Predicate; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; +import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; @@ -35,18 +35,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; @@ -57,18 +56,17 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; 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 +77,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 +89,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 +101,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,15 +127,16 @@ private class Op extends BatchUpdate.Op { private final Account reviewer; + private final DeleteReviewerInput input; ChangeMessage changeMessage; Change currChange; PatchSet currPs; - List<PatchSetApproval> del = new ArrayList<>(); 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 +157,64 @@ } StringBuilder msg = new StringBuilder(); + msg.append("Removed reviewer " + reviewer.getFullName()); + StringBuilder removedVotesMsg = new StringBuilder(); + removedVotesMsg.append(" with the following votes:\n\n"); + List<PatchSetApproval> del = new ArrayList<>(); + boolean votesRemoved = false; for (PatchSetApproval a : approvals(ctx, reviewerId)) { if (ctx.getControl().canRemoveReviewer(a)) { del.add(a); if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) { oldApprovals.put(a.getLabel(), a.getValue()); - if (msg.length() == 0) { - msg.append("Removed reviewer ").append(reviewer.getFullName()) - .append(" with the following votes:\n\n"); - } - msg.append("* ").append(a.getLabel()) + removedVotesMsg.append("* ").append(a.getLabel()) .append(formatLabelValue(a.getValue())).append(" by ") .append(userFactory.create(a.getAccountId()).getNameEmail()) .append("\n"); + votesRemoved = true; } } else { throw new AuthException("delete reviewer not permitted"); } } + if (votesRemoved) { + msg.append(removedVotesMsg); + } else { + msg.append("."); + } ctx.getDb().patchSetApprovals().delete(del); ChangeUpdate update = ctx.getUpdate(currPs.getId()); update.removeReviewer(reviewerId); - if (msg.length() > 0) { - changeMessage = new ChangeMessage( - new ChangeMessage.Key(currChange.getId(), - ChangeUtil.messageUUID(ctx.getDb())), - ctx.getAccountId(), ctx.getWhen(), currPs.getId()); - changeMessage.setMessage(msg.toString()); - cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); - } + changeMessage = ChangeMessagesUtil.newMessage(ctx, msg.toString(), + 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, 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 +230,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,27 +239,24 @@ } return Short.toString(value); } - } - 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()); + private void emailReviewers(Project.NameKey projectName, Change change, + ChangeMessage changeMessage) { + Account.Id userId = user.get().getAccountId(); + if (userId.equals(reviewer.getId())) { + // The user knows they removed themselves, don't bother emailing them. + return; } - } - if (!toMail.isEmpty()) { try { DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId()); cm.setFrom(userId); - cm.addReviewers(toMail); + cm.addReviewers(Collections.singleton(reviewer.getId())); cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn()); + cm.setNotify(input.notify); + cm.setAccountsToNotify( + notifyUtil.resolveAccounts(input.notifyDetails)); cm.send(); } catch (Exception err) { log.error("Cannot email update for change " + change.getId(), err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java index f1bdba5..9a0807d2 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,13 +14,15 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; 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.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; @@ -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,10 @@ 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.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 +72,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 +82,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 +92,7 @@ this.userFactory = userFactory; this.voteDeleted = voteDeleted; this.deleteVoteSenderFactory = deleteVoteSenderFactory; + this.notifyUtil = notifyUtil; } @Override @@ -103,6 +109,13 @@ } ReviewerResource r = rsrc.getReviewer(); Change change = r.getChange(); + + if (r.getRevisionResource() != null + && !r.getRevisionResource().isCurrent()) { + throw new MethodNotAllowedException( + "Cannot delete vote on non-current patch set"); + } + try (BatchUpdate bu = batchUpdateFactory.create(db.get(), change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) { bu.addOp(change.getId(), @@ -137,64 +150,63 @@ PatchSet.Id psId = change.currentPatchSetId(); ps = psUtil.current(db.get(), ctl.getNotes()); - PatchSetApproval psa = null; - StringBuilder msg = new StringBuilder(); - - // get all of the current approvals + boolean found = false; LabelTypes labelTypes = ctx.getControl().getLabelTypes(); - Map<String, Short> currentApprovals = new HashMap<>(); - for (LabelType lt : labelTypes.getLabelTypes()) { - currentApprovals.put(lt.getName(), (short) 0); - for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (lt.getLabelId().equals(a.getLabelId())) { - currentApprovals.put(lt.getName(), a.getValue()); - } - } - } - // removing votes so we need to determine the new set of approval scores - newApprovals.putAll(currentApprovals); + for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (ctl.canRemoveReviewer(a)) { - if (a.getLabel().equals(label)) { - // set the approval to 0 if vote is being removed - newApprovals.put(a.getLabel(), (short) 0); - // set old value only if the vote changed - oldApprovals.put(a.getLabel(), a.getValue()); - msg.append("Removed ") - .append(a.getLabel()).append(formatLabelValue(a.getValue())) - .append(" by ").append(userFactory.create(a.getAccountId()) - .getNameEmail()) - .append("\n"); - psa = a; - a.setValue((short)0); - ctx.getUpdate(psId).removeApprovalFor(a.getAccountId(), label); - break; - } - } else { + ctx.getDb(), ctl, psId, accountId)) { + if (labelTypes.byLabel(a.getLabelId()) == null) { + continue; // Ignore undefined labels. + } else if (!a.getLabel().equals(label)) { + // Populate map for non-matching labels, needed by VoteDeleted. + newApprovals.put(a.getLabel(), a.getValue()); + continue; + } else if (!ctl.canRemoveReviewer(a)) { throw new AuthException("delete vote not permitted"); } + // Set the approval to 0 if vote is being removed. + newApprovals.put(a.getLabel(), (short) 0); + found = true; + + // Set old value, as required by VoteDeleted. + oldApprovals.put(a.getLabel(), a.getValue()); + break; } - if (psa == null) { + if (!found) { throw new ResourceNotFoundException(); } - ctx.getDb().patchSetApprovals().update(Collections.singleton(psa)); - if (msg.length() > 0) { - changeMessage = - new ChangeMessage(new ChangeMessage.Key(change.getId(), - ChangeUtil.messageUUID(ctx.getDb())), - ctx.getAccountId(), - ctx.getWhen(), - change.currentPatchSetId()); - changeMessage.setMessage(msg.toString()); - cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), - changeMessage); - } + ctx.getUpdate(psId).removeApprovalFor(accountId, label); + ctx.getDb().patchSetApprovals().upsert( + Collections.singleton(deletedApproval(ctx))); + + StringBuilder msg = new StringBuilder(); + msg.append("Removed "); + LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label))); + msg.append(" by ") + .append(userFactory.create(accountId).getNameEmail()) + .append("\n"); + changeMessage = ChangeMessagesUtil.newMessage(ctx, msg.toString(), + 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 +214,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 +234,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..efa853a 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.ListMultimap; +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, + ListMultimap<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 ListMultimap<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 ListMultimap<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..ea81ad3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetAssignee.java
@@ -0,0 +1,47 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.AccountLoader; +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 AccountLoader.Factory accountLoaderFactory; + + @Inject + GetAssignee(AccountLoader.Factory accountLoaderFactory) { + this.accountLoaderFactory = accountLoaderFactory; + } + + @Override + public Response<AccountInfo> apply(ChangeResource rsrc) throws OrmException { + Optional<Account.Id> assignee = + Optional.ofNullable(rsrc.getChange().getAssignee()); + if (assignee.isPresent()) { + return Response.ok( + accountLoaderFactory.create(true).fillOne(assignee.get())); + } + 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..d8216f0 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(), @@ -76,7 +84,7 @@ } private String getMessage(ChangeNotes notes) - throws NoSuchChangeException, OrmException, IOException { + throws OrmException, IOException { Change.Id changeId = notes.getChangeId(); PatchSet ps = psUtil.current(db.get(), notes); if (ps == null) { @@ -92,4 +100,22 @@ throw new NoSuchChangeException(changeId, e); } } + + private byte[] getMergeList(ChangeNotes notes) + throws 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 6e284bb..c688f3a 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; @@ -206,7 +205,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, @@ -215,7 +214,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()) { @@ -280,9 +279,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..c37efed --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPastAssignees.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.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.AccountLoader; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Singleton +public class GetPastAssignees implements RestReadView<ChangeResource> { + private final AccountLoader.Factory accountLoaderFactory; + + @Inject + GetPastAssignees(AccountLoader.Factory accountLoaderFactory) { + this.accountLoaderFactory = accountLoaderFactory; + } + + @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()); + } + + AccountLoader accountLoader = accountLoaderFactory.create(true); + List<AccountInfo> infos = + pastAssignees.stream().map(accountLoader::get).collect(toList()); + accountLoader.fill(); + return Response.ok(infos); + } +}
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..0c2d079 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,22 +14,17 @@ package com.google.gerrit.server.change; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.extensions.api.changes.IncludedInInfo; import com.google.gerrit.extensions.config.ExternalIncludedIn; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.RestReadView; -import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.git.GitRepositoryManager; -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.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -40,40 +35,27 @@ import org.eclipse.jgit.revwalk.RevWalk; import java.io.IOException; -import java.util.Collection; -import java.util.Map; @Singleton -class IncludedIn implements RestReadView<ChangeResource> { - - private final Provider<ReviewDb> db; +public class IncludedIn { private final GitRepositoryManager repoManager; - private final PatchSetUtil psUtil; - private final DynamicSet<ExternalIncludedIn> includedIn; + private final DynamicSet<ExternalIncludedIn> externalIncludedIn; @Inject - IncludedIn(Provider<ReviewDb> db, - GitRepositoryManager repoManager, - PatchSetUtil psUtil, - DynamicSet<ExternalIncludedIn> includedIn) { - this.db = db; + IncludedIn(GitRepositoryManager repoManager, + DynamicSet<ExternalIncludedIn> externalIncludedIn) { this.repoManager = repoManager; - this.psUtil = psUtil; - this.includedIn = includedIn; + this.externalIncludedIn = externalIncludedIn; } - @Override - public IncludedInInfo apply(ChangeResource rsrc) throws BadRequestException, - ResourceConflictException, OrmException, IOException { - ChangeControl ctl = rsrc.getControl(); - PatchSet ps = psUtil.current(db.get(), rsrc.getNotes()); - Project.NameKey project = ctl.getProject().getNameKey(); + public IncludedInInfo apply(Project.NameKey project, String revisionId) + throws RestApiException, IOException { try (Repository r = repoManager.openRepository(project); RevWalk rw = new RevWalk(r)) { rw.setRetainBody(false); RevCommit rev; try { - rev = rw.parseCommit(ObjectId.fromString(ps.getRevision().get())); + rev = rw.parseCommit(ObjectId.fromString(revisionId)); } catch (IncorrectObjectTypeException err) { throw new BadRequestException(err.getMessage()); } catch (MissingObjectException err) { @@ -81,28 +63,17 @@ } IncludedInResolver.Result d = IncludedInResolver.resolve(r, rw, rev); - Multimap<String, String> external = ArrayListMultimap.create(); - for (ExternalIncludedIn ext : includedIn) { - Multimap<String, String> extIncludedIns = ext.getIncludedIn( + ListMultimap<String, String> external = + MultimapBuilder.hashKeys().arrayListValues().build(); + for (ExternalIncludedIn ext : externalIncludedIn) { + ListMultimap<String, String> extIncludedIns = ext.getIncludedIn( project.get(), rev.name(), d.getTags(), d.getBranches()); if (extIncludedIns != null) { external.putAll(extIncludedIns); } } - return new IncludedInInfo(d, + return new IncludedInInfo(d.getBranches(), d.getTags(), (!external.isEmpty() ? external.asMap() : null)); } } - - static class IncludedInInfo { - Collection<String> branches; - Collection<String> tags; - Map<String, Collection<String>> external; - - IncludedInInfo(IncludedInResolver.Result in, Map<String, Collection<String>> e) { - branches = in.getBranches(); - tags = in.getTags(); - external = e; - } - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java index 0c3ecd9..7843b15 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -15,8 +15,8 @@ package com.google.gerrit.server.change; import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -31,11 +31,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -76,7 +76,7 @@ private final RevCommit target; private final RevFlag containsTarget; - private Multimap<RevCommit, String> commitToRef; + private ListMultimap<RevCommit, String> commitToRef; private List<RevCommit> tipsByCommitTime; private IncludedInResolver(Repository repo, RevWalk rw, RevCommit target, @@ -108,8 +108,8 @@ private boolean includedInOne(final Collection<Ref> refs) throws IOException { parseCommits(refs); - List<RevCommit> before = new LinkedList<>(); - List<RevCommit> after = new LinkedList<>(); + List<RevCommit> before = new ArrayList<>(); + List<RevCommit> after = new ArrayList<>(); partition(before, after); rw.reset(); // It is highly likely that the target is reachable from the "after" set
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..b34404f --- /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. + */ + 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(); + } + + static 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/ListChangeRobotComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.java new file mode 100644 index 0000000..abfe869 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeRobotComments.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.gerrit.extensions.common.RobotCommentInfo; +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.CommentsUtil; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import java.util.List; +import java.util.Map; + +public class ListChangeRobotComments implements RestReadView<ChangeResource> { + private final Provider<ReviewDb> db; + private final ChangeData.Factory changeDataFactory; + private final Provider<CommentJson> commentJson; + private final CommentsUtil commentsUtil; + + @Inject + ListChangeRobotComments(Provider<ReviewDb> db, + ChangeData.Factory changeDataFactory, + Provider<CommentJson> commentJson, + CommentsUtil commentsUtil) { + this.db = db; + this.changeDataFactory = changeDataFactory; + this.commentJson = commentJson; + this.commentsUtil = commentsUtil; + } + + @Override + public Map<String, List<RobotCommentInfo>> apply( + ChangeResource rsrc) throws AuthException, OrmException { + ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl()); + return commentJson.get() + .setFillAccounts(true) + .setFillPatchSet(true) + .newRobotCommentFormatter() + .format(commentsUtil.robotCommentsByChange(cd.notes())); + } +} +
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/ListRevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java new file mode 100644 index 0000000..5d9819e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.api.changes.ReviewerInfo; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Singleton +class ListRevisionReviewers implements RestReadView<RevisionResource> { + private final Provider<ReviewDb> dbProvider; + private final ApprovalsUtil approvalsUtil; + private final ReviewerJson json; + private final ReviewerResource.Factory resourceFactory; + + @Inject + ListRevisionReviewers(Provider<ReviewDb> dbProvider, + ApprovalsUtil approvalsUtil, + ReviewerResource.Factory resourceFactory, + ReviewerJson json) { + this.dbProvider = dbProvider; + this.approvalsUtil = approvalsUtil; + this.resourceFactory = resourceFactory; + this.json = json; + } + + @Override + public List<ReviewerInfo> apply(RevisionResource rsrc) throws OrmException, + MethodNotAllowedException { + if (!rsrc.isCurrent()) { + throw new MethodNotAllowedException( + "Cannot list reviewers on non-current patch set"); + } + + Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>(); + ReviewDb db = dbProvider.get(); + for (Account.Id accountId + : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) { + if (!reviewers.containsKey(accountId)) { + reviewers.put(accountId, resourceFactory.create(rsrc, accountId)); + } + } + return json.format(reviewers.values()); + } +}
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..aca6ef1 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; @@ -35,13 +36,16 @@ bind(ChangesCollection.class); bind(Revisions.class); bind(Reviewers.class); + bind(RevisionReviewers.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,17 +54,23 @@ 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, "in").to(ChangeIncludedIn.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, "robotcomments").to(ListChangeRobotComments.class); get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class); get(CHANGE_KIND, "check").to(Check.class); 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 +88,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 +103,19 @@ 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, "reviewers").to(RevisionReviewers.class); child(REVISION_KIND, "drafts").to(DraftComments.class); put(REVISION_KIND, "drafts").to(CreateDraftComment.class); @@ -109,6 +126,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 +156,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..e5633cd --- /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.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +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 ListMultimap<RecipientType, Account.Id> resolveAccounts( + @Nullable Map<RecipientType, NotifyInfo> notifyDetails) + throws OrmException, BadRequestException { + if (isNullOrEmpty(notifyDetails)) { + return ImmutableListMultimap.of(); + } + + ListMultimap<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 = MultimapBuilder.hashKeys().arrayListValues().build(); + } + 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..c9b4df2 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.ListMultimap; 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,17 @@ 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 checkAddPatchSetPermission = true; private boolean draft; private List<String> groups = Collections.emptyList(); private boolean fireRevisionCreated = true; - private boolean sendMail = true; + private NotifyHandling notify = NotifyHandling.ALL; + private ListMultimap<RecipientType, Account.Id> accountsToNotify = + ImmutableListMultimap.of(); private boolean allowClosed; private boolean copyApprovals = true; @@ -144,8 +147,8 @@ return this; } - public PatchSetInserter setSshInfo(SshInfo sshInfo) { - this.sshInfo = sshInfo; + public PatchSetInserter setDescription(String description) { + this.description = description; return this; } @@ -154,6 +157,12 @@ return this; } + public PatchSetInserter setCheckAddPatchSetPermission( + boolean checkAddPatchSetPermission) { + this.checkAddPatchSetPermission = checkAddPatchSetPermission; + return this; + } + public PatchSetInserter setDraft(boolean draft) { this.draft = draft; return this; @@ -170,8 +179,14 @@ return this; } - public PatchSetInserter setSendMail(boolean sendMail) { - this.sendMail = sendMail; + public PatchSetInserter setNotify(NotifyHandling notify) { + this.notify = notify; + return this; + } + + public PatchSetInserter setAccountsToNotify( + ListMultimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); return this; } @@ -198,7 +213,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 +242,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( + patchSet.getId(), ctx.getUser(), ctx.getWhen(), message, + ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); changeMessage.setMessage(message); } @@ -257,7 +271,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 +280,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 +289,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())) { + if (checkAddPatchSetPermission && !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 +316,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..aaea82c 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,22 +14,23 @@ 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.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; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; 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 +42,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 +91,7 @@ import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.query.change.ChangeData; @@ -97,7 +114,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 +128,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 +145,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 +167,8 @@ this.email = email; this.commentAdded = commentAdded; this.postReviewers = postReviewers; + this.migration = migration; + this.notifyUtil = notifyUtil; } @Override @@ -162,6 +187,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 +196,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; } + ListMultimap<RecipientType, Account.Id> accountsToNotify = + notifyUtil.resolveAccounts(input.notifyDetails); + Map<String, AddReviewerResult> reviewerJsonResults = null; List<PostReviewers.Addition> reviewerResults = Lists.newArrayList(); boolean hasError = false; @@ -181,8 +217,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 +244,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 && !input.labels.isEmpty(); + + 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, + ListMultimap<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 +325,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 +346,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 +363,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,135 +423,321 @@ } } - 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 ListMultimap<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, + ListMultimap<RecipientType, Account.Id> accountsToNotify, + List<PostReviewers.Addition> reviewerResults) { this.psId = psId; this.in = in; + this.accountsToNotify = checkNotNull(accountsToNotify); + this.reviewerResults = reviewerResults; } @Override public boolean updateChange(ChangeContext ctx) - throws OrmException, ResourceConflictException { + throws OrmException, ResourceConflictException, + UnprocessableEntityException { user = ctx.getIdentifiedUser(); notes = ctx.getNotes(); 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(), approvals, oldApprovals, ctx.getWhen()); } - private boolean insertComments(ChangeContext ctx) throws OrmException { + private boolean insertComments(ChangeContext ctx) + throws OrmException, UnprocessableEntityException { Map<String, List<CommentInput>> map = in.comments; if (map == null) { 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,177 @@ } } - 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, + c.unresolved, parent); + } 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)); + 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(robotCommentInput.fixSuggestions); + return robotComment; + } + + private List<FixSuggestion> createFixSuggestionsFromInput( + List<FixSuggestionInfo> fixSuggestionInfos) { + if (fixSuggestionInfos == null) { + return Collections.emptyList(); + } + + List<FixSuggestion> fixSuggestions = + new ArrayList<>(fixSuggestionInfos.size()); + for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) { + fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo)); + } + return fixSuggestions; + } + + private FixSuggestion createFixSuggestionFromInput( + FixSuggestionInfo fixSuggestionInfo) { + List<FixReplacement> fixReplacements = + toFixReplacements(fixSuggestionInfo.replacements); + String fixId = ChangeUtil.messageUuid(); + return new FixSuggestion(fixId, fixSuggestionInfo.description, + fixReplacements); + } + + private List<FixReplacement> toFixReplacements( + List<FixReplacementInfo> fixReplacementInfos) { + return fixReplacementInfos.stream() + .map(this::toFixReplacement) + .collect(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 +930,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 +955,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 +992,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 +1058,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 +1069,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,16 +1082,87 @@ } } - if ((!del.isEmpty() || !ups.isEmpty()) - && ctx.getChange().getStatus().isClosed()) { - throw new ResourceConflictException("change is closed"); + 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); 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"); + } + + // 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, Map<String, PatchSetApproval> current, List<PatchSetApproval> ups, List<PatchSetApproval> del) { @@ -703,12 +1171,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 +1218,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 +1233,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( + 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..116f84e 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.ListMultimap; import com.google.common.collect.Lists; 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, + ListMultimap<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, + ListMultimap<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 ListMultimap<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, + ListMultimap<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, + ListMultimap<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..9f61e65 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; @@ -30,12 +30,12 @@ import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditUtil; import com.google.gerrit.server.git.UpdateException; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; +import java.util.Optional; @Singleton public class PublishChangeEdit implements @@ -71,21 +71,22 @@ } @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) - throws NoSuchChangeException, IOException, OrmException, - RestApiException, UpdateException { + public Response<?> apply(ChangeResource rsrc, PublishChangeEditInput in) + throws IOException, OrmException, RestApiException, UpdateException { Capable r = rsrc.getControl().getProjectControl().canPushToAtLeastOneRef(); if (r != Capable.OK) { @@ -98,7 +99,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..5a9a1e5 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; @@ -235,7 +235,7 @@ } else { sendReplacePatchSet(ctx); } - } catch (EmailException | OrmException e) { + } catch (EmailException e) { log.error("Cannot send email for publishing draft " + psId, e); } } @@ -250,17 +250,14 @@ cm.send(); } - 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() + "."); + private void sendReplacePatchSet(Context ctx) throws EmailException { + ChangeMessage msg = ChangeMessagesUtil.newMessage( + 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..8afb0e6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -0,0 +1,106 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.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.AccountLoader; +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; + private final AccountLoader.Factory accountLoaderFactory; + + @Inject + PutAssignee(SetAssigneeOp.Factory assigneeFactory, + BatchUpdate.Factory batchUpdateFactory, + Provider<ReviewDb> db, + PostReviewers postReviewers, + AccountLoader.Factory accountLoaderFactory) { + this.assigneeFactory = assigneeFactory; + this.batchUpdateFactory = batchUpdateFactory; + this.db = db; + this.postReviewers = postReviewers; + this.accountLoaderFactory = accountLoaderFactory; + } + + @Override + public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input) + throws RestApiException, UpdateException, OrmException, IOException { + if (!rsrc.getControl().canEditAssignee()) { + throw new AuthException("Changing Assignee not permitted"); + } + if (input.assignee == null || input.assignee.trim().isEmpty()) { + 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( + accountLoaderFactory.create(true).fillOne(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..5480fb4 --- /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(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..46bf6a9 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,39 @@ 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; + } + if (in.unresolved != null) { + e.unresolved = in.unresolved; } 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..db125eb 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; @@ -26,18 +25,16 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestView; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.PatchSetUtil; -import com.google.gerrit.server.edit.ChangeEdit; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.edit.ChangeEditModifier; -import com.google.gerrit.server.edit.ChangeEditUtil; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.project.InvalidChangeOperationException; 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.Repository; + import java.io.IOException; @Singleton @@ -78,41 +75,26 @@ public static class Input { } + private final GitRepositoryManager repositoryManager; private final ChangeEditModifier editModifier; - private final ChangeEditUtil editUtil; - private final PatchSetUtil psUtil; - private final Provider<ReviewDb> db; @Inject - Rebase(ChangeEditModifier editModifier, - ChangeEditUtil editUtil, - PatchSetUtil psUtil, - Provider<ReviewDb> db) { + Rebase(GitRepositoryManager repositoryManager, + ChangeEditModifier editModifier) { + this.repositoryManager = repositoryManager; this.editModifier = editModifier; - this.editUtil = editUtil; - this.psUtil = psUtil; - this.db = db; } @Override public Response<?> apply(ChangeResource rsrc, Rebase.Input in) throws AuthException, ResourceConflictException, IOException, - InvalidChangeOperationException, OrmException { - Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange()); - if (!edit.isPresent()) { - throw new ResourceConflictException(String.format( - "no edit exists for change %s", - rsrc.getChange().getChangeId())); + OrmException { + Project.NameKey project = rsrc.getProject(); + try (Repository repository = repositoryManager.openRepository(project)) { + editModifier.rebaseEdit(repository, rsrc.getControl()); + } catch (InvalidChangeOperationException e) { + throw new ResourceConflictException(e.getMessage()); } - - PatchSet current = psUtil.current(db.get(), rsrc.getNotes()); - if (current.getId().equals(edit.get().getBasePatchSet().getId())) { - throw new ResourceConflictException(String.format( - "edit for change %s is already on latest patch set: %s", - rsrc.getChange().getChangeId(), - current.getId())); - } - editModifier.rebaseEdit(edit.get(), current); return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java index 8909e60..91734ff 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; @@ -65,8 +66,11 @@ private PersonIdent committerIdent; private boolean fireRevisionCreated = true; private CommitValidators.Policy validate; + private boolean checkAddPatchSetPermission = true; private boolean forceContentMerge; private boolean copyApprovals = true; + private boolean detailedCommitMessage; + private boolean postMessage = true; private RevCommit rebasedCommit; private PatchSet.Id rebasedPatchSetId; @@ -101,6 +105,12 @@ return this; } + public RebaseChangeOp setCheckAddPatchSetPermission( + boolean checkAddPatchSetPermission) { + this.checkAddPatchSetPermission = checkAddPatchSetPermission; + return this; + } + public RebaseChangeOp setFireRevisionCreated(boolean fireRevisionCreated) { this.fireRevisionCreated = fireRevisionCreated; return this; @@ -116,6 +126,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 +148,7 @@ RevWalk rw = ctx.getRevWalk(); RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get())); rw.parseBody(original); + RevCommit baseCommit; if (baseCommitish != null) { baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish)); @@ -136,7 +158,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 +180,16 @@ 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() + .setCheckAddPatchSetPermission(checkAddPatchSetPermission); + 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 +248,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 +271,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/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java index 0956f9e..4a9d19c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -28,7 +28,6 @@ import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.ChangeControl; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; @@ -93,8 +92,7 @@ abstract PatchSet patchSet(); } - Base parseBase(RevisionResource rsrc, String base) - throws OrmException, NoSuchChangeException { + Base parseBase(RevisionResource rsrc, String base) throws OrmException { ReviewDb db = dbProvider.get(); // Try parsing the base as a ref string. @@ -137,7 +135,7 @@ } private ChangeControl controlFor(RevisionResource rsrc, Change.Id id) - throws OrmException, NoSuchChangeException { + throws OrmException { if (rsrc.getChange().getId().equals(id)) { return rsrc.getControl(); }
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..b1d9c0d 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; @@ -124,23 +123,15 @@ return true; } - private ChangeMessage newMessage(ChangeContext ctx) throws OrmException { + private ChangeMessage newMessage(ChangeContext ctx) { StringBuilder msg = new StringBuilder(); msg.append("Restored"); if (!Strings.nullToEmpty(input.message).trim().isEmpty()) { 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/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java index aac9252..f362a49 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -30,9 +30,11 @@ public interface Factory { ReviewerResource create(ChangeResource change, Account.Id id); + ReviewerResource create(RevisionResource revision, Account.Id id); } private final ChangeResource change; + private final RevisionResource revision; private final IdentifiedUser user; @AssistedInject @@ -40,6 +42,16 @@ @Assisted ChangeResource change, @Assisted Account.Id id) { this.change = change; + this.revision = null; + this.user = userFactory.create(id); + } + + @AssistedInject + ReviewerResource(IdentifiedUser.GenericFactory userFactory, + @Assisted RevisionResource revision, + @Assisted Account.Id id) { + this.revision = revision; + this.change = revision.getChangeResource(); this.user = userFactory.create(id); } @@ -47,6 +59,10 @@ return change; } + public RevisionResource getRevisionResource() { + return revision; + } + public Change.Id getChangeId() { return change.getId(); }
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/RevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java new file mode 100644 index 0000000..3ad860c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
@@ -0,0 +1,90 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ChildCollection; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.account.AccountsCollection; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.util.Collection; + +@Singleton +public class RevisionReviewers implements + ChildCollection<RevisionResource, ReviewerResource> { + private final DynamicMap<RestView<ReviewerResource>> views; + private final Provider<ReviewDb> dbProvider; + private final ApprovalsUtil approvalsUtil; + private final AccountsCollection accounts; + private final ReviewerResource.Factory resourceFactory; + private final ListRevisionReviewers list; + + @Inject + RevisionReviewers(Provider<ReviewDb> dbProvider, + ApprovalsUtil approvalsUtil, + AccountsCollection accounts, + ReviewerResource.Factory resourceFactory, + DynamicMap<RestView<ReviewerResource>> views, + ListRevisionReviewers list) { + this.dbProvider = dbProvider; + this.approvalsUtil = approvalsUtil; + this.accounts = accounts; + this.resourceFactory = resourceFactory; + this.views = views; + this.list = list; + } + + @Override + public DynamicMap<RestView<ReviewerResource>> views() { + return views; + } + + @Override + public RestView<RevisionResource> list() { + return list; + } + + @Override + public ReviewerResource parse(RevisionResource rsrc, IdString id) + throws OrmException, ResourceNotFoundException, AuthException, + MethodNotAllowedException { + if (!rsrc.isCurrent()) { + throw new MethodNotAllowedException( + "Cannot access on non-current patch set"); + } + + Account.Id accountId = + accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); + + Collection<Account.Id> reviewers = approvalsUtil.getReviewers( + dbProvider.get(), rsrc.getNotes()).all(); + if (reviewers.contains(accountId)) { + return resourceFactory.create(rsrc, accountId); + } + throw new ResourceNotFoundException(id); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java index 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..f687400 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -0,0 +1,161 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.AccountsCollection; +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; + +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 DynamicSet<AssigneeValidationListener> validationListeners; + private final String assignee; + private final AssigneeChanged assigneeChanged; + private final SetAssigneeSender.Factory setAssigneeSenderFactory; + private final Provider<IdentifiedUser> user; + private final IdentifiedUser.GenericFactory userFactory; + + private Change change; + private Account newAssignee; + private Account oldAssignee; + + @AssistedInject + SetAssigneeOp(AccountsCollection accounts, + ChangeMessagesUtil cmUtil, + DynamicSet<AssigneeValidationListener> validationListeners, + AssigneeChanged assigneeChanged, + SetAssigneeSender.Factory setAssigneeSenderFactory, + Provider<IdentifiedUser> user, + IdentifiedUser.GenericFactory userFactory, + @Assisted String assignee) { + this.accounts = accounts; + this.cmUtil = cmUtil; + this.validationListeners = validationListeners; + this.assigneeChanged = assigneeChanged; + this.setAssigneeSenderFactory = setAssigneeSenderFactory; + this.user = user; + this.userFactory = userFactory; + this.assignee = checkNotNull(assignee); + } + + @Override + public boolean updateChange(BatchUpdate.ChangeContext ctx) + throws OrmException, RestApiException { + change = ctx.getChange(); + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); + IdentifiedUser newAssigneeUser = accounts.parse(assignee); + newAssignee = newAssigneeUser.getAccount(); + IdentifiedUser oldAssigneeUser = null; + if (change.getAssignee() != null) { + oldAssigneeUser = userFactory.create(change.getAssignee()); + oldAssignee = oldAssigneeUser.getAccount(); + if (newAssignee.equals(oldAssignee)) { + return false; + } + } + if (!newAssignee.isActive()) { + throw new UnprocessableEntityException(String.format( + "Account of %s is not active", assignee)); + } + if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) { + throw new AuthException(String.format( + "Change %s is not visible to %s.", + change.getChangeId(), + assignee)); + } + try { + for (AssigneeValidationListener validator : validationListeners) { + validator.validateAssignee(change, newAssignee); + } + } catch (ValidationException e) { + throw new ResourceConflictException(e.getMessage()); + } + // notedb + update.setAssignee(newAssignee.getId()); + // reviewdb + change.setAssignee(newAssignee.getId()); + addMessage(ctx, update, oldAssigneeUser, newAssigneeUser); + return true; + } + + private void addMessage(BatchUpdate.ChangeContext ctx, ChangeUpdate update, + IdentifiedUser previousAssignee, IdentifiedUser newAssignee) + throws OrmException { + StringBuilder msg = new StringBuilder(); + msg.append("Assignee "); + if (previousAssignee == null) { + msg.append("added: "); + msg.append(newAssignee.getNameEmail()); + } else { + msg.append("changed from: "); + msg.append(previousAssignee.getNameEmail()); + msg.append(" to: "); + msg.append(newAssignee.getNameEmail()); + } + 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.Id getNewAssignee() { + return newAssignee != null ? newAssignee.getId() : null; + } +}
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..654471f 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,16 +14,14 @@ 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.ListMultimap; import com.google.common.collect.Sets; import com.google.gerrit.common.data.ParameterizedString; import com.google.gerrit.extensions.api.changes.SubmitInput; @@ -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(); } @@ -444,7 +418,7 @@ mergeabilityMap.add(change); } - Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch(); + ListMultimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch(); for (Branch.NameKey branch : cbb.keySet()) { Collection<ChangeData> targetBranch = cbb.get(branch); HashMap<Change.Id, RevCommit> commits = @@ -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..0753769 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
@@ -15,10 +15,12 @@ package com.google.gerrit.server.change; import com.google.gerrit.extensions.common.SuggestedReviewerInfo; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestReadView; 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.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser.GenericFactory; import com.google.gerrit.server.ReviewersUtil; @@ -30,26 +32,39 @@ 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; + + private final Provider<CurrentUser> self; + @Inject SuggestChangeReviewers(AccountVisibility av, GenericFactory identifiedUserFactory, Provider<ReviewDb> dbProvider, + Provider<CurrentUser> self, @GerritServerConfig Config cfg, ReviewersUtil reviewersUtil) { super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil); + this.self = self; } @Override public List<SuggestedReviewerInfo> apply(ChangeResource rsrc) - throws BadRequestException, OrmException, IOException { - return reviewersUtil.suggestReviewers(this, - rsrc.getControl().getProjectControl(), getVisibility(rsrc)); + throws AuthException, BadRequestException, OrmException, IOException { + if (!self.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + 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/change/SuggestedReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java new file mode 100644 index 0000000..353bf3b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.gerrit.server.change; + +import com.google.gerrit.reviewdb.client.Account; + +public class SuggestedReviewer { + + public Account.Id account; + public double score; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java index 3bba37e..78cdf75 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -18,6 +18,7 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestView; @@ -56,7 +57,13 @@ @Override public VoteResource parse(ReviewerResource reviewer, IdString id) - throws ResourceNotFoundException, OrmException, AuthException { + throws ResourceNotFoundException, OrmException, AuthException, + MethodNotAllowedException { + if (reviewer.getRevisionResource() != null + && !reviewer.getRevisionResource().isCurrent()) { + throw new MethodNotAllowedException( + "Cannot access on non-current patch set"); + } return new VoteResource(reviewer, id.get()); } @@ -73,7 +80,14 @@ } @Override - public Map<String, Short> apply(ReviewerResource rsrc) throws OrmException { + public Map<String, Short> apply(ReviewerResource rsrc) + throws OrmException, MethodNotAllowedException { + if (rsrc.getRevisionResource() != null + && !rsrc.getRevisionResource().isCurrent()) { + throw new MethodNotAllowedException( + "Cannot list votes on non-current patch set"); + } + Map<String, Short> votes = new TreeMap<>(); Iterable<PatchSetApproval> byPatchSetUser = approvalsUtil.byPatchSetUser( db.get(),
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..e0113b2 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.ListMultimap; +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; @@ -110,8 +105,8 @@ public Iterable<PatchSetData> sort(Iterable<ChangeData> in) throws OrmException, IOException { - Multimap<Project.NameKey, ChangeData> byProject = - ArrayListMultimap.create(); + ListMultimap<Project.NameKey, ChangeData> byProject = + MultimapBuilder.hashKeys().arrayListValues().build(); for (ChangeData cd : in) { byProject.put(cd.change().getProject(), cd); } @@ -131,7 +126,7 @@ try (Repository repo = repoManager.openRepository(project); RevWalk rw = new RevWalk(repo)) { rw.setRetainBody(retainBody); - Multimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in); + ListMultimap<RevCommit, PatchSetData> byCommit = byCommit(rw, in); if (byCommit.isEmpty()) { return ImmutableList.of(); } else if (byCommit.size() == 1) { @@ -156,8 +151,9 @@ // the input size is small enough that this is not an issue.) Set<RevCommit> commits = byCommit.keySet(); - Multimap<RevCommit, RevCommit> children = collectChildren(commits); - Multimap<RevCommit, RevCommit> pending = ArrayListMultimap.create(); + ListMultimap<RevCommit, RevCommit> children = collectChildren(commits); + ListMultimap<RevCommit, RevCommit> pending = + MultimapBuilder.hashKeys().arrayListValues().build(); Deque<RevCommit> todo = new ArrayDeque<>(); RevFlag done = rw.newFlag("done"); @@ -199,9 +195,10 @@ } } - private static Multimap<RevCommit, RevCommit> collectChildren( + private static ListMultimap<RevCommit, RevCommit> collectChildren( Set<RevCommit> commits) { - Multimap<RevCommit, RevCommit> children = ArrayListMultimap.create(); + ListMultimap<RevCommit, RevCommit> children = + MultimapBuilder.hashKeys().arrayListValues().build(); for (RevCommit c : commits) { for (RevCommit p : c.getParents()) { if (commits.contains(p)) { @@ -212,8 +209,9 @@ return children; } - private static int emit(RevCommit c, Multimap<RevCommit, PatchSetData> byCommit, - List<PatchSetData> result, RevFlag done) { + private static int emit(RevCommit c, + ListMultimap<RevCommit, PatchSetData> byCommit, List<PatchSetData> result, + RevFlag done) { if (c.has(done)) { return 0; } @@ -226,10 +224,10 @@ return 0; } - private Multimap<RevCommit, PatchSetData> byCommit(RevWalk rw, + private ListMultimap<RevCommit, PatchSetData> byCommit(RevWalk rw, Collection<ChangeData> in) throws OrmException, IOException { - Multimap<RevCommit, PatchSetData> byCommit = - ArrayListMultimap.create(in.size(), 1); + ListMultimap<RevCommit, PatchSetData> byCommit = + 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 3511705..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,9 +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;
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/DeleteTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java index a22f52d..1e8afba 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DeleteTask.java
@@ -16,11 +16,13 @@ import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK; import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import com.google.gerrit.extensions.annotations.RequiresAnyCapability; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.config.DeleteTask.Input; +import com.google.gerrit.server.git.WorkQueue.Task; import com.google.inject.Singleton; @Singleton @@ -31,7 +33,9 @@ @Override public Response<?> apply(TaskResource rsrc, Input input) { - rsrc.getTask().cancel(true); - return Response.none(); + Task<?> task = rsrc.getTask(); + boolean taskDeleted = task.cancel(true); + return taskDeleted ? Response.none() : Response.withStatusCode( + SC_INTERNAL_SERVER_ERROR, "Unable to kill task " + task); } }
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..e941de0 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; @@ -75,7 +77,6 @@ import com.google.gerrit.server.account.AccountByEmailCacheImpl; import com.google.gerrit.server.account.AccountCacheImpl; import com.google.gerrit.server.account.AccountControl; -import com.google.gerrit.server.account.AccountInfoCacheFactory; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountVisibility; @@ -100,10 +101,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,28 +121,32 @@ 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; +import com.google.gerrit.server.git.validators.OnSubmitValidationListener; +import com.google.gerrit.server.git.validators.OnSubmitValidators; import com.google.gerrit.server.git.validators.RefOperationValidationListener; import com.google.gerrit.server.git.validators.RefOperationValidators; import com.google.gerrit.server.git.validators.UploadValidationListener; import com.google.gerrit.server.git.validators.UploadValidators; -import com.google.gerrit.server.group.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.MailFilter; +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 +170,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 +180,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; @@ -230,7 +240,6 @@ bind(AccountResolver.class); - factory(AccountInfoCacheFactory.Factory.class); factory(AddReviewerSender.Factory.class); factory(DeleteReviewerSender.Factory.class); factory(AddKeySender.Factory.class); @@ -241,7 +250,6 @@ factory(ChangeJson.Factory.class); factory(CreateChangeSender.Factory.class); factory(GroupDetailFactory.Factory.class); - factory(GroupInfoCache.Factory.class); factory(GroupMembers.Factory.class); factory(EmailMerge.Factory.class); factory(MergedSender.Factory.class); @@ -252,6 +260,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 +285,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 +313,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,7 +345,9 @@ 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(), OnSubmitValidationListener.class); DynamicSet.setOf(binder(), MergeValidationListener.class); DynamicSet.setOf(binder(), ProjectCreationValidationListener.class); DynamicSet.setOf(binder(), GroupCreationValidationListener.class); @@ -345,6 +360,8 @@ DynamicMap.mapOf(binder(), DownloadScheme.class); DynamicMap.mapOf(binder(), DownloadCommand.class); DynamicMap.mapOf(binder(), CloneCommand.class); + DynamicMap.mapOf(binder(), ReviewerSuggestion.class); + DynamicMap.mapOf(binder(), MailFilter.class); DynamicSet.setOf(binder(), ExternalIncludedIn.class); DynamicMap.mapOf(binder(), ProjectConfigEntry.class); DynamicSet.setOf(binder(), PatchSetWebLink.class); @@ -359,17 +376,21 @@ 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(OnSubmitValidators.Factory.class); factory(MergeValidators.Factory.class); factory(ProjectConfigValidator.Factory.class); factory(NotesBranchUtil.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 aebe74a..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,13 +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.extensions.client.GitBasicAuthPolicy; +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; @@ -29,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> { @@ -62,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; @@ -70,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( @@ -80,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; @@ -103,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 @@ -136,6 +162,18 @@ 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: case LDAP_BIND: @@ -171,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); @@ -188,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) { @@ -198,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; } @@ -253,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; } @@ -324,86 +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 GitBasicAuthPolicy gitBasicAuthPolicy; - } - - 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/GitwebConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java index 7d86aa2..d8fc9f8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitwebConfig.java
@@ -19,6 +19,7 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.nullToEmpty; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GitwebType; import com.google.gerrit.common.data.ParameterizedString; import com.google.gerrit.extensions.common.WebLinkInfo; @@ -204,6 +205,12 @@ } } + /** @return GitwebType for gitweb viewer. */ + @Nullable + public GitwebType getGitwebType() { + return type; + } + /** * @return URL of the entry point into gitweb. This URL may be relative to our * context if gitweb is hosted by ourselves; or absolute if its hosted
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java index eb6169e..e12cc24 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -274,7 +274,11 @@ try { cfg.load(); - } catch (IOException | ConfigInvalidException e) { + } catch (ConfigInvalidException e) { + // This is an error in user input, don't spam logs with a stack trace. + log.warn( + "Failed to load " + pluginConfigFile.toAbsolutePath() + ": " + e); + } catch (IOException e) { log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e); }
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/config/TrackingFooters.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java index 672c461..ee01308 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.config; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import org.eclipse.jgit.revwalk.FooterLine; @@ -37,8 +37,9 @@ return trackingFooters.isEmpty(); } - public Multimap<String, String> extract(List<FooterLine> lines) { - Multimap<String, String> r = ArrayListMultimap.create(); + public ListMultimap<String, String> extract(List<FooterLine> lines) { + ListMultimap<String, String> r = + MultimapBuilder.hashKeys().arrayListValues().build(); if (lines == null) { return r; }
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..48a1744 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
@@ -23,9 +23,10 @@ public String branch; public String topic; public String id; - public String number; + public int 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/data/DependencyAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java index 4c796f2..3b7820c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/DependencyAttribute.java
@@ -16,7 +16,7 @@ public class DependencyAttribute { public String id; - public String number; + public int number; public String revision; public String ref; public Boolean isCurrentPatchSet;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java index 824d800..8e43657 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -19,7 +19,7 @@ import java.util.List; public class PatchSetAttribute { - public String number; + public int number; public String revision; public List<String> parents; public String ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java index 45128bd..e7e093a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -14,67 +14,54 @@ package com.google.gerrit.server.edit; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import com.google.common.base.Strings; -import com.google.common.io.ByteStreams; -import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.RawInput; -import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.edit.tree.ChangeFileContentModification; +import com.google.gerrit.server.edit.tree.DeleteFileModification; +import com.google.gerrit.server.edit.tree.RenameFileModification; +import com.google.gerrit.server.edit.tree.RestoreFileModification; +import com.google.gerrit.server.edit.tree.TreeCreator; +import com.google.gerrit.server.edit.tree.TreeModification; import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.project.ChangeControl; 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.Singleton; -import org.eclipse.jgit.dircache.DirCache; -import org.eclipse.jgit.dircache.DirCacheBuilder; -import org.eclipse.jgit.dircache.DirCacheEditor; -import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath; -import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; -import org.eclipse.jgit.dircache.DirCacheEntry; -import org.eclipse.jgit.errors.InvalidObjectIdException; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.CommitBuilder; -import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; -import org.eclipse.jgit.treewalk.TreeWalk; import java.io.IOException; -import java.io.InputStream; import java.sql.Timestamp; -import java.util.Map; +import java.util.Optional; import java.util.TimeZone; -import java.util.concurrent.atomic.AtomicReference; /** * Utility functions to manipulate change edits. @@ -87,454 +74,455 @@ @Singleton public class ChangeEditModifier { - private enum TreeOperation { - CHANGE_ENTRY, - DELETE_ENTRY, - RENAME_ENTRY, - RESTORE_ENTRY - } private final TimeZone tz; - private final GitRepositoryManager gitManager; private final ChangeIndexer indexer; private final Provider<ReviewDb> reviewDb; private final Provider<CurrentUser> currentUser; - private final ChangeControl.GenericFactory changeControlFactory; + private final ChangeEditUtil changeEditUtil; + private final PatchSetUtil patchSetUtil; @Inject ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent, - GitRepositoryManager gitManager, ChangeIndexer indexer, Provider<ReviewDb> reviewDb, Provider<CurrentUser> currentUser, - ChangeControl.GenericFactory changeControlFactory) { - this.gitManager = gitManager; + ChangeEditUtil changeEditUtil, + PatchSetUtil patchSetUtil) { this.indexer = indexer; this.reviewDb = reviewDb; this.currentUser = currentUser; this.tz = gerritIdent.getTimeZone(); - this.changeControlFactory = changeControlFactory; + this.changeEditUtil = changeEditUtil; + this.patchSetUtil = patchSetUtil; } /** - * Create new change edit. + * Creates a new change edit. * - * @param change to create change edit for - * @param ps patch set to create change edit on - * @return result - * @throws AuthException - * @throws IOException - * @throws ResourceConflictException When change edit already - * exists for the change - * @throws OrmException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change for which + * the change edit should be created + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if a change edit already existed + * for the change */ - public RefUpdate.Result createEdit(Change change, PatchSet ps) - throws AuthException, IOException, ResourceConflictException, OrmException { - if (!currentUser.get().isIdentifiedUser()) { - throw new AuthException("Authentication required"); - } - IdentifiedUser me = currentUser.get().asIdentifiedUser(); - String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId()); + public void createEdit(Repository repository, ChangeControl changeControl) + throws AuthException, IOException, InvalidChangeOperationException, + OrmException { + ensureAuthenticatedAndPermitted(changeControl); - try { - ChangeControl c = - changeControlFactory.controlFor(reviewDb.get(), change, me); - if (!c.canAddPatchSet(reviewDb.get())) { - return RefUpdate.Result.REJECTED; - } - } catch (NoSuchChangeException e) { - return RefUpdate.Result.NO_CHANGE; + Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl); + if (changeEdit.isPresent()) { + throw new InvalidChangeOperationException(String.format("A change edit " + + "already exists for change %s", changeControl.getId())); } - try (Repository repo = gitManager.openRepository(change.getProject())) { - Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix); - if (!refs.isEmpty()) { - throw new ResourceConflictException("edit already exists"); - } - - try (RevWalk rw = new RevWalk(repo)) { - ObjectId revision = ObjectId.fromString(ps.getRevision().get()); - String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(), - ps.getId()); - Result res = update(repo, me, editRefName, rw, ObjectId.zeroId(), - revision, TimeUtil.nowTs()); - indexer.index(reviewDb.get(), change); - return res; - } - } + PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl); + ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet); + createEditReference(repository, changeControl, currentPatchSet, + patchSetCommitId, TimeUtil.nowTs()); } /** * Rebase change edit on latest patch set * - * @param edit change edit that contains edit to rebase - * @param current patch set to rebase the edit on - * @throws AuthException - * @throws ResourceConflictException thrown if rebase fails due to merge conflicts - * @throws InvalidChangeOperationException - * @throws IOException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit should be rebased + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if a change edit doesn't exist + * for the specified change, the change edit is already based on the latest + * patch set, or the change represents the root commit + * @throws MergeConflictException if rebase fails due to merge conflicts */ - public void rebaseEdit(ChangeEdit edit, PatchSet current) - throws AuthException, ResourceConflictException, - InvalidChangeOperationException, IOException { - if (!currentUser.get().isIdentifiedUser()) { - throw new AuthException("Authentication required"); + public void rebaseEdit(Repository repository, ChangeControl changeControl) + throws AuthException, InvalidChangeOperationException, IOException, + OrmException, MergeConflictException { + ensureAuthenticatedAndPermitted(changeControl); + + Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); + if (!optionalChangeEdit.isPresent()) { + throw new InvalidChangeOperationException(String.format( + "No change edit exists for change %s", changeControl.getId())); + } + ChangeEdit changeEdit = optionalChangeEdit.get(); + + PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl); + if (isBasedOn(changeEdit, currentPatchSet)) { + throw new InvalidChangeOperationException(String.format( + "Change edit for change %s is already based on latest patch set %s", + changeControl.getId(), currentPatchSet.getId())); } - Change change = edit.getChange(); - IdentifiedUser me = currentUser.get().asIdentifiedUser(); - String refName = RefNames.refsEdit(me.getAccountId(), change.getId(), - current.getId()); - try (Repository repo = gitManager.openRepository(change.getProject()); - RevWalk rw = new RevWalk(repo); - ObjectInserter inserter = repo.newObjectInserter()) { - BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate(); - RevCommit editCommit = edit.getEditCommit(); - if (editCommit.getParentCount() == 0) { - throw new InvalidChangeOperationException( - "Rebase edit against root commit not supported"); - } - RevCommit tip = rw.parseCommit(ObjectId.fromString( - current.getRevision().get())); - ThreeWayMerger m = MergeStrategy.RESOLVE.newMerger(repo, true); - m.setObjectInserter(inserter); - m.setBase(ObjectId.fromString( - edit.getBasePatchSet().getRevision().get())); + rebase(repository, changeEdit, currentPatchSet); + } - if (m.merge(tip, editCommit)) { - ObjectId tree = m.getResultTreeId(); - - CommitBuilder commit = new CommitBuilder(); - commit.setTreeId(tree); - for (int i = 0; i < tip.getParentCount(); i++) { - commit.addParentId(tip.getParent(i)); - } - commit.setAuthor(editCommit.getAuthorIdent()); - commit.setCommitter(new PersonIdent( - editCommit.getCommitterIdent(), TimeUtil.nowTs())); - commit.setMessage(editCommit.getFullMessage()); - ObjectId newEdit = inserter.insert(commit); - inserter.flush(); - - ru.addCommand(new ReceiveCommand(ObjectId.zeroId(), newEdit, - refName)); - ru.addCommand(new ReceiveCommand(edit.getRef().getObjectId(), - ObjectId.zeroId(), edit.getRefName())); - ru.execute(rw, NullProgressMonitor.INSTANCE); - for (ReceiveCommand cmd : ru.getCommands()) { - if (cmd.getResult() != ReceiveCommand.Result.OK) { - throw new IOException("failed: " + cmd); - } - } - } else { - // TODO(davido): Allow to resolve conflicts inline - throw new ResourceConflictException("merge conflict"); - } + private void rebase(Repository repository, ChangeEdit changeEdit, + PatchSet currentPatchSet) throws IOException, MergeConflictException, + InvalidChangeOperationException, OrmException { + RevCommit currentEditCommit = changeEdit.getEditCommit(); + if (currentEditCommit.getParentCount() == 0) { + throw new InvalidChangeOperationException( + "Rebase change edit against root commit not supported"); } + + Change change = changeEdit.getChange(); + RevCommit basePatchSetCommit = lookupCommit(repository, currentPatchSet); + RevTree basePatchSetTree = basePatchSetCommit.getTree(); + + ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree); + Timestamp nowTimestamp = TimeUtil.nowTs(); + String commitMessage = currentEditCommit.getFullMessage(); + ObjectId newEditCommitId = createCommit(repository, basePatchSetCommit, + newTreeId, commitMessage, nowTimestamp); + + String newEditRefName = getEditRefName(change, currentPatchSet); + updateReferenceWithNameChange(repository, changeEdit.getRefName(), + currentEditCommit, newEditRefName, newEditCommitId, nowTimestamp); + reindex(change); } /** - * Modify commit message in existing change edit. + * Modifies the commit message of a change edit. If the change edit doesn't + * exist, a new one will be created based on the current patch set. * - * @param edit change edit - * @param msg new commit message - * @return result - * @throws AuthException - * @throws InvalidChangeOperationException - * @throws IOException - * @throws UnchangedCommitMessageException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit's message should be modified + * @param newCommitMessage the new commit message + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws UnchangedCommitMessageException if the commit message is the same + * as before */ - public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg) - throws AuthException, InvalidChangeOperationException, IOException, - UnchangedCommitMessageException { - msg = msg.trim() + "\n"; - checkState(!Strings.isNullOrEmpty(msg), "message cannot be null"); - if (!currentUser.get().isIdentifiedUser()) { - throw new AuthException("Authentication required"); - } + public void modifyMessage(Repository repository, ChangeControl changeControl, + String newCommitMessage) throws AuthException, IOException, + UnchangedCommitMessageException, OrmException { + ensureAuthenticatedAndPermitted(changeControl); + newCommitMessage = getWellFormedCommitMessage(newCommitMessage); - RevCommit prevEdit = edit.getEditCommit(); - if (prevEdit.getFullMessage().equals(msg)) { + Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); + PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl); + RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet); + RevCommit baseCommit = optionalChangeEdit.map(ChangeEdit::getEditCommit) + .orElse(basePatchSetCommit); + + String currentCommitMessage = baseCommit.getFullMessage(); + if (newCommitMessage.equals(currentCommitMessage)) { throw new UnchangedCommitMessageException(); } - IdentifiedUser me = currentUser.get().asIdentifiedUser(); - Project.NameKey project = edit.getChange().getProject(); - try (Repository repo = gitManager.openRepository(project); - RevWalk rw = new RevWalk(repo); - ObjectInserter inserter = repo.newObjectInserter()) { - String refName = edit.getRefName(); - Timestamp now = TimeUtil.nowTs(); - ObjectId commit = createCommit(me, inserter, prevEdit, - prevEdit.getTree(), - msg, now); - inserter.flush(); - return update(repo, me, refName, rw, prevEdit, commit, now); + RevTree baseTree = baseCommit.getTree(); + Timestamp nowTimestamp = TimeUtil.nowTs(); + ObjectId newEditCommit = createCommit(repository, basePatchSetCommit, + baseTree, newCommitMessage, nowTimestamp); + + if (optionalChangeEdit.isPresent()) { + updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, + nowTimestamp); + } else { + createEditReference(repository, changeControl, basePatchSet, + newEditCommit, nowTimestamp); } } /** - * Modify file in existing change edit from its base commit. + * Modifies the contents of a file of a change edit. If the change edit + * doesn't exist, a new one will be created based on the current patch set. * - * @param edit change edit - * @param file path to modify - * @param content new content - * @return result - * @throws AuthException - * @throws InvalidChangeOperationException - * @throws IOException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit should be modified + * @param filePath the path of the file whose contents should be modified + * @param newContent the new file content + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if the file already had the + * specified content */ - public RefUpdate.Result modifyFile(ChangeEdit edit, - String file, RawInput content) throws AuthException, - InvalidChangeOperationException, IOException { - return modify(TreeOperation.CHANGE_ENTRY, edit, file, null, content); + public void modifyFile(Repository repository, ChangeControl changeControl, + String filePath, RawInput newContent) throws AuthException, + InvalidChangeOperationException, IOException, OrmException { + modifyTree(repository, changeControl, + new ChangeFileContentModification(filePath, newContent)); } /** - * Delete file in existing change edit. + * Deletes a file from the Git tree of a change edit. If the change edit + * doesn't exist, a new one will be created based on the current patch set. * - * @param edit change edit - * @param file path to delete - * @return result - * @throws AuthException - * @throws InvalidChangeOperationException - * @throws IOException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit should be modified + * @param file path of the file which should be deleted + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if the file does not exist */ - public RefUpdate.Result deleteFile(ChangeEdit edit, + public void deleteFile(Repository repository, ChangeControl changeControl, String file) throws AuthException, InvalidChangeOperationException, - IOException { - return modify(TreeOperation.DELETE_ENTRY, edit, file, null, null); + IOException, OrmException { + modifyTree(repository, changeControl, new DeleteFileModification(file)); } /** - * Rename file in existing change edit. + * Renames a file of a change edit or moves it to another directory. If the + * change edit doesn't exist, a new one will be created based on the current + * patch set. * - * @param edit change edit - * @param file path to rename - * @param newFile path to rename the file to - * @return result - * @throws AuthException - * @throws InvalidChangeOperationException - * @throws IOException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit should be modified + * @param currentFilePath the current path/name of the file + * @param newFilePath the desired path/name of the file + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if the file was already renamed + * to the specified new name */ - public RefUpdate.Result renameFile(ChangeEdit edit, String file, - String newFile) throws AuthException, InvalidChangeOperationException, - IOException { - return modify(TreeOperation.RENAME_ENTRY, edit, file, newFile, null); + public void renameFile(Repository repository, ChangeControl changeControl, + String currentFilePath, String newFilePath) throws AuthException, + InvalidChangeOperationException, IOException, OrmException { + modifyTree(repository, changeControl, + new RenameFileModification(currentFilePath, newFilePath)); } /** - * Restore file in existing change edit. + * Restores a file of a change edit to the state it was in before the patch + * set on which the change edit is based. If the change edit doesn't exist, a + * new one will be created based on the current patch set. * - * @param edit change edit - * @param file path to restore - * @return result - * @throws AuthException - * @throws InvalidChangeOperationException - * @throws IOException + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change whose change + * edit should be modified + * @param file the path of the file which should be restored + * @throws AuthException if the user isn't authenticated or not allowed to + * use change edits + * @throws InvalidChangeOperationException if the file was already restored */ - public RefUpdate.Result restoreFile(ChangeEdit edit, + public void restoreFile(Repository repository, ChangeControl changeControl, String file) throws AuthException, InvalidChangeOperationException, - IOException { - return modify(TreeOperation.RESTORE_ENTRY, edit, file, null, null); + IOException, OrmException { + modifyTree(repository, changeControl, new RestoreFileModification(file)); } - private RefUpdate.Result modify(TreeOperation op, ChangeEdit edit, - String file, @Nullable String newFile, @Nullable RawInput content) - throws AuthException, IOException, InvalidChangeOperationException { + private void modifyTree(Repository repository, ChangeControl changeControl, + TreeModification treeModification) throws AuthException, IOException, + OrmException, InvalidChangeOperationException { + ensureAuthenticatedAndPermitted(changeControl); + + Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); + PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl); + RevCommit basePatchSetCommit = lookupCommit(repository, basePatchSet); + RevCommit baseCommit = optionalChangeEdit.map(ChangeEdit::getEditCommit) + .orElse(basePatchSetCommit); + + ObjectId newTreeId = createNewTree(repository, baseCommit, + treeModification); + + String commitMessage = baseCommit.getFullMessage(); + Timestamp nowTimestamp = TimeUtil.nowTs(); + ObjectId newEditCommit = createCommit(repository, basePatchSetCommit, + newTreeId, commitMessage, nowTimestamp); + + if (optionalChangeEdit.isPresent()) { + updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, + nowTimestamp); + } else { + createEditReference(repository, changeControl, basePatchSet, + newEditCommit, nowTimestamp); + } + } + + private void ensureAuthenticatedAndPermitted(ChangeControl changeControl) + throws AuthException, OrmException { + ensureAuthenticated(); + ensurePermitted(changeControl); + } + + private void ensureAuthenticated() throws AuthException { if (!currentUser.get().isIdentifiedUser()) { throw new AuthException("Authentication required"); } + } + + private void ensurePermitted(ChangeControl changeControl) + throws OrmException, AuthException { + if (!changeControl.canAddPatchSet(reviewDb.get())) { + throw new AuthException("Not allowed to edit a change."); + } + } + + private String getWellFormedCommitMessage(String commitMessage) { + String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim(); + checkState(!wellFormedMessage.isEmpty(), + "Commit message cannot be null or empty"); + wellFormedMessage = wellFormedMessage + "\n"; + return wellFormedMessage; + } + + private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl) + throws AuthException, IOException { + return changeEditUtil.byChange(changeControl); + } + + private PatchSet getBasePatchSet(Optional<ChangeEdit> optionalChangeEdit, + ChangeControl changeControl) throws OrmException { + Optional<PatchSet> editBasePatchSet = + optionalChangeEdit.map(ChangeEdit::getBasePatchSet); + return editBasePatchSet.isPresent() + ? editBasePatchSet.get() + : lookupCurrentPatchSet(changeControl); + } + + private PatchSet lookupCurrentPatchSet(ChangeControl changeControl) + throws OrmException { + return patchSetUtil.current(reviewDb.get(), changeControl.getNotes()); + } + + private static boolean isBasedOn(ChangeEdit changeEdit, PatchSet patchSet) { + PatchSet editBasePatchSet = changeEdit.getBasePatchSet(); + return editBasePatchSet.getId().equals(patchSet.getId()); + } + + private static RevCommit lookupCommit(Repository repository, + PatchSet patchSet) throws IOException { + ObjectId patchSetCommitId = getPatchSetCommitId(patchSet); + try (RevWalk revWalk = new RevWalk(repository)) { + return revWalk.parseCommit(patchSetCommitId); + } + } + + private static ObjectId createNewTree(Repository repository, + RevCommit baseCommit, TreeModification treeModification) + throws IOException, InvalidChangeOperationException { + TreeCreator treeCreator = new TreeCreator(baseCommit); + treeCreator.addTreeModification(treeModification); + ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository); + + if (ObjectId.equals(newTreeId, baseCommit.getTree())) { + throw new InvalidChangeOperationException("no changes were made"); + } + return newTreeId; + } + + private ObjectId merge(Repository repository, ChangeEdit changeEdit, + ObjectId newTreeId) throws IOException, MergeConflictException { + PatchSet basePatchSet = changeEdit.getBasePatchSet(); + ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet); + ObjectId editCommitId = changeEdit.getEditCommit(); + + ThreeWayMerger threeWayMerger = + MergeStrategy.RESOLVE.newMerger(repository, true); + threeWayMerger.setBase(basePatchSetCommitId); + boolean successful = threeWayMerger.merge(newTreeId, editCommitId); + + if (!successful) { + throw new MergeConflictException( + "The existing change edit could not be merged with another tree."); + } + return threeWayMerger.getResultTreeId(); + } + + private ObjectId createCommit(Repository repository, + RevCommit basePatchSetCommit, ObjectId tree, String commitMessage, + Timestamp timestamp) throws IOException { + try (ObjectInserter objectInserter = repository.newObjectInserter()) { + CommitBuilder builder = new CommitBuilder(); + builder.setTreeId(tree); + builder.setParentIds(basePatchSetCommit.getParents()); + builder.setAuthor(basePatchSetCommit.getAuthorIdent()); + builder.setCommitter(getCommitterIdent(timestamp)); + builder.setMessage(commitMessage); + ObjectId newCommitId = objectInserter.insert(builder); + objectInserter.flush(); + return newCommitId; + } + } + + private PersonIdent getCommitterIdent(Timestamp commitTimestamp) { + IdentifiedUser user = currentUser.get().asIdentifiedUser(); + return user.newCommitterIdent(commitTimestamp, tz); + } + + private static ObjectId getPatchSetCommitId(PatchSet patchSet) { + return ObjectId.fromString(patchSet.getRevision().get()); + } + + private void createEditReference(Repository repository, + ChangeControl changeControl, PatchSet basePatchSet, + ObjectId newEditCommit, Timestamp timestamp) + throws IOException, OrmException { + Change change = changeControl.getChange(); + String editRefName = getEditRefName(change, basePatchSet); + updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommit, + timestamp); + reindex(change); + } + + private String getEditRefName(Change change, PatchSet basePatchSet) { IdentifiedUser me = currentUser.get().asIdentifiedUser(); - Project.NameKey project = edit.getChange().getProject(); - try (Repository repo = gitManager.openRepository(project); - RevWalk rw = new RevWalk(repo); - ObjectInserter inserter = repo.newObjectInserter(); - ObjectReader reader = repo.newObjectReader()) { - String refName = edit.getRefName(); - RevCommit prevEdit = edit.getEditCommit(); - ObjectId newTree = writeNewTree( - op, - rw, - inserter, - prevEdit, - reader, - file, - newFile, - content); - if (ObjectId.equals(newTree, prevEdit.getTree())) { - throw new InvalidChangeOperationException("no changes were made"); - } - - Timestamp now = TimeUtil.nowTs(); - ObjectId commit = createCommit(me, inserter, prevEdit, newTree, now); - inserter.flush(); - return update(repo, me, refName, rw, prevEdit, commit, now); - } + return RefNames.refsEdit(me.getAccountId(), change.getId(), + basePatchSet.getId()); } - private static ObjectId toBlob(ObjectInserter ins, @Nullable RawInput content) + private void updateEditReference(Repository repository, ChangeEdit changeEdit, + ObjectId newEditCommit, Timestamp timestamp) + throws IOException, OrmException { + String editRefName = changeEdit.getRefName(); + RevCommit currentEditCommit = changeEdit.getEditCommit(); + updateReference(repository, editRefName, currentEditCommit, newEditCommit, + timestamp); + reindex(changeEdit.getChange()); + } + + private void updateReference(Repository repository, String refName, + ObjectId currentObjectId, ObjectId targetObjectId, Timestamp timestamp) throws IOException { - if (content == null) { - return null; - } - - long len = content.getContentLength(); - InputStream in = content.getInputStream(); - if (len < 0) { - return ins.insert(OBJ_BLOB, ByteStreams.toByteArray(in)); - } - return ins.insert(OBJ_BLOB, len, in); - } - - private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter, - RevCommit revision, ObjectId tree, Timestamp when) throws IOException { - return createCommit(me, inserter, revision, tree, - revision.getFullMessage(), when); - } - - private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter, - RevCommit revision, ObjectId tree, String msg, Timestamp when) - throws IOException { - CommitBuilder builder = new CommitBuilder(); - builder.setTreeId(tree); - builder.setParentIds(revision.getParents()); - builder.setAuthor(revision.getAuthorIdent()); - builder.setCommitter(getCommitterIdent(me, when)); - builder.setMessage(msg); - return inserter.insert(builder); - } - - private RefUpdate.Result update(Repository repo, IdentifiedUser me, - String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit, - Timestamp when) throws IOException { - RefUpdate ru = repo.updateRef(refName); - ru.setExpectedOldObjectId(oldObjectId); - ru.setNewObjectId(newEdit); - ru.setRefLogIdent(getRefLogIdent(me, when)); + RefUpdate ru = repository.updateRef(refName); + ru.setExpectedOldObjectId(currentObjectId); + ru.setNewObjectId(targetObjectId); + ru.setRefLogIdent(getRefLogIdent(timestamp)); ru.setRefLogMessage("inline edit (amend)", false); ru.setForceUpdate(true); - RefUpdate.Result res = ru.update(rw); - if (res != RefUpdate.Result.NEW && - res != RefUpdate.Result.FORCED) { - throw new IOException("update failed: " + ru); - } - return res; - } - - private static ObjectId writeNewTree( - TreeOperation op, - RevWalk rw, - final ObjectInserter ins, - RevCommit prevEdit, - ObjectReader reader, - String fileName, - @Nullable String newFile, - @Nullable final RawInput content) - throws InvalidChangeOperationException, IOException { - DirCache newTree = readTree(reader, prevEdit); - DirCacheEditor dce = newTree.editor(); - switch (op) { - case DELETE_ENTRY: - dce.add(new DeletePath(fileName)); - break; - - case RENAME_ENTRY: - rw.parseHeaders(prevEdit); - TreeWalk tw = - TreeWalk.forPath(rw.getObjectReader(), fileName, prevEdit.getTree()); - if (tw != null) { - dce.add(new DeletePath(fileName)); - addFileToCommit(newFile, dce, tw); - } - break; - - case CHANGE_ENTRY: - checkNotNull(content, "new content required"); - - final AtomicReference<IOException> ioe = - new AtomicReference<>(null); - final AtomicReference<InvalidChangeOperationException> icoe = - new AtomicReference<>(null); - dce.add(new PathEdit(fileName) { - @Override - public void apply(DirCacheEntry ent) { - try { - if (ent.getFileMode() == FileMode.GITLINK) { - ent.setLength(0); - ent.setLastModified(0); - ent.setObjectId(ObjectId.fromString( - ByteStreams.toByteArray(content.getInputStream()), 0)); - } else { - if (ent.getRawMode() == 0) { - ent.setFileMode(FileMode.REGULAR_FILE); - } - ent.setObjectId(toBlob(ins, content)); - } - } catch (IOException e) { - ioe.set(e); - } catch (InvalidObjectIdException e) { - icoe.set(new InvalidChangeOperationException( - "Invalid object id in submodule link: " + e.getMessage())); - icoe.get().initCause(e); - } - } - }); - if (ioe.get() != null) { - throw ioe.get(); - } - if (icoe.get() != null) { - throw icoe.get(); - } - break; - - case RESTORE_ENTRY: - if (prevEdit.getParentCount() == 0) { - dce.add(new DeletePath(fileName)); - break; - } - - RevCommit base = prevEdit.getParent(0); - rw.parseHeaders(base); - tw = TreeWalk.forPath(rw.getObjectReader(), fileName, base.getTree()); - if (tw == null) { - dce.add(new DeletePath(fileName)); - break; - } - - addFileToCommit(fileName, dce, tw); - break; - } - dce.finish(); - return newTree.writeTree(ins); - } - - private static void addFileToCommit(String newFile, DirCacheEditor dce, - TreeWalk tw) { - final FileMode mode = tw.getFileMode(0); - final ObjectId oid = tw.getObjectId(0); - dce.add(new PathEdit(newFile) { - @Override - public void apply(DirCacheEntry ent) { - ent.setFileMode(mode); - ent.setObjectId(oid); + try (RevWalk revWalk = new RevWalk(repository)) { + RefUpdate.Result res = ru.update(revWalk); + if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) { + throw new IOException("update failed: " + ru); } - }); + } } - private static DirCache readTree(ObjectReader reader, RevCommit prevEdit) + private void updateReferenceWithNameChange(Repository repository, + String currentRefName, ObjectId currentObjectId, String newRefName, + ObjectId targetObjectId, Timestamp timestamp) throws IOException { - DirCache dc = DirCache.newInCore(); - DirCacheBuilder b = dc.builder(); - b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, prevEdit.getTree()); - b.finish(); - return dc; + BatchRefUpdate batchRefUpdate = + repository.getRefDatabase().newBatchUpdate(); + batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), + targetObjectId, newRefName)); + batchRefUpdate.addCommand(new ReceiveCommand(currentObjectId, + ObjectId.zeroId(), currentRefName)); + batchRefUpdate.setRefLogMessage("rebase edit", false); + batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp)); + try (RevWalk revWalk = new RevWalk(repository)) { + batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE); + } + for (ReceiveCommand cmd : batchRefUpdate.getCommands()) { + if (cmd.getResult() != ReceiveCommand.Result.OK) { + throw new IOException("failed: " + cmd); + } + } } - private PersonIdent getCommitterIdent(IdentifiedUser user, Timestamp when) { - return user.newCommitterIdent(when, tz); + private PersonIdent getRefLogIdent(Timestamp timestamp) { + IdentifiedUser user = currentUser.get().asIdentifiedUser(); + return user.newRefLogIdent(timestamp, tz); } - private PersonIdent getRefLogIdent(IdentifiedUser user, Timestamp when) { - return user.newRefLogIdent(when, tz); + private void reindex(Change change) throws IOException, OrmException { + indexer.index(reviewDb.get(), change); } }
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..784d4d6 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.ListMultimap; 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,83 @@ } /** - * 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 - * @throws NoSuchChangeException + * @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 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, + ListMultimap<RecipientType, Account.Id> accountsToNotify) + throws IOException, OrmException, RestApiException, UpdateException { Change change = edit.getChange(); try (Repository repo = gitManager.openRepository(change.getProject()); RevWalk rw = new RevWalk(repo); - ObjectInserter 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."); + inserter.setDescription("Edit commit message"); + } 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 +279,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/edit/tree/AddPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/AddPath.java new file mode 100644 index 0000000..9bf8e2c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/AddPath.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; + +/** + * A {@code PathEdit} which adds a file path to the index. This operation is the + * counterpart to {@link org.eclipse.jgit.dircache.DirCacheEditor.DeletePath}. + */ +class AddPath extends DirCacheEditor.PathEdit { + + private final FileMode fileMode; + private final ObjectId objectId; + + AddPath(String filePath, FileMode fileMode, ObjectId objectId) { + super(filePath); + this.fileMode = fileMode; + this.objectId = objectId; + } + + @Override + public void apply(DirCacheEntry dirCacheEntry) { + dirCacheEntry.setFileMode(fileMode); + dirCacheEntry.setObjectId(objectId); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java new file mode 100644 index 0000000..e83155d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -0,0 +1,126 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import com.google.common.io.ByteStreams; +import com.google.gerrit.extensions.restapi.RawInput; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; + +/** + * A {@code TreeModification} which changes the content of a file. + */ +public class ChangeFileContentModification implements TreeModification { + + private static final Logger log = + LoggerFactory.getLogger(ChangeFileContentModification.class); + + private final String filePath; + private final RawInput newContent; + + public ChangeFileContentModification(String filePath, RawInput newContent) { + this.filePath = filePath; + this.newContent = checkNotNull(newContent, "new content required"); + } + + @Override + public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, + RevCommit baseCommit) { + DirCacheEditor.PathEdit changeContentEdit = new ChangeContent(filePath, + newContent, repository); + return Collections.singletonList(changeContentEdit); + } + + /** + * A {@code PathEdit} which changes the contents of a file. + */ + private static class ChangeContent extends DirCacheEditor.PathEdit { + + private final RawInput newContent; + private final Repository repository; + + ChangeContent(String filePath, RawInput newContent, Repository repository) { + super(filePath); + this.newContent = newContent; + this.repository = repository; + } + + @Override + public void apply(DirCacheEntry dirCacheEntry) { + try { + if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { + dirCacheEntry.setLength(0); + dirCacheEntry.setLastModified(0); + ObjectId newObjectId = ObjectId.fromString(getNewContentBytes(), 0); + dirCacheEntry.setObjectId(newObjectId); + } else { + if (dirCacheEntry.getRawMode() == 0) { + dirCacheEntry.setFileMode(FileMode.REGULAR_FILE); + } + ObjectId newBlobObjectId = createNewBlobAndGetItsId(); + dirCacheEntry.setObjectId(newBlobObjectId); + } + // Previously, these two exceptions were swallowed. To improve the + // situation, we log them now. However, we should think of a better + // approach. + } catch (IOException e) { + String message = String.format("Could not change the content of %s", + dirCacheEntry.getPathString()); + log.error(message, e); + } catch (InvalidObjectIdException e) { + log.error("Invalid object id in submodule link", e); + } + } + + private ObjectId createNewBlobAndGetItsId() throws IOException { + try (ObjectInserter objectInserter = repository.newObjectInserter()) { + ObjectId blobObjectId = createNewBlobAndGetItsId(objectInserter); + objectInserter.flush(); + return blobObjectId; + } + } + + private ObjectId createNewBlobAndGetItsId(ObjectInserter objectInserter) + throws IOException { + long contentLength = newContent.getContentLength(); + if (contentLength < 0) { + return objectInserter.insert(OBJ_BLOB, getNewContentBytes()); + } + InputStream contentInputStream = newContent.getInputStream(); + return objectInserter.insert(OBJ_BLOB, contentLength, contentInputStream); + } + + private byte[] getNewContentBytes() throws IOException { + return ByteStreams.toByteArray(newContent.getInputStream()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java new file mode 100644 index 0000000..64fe63b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +import java.util.Collections; +import java.util.List; + +/** + * A {@code TreeModification} which deletes a file. + */ +public class DeleteFileModification implements TreeModification { + + private final String filePath; + + public DeleteFileModification(String filePath) { + this.filePath = filePath; + } + + @Override + public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, + RevCommit baseCommit) { + DirCacheEditor.DeletePath deletePathEdit = + new DirCacheEditor.DeletePath(filePath); + return Collections.singletonList(deletePathEdit); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java new file mode 100644 index 0000000..32c1e0d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
@@ -0,0 +1,61 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A {@code TreeModification} which renames a file or moves it to a different + * path. + */ +public class RenameFileModification implements TreeModification { + + private final String currentFilePath; + private final String newFilePath; + + public RenameFileModification(String currentFilePath, String newFilePath) { + this.currentFilePath = currentFilePath; + this.newFilePath = newFilePath; + } + + @Override + public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, + RevCommit baseCommit) + throws IOException { + try (RevWalk revWalk = new RevWalk(repository)) { + revWalk.parseHeaders(baseCommit); + try (TreeWalk treeWalk = TreeWalk.forPath(revWalk.getObjectReader(), + currentFilePath, baseCommit.getTree())) { + if (treeWalk == null) { + return Collections.emptyList(); + } + DirCacheEditor.DeletePath deletePathEdit = + new DirCacheEditor.DeletePath(currentFilePath); + AddPath addPathEdit = new AddPath(newFilePath, treeWalk.getFileMode(0), + treeWalk.getObjectId(0)); + return Arrays.asList(deletePathEdit, addPathEdit); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java new file mode 100644 index 0000000..b5cc9e6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
@@ -0,0 +1,66 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * A {@code TreeModification} which restores a file. The file is added again if + * it was present before the specified commit or deleted if it was absent. + */ +public class RestoreFileModification implements TreeModification { + + private final String filePath; + + public RestoreFileModification(String filePath) { + this.filePath = filePath; + } + + @Override + public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, + RevCommit baseCommit) + throws IOException { + if (baseCommit.getParentCount() == 0) { + DirCacheEditor.DeletePath deletePath = + new DirCacheEditor.DeletePath(filePath); + return Collections.singletonList(deletePath); + } + + RevCommit base = baseCommit.getParent(0); + try (RevWalk revWalk = new RevWalk(repository)) { + revWalk.parseHeaders(base); + try (TreeWalk treeWalk = TreeWalk.forPath(revWalk.getObjectReader(), + filePath, base.getTree())) { + if (treeWalk == null) { + DirCacheEditor.DeletePath deletePath = + new DirCacheEditor.DeletePath(filePath); + return Collections.singletonList(deletePath); + } + + AddPath addPath = new AddPath(filePath, treeWalk.getFileMode(0), + treeWalk.getObjectId(0)); + return Collections.singletonList(addPath); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java new file mode 100644 index 0000000..39e6ac4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -0,0 +1,118 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import static com.google.common.base.Preconditions.checkNotNull; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A creator for a new Git tree. To create the new tree, the tree of another + * commit is taken as a basis and modified. + */ +public class TreeCreator { + + private final RevCommit baseCommit; + // At the moment, a list wouldn't be necessary as only one modification is + // applied per created tree. This is going to change in the near future. + private final List<TreeModification> treeModifications = new ArrayList<>(); + + public TreeCreator(RevCommit baseCommit) { + this.baseCommit = checkNotNull(baseCommit, "baseCommit is required"); + } + + /** + * Apply a modification to the tree which is taken as a basis. If this + * method is called multiple times, the modifications are applied + * subsequently in exactly the order they were provided. + * + * @param treeModification a modification which should be applied to the + * base tree + */ + public void addTreeModification(TreeModification treeModification) { + checkNotNull(treeModification, "treeModification must not be null"); + treeModifications.add(treeModification); + } + + /** + * Creates the new tree. When this method is called, the specified base tree + * is read from the repository, the specified modifications are applied, and + * the resulting tree is written to the object store of the repository. + * + * @param repository the affected Git repository + * @return the {@code ObjectId} of the created tree + * @throws IOException if problems arise when accessing the repository + */ + public ObjectId createNewTreeAndGetId(Repository repository) + throws IOException { + DirCache newTree = createNewTree(repository); + return writeAndGetId(repository, newTree); + } + + private DirCache createNewTree(Repository repository) throws IOException { + DirCache newTree = readBaseTree(repository); + List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository); + applyPathEdits(newTree, pathEdits); + return newTree; + } + + private DirCache readBaseTree(Repository repository) throws IOException { + try (ObjectReader objectReader = repository.newObjectReader()) { + DirCache dirCache = DirCache.newInCore(); + DirCacheBuilder dirCacheBuilder = dirCache.builder(); + dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, objectReader, + baseCommit.getTree()); + dirCacheBuilder.finish(); + return dirCache; + } + } + + private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) + throws IOException { + List<DirCacheEditor.PathEdit> pathEdits = new ArrayList<>(); + for (TreeModification treeModification : treeModifications) { + pathEdits.addAll(treeModification.getPathEdits(repository, baseCommit)); + } + return pathEdits; + } + + private static void applyPathEdits(DirCache tree, + List<DirCacheEditor.PathEdit> pathEdits) { + DirCacheEditor dirCacheEditor = tree.editor(); + pathEdits.forEach(dirCacheEditor::add); + dirCacheEditor.finish(); + } + + private static ObjectId writeAndGetId(Repository repository, DirCache tree) + throws IOException { + try (ObjectInserter objectInserter = repository.newObjectInserter()) { + ObjectId treeId = tree.writeTree(objectInserter); + objectInserter.flush(); + return treeId; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java new file mode 100644 index 0000000..4b66cd4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.edit.tree; + +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +import java.io.IOException; +import java.util.List; + +/** + * A specific modification of a Git tree. + */ +public interface TreeModification { + + /** + * Returns a list of {@code PathEdit}s which are necessary in order to + * achieve the desired modification of the Git tree. The order of the + * {@code PathEdit}s can be crucial and hence shouldn't be changed. + * + * @param repository the affected Git repository + * @param baseCommit the commit to whose tree this modification is applied + * @return an ordered list of necessary {@code PathEdit}s + * @throws IOException if problems arise when accessing the repository + */ + List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, + RevCommit baseCommit) throws IOException; + +}
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..88df57d 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.ListMultimap; 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; @@ -149,7 +148,7 @@ a.branch = change.getDest().getShortName(); a.topic = change.getTopic(); a.id = change.getKey().get(); - a.number = change.getId().toString(); + a.number = change.getId().get(); a.subject = change.getSubject(); try { a.commitMessage = changeDataFactory.create(db, change).commitMessage(); @@ -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; } @@ -348,14 +347,15 @@ private DependencyAttribute newDependencyAttribute(Change c, PatchSet ps) { DependencyAttribute d = new DependencyAttribute(); - d.number = c.getId().toString(); + d.number = c.getId().get(); d.id = c.getKey().toString(); d.revision = ps.getRevision().get(); d.ref = ps.getRefName(); return d; } - public void addTrackingIds(ChangeAttribute a, Multimap<String, String> set) { + public void addTrackingIds(ChangeAttribute a, + ListMultimap<String, String> set) { if (!set.isEmpty()) { a.trackingIds = new ArrayList<>(set.size()); for (Map.Entry<String, Collection<String>> e : set.asMap().entrySet()) { @@ -400,10 +400,9 @@ } 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 == patchSetAttribute.number) { if (patchSetAttribute.comments == null) { patchSetAttribute.comments = new ArrayList<>(); } @@ -474,7 +473,7 @@ Change change, PatchSet patchSet) { PatchSetAttribute p = new PatchSetAttribute(); p.revision = patchSet.getRevision().get(); - p.number = Integer.toString(patchSet.getPatchSetId()); + p.number = patchSet.getPatchSetId(); p.ref = patchSet.getRefName(); p.uploader = asAccountAttribute(patchSet.getUploader()); p.createdOn = patchSet.getCreatedOn().getTime() / 1000L; @@ -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..a267802 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
@@ -83,20 +83,20 @@ 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; + AccountInfo accountInfo = new AccountInfo(a.getId().get()); + accountInfo.email = a.getPreferredEmail(); + accountInfo.name = a.getFullName(); + accountInfo.username = a.getUserName(); + return accountInfo; } 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 +104,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..f4e84b2e --- /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.ListMultimap; +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 ListMultimap<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 ListMultimap<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 ListMultimap<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) { + 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..93571d6 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,31 @@ 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.git.validators.OnSubmitValidators; 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; +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,9 +66,10 @@ 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; @@ -226,7 +234,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 +354,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 +403,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 +426,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 +446,7 @@ } listener.afterUpdateRepos(); for (BatchUpdate u : updates) { - u.executeRefUpdates(); + u.executeRefUpdates(dryrun); } listener.afterRefUpdates(); break; @@ -447,9 +474,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 +494,7 @@ throw new ResourceNotFoundException(e.getMessage(), e); } catch (Exception e) { - Throwables.propagateIfPossible(e); + Throwables.throwIfUnchecked(e); throw new UpdateException(e); } } @@ -479,12 +507,13 @@ 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 skewMs; - private final long logThresholdNanos; private final Project.NameKey project; private final CurrentUser user; private final Timestamp when; @@ -504,6 +533,7 @@ private BatchRefUpdate batchRefUpdate; private boolean closeRepo; private Order order; + private OnSubmitValidators onSubmitValidators; private boolean updateChangesInParallel; private RequestId requestId; @@ -519,6 +549,7 @@ @GerritPersonIdent PersonIdent serverIdent, GitReferenceUpdated gitRefUpdated, GitRepositoryManager repoManager, + Metrics metrics, NoteDbUpdateManager.Factory updateManagerFactory, NotesMigration notesMigration, SchemaFactory<ReviewDb> schemaFactory, @@ -533,21 +564,18 @@ 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; this.when = when; tz = serverIdent.getTimeZone(); order = Order.REPO_BEFORE_DB; + skewMs = NoteDbChangeState.getReadOnlySkew(cfg); } @Override @@ -581,6 +609,15 @@ } /** + * Add a validation step for intended ref operations, which will be performed + * at the end of {@link RepoOnlyOp#updateRepo(RepoContext)} step. + */ + BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) { + this.onSubmitValidators = onSubmitValidators; + return this; + } + + /** * Execute {@link Op#updateChange(ChangeContext)} in parallel for each change. */ public BatchUpdate updateChangesInParallel() { @@ -640,13 +677,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 +698,21 @@ 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 (onSubmitValidators != null && commands != null + && !commands.isEmpty()) { + // Validation of refs has to take place here and not at the beginning + // executeRefUpdates. Otherwise failing validation in a second + // BatchUpdate object will happen *after* first object's + // executeRefUpdates has finished, hence after first repo's refs have + // been updated, which is too late. + onSubmitValidators.validate(project, + new ReadOnlyRepository(getRepository()), + ctx.getInserter().newReader(), commands.getCommands()); } if (inserter != null) { @@ -671,12 +722,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 +739,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 +752,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,27 +831,7 @@ } } - 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) { + private void executeNoteDbUpdates(List<ChangeTask> tasks) throws IOException { // Aggregate together all NoteDb ref updates from the ops we executed, // possibly in parallel. Each task had its own NoteDbUpdateManager instance // with its own thread-local copy of the repo(s), but each of those was just @@ -844,13 +892,21 @@ logDebug("No All-Users updates"); } } catch (IOException e) { - // Ignore all errors trying to update NoteDb at this point. We've - // already written the NoteDbChangeState to ReviewDb, which means - // if the state is out of date it will be rebuilt the next time it - // is needed. - // Always log even without RequestId. - log.debug( - "Ignoring NoteDb update error after ReviewDb write", e); + if (tasks.stream().allMatch(t -> t.storage == PrimaryStorage.REVIEW_DB)) { + // Ignore all errors trying to update NoteDb at this point. We've + // already written the NoteDbChangeStates to ReviewDb, which means + // if any state is out of date it will be rebuilt the next time it + // is needed. + // Always log even without RequestId. + log.debug( + "Ignoring NoteDb update error after ReviewDb write", e); + } else { + // We can't prove it's safe to ignore the error, either because some + // change had NOTE_DB primary, or a task failed before determining the + // primary storage. + throw e; + } + } } @@ -864,6 +920,7 @@ bru.setAllowNonFastForwards(true); bru.execute(rw, NullProgressMonitor.INSTANCE); for (ReceiveCommand cmd : bru.getCommands()) { + // TODO(dborowitz): LOCK_FAILURE for NoteDb primary should be retried. if (cmd.getResult() != ReceiveCommand.Result.OK) { throw new IOException("Update failed: " + bru); } @@ -874,17 +931,20 @@ final Change.Id id; private final Collection<Op> changeOps; private final Thread mainThread; + private final boolean dryrun; + PrimaryStorage storage; NoteDbUpdateManager.StagedResult noteDbResult; boolean dirty; boolean deleted; 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 +977,19 @@ @SuppressWarnings("resource") // Not always opened. NoteDbUpdateManager updateManager = null; try { - ChangeContext ctx; db.changes().beginTransaction(id); try { - ctx = newChangeContext(db, repo, rw, id); + ChangeContext ctx = newChangeContext(db, repo, rw, id); + NoteDbChangeState oldState = NoteDbChangeState.parse(ctx.getChange()); + NoteDbChangeState.checkNotReadOnly(oldState, skewMs); + + storage = PrimaryStorage.of(oldState); + 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 +1009,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 @@ -988,19 +1070,42 @@ } private ChangeContext newChangeContext(ReviewDb db, Repository repo, - RevWalk rw, Change.Id id) throws OrmException, NoSuchChangeException { + RevWalk rw, Change.Id id) throws OrmException { Change c = newChanges.get(id); - if (c == null) { - c = ReviewDbUtil.unwrapDb(db).changes().get(id); - if (c == null) { - logDebug("Failed to get change {} from unwrapped db", id); - throw new NoSuchChangeException(id); + boolean isNew = c != null; + if (isNew) { + // New change: populate noteDbState. + checkState(c.getNoteDbState() == null, + "noteDbState should not be filled in by callers"); + if (notesMigration.changePrimaryStorage() == PrimaryStorage.NOTE_DB) { + c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); } + } else { + // Existing change. + c = ChangeNotes.readOneReviewDbChange(db, id); + if (c == null) { + // Not in ReviewDb, but new changes are created with default primary + // storage as NOTE_DB, so we can assume that a missing change is + // NoteDb primary. Pass a synthetic change into ChangeNotes.Factory, + // which lets ChangeNotes take care of the existence check. + // + // TODO(dborowitz): This assumption is potentially risky, because + // it means once we turn this option on and start creating changes + // without writing anything to ReviewDb, we can't turn this option + // back off without making those changes inaccessible. The problem + // is we have no way of distinguishing a change that only exists in + // NoteDb because it only ever existed in NoteDb, from a change that + // only exists in NoteDb because it used to exist in ReviewDb and + // deleting from ReviewDb succeeded but deleting from NoteDb failed. + // + // TODO(dborowitz): We actually still have that problem anyway. Maybe + // we need a cutoff timestamp? Or maybe we need to start leaving + // tombstones in ReviewDb? + c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id); + } + NoteDbChangeState.checkNotReadOnly(c, skewMs); } - // Pass in preloaded change to controlFor, to avoid: - // - reading from a db that does not belong to this update - // - attempting to read a change that doesn't exist yet - ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c); + ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew); ChangeControl ctl = changeControlFactory.controlFor(notes, user); return new ChangeContext(ctl, new BatchUpdateReviewDb(db), repo, rw); } @@ -1012,23 +1117,36 @@ .create(ctx.getProject()) .setChangeRepo(ctx.getRepository(), ctx.getRevWalk(), null, new ChainedReceiveCommands(repo)); + if (ctx.getUser().isIdentifiedUser()) { + updateManager.setRefLogIdent( + ctx.getUser().asIdentifiedUser().newRefLogIdent(ctx.getWhen(), tz)); + } 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 16e4bd9..400532d 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,13 +14,12 @@ package com.google.gerrit.server.git; -import com.google.common.collect.ArrayListMultimap; 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.MultimapBuilder; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.query.change.ChangeData; @@ -82,10 +81,10 @@ return changeData; } - public Multimap<Branch.NameKey, ChangeData> changesByBranch() + public ListMultimap<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..6fa3c2a 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; @@ -33,7 +32,6 @@ import org.eclipse.jgit.revwalk.RevWalk; import java.io.IOException; -import java.util.List; /** Extended commit entity with code review specific metadata. */ public class CodeReviewCommit extends RevCommit { @@ -46,14 +44,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); @@ -129,9 +124,6 @@ */ private CommitMergeStatus statusCode; - /** Commits which are missing ancestors of this commit. */ - List<CodeReviewCommit> missing; - public CodeReviewCommit(final AnyObjectId id) { super(id); } @@ -160,7 +152,6 @@ control = src.control; patchsetId = src.patchsetId; statusCode = src.statusCode; - missing = src.missing; } public Change change() {
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..1da5257 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.ListMultimap; 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, + ListMultimap<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 ListMultimap<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 ListMultimap<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 26c59c2..192691d 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
@@ -65,35 +65,40 @@ this.submissionId = orm.getSubmissionId(); Project.NameKey project = branch.getParentKey(); logDebug("Loading .gitmodules of {} for project {}", branch, project); - OpenRepo or; + OpenRepo or = null; try { or = orm.openRepo(project); + ObjectId id = or.repo.resolve(branch.get()); + if (id == null) { + throw new IOException("Cannot open branch " + branch.get()); + } + RevCommit commit = or.rw.parseCommit(id); + + try (TreeWalk tw = + TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree())) { + if (tw == null + || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) { + subscriptions = Collections.emptySet(); + logDebug("The .gitmodules file doesn't exist in " + branch); + return; + } + } + BlobBasedConfig bbc; + try { + bbc = new BlobBasedConfig(null, or.repo, commit, GIT_MODULES); + } catch (ConfigInvalidException e) { + throw new IOException("Could not read .gitmodules of super project: " + + branch.getParentKey(), e); + } + subscriptions = new SubmoduleSectionParser(bbc, canonicalWebUrl, + branch).parseAllSections(); } catch (NoSuchProjectException e) { throw new IOException(e); + } finally { + if (or != null) { + or.close(); + } } - - ObjectId id = or.repo.resolve(branch.get()); - if (id == null) { - throw new IOException("Cannot open branch " + branch.get()); - } - RevCommit commit = or.rw.parseCommit(id); - - TreeWalk tw = TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree()); - if (tw == null - || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) { - subscriptions = Collections.emptySet(); - logDebug("The .gitmodules file doesn't exist in " + branch); - return; - } - BlobBasedConfig bbc; - try { - bbc = new BlobBasedConfig(null, or.repo, commit, GIT_MODULES); - } catch (ConfigInvalidException e) { - throw new IOException("Could not read .gitmodules of super project: " + - branch.getParentKey(), e); - } - subscriptions = new SubmoduleSectionParser(bbc, canonicalWebUrl, - branch).parseAllSections(); } public Collection<SubmoduleSubscription> subscribedTo(Branch.NameKey src) {
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..0ac39ad 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,14 +18,10 @@ 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; 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.SetMultimap; @@ -37,7 +33,6 @@ import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.notedb.ChangeNotes; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import org.eclipse.jgit.lib.ObjectId; @@ -104,26 +99,24 @@ } private interface Lookup { - List<String> lookup(PatchSet.Id psId) - throws OrmException, NoSuchChangeException; + List<String> lookup(PatchSet.Id psId) throws OrmException; } - private final Multimap<ObjectId, PatchSet.Id> patchSetsBySha; - private final Multimap<ObjectId, String> groups; + private final ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha; + private final ListMultimap<ObjectId, String> groups; private final SetMultimap<String, String> groupAliases; private final Lookup groupLookup; private boolean done; - public static GroupCollector create(Multimap<ObjectId, Ref> changeRefsById, - final ReviewDb db, final PatchSetUtil psUtil, - final ChangeNotes.Factory notesFactory, final Project.NameKey project) { + public static GroupCollector create(ListMultimap<ObjectId, Ref> changeRefsById, + ReviewDb db, PatchSetUtil psUtil, ChangeNotes.Factory notesFactory, + Project.NameKey project) { return new GroupCollector( transformRefs(changeRefsById), new Lookup() { @Override - public List<String> lookup(PatchSet.Id psId) - throws OrmException, NoSuchChangeException { + public List<String> lookup(PatchSet.Id psId) throws OrmException { // TODO(dborowitz): Reuse open repository from caller. ChangeNotes notes = notesFactory.createChecked(db, project, psId.getParentKey()); @@ -134,7 +127,7 @@ } public static GroupCollector createForSchemaUpgradeOnly( - Multimap<ObjectId, Ref> changeRefsById, final ReviewDb db) { + ListMultimap<ObjectId, Ref> changeRefsById, ReviewDb db) { return new GroupCollector( transformRefs(changeRefsById), new Lookup() { @@ -147,30 +140,24 @@ } private GroupCollector( - Multimap<ObjectId, PatchSet.Id> patchSetsBySha, + ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha, 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) { + private static ListMultimap<ObjectId, PatchSet.Id> transformRefs( + ListMultimap<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 GroupCollector( - Multimap<ObjectId, PatchSet.Id> patchSetsBySha, - final ListMultimap<PatchSet.Id, String> groupLookup) { + ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha, + ListMultimap<PatchSet.Id, String> groupLookup) { this( patchSetsBySha, new Lookup() { @@ -242,8 +229,7 @@ } } - public SortedSetMultimap<ObjectId, String> getGroups() - throws OrmException, NoSuchChangeException { + public SortedSetMultimap<ObjectId, String> getGroups() throws OrmException { done = true; SortedSetMultimap<ObjectId, String> result = MultimapBuilder .hashKeys(groups.keySet().size()) @@ -274,8 +260,7 @@ } private Set<String> resolveGroups(ObjectId forCommit, - Collection<String> candidates) - throws OrmException, NoSuchChangeException { + Collection<String> candidates) throws OrmException { Set<String> actual = Sets.newTreeSet(); Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size()); Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size()); @@ -312,7 +297,7 @@ } private Iterable<String> resolveGroup(ObjectId forCommit, String group) - throws OrmException, NoSuchChangeException { + throws OrmException { ObjectId id = parseGroup(forCommit, group); if (id != null) { PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
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/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java index f3b2ac9..7832e0b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -32,7 +32,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.project.ChangeControl; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -95,11 +94,10 @@ * type and permissions for the user. Approvals for unknown labels are not * included in the output, nor are approvals where the user has no * permissions for that label. - * @throws NoSuchChangeException * @throws OrmException */ public Result normalize(Change change, Collection<PatchSetApproval> approvals) - throws NoSuchChangeException, OrmException { + throws OrmException { IdentifiedUser user = userFactory.create(change.getOwner()); return normalize( changeFactory.controlFor(db.get(), change, user), approvals);
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/git/LockFailureException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LockFailureException.java new file mode 100644 index 0000000..7380b0a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LockFailureException.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.server.git; + +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 f16c997..55b236a 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.ImmutableListMultimap; 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.ListMultimap; import com.google.common.collect.MultimapBuilder; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; +import com.google.common.collect.SetMultimap; 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; @@ -87,6 +84,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * Merges changes in submission order into a single branch. @@ -105,11 +103,14 @@ 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; private final Map<Change.Id, CodeReviewCommit> commits; - private final Multimap<Change.Id, String> problems; + private final ListMultimap<Change.Id, String> problems; private CommitStatus(ChangeSet cs) throws OrmException { checkArgument(!cs.furtherHiddenChanges(), @@ -122,13 +123,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() { @@ -166,8 +164,8 @@ return problems.isEmpty(); } - public ImmutableMultimap<Change.Id, String> getProblems() { - return ImmutableMultimap.copyOf(problems); + public ImmutableListMultimap<Change.Id, String> getProblems() { + return ImmutableListMultimap.copyOf(problems); } public List<SubmitRecord> getSubmitRecords(Change.Id id) { @@ -180,7 +178,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,14 +220,18 @@ private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; private final MergeOpRepoManager orm; + private final NotifyUtil notifyUtil; private Timestamp ts; private RequestId submissionId; private IdentifiedUser caller; - private CommitStatus commits; + private CommitStatus commitStatus; private ReviewDb db; private SubmitInput submitInput; + private ListMultimap<RecipientType, Account.Id> accountsToNotify; + private Set<Project.NameKey> allProjects; + private boolean dryrun; @Inject MergeOp(ChangeMessagesUtil cmUtil, @@ -240,7 +242,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 +253,7 @@ this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; this.orm = orm; + this.notifyUtil = notifyUtil; } @Override @@ -257,19 +261,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 +269,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 +309,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, @@ -366,20 +352,20 @@ for (ChangeData cd : cs.changes()) { try { if (cd.change().getStatus() != Change.Status.NEW) { - commits.problem(cd.getId(), "Change " + cd.getId() + " is " + commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase()); } else { checkSubmitRule(cd); } } catch (ResourceConflictException e) { - commits.problem(cd.getId(), e.getMessage()); + commitStatus.problem(cd.getId(), e.getMessage()); } catch (OrmException e) { String msg = "Error checking submit rules for change"; log.warn(msg + " " + cd.getId(), e); - commits.problem(cd.getId(), msg); + commitStatus.problem(cd.getId(), msg); } } - commits.maybeFailVerbose(); + commitStatus.maybeFailVerbose(); } private void bypassSubmitRules(ChangeSet cs) { @@ -396,14 +382,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,14 +416,15 @@ 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()) { throw new AuthException("A change to be submitted with " + change.getId() + " is not visible"); } - this.commits = new CommitStatus(cs); + this.commitStatus = new CommitStatus(cs); MergeSuperSet.reloadChanges(cs); logDebug("Calculated to merge {}", cs); if (checkSubmitRules) { @@ -448,7 +453,7 @@ logDebug("Beginning merge attempt on {}", cs); Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>(); - Multimap<Branch.NameKey, ChangeData> cbb; + ListMultimap<Branch.NameKey, ChangeData> cbb; try { cbb = cs.changesByBranch(); } catch (OrmException e) { @@ -462,14 +467,15 @@ } } // Done checks that don't involve running submit strategies. - commits.maybeFailVerbose(); + commitStatus.maybeFailVerbose(); SubmoduleOp submoduleOp = subOpFactory.create(branches, orm); try { - List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp); - Set<Project.NameKey> allProjects = submoduleOp.getProjectsInOrder(); + List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, + submoduleOp, dryrun); + this.allProjects = submoduleOp.getProjectsInOrder(); BatchUpdate.execute(orm.batchUpdates(allProjects), - new SubmitStrategyListener(submitInput, strategies, commits), - submissionId); + new SubmitStrategyListener(submitInput, strategies, commitStatus), + submissionId, dryrun); } catch (SubmoduleException e) { throw new IntegrationException(e); } catch (UpdateException e) { @@ -490,11 +496,22 @@ } } + 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(); + Set<CodeReviewCommit> allCommits = + toSubmit.values().stream().map(BranchBatch::commits) + .flatMap(Set::stream).collect(Collectors.toSet()); for (Branch.NameKey branch : allBranches) { OpenRepo or = orm.getRepo(branch.getParentKey()); if (toSubmit.containsKey(branch)) { @@ -503,10 +520,13 @@ checkNotNull(submitting.submitType(), "null submit type for %s; expected to previously fail fast", submitting); - Set<CodeReviewCommit> commitsToSubmit = commits(submitting.changes()); + Set<CodeReviewCommit> commitsToSubmit = submitting.commits(); ob.mergeTip = new MergeTip(ob.oldTip, commitsToSubmit); - SubmitStrategy strategy = createStrategy(or, ob.mergeTip, branch, - submitting.submitType(), ob.oldTip, submoduleOp); + SubmitStrategy strategy = submitStrategyFactory + .create(submitting.submitType(), db, or.repo, or.rw, or.ins, + or.canMergeFlag, getAlreadyAccepted(or, ob.oldTip), allCommits, + branch, caller, ob.mergeTip, commitStatus, submissionId, + submitInput.notify, accountsToNotify, submoduleOp, dryrun); strategies.add(strategy); strategy.addOps(or.getUpdate(), commitsToSubmit); if (submitting.submitType().equals(SubmitType.FAST_FORWARD_ONLY) && @@ -522,26 +542,6 @@ return strategies; } - private Set<CodeReviewCommit> commits(List<ChangeData> cds) { - LinkedHashSet<CodeReviewCommit> result = - Sets.newLinkedHashSetWithExpectedSize(cds.size()); - for (ChangeData cd : cds) { - CodeReviewCommit commit = commits.get(cd.getId()); - checkState(commit != null, - "commit for %s not found by validateChangeList", cd.getId()); - result.add(commit); - } - return result; - } - - private SubmitStrategy createStrategy(OpenRepo or, - MergeTip mergeTip, Branch.NameKey destBranch, SubmitType submitType, - CodeReviewCommit branchTip, SubmoduleOp submoduleOp) 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); - } - private Set<RevCommit> getAlreadyAccepted(OpenRepo or, CodeReviewCommit branchTip) throws IntegrationException { Set<RevCommit> alreadyAccepted = new HashSet<>(); @@ -555,7 +555,7 @@ .values()) { try { CodeReviewCommit aac = or.rw.parseCommit(r.getObjectId()); - if (!commits.commits.values().contains(aac)) { + if (!commitStatus.commits.values().contains(aac)) { alreadyAccepted.add(aac); } } catch (IncorrectObjectTypeException iote) { @@ -574,14 +574,14 @@ @AutoValue abstract static class BranchBatch { @Nullable abstract SubmitType submitType(); - abstract List<ChangeData> changes(); + abstract Set<CodeReviewCommit> commits(); } private BranchBatch validateChangeList(OpenRepo or, Collection<ChangeData> submitted) throws IntegrationException { logDebug("Validating {} changes", submitted.size()); - List<ChangeData> toSubmit = new ArrayList<>(submitted.size()); - Multimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted); + Set<CodeReviewCommit> toSubmit = new LinkedHashSet<>(submitted.size()); + SetMultimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted); SubmitType submitType = null; ChangeData choseSubmitTypeFrom = null; @@ -593,20 +593,20 @@ ctl = cd.changeControl(); chg = cd.change(); } catch (OrmException e) { - commits.logProblem(changeId, e); + commitStatus.logProblem(changeId, e); continue; } SubmitType st = getSubmitType(cd); if (st == null) { - commits.logProblem(changeId, "No submit type for change"); + commitStatus.logProblem(changeId, "No submit type for change"); continue; } if (submitType == null) { submitType = st; choseSubmitTypeFrom = cd; } else if (st != submitType) { - commits.problem(changeId, String.format( + commitStatus.problem(changeId, String.format( "Change has submit type %s, but previously chose submit type %s " + "from change %s in the same batch", st, submitType, choseSubmitTypeFrom.getId())); @@ -615,7 +615,7 @@ if (chg.currentPatchSetId() == null) { String msg = "Missing current patch set on change"; logError(msg + " " + changeId); - commits.problem(changeId, msg); + commitStatus.problem(changeId, msg); continue; } @@ -624,12 +624,12 @@ try { ps = cd.currentPatchSet(); } catch (OrmException e) { - commits.logProblem(changeId, e); + commitStatus.logProblem(changeId, e); continue; } if (ps == null || ps.getRevision() == null || ps.getRevision().get() == null) { - commits.logProblem(changeId, "Missing patch set or revision on change"); + commitStatus.logProblem(changeId, "Missing patch set or revision on change"); continue; } @@ -638,7 +638,7 @@ try { id = ObjectId.fromString(idstr); } catch (IllegalArgumentException e) { - commits.logProblem(changeId, e); + commitStatus.logProblem(changeId, e); continue; } @@ -647,7 +647,7 @@ // want to merge the issue. We can't safely do that if the // tip is not reachable. // - commits.logProblem(changeId, "Revision " + idstr + " of patch set " + commitStatus.logProblem(changeId, "Revision " + idstr + " of patch set " + ps.getPatchSetId() + " does not match " + ps.getId().toRefName() + " for change"); continue; @@ -657,31 +657,31 @@ try { commit = or.rw.parseCommit(id); } catch (IOException e) { - commits.logProblem(changeId, e); + commitStatus.logProblem(changeId, e); continue; } // TODO(dborowitz): Consider putting ChangeData in CodeReviewCommit. commit.setControl(ctl); commit.setPatchsetId(ps.getId()); - commits.put(commit); + commitStatus.put(commit); MergeValidators mergeValidators = mergeValidatorsFactory.create(); try { mergeValidators.validatePreMerge( or.repo, commit, or.project, destBranch, ps.getId(), caller); } catch (MergeValidationException mve) { - commits.problem(changeId, mve.getMessage()); + commitStatus.problem(changeId, mve.getMessage()); continue; } commit.add(or.canMergeFlag); - toSubmit.add(cd); + toSubmit.add(commit); } logDebug("Submitting on this run: {}", toSubmit); return new AutoValue_MergeOp_BranchBatch(submitType, toSubmit); } - private Multimap<ObjectId, PatchSet.Id> getRevisions(OpenRepo or, + private SetMultimap<ObjectId, PatchSet.Id> getRevisions(OpenRepo or, Collection<ChangeData> cds) throws IntegrationException { try { List<String> refNames = new ArrayList<>(cds.size()); @@ -691,8 +691,8 @@ refNames.add(c.currentPatchSetId().toRefName()); } } - Multimap<ObjectId, PatchSet.Id> revisions = - HashMultimap.create(cds.size(), 1); + SetMultimap<ObjectId, PatchSet.Id> revisions = + 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( @@ -745,11 +745,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( + 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 cd76aff..1244ad3 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
@@ -22,6 +22,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; +import com.google.gerrit.server.git.validators.OnSubmitValidators; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; @@ -90,16 +91,25 @@ 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) .setRepository(repo, rw, ins) - .setRequestId(submissionId); + .setRequestId(submissionId) + .setOnSubmitValidators(onSubmitValidatorsFactory.create()); } return update; } @@ -149,6 +159,7 @@ private final Map<Project.NameKey, OpenRepo> openRepos; private final BatchUpdate.Factory batchUpdateFactory; + private final OnSubmitValidators.Factory onSubmitValidatorsFactory; private final GitRepositoryManager repoManager; private final ProjectCache projectCache; @@ -161,10 +172,12 @@ MergeOpRepoManager( GitRepositoryManager repoManager, ProjectCache projectCache, - BatchUpdate.Factory batchUpdateFactory) { + BatchUpdate.Factory batchUpdateFactory, + OnSubmitValidators.Factory onSubmitValidatorsFactory) { this.repoManager = repoManager; this.projectCache = projectCache; this.batchUpdateFactory = batchUpdateFactory; + this.onSubmitValidatorsFactory = onSubmitValidatorsFactory; openRepos = new HashMap<>(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java index 197b8c5..059a23e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -22,7 +22,6 @@ import org.eclipse.jgit.revwalk.RevFlag; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -40,10 +39,15 @@ this.accepted = alreadyAccepted; } - Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming) - throws IOException { + Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> toMerge) + throws IOException { + return sort(toMerge, toMerge); + } + + Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> toMerge, + final Collection<CodeReviewCommit> incoming) throws IOException { final Set<CodeReviewCommit> heads = new HashSet<>(); - final Set<CodeReviewCommit> sort = new HashSet<>(incoming); + final Set<CodeReviewCommit> sort = new HashSet<>(toMerge); while (!sort.isEmpty()) { final CodeReviewCommit n = removeOne(sort); @@ -60,14 +64,10 @@ // We cannot merge n as it would bring something we // aren't permitted to merge at this time. Drop n. // - if (n.missing == null) { - n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY); - n.missing = new ArrayList<>(); - } - n.missing.add(c); - } else { - contents.add(c); + n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY); + break; } + contents.add(c); } if (n.getStatusCode() == CommitMergeStatus.MISSING_DEPENDENCY) {
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..0c304a2 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( @@ -171,10 +205,11 @@ } public List<CodeReviewCommit> reduceToMinimalMerge(MergeSorter mergeSorter, - Collection<CodeReviewCommit> toSort) throws IntegrationException { + Collection<CodeReviewCommit> toSort, Set<CodeReviewCommit> incoming) + throws IntegrationException { List<CodeReviewCommit> result = new ArrayList<>(); try { - result.addAll(mergeSorter.sort(toSort)); + result.addAll(mergeSorter.sort(toSort, incoming)); } catch (IOException e) { throw new IntegrationException("Branch head sorting failed", e); } @@ -185,16 +220,16 @@ public CodeReviewCommit createCherryPickFromCommit(Repository repo, ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit, PersonIdent cherryPickCommitterIdent, String commitMsg, - CodeReviewRevWalk rw) + CodeReviewRevWalk rw, int parentIndex, boolean ignoreIdenticalTree) 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())) { + if (tree.equals(mergeTip.getTree()) && !ignoreIdenticalTree) { throw new MergeIdenticalTreeException("identical tree"); } @@ -214,7 +249,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 +282,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 +403,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) { @@ -424,7 +497,8 @@ } try { - return mergeTip == null || rw.isMergedInto(mergeTip, toMerge); + return mergeTip == null || rw.isMergedInto(mergeTip, toMerge) + || rw.isMergedInto(toMerge, mergeTip); } catch (IOException e) { throw new IntegrationException("Cannot fast-forward test during merge", e); } @@ -599,14 +673,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..1502c4a 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( + 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/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java index d8ed075..b04b0f7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -16,7 +16,7 @@ import com.google.common.base.Strings; import com.google.gerrit.common.data.GroupReference; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.mail.Address; import java.util.EnumSet;
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 585909a..c101bbb 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
@@ -45,11 +45,11 @@ import com.google.gerrit.extensions.client.ProjectState; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.mail.Address; @@ -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 { @@ -485,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)); @@ -499,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)); @@ -631,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); } @@ -638,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()); } @@ -788,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)); @@ -809,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); } } @@ -908,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() { @@ -950,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()); @@ -1151,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); } } @@ -1181,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(), @@ -1291,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/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java index 6448d06..3284c12 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -14,9 +14,19 @@ package com.google.gerrit.server.git; +import com.google.gerrit.extensions.client.ChangeKind; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.change.ChangeKindCache; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.strategy.CommitMergeStatus; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gwtorm.server.OrmException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; @@ -34,13 +44,21 @@ private final RevFlag canMergeFlag; private final RevCommit initialTip; private final Set<RevCommit> alreadyAccepted; + private final InternalChangeQuery internalChangeQuery; + private final ChangeKindCache changeKindCache; + private final Repository repo; public RebaseSorter(CodeReviewRevWalk rw, RevCommit initialTip, - Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag) { + Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag, + InternalChangeQuery internalChangeQuery, + ChangeKindCache changeKindCache, Repository repo) { this.rw = rw; this.canMergeFlag = canMergeFlag; this.initialTip = initialTip; this.alreadyAccepted = alreadyAccepted; + this.internalChangeQuery = internalChangeQuery; + this.changeKindCache = changeKindCache; + this.repo = repo; } public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming) @@ -60,21 +78,19 @@ final List<CodeReviewCommit> contents = new ArrayList<>(); while ((c = rw.next()) != null) { if (!c.has(canMergeFlag) || !incoming.contains(c)) { - if (isAlreadyMerged(c)) { + if (isAlreadyMerged(c, n.change().getDest())) { rw.markUninteresting(c); - break; - } - // We cannot merge n as it would bring something we - // aren't permitted to merge at this time. Drop n. - // - if (n.missing == null) { + } else { + // We cannot merge n as it would bring something we + // aren't permitted to merge at this time. Drop n. + // n.setStatusCode(CommitMergeStatus.MISSING_DEPENDENCY); - n.missing = new ArrayList<>(); } - n.missing.add(c); - } else { - contents.add(c); + // Stop RevWalk because c is either a merged commit or a missing + // dependency. Not need to walk further. + break; } + contents.add(c); } if (n.getStatusCode() == CommitMergeStatus.MISSING_DEPENDENCY) { @@ -89,19 +105,41 @@ return sorted; } - private boolean isAlreadyMerged(CodeReviewCommit commit) throws IOException { + private boolean isAlreadyMerged(CodeReviewCommit commit, Branch.NameKey dest) + throws IOException { try (CodeReviewRevWalk mirw = CodeReviewCommit.newRevWalk(rw.getObjectReader())) { mirw.reset(); mirw.markStart(commit); + // check if the commit is merged in other branches for (RevCommit accepted : alreadyAccepted) { if (mirw.isMergedInto(mirw.parseCommit(accepted), mirw.parseCommit(commit))) { return true; } } + + // check if the commit associated change is merged in the same branch + List<ChangeData> changes = internalChangeQuery.byCommit(commit); + for (ChangeData change : changes) { + if (change.change().getStatus() == Status.MERGED + && change.change().getDest().equals(dest) + && !isRework(dest.getParentKey(), commit, change)) { + return true; + } + } + return false; + } catch (OrmException e) { + return false; } - return false; + } + + private boolean isRework(Project.NameKey project, RevCommit oldCommit, + ChangeData change) throws OrmException, IOException { + RevCommit currentCommit = rw.parseCommit( + ObjectId.fromString(change.currentPatchSet().getRevision().get())); + return ChangeKind.REWORK == changeKindCache + .getChangeKind(project, repo, oldCommit, currentCommit); } private static <T> T removeOne(final Collection<T> c) {
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 c3ea968..bc7c522 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,25 +33,18 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; import com.google.common.base.Function; -import com.google.common.base.Joiner; -import com.google.common.base.Optional; -import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Strings; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.BiMap; -import com.google.common.collect.Collections2; -import com.google.common.collect.FluentIterable; import com.google.common.collect.HashBiMap; -import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import com.google.common.collect.SetMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Sets; import com.google.common.collect.SortedSetMultimap; import com.google.gerrit.common.Nullable; @@ -60,6 +56,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 +100,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 +167,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 +287,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 +313,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 = @@ -323,7 +323,7 @@ private final Set<ObjectId> validCommits = new HashSet<>(); private ListMultimap<Change.Id, Ref> refsByChange; - private SetMultimap<ObjectId, Ref> refsById; + private ListMultimap<ObjectId, Ref> refsById; private Map<String, Ref> allRefs; private final SubmoduleOp.Factory subOpFactory; @@ -332,6 +332,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 +354,6 @@ PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, ProjectCache projectCache, - GitRepositoryManager repoManager, TagCache tagCache, AccountCache accountCache, @Nullable SearchingChangeCacheImpl changeCache, @@ -376,6 +376,7 @@ DynamicMap<ProjectConfigEntry> pluginConfigEntries, NotesMigration notesMigration, ChangeEditUtil editUtil, + ChangeIndexer indexer, BatchUpdate.Factory batchUpdateFactory, SetHashtagsOp.Factory hashtagsFactory, ReplaceOp.Factory replaceOpFactory, @@ -391,7 +392,6 @@ this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.projectCache = projectCache; - this.repoManager = repoManager; this.canonicalWebUrl = canonicalWebUrl; this.tagCache = tagCache; this.accountCache = accountCache; @@ -423,6 +423,7 @@ this.notesMigration = notesMigration; this.editUtil = editUtil; + this.indexer = indexer; this.messageSender = new ReceivePackMessageSender(); @@ -490,6 +491,7 @@ advHooks.add(new HackPushNegotiateHook()); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setPostReceiveHook(lazyPostReceive.get()); + rp.setAllowPushOptions(true); } public void init() { @@ -643,16 +645,15 @@ 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) - && !refName.startsWith(REFS_CHANGES)) { + if (!MagicBranch.isMagicBranch(refName)) { logDebug("Firing ref update for {}", c.getRefName()); - // We only fire gitRefUpdated for direct refs updates. - // Events for change refs are fired when they are created. - // gitRefUpdated.fire(project.getNameKey(), c, user.getAccount()); } else { logDebug("Assuming ref update event for {} has fired", @@ -677,14 +678,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 +691,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,15 +812,16 @@ // 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())); return; } - try (BatchUpdate bu = batchUpdateFactory.create(db, - magicBranch.dest.getParentKey(), user, TimeUtil.nowTs()); + try (BatchUpdate bu = + batchUpdateFactory.create(db, magicBranch.dest.getParentKey(), + user.materializedCopy(), TimeUtil.nowTs()); ObjectInserter ins = repo.newObjectInserter()) { bu.setRepository(repo, rp.getRevWalk(), ins) .updateChangesInParallel(); @@ -915,6 +901,18 @@ } private void parseCommands(Collection<ReceiveCommand> commands) { + List<String> optionList = rp.getPushOptions(); + if (optionList != null) { + for (String option : optionList) { + int e = option.indexOf('='); + if (e > 0) { + pushOptions.put(option.substring(0, e), option.substring(e + 1)); + } else { + pushOptions.put(option, ""); + } + } + } + logDebug("Parsing {} commands", commands.size()); for (ReceiveCommand cmd : commands) { if (cmd.getResult() != NOT_ATTEMPTED) { @@ -1040,11 +1038,12 @@ .getPluginConfig(e.getPluginName()) .getString(e.getExportName()); if (configEntry.getType() == ProjectConfigEntryType.ARRAY) { - List<String> l = - Arrays.asList(projectControl.getProjectState() - .getConfig().getPluginConfig(e.getPluginName()) - .getStringList(e.getExportName())); - oldValue = Joiner.on("\n").join(l); + oldValue = + Arrays.stream( + projectControl.getProjectState() + .getConfig().getPluginConfig(e.getPluginName()) + .getStringList(e.getExportName())) + .collect(joining("\n")); } if ((value == null ? oldValue != null : !value.equals(oldValue)) && @@ -1238,12 +1237,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 +1319,23 @@ return new MailRecipients(reviewer, cc); } - String parse(CmdLineParser clp, Repository repo, Set<String> refs) - throws CmdLineException { + ListMultimap<RecipientType, Account.Id> getAccountsToNotify() { + ListMultimap<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 +1344,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 +1373,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 +1401,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 +1449,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 +1470,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 +1486,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 +1597,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) { @@ -1561,14 +1628,14 @@ try { changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId) .getChange(); - } catch (OrmException e) { - logError("Cannot lookup existing change " + changeId, e); - reject(cmd, "database error"); - return; } catch (NoSuchChangeException e) { logError("Change not found " + changeId, e); reject(cmd, "change " + changeId + " not found"); return; + } catch (OrmException e) { + logError("Cannot lookup existing change " + changeId, e); + reject(cmd, "database error"); + return; } if (!project.getNameKey().equals(changeEnt.getProject())) { reject(cmd, "change " + changeId + " does not belong to project " + project.getName()); @@ -1600,33 +1667,15 @@ logDebug("Finding new and replaced changes"); newChanges = new ArrayList<>(); - SetMultimap<ObjectId, Ref> existing = changeRefsById(); - GroupCollector groupCollector = GroupCollector.create(changeRefsById(), db, psUtil, + ListMultimap<ObjectId, Ref> existing = changeRefsById(); + 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 +1685,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 +1708,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); } @@ -1742,19 +1793,16 @@ 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(); - } - })); - // WTF, multiple changes in this project have the same key? + logDebug("Multiple changes in branch {} with Change-Id {}: {}", + magicBranch.dest, + p.changeKey, + changes.stream() + .map(cd -> cd.getId().toString()) + .collect(joining())); + // WTF, multiple changes in this branch 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 - // this error message as Change-Id should be unique. + // this error message as Change-Id should be unique per branch. // reject(magicBranch.cmd, p.changeKey.get() + " has duplicates"); newChanges = Collections.emptyList(); @@ -1794,6 +1842,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())); @@ -1840,15 +1900,69 @@ update.groups = ImmutableList.copyOf((groups.get(update.commit))); } logDebug("Finished updating groups from GroupCollector"); - } catch (OrmException | NoSuchChangeException e) { + } catch (OrmException e) { logError("Error collecting groups for changes", e); reject(magicBranch.cmd, "internal server error"); return; } } + 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) { @@ -1931,13 +2045,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()); } @@ -1974,9 +2092,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, @@ -2028,7 +2148,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); } } @@ -2092,14 +2212,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; } @@ -2332,10 +2446,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)); @@ -2400,10 +2516,11 @@ private void initChangeRefMaps() { if (refsByChange == null) { int estRefsPerChange = 4; - refsById = HashMultimap.create(); - refsByChange = ArrayListMultimap.create( - allRefs.size() / estRefsPerChange, - estRefsPerChange); + refsById = MultimapBuilder.hashKeys().arrayListValues().build(); + refsByChange = + MultimapBuilder.hashKeys(allRefs.size() / estRefsPerChange) + .arrayListValues(estRefsPerChange) + .build(); for (Ref ref : allRefs.values()) { ObjectId obj = ref.getObjectId(); if (obj != null) { @@ -2422,7 +2539,7 @@ return refsByChange; } - private SetMultimap<ObjectId, Ref> changeRefsById() { + private ListMultimap<ObjectId, Ref> changeRefsById() { initChangeRefMaps(); return refsById; } @@ -2502,7 +2619,7 @@ if (!(parsedObject instanceof RevCommit)) { return; } - SetMultimap<ObjectId, Ref> existing = changeRefsById(); + ListMultimap<ObjectId, Ref> existing = changeRefsById(); walk.markStart((RevCommit)parsedObject); markHeadsAsUninteresting(walk, cmd.getRefName()); int i = 0; @@ -2549,12 +2666,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()); @@ -2591,7 +2716,7 @@ rw.markUninteresting(rw.parseCommit(cmd.getOldId())); } - SetMultimap<ObjectId, Ref> byCommit = changeRefsById(); + ListMultimap<ObjectId, Ref> byCommit = changeRefsById(); Map<Change.Key, ChangeNotes> byKey = null; List<ReplaceRequest> replaceAndClose = new ArrayList<>();
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..2de5378 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,10 +45,9 @@ 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; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.util.RequestScopePropagator; @@ -65,11 +63,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 +77,6 @@ public class ReplaceOp extends BatchUpdate.Op { public interface Factory { ReplaceOp create( - RequestScopePropagator requestScopePropagator, ProjectControl projectControl, Branch.NameKey dest, boolean checkMergedInto, @@ -113,7 +110,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 +130,8 @@ private ChangeMessage msg; private String rejectMessage; private MergedByPushOp mergedByPushOp; + private RequestScopePropagator requestScopePropagator; + private boolean updateRef; @AssistedInject ReplaceOp(AccountResolver accountResolver, @@ -150,7 +148,6 @@ PatchSetUtil psUtil, ReplacePatchSetSender.Factory replacePatchSetFactory, @SendEmailExecutor ExecutorService sendEmailExecutor, - @Assisted RequestScopePropagator requestScopePropagator, @Assisted ProjectControl projectControl, @Assisted Branch.NameKey dest, @Assisted boolean checkMergedInto, @@ -177,7 +174,6 @@ this.replacePatchSetFactory = replacePatchSetFactory; this.sendEmailExecutor = sendEmailExecutor; - this.requestScopePropagator = requestScopePropagator; this.projectControl = projectControl; this.dest = dest; this.checkMergedInto = checkMergedInto; @@ -189,11 +185,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 +201,12 @@ requestScopePropagator, patchSetId, mergedInto.getName()); } } + + if (updateRef) { + ctx.addRefUpdate( + new ReceiveCommand(ObjectId.zeroId(), commit, + patchSetId.toRefName())); + } } @Override @@ -224,9 +228,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 +253,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 +285,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(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 +334,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 +365,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 +380,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()); @@ -419,8 +420,7 @@ } } - private void fireCommentAddedEvent(final Context ctx) - throws NoSuchChangeException, OrmException { + private void fireCommentAddedEvent(Context ctx) throws OrmException { if (approvals.isEmpty()) { return; } @@ -455,10 +455,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 470ea84..bd53ff5 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,8 @@ 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; @@ -115,7 +114,7 @@ // 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; + private final SetMultimap<Branch.NameKey, SubmoduleSubscription> targets; // map of superproject and its branches which has submodule subscriptions private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject; @@ -139,11 +138,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 = HashMultimap.create(); + this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build(); this.sortedBranches = calculateSubscriptionMap(); } @@ -350,7 +349,7 @@ } } 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); @@ -379,6 +378,7 @@ "The branch was probably deleted from the subscriber repository"); } currentCommit = or.rw.parseCommit(r.getObjectId()); + addBranchTip(subscriber, currentCommit); } StringBuilder msgbuf = new StringBuilder(""); @@ -483,7 +483,7 @@ oldCommit = subOr.rw.parseCommit(dce.getObjectId()); } - final RevCommit newCommit; + final CodeReviewCommit newCommit; if (branchTips.containsKey(s.getSubmodule())) { newCommit = branchTips.get(s.getSubmodule()); } else { @@ -493,6 +493,7 @@ return null; } newCommit = subOr.rw.parseCommit(ref.getObjectId()); + addBranchTip(s.getSubmodule(), newCommit); } if (Objects.equals(newCommit, oldCommit)) { @@ -558,7 +559,7 @@ throws SubmoduleException { LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>(); for (Project.NameKey project : branchesByProject.keySet()) { - addAllSubmoduleProjects(project, new LinkedHashSet<Project.NameKey>(), projects); + addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects); } for (Branch.NameKey branch : updatedBranches) {
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..2d63fd6 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: @@ -459,8 +463,14 @@ try { rc.fromText(text); } catch (ConfigInvalidException err) { - throw new ConfigInvalidException("Invalid config file " + fileName - + " in commit " + revision.name(), err); + StringBuilder msg = new StringBuilder("Invalid config file ") + .append(fileName) + .append(" in commit ") + .append(revision.name()); + if (err.getCause() != null) { + msg.append(": ").append(err.getCause()); + } + throw new ConfigInvalidException(msg.toString(), err); } } return rc; @@ -499,14 +509,15 @@ } public List<PathInfo> getPathInfos(boolean recursive) throws IOException { - TreeWalk tw = new TreeWalk(reader); - tw.addTree(revision.getTree()); - tw.setRecursive(recursive); - List<PathInfo> paths = new ArrayList<>(); - while (tw.next()) { - paths.add(new PathInfo(tw)); + try (TreeWalk tw = new TreeWalk(reader)) { + tw.addTree(revision.getTree()); + tw.setRecursive(recursive); + List<PathInfo> paths = new ArrayList<>(); + while (tw.next()) { + paths.add(new PathInfo(tw)); + } + return paths; } - return paths; } protected static void set(Config rc, String section, String subsection,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java index a0f729a..220df8c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -113,7 +113,7 @@ public Executor createQueue(int poolsize, String prefix) { final Executor r = new Executor(poolsize, prefix); r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); - r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + r.setExecuteExistingDelayedTasksAfterShutdownPolicy(true); queues.add(r); return r; }
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 31da05c..d5760e6 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; @@ -99,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, false); } catch (MergeConflictException mce) { // Keep going in the case of a single merge failure; the goal is to // cherry-pick as many commits as possible. @@ -124,7 +127,7 @@ newCommit.setPatchsetId(psId); newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK); args.mergeTip.moveTipTo(newCommit, newCommit); - args.commits.put(newCommit); + args.commitStatus.put(newCommit); ctx.addRefUpdate( new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName())); @@ -146,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
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java index 66eb40e..7c5702b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -30,8 +30,8 @@ @Override public List<SubmitStrategyOp> buildOps( Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge( + args.mergeSorter, toMerge, args.incoming); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); CodeReviewCommit newTipCommit = args.mergeUtil.getFirstFastForward( args.mergeTip.getInitialTip(), args.rw, sorted);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java index dfa13dc..621e3b9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -29,8 +29,8 @@ @Override public List<SubmitStrategyOp> buildOps( Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge( + args.mergeSorter, toMerge, args.incoming); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) { // The branch is unborn. Take a fast-forward resolution to
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java index 5b2e213..5cc2a89 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
@@ -29,8 +29,8 @@ @Override public List<SubmitStrategyOp> buildOps( Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge( + args.mergeSorter, toMerge, args.incoming); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); if (args.mergeTip.getInitialTip() == null || !args.submoduleOp
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.java new file mode 100644 index 0000000..26bb4c1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseAlways.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git.strategy; + +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 a309e6e..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,209 +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 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(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); - toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE); - 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) && - !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); - } - 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..ee19958 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -0,0 +1,295 @@ +// 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.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); + 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, args.incoming); + 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, true); + } 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) { + // this should not happen + 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) + .setCheckAddPatchSetPermission(false) + // RebaseAlways should set always modify commit message like + // Cherry-Pick strategy. + .setDetailedCommitMessage(rebaseAlways) + // Do not post message after inserting new patchset because there + // will be one about change being merged already. + .setPostMessage(false); + 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.commitStatus.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"); + // otherwise, 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); + } + 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) + throws IntegrationException { + try { + return new RebaseSorter( + args.rw, args.mergeTip.getInitialTip(), args.alreadyAccepted, args.canMergeFlag, + args.internalChangeQuery, args.changeKindCache, args.repo).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..441897c 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.ListMultimap; import com.google.common.collect.Sets; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.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; @@ -28,6 +31,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.change.ChangeKindCache; import com.google.gerrit.server.change.RebaseChangeOp; import com.google.gerrit.server.extensions.events.ChangeMerged; import com.google.gerrit.server.git.BatchUpdate; @@ -43,10 +47,12 @@ import com.google.gerrit.server.git.MergeUtil; import com.google.gerrit.server.git.SubmoduleOp; import com.google.gerrit.server.git.TagCache; +import com.google.gerrit.server.git.validators.OnSubmitValidators; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.util.RequestId; import com.google.inject.Module; import com.google.inject.assistedinject.Assisted; @@ -85,7 +91,7 @@ Arguments create( SubmitType submitType, Branch.NameKey destBranch, - CommitStatus commits, + CommitStatus commitStatus, CodeReviewRevWalk rw, IdentifiedUser caller, MergeTip mergeTip, @@ -94,9 +100,12 @@ RevFlag canMergeFlag, ReviewDb db, Set<RevCommit> alreadyAccepted, + Set<CodeReviewCommit> incoming, RequestId submissionId, NotifyHandling notifyHandling, - SubmoduleOp submoduleOp); + ListMultimap<RecipientType, Account.Id> accountsToNotify, + SubmoduleOp submoduleOp, + boolean dryrun); } final AccountCache accountCache; @@ -113,11 +122,14 @@ final ProjectCache projectCache; final PersonIdent serverIdent; final RebaseChangeOp.Factory rebaseFactory; + final OnSubmitValidators.Factory onSubmitValidatorsFactory; final TagCache tagCache; + final InternalChangeQuery internalChangeQuery; + final ChangeKindCache changeKindCache; final Branch.NameKey destBranch; final CodeReviewRevWalk rw; - final CommitStatus commits; + final CommitStatus commitStatus; final IdentifiedUser caller; final MergeTip mergeTip; final ObjectInserter inserter; @@ -125,14 +137,17 @@ final RevFlag canMergeFlag; final ReviewDb db; final Set<RevCommit> alreadyAccepted; + final Set<CodeReviewCommit> incoming; final RequestId submissionId; final SubmitType submitType; final NotifyHandling notifyHandling; + final ListMultimap<RecipientType, Account.Id> accountsToNotify; final SubmoduleOp submoduleOp; final ProjectState project; final MergeSorter mergeSorter; final MergeUtil mergeUtil; + final boolean dryrun; @AssistedInject Arguments( @@ -151,9 +166,12 @@ @GerritPersonIdent PersonIdent serverIdent, ProjectCache projectCache, RebaseChangeOp.Factory rebaseFactory, + OnSubmitValidators.Factory onSubmitValidatorsFactory, TagCache tagCache, + InternalChangeQuery internalChangeQuery, + ChangeKindCache changeKindCache, @Assisted Branch.NameKey destBranch, - @Assisted CommitStatus commits, + @Assisted CommitStatus commitStatus, @Assisted CodeReviewRevWalk rw, @Assisted IdentifiedUser caller, @Assisted MergeTip mergeTip, @@ -162,10 +180,13 @@ @Assisted RevFlag canMergeFlag, @Assisted ReviewDb db, @Assisted Set<RevCommit> alreadyAccepted, + @Assisted Set<CodeReviewCommit> incoming, @Assisted RequestId submissionId, @Assisted SubmitType submitType, @Assisted NotifyHandling notifyHandling, - @Assisted SubmoduleOp submoduleOp) { + @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify, + @Assisted SubmoduleOp submoduleOp, + @Assisted boolean dryrun) { this.accountCache = accountCache; this.approvalsUtil = approvalsUtil; this.batchUpdateFactory = batchUpdateFactory; @@ -180,10 +201,12 @@ this.projectCache = projectCache; this.rebaseFactory = rebaseFactory; this.tagCache = tagCache; + this.internalChangeQuery = internalChangeQuery; + this.changeKindCache = changeKindCache; this.serverIdent = serverIdent; this.destBranch = destBranch; - this.commits = commits; + this.commitStatus = commitStatus; this.rw = rw; this.caller = caller; this.mergeTip = mergeTip; @@ -192,15 +215,19 @@ this.canMergeFlag = canMergeFlag; this.db = db; this.alreadyAccepted = alreadyAccepted; + this.incoming = incoming; 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()); this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag); this.mergeUtil = mergeUtilFactory.create(project); + this.onSubmitValidatorsFactory = onSubmitValidatorsFactory; } }
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..fc22cfc 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,11 +14,15 @@ package com.google.gerrit.server.git.strategy; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.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; +import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.MergeOp.CommitStatus; @@ -53,13 +57,15 @@ public SubmitStrategy create(SubmitType submitType, ReviewDb db, 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 { + Set<CodeReviewCommit> incoming, Branch.NameKey destBranch, + IdentifiedUser caller, MergeTip mergeTip, CommitStatus commitStatus, + RequestId submissionId, NotifyHandling notifyHandling, + ListMultimap<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); + commitStatus, rw, caller, mergeTip, inserter, repo, canMergeFlag, db, + alreadyAccepted, incoming, submissionId, notifyHandling, + accountsToNotify, submoduleOp, dryrun); switch (submitType) { case CHERRY_PICK: return new CherryPick(args); @@ -71,6 +77,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/SubmitStrategyListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java index eedfe70..68ee8c8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyListener.java
@@ -34,13 +34,13 @@ public class SubmitStrategyListener extends BatchUpdate.Listener { private final Collection<SubmitStrategy> strategies; - private final CommitStatus commits; + private final CommitStatus commitStatus; private final boolean failAfterRefUpdates; public SubmitStrategyListener(SubmitInput input, - Collection<SubmitStrategy> strategies, CommitStatus commits) { + Collection<SubmitStrategy> strategies, CommitStatus commitStatus) { this.strategies = strategies; - this.commits = commits; + this.commitStatus = commitStatus; if (input instanceof TestSubmitInput) { failAfterRefUpdates = ((TestSubmitInput) input).failAfterRefUpdates; } else { @@ -70,20 +70,22 @@ throws ResourceConflictException, IntegrationException { for (SubmitStrategy strategy : strategies) { if (strategy instanceof CherryPick) { - // Might have picked a subset of changes, can't do this sanity check. + // Can't do this sanity check for CherryPick since: + // * CherryPick might have picked a subset of changes + // * CherryPick might have status SKIPPED_IDENTICAL_TREE continue; } SubmitStrategy.Arguments args = strategy.args; Set<Change.Id> unmerged = args.mergeUtil.findUnmergedChanges( - args.commits.getChangeIds(args.destBranch), args.rw, + args.commitStatus.getChangeIds(args.destBranch), args.rw, args.canMergeFlag, args.mergeTip.getInitialTip(), args.mergeTip.getCurrentTip(), alreadyMerged); for (Change.Id id : unmerged) { - commits.problem(id, + commitStatus.problem(id, "internal error: change not reachable from new branch tip"); } } - commits.maybeFailVerbose(); + commitStatus.maybeFailVerbose(); } private void markCleanMerges() throws IntegrationException { @@ -98,12 +100,12 @@ private List<Change.Id> checkCommitStatus() throws ResourceConflictException { List<Change.Id> alreadyMerged = - new ArrayList<>(commits.getChangeIds().size()); - for (Change.Id id : commits.getChangeIds()) { - CodeReviewCommit commit = commits.get(id); + new ArrayList<>(commitStatus.getChangeIds().size()); + for (Change.Id id : commitStatus.getChangeIds()) { + CodeReviewCommit commit = commitStatus.get(id); CommitMergeStatus s = commit != null ? commit.getStatusCode() : null; if (s == null) { - commits.problem(id, + commitStatus.problem(id, "internal error: change not processed by merge strategy"); continue; } @@ -127,25 +129,25 @@ case NOT_FAST_FORWARD: // TODO(dborowitz): Reformat these messages to be more appropriate for // short problem descriptions. - commits.problem(id, + commitStatus.problem(id, CharMatcher.is('\n').collapseFrom(s.getMessage(), ' ')); break; case MISSING_DEPENDENCY: - commits.problem(id, "depends on change that was not submitted"); + commitStatus.problem(id, "depends on change that was not submitted"); break; default: - commits.problem(id, "unspecified merge failure: " + s); + commitStatus.problem(id, "unspecified merge failure: " + s); break; } } - commits.maybeFailVerbose(); + commitStatus.maybeFailVerbose(); return alreadyMerged; } @Override public void afterUpdateChanges() throws ResourceConflictException { - commits.maybeFail("Error updating status"); + commitStatus.maybeFail("Error updating status"); } }
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 d62edb5..e9b9cbb 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; @@ -212,7 +208,7 @@ result.copyFrom(toMerge); result.setPatchsetId(psId); // Got overwriten by copyFrom. result.setStatusCode(CommitMergeStatus.ALREADY_MERGED); - args.commits.put(result); + args.commitStatus.put(result); return result; } @@ -251,7 +247,7 @@ Change c = ctx.getChange(); Change.Id id = c.getId(); - CodeReviewCommit commit = args.commits.get(id); + CodeReviewCommit commit = args.commitStatus.get(id); checkNotNull(commit, "missing commit for change " + id); CommitMergeStatus s = commit.getStatusCode(); checkNotNull(s, @@ -272,7 +268,7 @@ } catch (OrmException err) { String msg = "Error updating change status for " + id; log.error(msg, err); - args.commits.logProblem(id, msg); + args.commitStatus.logProblem(id, msg); // It's possible this happened before updating anything in the db, but // it's hard to know for sure, so just return true below to be safe. } @@ -301,13 +297,13 @@ ? 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) throws OrmException { Change.Id id = ctx.getChange().getId(); - List<SubmitRecord> records = args.commits.getSubmitRecords(id); + List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id); PatchSet.Id oldPsId = toMerge.getPatchsetId(); PatchSet.Id newPsId = ctx.getChange().currentPatchSetId(); @@ -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 " @@ -464,18 +450,9 @@ private ChangeMessage message(ChangeContext ctx, PatchSet.Id psId, String body) { - checkNotNull(psId); - String uuid; - try { - uuid = ChangeUtil.messageUUID(ctx.getDb()); - } catch (OrmException e) { - return null; - } - ChangeMessage m = new ChangeMessage( - new ChangeMessage.Key(psId.getParentKey(), uuid), - ctx.getAccountId(), ctx.getWhen(), psId); - m.setMessage(body); - return m; + return ChangeMessagesUtil.newMessage( + 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,
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 a49e02c..cb38096 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; @@ -58,8 +61,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; @@ -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<>(); - + List<CommitValidationMessage> messages = new ArrayList<>(); try { for (CommitValidationListener commitValidator : validators) { messages.addAll(commitValidator.onCommitReceived(receiveEvent)); @@ -221,8 +241,11 @@ @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<CommitValidationMessage> messages = new ArrayList<>(); List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID); String sha1 = commit.abbreviate(SHA1_LENGTH).name(); @@ -257,6 +280,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(); @@ -346,7 +374,7 @@ IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser(); if (REFS_CONFIG.equals(refControl.getRefName())) { - List<CommitValidationMessage> messages = new LinkedList<>(); + List<CommitValidationMessage> messages = new ArrayList<>(); try { ProjectConfig cfg = @@ -372,7 +400,7 @@ if (allUsers.equals( refControl.getProjectControl().getProject().getNameKey()) && RefNames.isRefsUsers(refControl.getRefName())) { - List<CommitValidationMessage> messages = new LinkedList<>(); + List<CommitValidationMessage> messages = new ArrayList<>(); Account.Id accountId = Account.Id.fromRef(refControl.getRefName()); if (accountId != null) { try { @@ -433,7 +461,7 @@ @Override public List<CommitValidationMessage> onCommitReceived( CommitReceivedEvent receiveEvent) throws CommitValidationException { - List<CommitValidationMessage> messages = new LinkedList<>(); + List<CommitValidationMessage> messages = new ArrayList<>(); for (CommitValidationListener validator : commitValidationListeners) { try { @@ -505,7 +533,7 @@ if (!currentUser.hasEmailAddress(author.getEmailAddress()) && !refControl.canForgeAuthor()) { - List<CommitValidationMessage> messages = new LinkedList<>(); + List<CommitValidationMessage> messages = new ArrayList<>(); messages.add(getInvalidEmailError(receiveEvent.commit, "author", author, currentUser, canonicalWebUrl)); @@ -534,7 +562,7 @@ final PersonIdent committer = receiveEvent.commit.getCommitterIdent(); if (!currentUser.hasEmailAddress(committer.getEmailAddress()) && !refControl.canForgeCommitter()) { - List<CommitValidationMessage> messages = new LinkedList<>(); + List<CommitValidationMessage> messages = new ArrayList<>(); messages.add(getInvalidEmailError(receiveEvent.commit, "committer", committer, currentUser, canonicalWebUrl)); throw new CommitValidationException("invalid committer", messages);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java index 4bf1deb..17a92ab 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.git.validators; +import com.google.common.collect.ImmutableList; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicMap.Entry; @@ -36,7 +37,6 @@ import org.eclipse.jgit.lib.Repository; import java.io.IOException; -import java.util.LinkedList; import java.util.List; public class MergeValidators { @@ -61,10 +61,9 @@ PatchSet.Id patchSetId, IdentifiedUser caller) throws MergeValidationException { - List<MergeValidationListener> validators = new LinkedList<>(); - - validators.add(new PluginMergeValidationListener(mergeValidationListeners)); - validators.add(projectConfigValidatorFactory.create()); + List<MergeValidationListener> validators = ImmutableList.of( + new PluginMergeValidationListener(mergeValidationListeners), + projectConfigValidatorFactory.create()); for (MergeValidationListener validator : validators) { validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java new file mode 100644 index 0000000..c736320 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.gerrit.server.git.validators; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.Project.NameKey; +import com.google.gerrit.server.validators.ValidationException; + +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.util.Map; + +/** + * Listener to validate ref updates performed during submit operation. + * + * As submit strategies may generate new commits (e.g. Cherry Pick), this + * listener allows validation of resulting new commit before destination branch + * is updated and new patchset ref is created. + * + * If you only care about validating the change being submitted and not the + * resulting new commit, consider using {@link MergeValidationListener} instead. + */ +@ExtensionPoint +public interface OnSubmitValidationListener { + class Arguments { + private Project.NameKey project; + private Repository repository; + private ObjectReader objectReader; + private Map<String, ReceiveCommand> commands; + + public Arguments(NameKey project, Repository repository, + ObjectReader objectReader, Map<String, ReceiveCommand> commands) { + this.project = project; + this.repository = repository; + this.objectReader = objectReader; + this.commands = commands; + } + + public Project.NameKey getProject() { + return project; + } + + /** + * @return a read only repository + */ + public Repository getRepository() { + return repository; + } + + public RevWalk newRevWalk() { + return new RevWalk(objectReader); + } + + /** + * @return a map from ref to op on it covering all ref ops to be performed + * on this repository as part of ongoing submit operation. + */ + public Map<String, ReceiveCommand> getCommands(){ + return commands; + } + } + + /** + * Called right before branch is updated with new commit or commits as a + * result of submit. + * + * If ValidationException is thrown, submitting is aborted. + */ + void preBranchUpdate(Arguments args) throws ValidationException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java new file mode 100644 index 0000000..568c597 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
@@ -0,0 +1,53 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.gerrit.server.git.validators; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.IntegrationException; +import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments; +import com.google.gerrit.server.validators.ValidationException; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.util.Map; + +public class OnSubmitValidators { + public interface Factory { + OnSubmitValidators create(); + } + + private final DynamicSet<OnSubmitValidationListener> listeners; + + @Inject + OnSubmitValidators(DynamicSet<OnSubmitValidationListener> listeners) { + this.listeners = listeners; + } + + public void validate(Project.NameKey project, Repository repo, + ObjectReader objectReader, Map<String, ReceiveCommand> commands) + throws IntegrationException { + try { + for (OnSubmitValidationListener listener : this.listeners) { + listener.preBranchUpdate( + new Arguments(project, repo, objectReader, commands)); + } + } catch (ValidationException e) { + throw new IntegrationException(e.getMessage()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java index 2deb44a..84d1fad 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -38,8 +38,8 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -100,7 +100,7 @@ GroupControl control = resource.getControl(); Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = new HashMap<>(); - List<GroupInfo> result = new LinkedList<>(); + List<GroupInfo> result = new ArrayList<>(); Account.Id me = control.getUser().getAccountId(); for (String includedGroup : input.groups) {
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..55bb1e9 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; @@ -46,10 +46,10 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -216,7 +216,7 @@ private List<AccountInfo> toAccountInfoList(Set<Account.Id> accountIds) throws OrmException { - List<AccountInfo> result = new LinkedList<>(); + List<AccountInfo> result = new ArrayList<>(); AccountLoader loader = infoFactory.create(true); for (Account.Id accId : accountIds) { result.add(loader.get(accId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java index 0fd4728..70fc7f6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -73,6 +73,7 @@ private final GroupJson json; private final DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners; private final AddMembers addMembers; + private final SystemGroupBackend systemGroupBackend; private final boolean defaultVisibleToAll; private final String name; @@ -86,6 +87,7 @@ GroupJson json, DynamicSet<GroupCreationValidationListener> groupCreationValidationListeners, AddMembers addMembers, + SystemGroupBackend systemGroupBackend, @GerritServerConfig Config cfg, @Assisted String name) { this.self = self; @@ -96,6 +98,7 @@ this.json = json; this.groupCreationValidationListeners = groupCreationValidationListeners; this.addMembers = addMembers; + this.systemGroupBackend = systemGroupBackend; this.defaultVisibleToAll = cfg.getBoolean("groups", "newGroupsVisibleToAll", false); this.name = name; } @@ -169,8 +172,7 @@ throws OrmException, ResourceConflictException, IOException { // Do not allow creating groups with the same name as system groups - List<String> sysGroupNames = SystemGroupBackend.getNames(); - for (String name : sysGroupNames) { + for (String name : systemGroupBackend.getNames()) { if (name.toLowerCase(Locale.US).equals( createGroupArgs.getGroupName().toLowerCase(Locale.US))) { throw new ResourceConflictException("group '" + name @@ -178,6 +180,14 @@ } } + for (String name : systemGroupBackend.getReservedNames()) { + if (name.toLowerCase(Locale.US).equals( + createGroupArgs.getGroupName().toLowerCase(Locale.US))) { + throw new ResourceConflictException("group name '" + name + + "' is reserved"); + } + } + AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId()); AccountGroup.UUID uuid = GroupUUID.make(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java index 30b856a..23d2b59 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -36,7 +36,6 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; class DbGroupMemberAuditListener implements GroupMemberAuditListener { @@ -61,7 +60,7 @@ @Override public void onAddAccountsToGroup(Account.Id me, Collection<AccountGroupMember> added) { - List<AccountGroupMemberAudit> auditInserts = new LinkedList<>(); + List<AccountGroupMemberAudit> auditInserts = new ArrayList<>(); for (AccountGroupMember m : added) { AccountGroupMemberAudit audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs()); @@ -79,8 +78,8 @@ @Override public void onDeleteAccountsFromGroup(Account.Id me, Collection<AccountGroupMember> removed) { - List<AccountGroupMemberAudit> auditInserts = new LinkedList<>(); - List<AccountGroupMemberAudit> auditUpdates = new LinkedList<>(); + List<AccountGroupMemberAudit> auditInserts = new ArrayList<>(); + List<AccountGroupMemberAudit> auditUpdates = new ArrayList<>(); try (ReviewDb db = schema.open()) { for (AccountGroupMember m : removed) { AccountGroupMemberAudit audit = null; @@ -131,7 +130,7 @@ @Override public void onDeleteGroupsFromGroup(Account.Id me, Collection<AccountGroupById> removed) { - final List<AccountGroupByIdAud> auditUpdates = new LinkedList<>(); + final List<AccountGroupByIdAud> auditUpdates = new ArrayList<>(); try (ReviewDb db = schema.open()) { for (final AccountGroupById g : removed) { AccountGroupByIdAud audit = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java index da683a3..3985c80 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -35,8 +35,8 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -71,7 +71,7 @@ final GroupControl control = resource.getControl(); final Map<AccountGroup.UUID, AccountGroupById> includedGroups = getIncludedGroups(internalGroup.getId()); - final List<AccountGroupById> toRemove = new LinkedList<>(); + final List<AccountGroupById> toRemove = new ArrayList<>(); for (final String includedGroup : input.groups) { GroupDescription.Basic d = groupsCollection.parse(includedGroup);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java index e1a6921..107f1bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -35,8 +35,8 @@ import com.google.inject.Singleton; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -72,7 +72,7 @@ final GroupControl control = resource.getControl(); final Map<Account.Id, AccountGroupMember> members = getMembers(internalGroup.getId()); - final List<AccountGroupMember> toRemove = new LinkedList<>(); + final List<AccountGroupMember> toRemove = new ArrayList<>(); for (final String nameOrEmail : input.members) { Account a = accounts.parse(nameOrEmail).getAccount();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java deleted file mode 100644 index d660db0..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupInfoCache.java +++ /dev/null
@@ -1,55 +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. - -package com.google.gerrit.server.group; - -import com.google.gerrit.common.data.GroupDescription; -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.server.account.GroupBackend; -import com.google.inject.Inject; - -import java.util.HashMap; -import java.util.Map; - -/** Efficiently builds a {@link GroupInfoCache}. */ -public class GroupInfoCache { - public interface Factory { - GroupInfoCache create(); - } - - private final GroupBackend groupBackend; - private final Map<AccountGroup.UUID, GroupDescription.Basic> out; - - @Inject - GroupInfoCache(GroupBackend groupBackend) { - this.groupBackend = groupBackend; - this.out = new HashMap<>(); - } - - /** - * Indicate a group will be needed later on. - * - * @param uuid identity that will be needed in the future; may be null. - */ - public void want(final AccountGroup.UUID uuid) { - if (uuid != null && !out.containsKey(uuid)) { - out.put(uuid, groupBackend.get(uuid)); - } - } - - public GroupDescription.Basic get(final AccountGroup.UUID uuid) { - want(uuid); - return out.get(uuid); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java index 6268d72..72c29d0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.group; +import com.google.common.collect.ListMultimap; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescriptions; import com.google.gerrit.common.data.GroupReference; @@ -21,7 +22,9 @@ import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AcceptsCreate; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.NeedsParams; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestCollection; import com.google.gerrit.extensions.restapi.RestView; @@ -35,28 +38,31 @@ import com.google.gerrit.server.account.GroupControl; import com.google.inject.Inject; import com.google.inject.Provider; -import com.google.inject.Singleton; -@Singleton public class GroupsCollection implements RestCollection<TopLevelResource, GroupResource>, - AcceptsCreate<TopLevelResource> { + AcceptsCreate<TopLevelResource>, NeedsParams { private final DynamicMap<RestView<GroupResource>> views; private final Provider<ListGroups> list; + private final Provider<QueryGroups> queryGroups; private final CreateGroup.Factory createGroup; private final GroupControl.Factory groupControlFactory; private final GroupBackend groupBackend; private final Provider<CurrentUser> self; + private boolean hasQuery2; + @Inject - GroupsCollection(final DynamicMap<RestView<GroupResource>> views, - final Provider<ListGroups> list, - final CreateGroup.Factory createGroup, - final GroupControl.Factory groupControlFactory, - final GroupBackend groupBackend, - final Provider<CurrentUser> self) { + GroupsCollection(DynamicMap<RestView<GroupResource>> views, + Provider<ListGroups> list, + Provider<QueryGroups> queryGroups, + CreateGroup.Factory createGroup, + GroupControl.Factory groupControlFactory, + GroupBackend groupBackend, + Provider<CurrentUser> self) { this.views = views; this.list = list; + this.queryGroups = queryGroups; this.createGroup = createGroup; this.groupControlFactory = groupControlFactory; this.groupBackend = groupBackend; @@ -64,6 +70,18 @@ } @Override + public void setParams(ListMultimap<String, String> params) + throws BadRequestException { + if (params.containsKey("query") && params.containsKey("query2")) { + throw new BadRequestException( + "\"query\" and \"query2\" options are mutually exclusive"); + } + + // The --query2 option is defined in QueryGroups + this.hasQuery2 = params.containsKey("query2"); + } + + @Override public RestView<TopLevelResource> list() throws ResourceNotFoundException, AuthException { final CurrentUser user = self.get(); @@ -73,6 +91,10 @@ throw new ResourceNotFoundException(); } + if (hasQuery2) { + return queryGroups.get(); + } + return list.get(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java new file mode 100644 index 0000000..5a8978a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Index.java
@@ -0,0 +1,59 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.group; + +import com.google.gerrit.common.data.GroupDescriptions; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.group.Index.Input; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.io.IOException; + +@Singleton +public class Index implements RestModifyView<GroupResource, Input> { + public static class Input { + } + + private final GroupCache groupCache; + + @Inject + Index(GroupCache groupCache) { + this.groupCache = groupCache; + } + + @Override + public Response<?> apply(GroupResource rsrc, Input input) + throws IOException, AuthException, UnprocessableEntityException { + if (!rsrc.getControl().isOwner()) { + throw new AuthException("not allowed to index group"); + } + + AccountGroup group = GroupDescriptions.toAccountGroup(rsrc.getGroup()); + if (group == null) { + throw new UnprocessableEntityException(String + .format("External Group Not Allowed: %s", rsrc.getGroupUUID().get())); + } + + // evicting the group from the cache, reindexes the group + groupCache.evict(group); + return Response.none(); + } +}
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..80a639b 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; @@ -105,9 +106,23 @@ this.owned = owned; } - @Option(name = "-q", usage = "group to inspect") - public void addGroup(AccountGroup.UUID id) { - groupsToInspect.add(id); + + /** + * Add a group to inspect. + * + * @param uuid UUID of the group + * @deprecated use {@link #addGroup(AccountGroup.UUID)}. + */ + @Deprecated + @Option(name = "--query", aliases = {"-q"}, + usage = "group to inspect (deprecated: use --group/-g instead)") + void addGroup_Deprecated(AccountGroup.UUID uuid) { + addGroup(uuid); + } + + @Option(name = "--group", aliases = {"-g"}, usage = "group to inspect") + public void addGroup(AccountGroup.UUID uuid) { + groupsToInspect.add(uuid); } @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", @@ -314,11 +329,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 +348,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/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java index 58b3ffb..f6d0453 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -39,6 +39,7 @@ get(GROUP_KIND).to(GetGroup.class); put(GROUP_KIND).to(PutGroup.class); get(GROUP_KIND, "detail").to(GetDetail.class); + post(GROUP_KIND, "index").to(Index.class); post(GROUP_KIND, "members").to(AddMembers.class); post(GROUP_KIND, "members.add").to(AddMembers.class); post(GROUP_KIND, "members.delete").to(DeleteMembers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java index c2dc23a..102e17c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
@@ -30,6 +30,7 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; @Singleton @@ -51,7 +52,7 @@ @Override public Response<String> apply(GroupResource resource, Input input) throws AuthException, MethodNotAllowedException, - ResourceNotFoundException, OrmException { + ResourceNotFoundException, OrmException, IOException { if (input == null) { input = new Input(); // Delete would set description to null. }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java index c59f0ff..cf6adf4e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -36,6 +36,7 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.TimeZone; @@ -70,7 +71,8 @@ @Override public String apply(GroupResource rsrc, Input input) throws MethodNotAllowedException, AuthException, BadRequestException, - ResourceConflictException, OrmException, NoSuchGroupException { + ResourceConflictException, OrmException, NoSuchGroupException, + IOException { if (rsrc.toAccountGroup() == null) { throw new MethodNotAllowedException(); } else if (!rsrc.getControl().isOwner()) { @@ -92,7 +94,7 @@ private GroupDetail renameGroup(AccountGroup group, String newName) throws ResourceConflictException, OrmException, - NoSuchGroupException { + NoSuchGroupException, IOException { AccountGroup.Id groupId = group.getId(); AccountGroup.NameKey old = group.getNameKey(); AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java index 5788503..8af03c4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
@@ -28,6 +28,7 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; @Singleton @@ -45,7 +46,7 @@ @Override public GroupOptionsInfo apply(GroupResource resource, GroupOptionsInfo input) throws MethodNotAllowedException, AuthException, BadRequestException, - ResourceNotFoundException, OrmException { + ResourceNotFoundException, OrmException, IOException { if (resource.toAccountGroup() == null) { throw new MethodNotAllowedException(); } else if (!resource.getControl().isOwner()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java index b88ead5..6654f5f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -33,6 +33,7 @@ import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; @Singleton @@ -60,7 +61,7 @@ public GroupInfo apply(GroupResource resource, Input input) throws ResourceNotFoundException, MethodNotAllowedException, AuthException, BadRequestException, UnprocessableEntityException, - OrmException { + OrmException, IOException { AccountGroup group = resource.toAccountGroup(); if (group == null) { throw new MethodNotAllowedException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java new file mode 100644 index 0000000..bbbfd7c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/QueryGroups.java
@@ -0,0 +1,134 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.group; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.gerrit.common.data.GroupDescriptions; +import com.google.gerrit.extensions.client.ListGroupsOption; +import com.google.gerrit.extensions.common.GroupInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.group.GroupIndex; +import com.google.gerrit.server.index.group.GroupIndexCollection; +import com.google.gerrit.server.query.QueryParseException; +import com.google.gerrit.server.query.QueryResult; +import com.google.gerrit.server.query.group.GroupQueryBuilder; +import com.google.gerrit.server.query.group.GroupQueryProcessor; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; + +import org.kohsuke.args4j.Option; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +public class QueryGroups implements RestReadView<TopLevelResource> { + private final GroupIndexCollection indexes; + private final GroupQueryBuilder queryBuilder; + private final GroupQueryProcessor queryProcessor; + private final GroupJson json; + + private String query; + private int limit; + private int start; + private EnumSet<ListGroupsOption> options = + EnumSet.noneOf(ListGroupsOption.class); + + // TODO(ekempin): --query in ListGroups is marked as deprecated, once it is + // removed we want to rename --query2 to --query here. + /** --query (-q) is already used by {@link ListGroups} */ + @Option(name = "--query2", aliases = {"-q2"}, usage = "group query") + public void setQuery(String query) { + this.query = query; + } + + @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", + usage = "maximum number of groups to list") + public void setLimit(int limit) { + this.limit = limit; + } + + @Option(name = "--start", aliases = {"-S"}, metaVar = "CNT", + usage = "number of groups to skip") + public void setStart(int start) { + this.start = start; + } + + @Option(name = "-o", usage = "Output options per group") + public void addOption(ListGroupsOption o) { + options.add(o); + } + + @Option(name = "-O", usage = "Output option flags, in hex") + public void setOptionFlagsHex(String hex) { + options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16))); + } + + @Inject + protected QueryGroups(GroupIndexCollection indexes, + GroupQueryBuilder queryBuilder, + GroupQueryProcessor queryProcessor, + GroupJson json) { + this.indexes = indexes; + this.queryBuilder = queryBuilder; + this.queryProcessor = queryProcessor; + this.json = json; + } + + @Override + public List<GroupInfo> apply(TopLevelResource resource) + throws BadRequestException, MethodNotAllowedException, OrmException { + if (Strings.isNullOrEmpty(query)) { + throw new BadRequestException("missing query field"); + } + + GroupIndex searchIndex = indexes.getSearchIndex(); + if (searchIndex == null) { + throw new MethodNotAllowedException("no group index"); + } + + if (start != 0) { + queryProcessor.setStart(start); + } + + if (limit != 0) { + queryProcessor.setLimit(limit); + } + + try { + QueryResult<AccountGroup> result = + queryProcessor.query(queryBuilder.parse(query)); + List<AccountGroup> groups = result.entities(); + + ArrayList<GroupInfo> groupInfos = + Lists.newArrayListWithCapacity(groups.size()); + json.addOptions(options); + for (AccountGroup group : groups) { + groupInfos.add(json.format(GroupDescriptions.forAccountGroup(group))); + } + if (!groupInfos.isEmpty() && result.more()) { + groupInfos.get(groupInfos.size() - 1)._moreGroups = true; + } + return groupInfos; + } catch (QueryParseException e) { + throw new BadRequestException(e.getMessage()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java index 9809ef3..244fac0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -15,27 +15,41 @@ package com.google.gerrit.server.group; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toSet; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StartupCheck; +import com.google.gerrit.server.StartupException; import com.google.gerrit.server.account.AbstractGroupBackend; +import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.ListGroupMembership; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.ProjectControl; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.Config; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; +@Singleton public class SystemGroupBackend extends AbstractGroupBackend { public static final String SYSTEM_GROUP_SCHEME = "global:"; @@ -55,8 +69,6 @@ public static final AccountGroup.UUID CHANGE_OWNER = new AccountGroup.UUID(SYSTEM_GROUP_SCHEME + "Change-Owner"); - private static final SortedMap<String, GroupReference> names; - private static final ImmutableMap<AccountGroup.UUID, GroupReference> uuids; private static final AccountGroup.UUID[] all = { ANONYMOUS_USERS, REGISTERED_USERS, @@ -64,22 +76,6 @@ CHANGE_OWNER, }; - static { - SortedMap<String, GroupReference> n = new TreeMap<>(); - ImmutableMap.Builder<AccountGroup.UUID, GroupReference> u = - ImmutableMap.builder(); - - for (AccountGroup.UUID uuid : all) { - int c = uuid.get().indexOf(':'); - String name = uuid.get().substring(c + 1).replace('-', ' '); - GroupReference ref = new GroupReference(uuid, name); - n.put(ref.getName().toLowerCase(Locale.US), ref); - u.put(ref.getUUID(), ref); - } - names = Collections.unmodifiableSortedMap(n); - uuids = u.build(); - } - public static boolean isSystemGroup(AccountGroup.UUID uuid) { return uuid.get().startsWith(SYSTEM_GROUP_SCHEME); } @@ -92,17 +88,43 @@ return ANONYMOUS_USERS.equals(uuid) || REGISTERED_USERS.equals(uuid); } - public static GroupReference getGroup(AccountGroup.UUID uuid) { + private final ImmutableSet<String> reservedNames; + private final SortedMap<String, GroupReference> names; + private final ImmutableMap<AccountGroup.UUID, GroupReference> uuids; + + @Inject + @VisibleForTesting + public SystemGroupBackend(@GerritServerConfig Config cfg) { + SortedMap<String, GroupReference> n = new TreeMap<>(); + ImmutableMap.Builder<AccountGroup.UUID, GroupReference> u = + ImmutableMap.builder(); + + ImmutableSet.Builder<String> reservedNamesBuilder = ImmutableSet.builder(); + for (AccountGroup.UUID uuid : all) { + int c = uuid.get().indexOf(':'); + String defaultName = uuid.get().substring(c + 1).replace('-', ' '); + reservedNamesBuilder.add(defaultName); + String configuredName = cfg.getString("groups", uuid.get(), "name"); + GroupReference ref = new GroupReference(uuid, + MoreObjects.firstNonNull(configuredName, defaultName)); + n.put(ref.getName().toLowerCase(Locale.US), ref); + u.put(ref.getUUID(), ref); + } + reservedNames = reservedNamesBuilder.build(); + names = Collections.unmodifiableSortedMap(n); + uuids = u.build(); + } + + public GroupReference getGroup(AccountGroup.UUID uuid) { return checkNotNull(uuids.get(uuid), "group %s not found", uuid.get()); } - public static List<String> getNames() { - List<String> names = new ArrayList<>(); - for (AccountGroup.UUID uuid : all) { - int c = uuid.get().indexOf(':'); - names.add(uuid.get().substring(c + 1).replace('-', ' ')); - } - return names; + public Set<String> getNames() { + return names.values().stream().map(r -> r.getName()).collect(toSet()); + } + + public Set<String> getReservedNames() { + return reservedNames; } @Override @@ -112,7 +134,10 @@ @Override public GroupDescription.Basic get(AccountGroup.UUID uuid) { - final GroupReference ref = getGroup(uuid); + final GroupReference ref = uuids.get(uuid); + if (ref == null) { + return null; + } return new GroupDescription.Basic() { @Override public String getName() { @@ -161,4 +186,48 @@ ANONYMOUS_USERS, REGISTERED_USERS)); } + + public static class NameCheck implements StartupCheck { + private final Config cfg; + private final GroupCache groupCache; + + @Inject + NameCheck(@GerritServerConfig Config cfg, + GroupCache groupCache) { + this.cfg = cfg; + this.groupCache = groupCache; + } + + @Override + public void check() throws StartupException { + Map<AccountGroup.UUID, String> configuredNames = new HashMap<>(); + Map<String, AccountGroup.UUID> byLowerCaseConfiguredName = + new HashMap<>(); + for (AccountGroup.UUID uuid : all) { + String configuredName = cfg.getString("groups", uuid.get(), "name"); + if (configuredName != null) { + configuredNames.put(uuid, configuredName); + byLowerCaseConfiguredName.put(configuredName.toLowerCase(Locale.US), + uuid); + } + } + if (configuredNames.isEmpty()) { + return; + } + for (AccountGroup g : groupCache.all()) { + String name = g.getName().toLowerCase(Locale.US); + if (byLowerCaseConfiguredName.keySet().contains(name)) { + AccountGroup.UUID uuidSystemGroup = + byLowerCaseConfiguredName.get(name); + throw new StartupException(String.format( + "The configured name '%s' for system group '%s' is ambiguous" + + " with the name '%s' of existing group '%s'." + + " Please remove/change the value for groups.%s.name in" + + " gerrit.config.", + configuredNames.get(uuidSystemGroup), uuidSystemGroup.get(), + g.getName(), g.getGroupUUID().get(), uuidSystemGroup.get())); + } + } + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java index ed196c1..1706761 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
@@ -14,10 +14,12 @@ package com.google.gerrit.server.index; +import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.index.account.AccountIndex; import com.google.gerrit.server.index.change.ChangeIndex; import com.google.gerrit.server.index.change.DummyChangeIndex; +import com.google.gerrit.server.index.group.GroupIndex; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.AbstractModule; @@ -36,6 +38,13 @@ } } + private static class DummyGroupIndexFactory implements GroupIndex.Factory { + @Override + public GroupIndex create(Schema<AccountGroup> schema) { + throw new UnsupportedOperationException(); + } + } + @Override protected void configure() { install(new IndexModule(1)); @@ -43,5 +52,6 @@ bind(Index.class).toInstance(new DummyChangeIndex()); bind(AccountIndex.Factory.class).toInstance(new DummyAccountIndexFactory()); bind(ChangeIndex.Factory.class).toInstance(new DummyChangeIndexFactory()); + bind(GroupIndex.Factory.class).toInstance(new DummyGroupIndexFactory()); } }
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-server/src/main/java/com/google/gerrit/server/index/GerritIndexStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/GerritIndexStatus.java new file mode 100644 index 0000000..5763185 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/GerritIndexStatus.java
@@ -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. + +package com.google.gerrit.server.index; + +import com.google.common.primitives.Ints; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; + +import java.io.IOException; + +public class GerritIndexStatus { + private static final String SECTION = "index"; + private static final String KEY_READY = "ready"; + + private final FileBasedConfig cfg; + + public GerritIndexStatus(SitePaths sitePaths) + throws ConfigInvalidException, IOException { + cfg = new FileBasedConfig( + sitePaths.index_dir.resolve("gerrit_index.config").toFile(), + FS.detect()); + cfg.load(); + convertLegacyConfig(); + } + + public void setReady(String indexName, int version, boolean ready) { + cfg.setBoolean(SECTION, indexDirName(indexName, version), KEY_READY, ready); + } + + public boolean getReady(String indexName, int version) { + return cfg.getBoolean(SECTION, indexDirName(indexName, version), KEY_READY, + false); + } + + public void save() throws IOException { + cfg.save(); + } + + private void convertLegacyConfig() throws IOException { + boolean dirty = false; + // Convert legacy [index "25"] to modern [index "changes_0025"]. + for (String subsection : cfg.getSubsections(SECTION)) { + Integer v = Ints.tryParse(subsection); + if (v != null) { + String ready = cfg.getString(SECTION, subsection, KEY_READY); + if (ready != null) { + dirty = false; + cfg.unset(SECTION, subsection, KEY_READY); + cfg.setString(SECTION, indexDirName(ChangeSchemaDefinitions.NAME, v), + KEY_READY, ready); + } + } + } + if (dirty) { + cfg.save(); + } + } + + private static String indexDirName(String indexName, int version) { + return String.format("%s_%04d", indexName, version); + } +}
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 d12de44..887715f 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. @@ -35,7 +38,7 @@ Schema<V> getSchema(); /** Stop and await termination of all executor threads */ - void stop(); + default void stop() {} /** Close this index. */ void close(); @@ -94,6 +97,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/IndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexDefinition.java index 629dff8..8787b2f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexDefinition.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexDefinition.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.index; import com.google.common.collect.ImmutableSortedMap; +import com.google.inject.Provider; /** * Definition of an index over a Gerrit data type. @@ -32,13 +33,13 @@ private final SchemaDefinitions<V> schemaDefs; private final IndexCollection<K, V, I> indexCollection; private final IndexFactory<K, V, I> indexFactory; - private final SiteIndexer<K, V, I> siteIndexer; + private final Provider<SiteIndexer<K, V, I>> siteIndexer; protected IndexDefinition( SchemaDefinitions<V> schemaDefs, IndexCollection<K, V, I> indexCollection, IndexFactory<K, V, I> indexFactory, - SiteIndexer<K, V, I> siteIndexer) { + Provider<SiteIndexer<K, V, I>> siteIndexer) { this.schemaDefs = schemaDefs; this.indexCollection = indexCollection; this.indexFactory = indexFactory; @@ -66,6 +67,6 @@ } public final SiteIndexer<K, V, I> getSiteIndexer() { - return siteIndexer; + return siteIndexer.get(); } }
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..73812c6 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; @@ -37,6 +36,12 @@ import com.google.gerrit.server.index.change.ChangeIndexRewriter; import com.google.gerrit.server.index.change.ChangeIndexer; import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.server.index.group.GroupIndexCollection; +import com.google.gerrit.server.index.group.GroupIndexDefinition; +import com.google.gerrit.server.index.group.GroupIndexRewriter; +import com.google.gerrit.server.index.group.GroupIndexer; +import com.google.gerrit.server.index.group.GroupIndexerImpl; +import com.google.gerrit.server.index.group.GroupSchemaDefinitions; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provides; @@ -56,13 +61,14 @@ */ public class IndexModule extends LifecycleModule { public enum IndexType { - LUCENE + LUCENE, ELASTICSEARCH } public static final ImmutableCollection<SchemaDefinitions<?>> ALL_SCHEMA_DEFS = ImmutableList.<SchemaDefinitions<?>> of( AccountSchemaDefinitions.INSTANCE, - ChangeSchemaDefinitions.INSTANCE); + ChangeSchemaDefinitions.INSTANCE, + GroupSchemaDefinitions.INSTANCE); /** Type of secondary index. */ public static IndexType getIndexType(Injector injector) { @@ -99,30 +105,29 @@ bind(ChangeIndexCollection.class); listener().to(ChangeIndexCollection.class); factory(ChangeIndexer.Factory.class); + + bind(GroupIndexRewriter.class); + bind(GroupIndexCollection.class); + listener().to(GroupIndexCollection.class); + factory(GroupIndexerImpl.Factory.class); } @Provides Collection<IndexDefinition<?, ?, ?>> getIndexDefinitions( AccountIndexDefinition accounts, - ChangeIndexDefinition changes) { + ChangeIndexDefinition changes, + GroupIndexDefinition groups) { Collection<IndexDefinition<?, ?, ?>> result = ImmutableList.<IndexDefinition<?, ?, ?>> of( accounts, + groups, 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: " @@ -151,6 +156,13 @@ @Provides @Singleton + GroupIndexer getGroupIndexer(GroupIndexerImpl.Factory factory, + GroupIndexCollection indexes) { + return factory.create(indexes); + } + + @Provides + @Singleton @IndexExecutor(INTERACTIVE) ListeningExecutorService getInteractiveIndexExecutor( @GerritServerConfig Config config,
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..eceec59 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java
@@ -0,0 +1,82 @@ +// 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.change.ChangeField.CHANGE; +import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID; +import static com.google.gerrit.server.index.change.ChangeField.PROJECT; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.index.account.AccountField; +import com.google.gerrit.server.index.group.GroupField; + +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(AccountField.ID.getName()) + ? fs + : Sets.union(fs, ImmutableSet.of(AccountField.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())); + } + + public static Set<String> groupFields(QueryOptions opts) { + Set<String> fs = opts.fields(); + return fs.contains(GroupField.UUID.getName()) + ? fs + : Sets.union(fs, ImmutableSet.of(GroupField.UUID.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/SchemaUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java index ca61b00..28e43a7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaUtil.java
@@ -32,6 +32,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -90,27 +91,31 @@ if (person == null) { return ImmutableSet.of(); } - return getPersonParts( + return getNameParts( person.getName(), Collections.singleton(person.getEmailAddress())); } - public static Set<String> getPersonParts(String name, + public static Set<String> getNameParts(String name) { + return getNameParts(name, Collections.emptySet()); + } + + public static Set<String> getNameParts(String name, Iterable<String> emails) { Splitter at = Splitter.on('@'); - Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings(); + Splitter s = Splitter.on(CharMatcher.anyOf("@.- /_")).omitEmptyStrings(); HashSet<String> parts = new HashSet<>(); for (String email : emails) { if (email == null) { continue; } - String lowerEmail = email.toLowerCase(); + String lowerEmail = email.toLowerCase(Locale.US); parts.add(lowerEmail); Iterables.addAll(parts, at.split(lowerEmail)); Iterables.addAll(parts, s.split(lowerEmail)); } if (name != null) { - Iterables.addAll(parts, s.split(name.toLowerCase())); + Iterables.addAll(parts, s.split(name.toLowerCase(Locale.US))); } return parts; }
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..b86ec38 --- /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 { + public 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 + public 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..de5428f 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,20 +14,19 @@ 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; import java.sql.Timestamp; import java.util.Collections; +import java.util.Locale; import java.util.Set; /** Secondary index schemas for accounts. */ @@ -47,13 +46,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()); } }; @@ -64,21 +57,16 @@ @Override public Iterable<String> get(AccountState input, FillArgs args) { String fullName = input.getAccount().getFullName(); - Set<String> parts = SchemaUtil.getPersonParts( + Set<String> parts = SchemaUtil.getNameParts( 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. if (fullName != null) { - parts.add(fullName.toLowerCase()); + parts.add(fullName.toLowerCase(Locale.US)); } return parts; } @@ -108,23 +96,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 +129,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/account/AccountIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java index ea16e13..31a9250 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountIndexDefinition.java
@@ -14,10 +14,12 @@ package com.google.gerrit.server.index.account; +import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.index.IndexDefinition; import com.google.inject.Inject; +import com.google.inject.util.Providers; public class AccountIndexDefinition extends IndexDefinition<Account.Id, AccountState, AccountIndex> { @@ -26,8 +28,8 @@ AccountIndexDefinition( AccountIndexCollection indexCollection, AccountIndex.Factory indexFactory, - AllAccountsIndexer allAccountsIndexer) { + @Nullable AllAccountsIndexer allAccountsIndexer) { super(AccountSchemaDefinitions.INSTANCE, indexCollection, indexFactory, - allAccountsIndexer); + Providers.of(allAccountsIndexer)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java index bebe668..05e49b9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -21,6 +21,7 @@ import com.google.gerrit.server.index.SchemaDefinitions; public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> { + @Deprecated static final Schema<AccountState> V1 = schema( AccountField.ID, AccountField.ACTIVE, @@ -30,12 +31,16 @@ AccountField.REGISTERED, AccountField.USERNAME); + @Deprecated static final Schema<AccountState> V2 = schema(V1, AccountField.WATCHED_PROJECT); + @Deprecated static final Schema<AccountState> V3 = schema(V2, AccountField.FULL_NAME); + static final Schema<AccountState> V4 = schema(V3); + public static final AccountSchemaDefinitions INSTANCE = new AccountSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java index 1c008b4..14264af 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -75,7 +75,7 @@ ids = collectAccounts(progress); } catch (OrmException e) { log.error("Error collecting accounts", e); - return new Result(sw, false, 0, 0); + return new SiteIndexer.Result(sw, false, 0, 0); } return reindexAccounts(index, ids, progress); } @@ -97,9 +97,7 @@ try { accountCache.evict(id); index.replace(accountCache.get(id)); - if (verboseWriter != null) { - verboseWriter.println("Reindexed " + desc); - } + verboseWriter.println("Reindexed " + desc); done.incrementAndGet(); } catch (Exception e) { failed.incrementAndGet(); @@ -116,11 +114,11 @@ Futures.successfulAsList(futures).get(); } catch (ExecutionException | InterruptedException e) { log.error("Error waiting on account futures", e); - return new Result(sw, false, 0, 0); + return new SiteIndexer.Result(sw, false, 0, 0); } progress.endTask(); - return new Result(sw, ok.get(), done.get(), failed.get()); + return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); } private List<Account.Id> collectAccounts(ProgressMonitor progress)
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..080aa5b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -14,18 +14,19 @@ package com.google.gerrit.server.index.change; +import static com.google.common.util.concurrent.Futures.successfulAsList; +import static com.google.common.util.concurrent.Futures.transform; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH; 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.ListMultimap; 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; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.gerrit.reviewdb.client.Change; @@ -183,14 +184,14 @@ } try { - mpm.waitFor(Futures.transformAsync(Futures.successfulAsList(futures), - new AsyncFunction<List<?>, Void>() { - @Override - public ListenableFuture<Void> apply(List<?> input) { - mpm.end(); - return Futures.immediateFuture(null); - } - })); + mpm.waitFor( + transform( + successfulAsList(futures), + x -> { + mpm.end(); + return null; + }, + directExecutor())); } catch (ExecutionException e) { log.error("Error in batch indexer", e); ok.set(false); @@ -199,14 +200,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 +217,18 @@ return new Callable<Void>() { @Override public Void call() throws Exception { - Multimap<ObjectId, ChangeData> byId = ArrayListMultimap.create(); + ListMultimap<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) { @@ -253,7 +260,7 @@ private final ChangeIndexer indexer; private final ThreeWayMergeStrategy mergeStrategy; private final AutoMerger autoMerger; - private final Multimap<ObjectId, ChangeData> byId; + private final ListMultimap<ObjectId, ChangeData> byId; private final ProgressMonitor done; private final ProgressMonitor failed; private final PrintWriter verboseWriter; @@ -262,7 +269,7 @@ private ProjectIndexer(ChangeIndexer indexer, ThreeWayMergeStrategy mergeStrategy, AutoMerger autoMerger, - Multimap<ObjectId, ChangeData> changesByCommitId, + ListMultimap<ObjectId, ChangeData> changesByCommitId, Repository repo, ProgressMonitor done, ProgressMonitor failed, @@ -290,6 +297,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)) { @@ -324,9 +334,7 @@ cd.setCurrentFilePaths(paths); indexer.index(cd); done.update(1); - if (verboseWriter != null) { - verboseWriter.println("Reindexed change " + cd.getId()); - } + verboseWriter.println("Reindexed change " + cd.getId()); } catch (Exception e) { fail("Failed to index change " + cd.getId(), true, e); } @@ -389,9 +397,7 @@ log.warn(error); } - if (verboseWriter != null) { - verboseWriter.println(error); - } + verboseWriter.println(error); } } }
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/ChangeIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java index 9bfd11f..2f1e4bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexDefinition.java
@@ -14,10 +14,12 @@ package com.google.gerrit.server.index.change; +import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.index.IndexDefinition; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; +import com.google.inject.util.Providers; public class ChangeIndexDefinition extends IndexDefinition<Change.Id, ChangeData, ChangeIndex> { @@ -26,8 +28,8 @@ ChangeIndexDefinition( ChangeIndexCollection indexCollection, ChangeIndex.Factory indexFactory, - AllChangesIndexer allChangesIndexer) { + @Nullable AllChangesIndexer allChangesIndexer) { super(ChangeSchemaDefinitions.INSTANCE, indexCollection, indexFactory, - allChangesIndexer); + Providers.of(allChangesIndexer)); } }
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..b204d76 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,38 @@ 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); + + @Deprecated + static final Schema<ChangeData> V36 = + schema(V35, + ChangeField.REF_STATE, + ChangeField.REF_STATE_PATTERN); + + static final Schema<ChangeData> V37 = schema(V36); 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/DummyChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/DummyChangeIndex.java index 78c463c..ff68106 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/DummyChangeIndex.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/DummyChangeIndex.java
@@ -57,8 +57,4 @@ public int getMaxLimit() { return Integer.MAX_VALUE; } - - @Override - public void stop() { - } }
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/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java index 942ce88..47d120a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java
@@ -147,8 +147,7 @@ } @Override - protected Void impl(RequestContext ctx) - throws OrmException, IOException, NoSuchChangeException { + protected Void impl(RequestContext ctx) throws OrmException, IOException { // Reload change, as some time may have passed since GetChanges. ReviewDb db = ctx.getReviewDbProvider().get(); try {
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..8194b6f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -0,0 +1,313 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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, + ListMultimap<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, + ListMultimap<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 ListMultimap<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, + ListMultimap<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/index/group/AllGroupsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java new file mode 100644 index 0000000..18b6cc3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -0,0 +1,138 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH; + +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.index.IndexExecutor; +import com.google.gerrit.server.index.SiteIndexer; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +@Singleton +public class AllGroupsIndexer + extends SiteIndexer<AccountGroup.UUID, AccountGroup, GroupIndex> { + private static final Logger log = + LoggerFactory.getLogger(AllGroupsIndexer.class); + + private final SchemaFactory<ReviewDb> schemaFactory; + private final ListeningExecutorService executor; + private final GroupCache groupCache; + + @Inject + AllGroupsIndexer( + SchemaFactory<ReviewDb> schemaFactory, + @IndexExecutor(BATCH) ListeningExecutorService executor, + GroupCache groupCache) { + this.schemaFactory = schemaFactory; + this.executor = executor; + this.groupCache = groupCache; + } + + @Override + public SiteIndexer.Result indexAll(GroupIndex index) { + ProgressMonitor progress = + new TextProgressMonitor(new PrintWriter(progressOut)); + progress.start(2); + Stopwatch sw = Stopwatch.createStarted(); + List<AccountGroup.UUID> uuids; + try { + uuids = collectGroups(progress); + } catch (OrmException e) { + log.error("Error collecting groups", e); + return new SiteIndexer.Result(sw, false, 0, 0); + } + return reindexGroups(index, uuids, progress); + } + + private SiteIndexer.Result reindexGroups(GroupIndex index, + List<AccountGroup.UUID> uuids, ProgressMonitor progress) { + progress.beginTask("Reindexing groups", uuids.size()); + List<ListenableFuture<?>> futures = new ArrayList<>(uuids.size()); + AtomicBoolean ok = new AtomicBoolean(true); + final AtomicInteger done = new AtomicInteger(); + final AtomicInteger failed = new AtomicInteger(); + Stopwatch sw = Stopwatch.createStarted(); + for (final AccountGroup.UUID uuid : uuids) { + final String desc = "group " + uuid; + ListenableFuture<?> future = executor.submit( + new Callable<Void>() { + @Override + public Void call() throws Exception { + try { + AccountGroup oldGroup = groupCache.get(uuid); + if (oldGroup != null) { + groupCache.evict(oldGroup); + } + index.replace(groupCache.get(uuid)); + verboseWriter.println("Reindexed " + desc); + done.incrementAndGet(); + } catch (Exception e) { + failed.incrementAndGet(); + throw e; + } + return null; + } + }); + addErrorListener(future, desc, progress, ok); + futures.add(future); + } + + try { + Futures.successfulAsList(futures).get(); + } catch (ExecutionException | InterruptedException e) { + log.error("Error waiting on group futures", e); + return new SiteIndexer.Result(sw, false, 0, 0); + } + + progress.endTask(); + return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); + } + + private List<AccountGroup.UUID> collectGroups(ProgressMonitor progress) + throws OrmException { + progress.beginTask("Collecting groups", ProgressMonitor.UNKNOWN); + List<AccountGroup.UUID> uuids = new ArrayList<>(); + try (ReviewDb db = schemaFactory.open()) { + for (AccountGroup group : db.accountGroups().all()) { + uuids.add(group.getGroupUUID()); + } + } + progress.endTask(); + return uuids; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java new file mode 100644 index 0000000..6c0ab86 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java
@@ -0,0 +1,95 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.FieldDef; +import com.google.gerrit.server.index.FieldType; +import com.google.gerrit.server.index.SchemaUtil; +import com.google.gwtorm.server.OrmException; + +/** Secondary index schemas for groups. */ +public class GroupField { + /** Legacy group ID. */ + public static final FieldDef<AccountGroup, Integer> ID = + new FieldDef.Single<AccountGroup, Integer>( + "id", FieldType.INTEGER, false) { + @Override + public Integer get(AccountGroup input, FillArgs args) { + return input.getId().get(); + } + }; + + /** Group UUID. */ + public static final FieldDef<AccountGroup, String> UUID = + new FieldDef.Single<AccountGroup, String>( + "uuid", FieldType.EXACT, true) { + @Override + public String get(AccountGroup input, FillArgs args) { + return input.getGroupUUID().get(); + } + }; + + /** Group owner UUID. */ + public static final FieldDef<AccountGroup, String> OWNER_UUID = + new FieldDef.Single<AccountGroup, String>( + "owner_uuid", FieldType.EXACT, false) { + @Override + public String get(AccountGroup input, FillArgs args) { + return input.getOwnerGroupUUID().get(); + } + }; + + /** Group name. */ + public static final FieldDef<AccountGroup, String> NAME = + new FieldDef.Single<AccountGroup, String>( + "name", FieldType.EXACT, false) { + @Override + public String get(AccountGroup input, FillArgs args) { + return input.getName(); + } + }; + + /** Prefix match on group name parts. */ + public static final FieldDef<AccountGroup, Iterable<String>> NAME_PART = + new FieldDef.Repeatable<AccountGroup, String>( + "name_part", FieldType.PREFIX, false) { + @Override + public Iterable<String> get(AccountGroup input, FillArgs args) { + return SchemaUtil.getNameParts(input.getName()); + } + }; + + /** Group description. */ + public static final FieldDef<AccountGroup, String> DESCRIPTION = + new FieldDef.Single<AccountGroup, String>( + "description", FieldType.FULL_TEXT, false) { + @Override + public String get(AccountGroup input, FillArgs args) { + return input.getDescription(); + } + }; + + /** Whether the group is visible to all users. */ + public static final FieldDef<AccountGroup, String> IS_VISIBLE_TO_ALL = + new FieldDef.Single<AccountGroup, String>( + "is_visible_to_all", FieldType.EXACT, false) { + @Override + public String get(AccountGroup input, FillArgs args) + throws OrmException { + return input.isVisibleToAll() ? "1" : "0"; + } + }; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java new file mode 100644 index 0000000..773da67 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.gerrit.reviewdb.client.AccountGroup; +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.group.GroupPredicates; + +public interface GroupIndex extends Index<AccountGroup.UUID, AccountGroup> { + public interface Factory extends + IndexDefinition.IndexFactory<AccountGroup.UUID, AccountGroup, GroupIndex> { + } + + @Override + default Predicate<AccountGroup> keyPredicate(AccountGroup.UUID uuid) { + return GroupPredicates.uuid(uuid); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java new file mode 100644 index 0000000..f8ca15d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.IndexCollection; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class GroupIndexCollection + extends IndexCollection<AccountGroup.UUID, AccountGroup, GroupIndex> { + @Inject + @VisibleForTesting + public GroupIndexCollection() { + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java new file mode 100644 index 0000000..0f89e73 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexDefinition.java
@@ -0,0 +1,33 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package com.google.gerrit.server.index.group; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.IndexDefinition; +import com.google.inject.Inject; +import com.google.inject.util.Providers; + +public class GroupIndexDefinition + extends IndexDefinition<AccountGroup.UUID, AccountGroup, GroupIndex> { + + @Inject + GroupIndexDefinition(GroupIndexCollection indexCollection, + GroupIndex.Factory indexFactory, + @Nullable AllGroupsIndexer allGroupsIndexer) { + super(GroupSchemaDefinitions.INSTANCE, indexCollection, indexFactory, + Providers.of(allGroupsIndexer)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java new file mode 100644 index 0000000..e64631e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexRewriter.java
@@ -0,0 +1,43 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.IndexRewriter; +import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class GroupIndexRewriter implements IndexRewriter<AccountGroup> { + private final GroupIndexCollection indexes; + + @Inject + GroupIndexRewriter(GroupIndexCollection indexes) { + this.indexes = indexes; + } + + @Override + public Predicate<AccountGroup> rewrite(Predicate<AccountGroup> in, + QueryOptions opts) throws QueryParseException { + GroupIndex index = indexes.getSearchIndex(); + checkNotNull(index, "no active search index configured for groups"); + return new IndexedGroupQuery(index, in, opts); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexer.java new file mode 100644 index 0000000..6606f8e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -0,0 +1,29 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.gerrit.reviewdb.client.AccountGroup; + +import java.io.IOException; + +public interface GroupIndexer { + + /** + * Synchronously index a group. + * + * @param uuid group UUID to index. + */ + void index(AccountGroup.UUID uuid) throws IOException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java new file mode 100644 index 0000000..dc2e81a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -0,0 +1,71 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.index.Index; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +public class GroupIndexerImpl implements GroupIndexer { + public interface Factory { + GroupIndexerImpl create(GroupIndexCollection indexes); + GroupIndexerImpl create(@Nullable GroupIndex index); + } + + private final GroupCache groupCache; + private final GroupIndexCollection indexes; + private final GroupIndex index; + + @AssistedInject + GroupIndexerImpl(GroupCache groupCache, + @Assisted GroupIndexCollection indexes) { + this.groupCache = groupCache; + this.indexes = indexes; + this.index = null; + } + + @AssistedInject + GroupIndexerImpl(GroupCache groupCache, + @Assisted GroupIndex index) { + this.groupCache = groupCache; + this.indexes = null; + this.index = index; + } + + @Override + public void index(AccountGroup.UUID uuid) throws IOException { + for (Index<?, AccountGroup> i : getWriteIndexes()) { + i.replace(groupCache.get(uuid)); + } + } + + private Collection<GroupIndex> getWriteIndexes() { + if (indexes != null) { + return indexes.getWriteIndexes(); + } + + return index != null + ? Collections.singleton(index) + : ImmutableSet.of(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java new file mode 100644 index 0000000..42062cd --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import static com.google.gerrit.server.index.SchemaUtil.schema; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.SchemaDefinitions; + +public class GroupSchemaDefinitions extends SchemaDefinitions<AccountGroup> { + @Deprecated + static final Schema<AccountGroup> V1 = schema( + GroupField.ID, + GroupField.UUID, + GroupField.OWNER_UUID, + GroupField.NAME, + GroupField.NAME_PART, + GroupField.DESCRIPTION, + GroupField.IS_VISIBLE_TO_ALL); + + static final Schema<AccountGroup> V2 = schema(V1); + + public static final GroupSchemaDefinitions INSTANCE = + new GroupSchemaDefinitions(); + + private GroupSchemaDefinitions() { + super("groups", AccountGroup.class); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java new file mode 100644 index 0000000..9ee9b78 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/IndexedGroupQuery.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.index.group; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.Index; +import com.google.gerrit.server.index.IndexedQuery; +import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.query.DataSource; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; + +public class IndexedGroupQuery + extends IndexedQuery<AccountGroup.UUID, AccountGroup> + implements DataSource<AccountGroup> { + + public IndexedGroupQuery(Index<AccountGroup.UUID, AccountGroup> index, + Predicate<AccountGroup> pred, QueryOptions opts) + throws QueryParseException { + super(index, pred, opts.convertForBackend()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java deleted file mode 100644 index 1e8bdf4..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java +++ /dev/null
@@ -1,55 +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 com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; -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; - -/** Send notice about a change being abandoned by its owner. */ -public class AbandonedSender extends ReplyToChangeSender { - public interface Factory extends - ReplyToChangeSender.Factory<AbandonedSender> { - @Override - AbandonedSender create(Project.NameKey project, Change.Id change); - } - - @Inject - public AbandonedSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "abandon", newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - includeWatchers(NotifyType.ABANDONED_CHANGES); - includeWatchers(NotifyType.ALL_COMMENTS); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("Abandoned.vm")); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java deleted file mode 100644 index f825d1c..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java +++ /dev/null
@@ -1,113 +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.mail; - -import com.google.common.base.Joiner; -import com.google.gerrit.common.errors.EmailException; -import com.google.gerrit.reviewdb.client.AccountSshKey; -import com.google.gerrit.server.IdentifiedUser; -import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; - -import java.util.List; - -public class AddKeySender extends OutgoingEmail { - public interface Factory { - AddKeySender create(IdentifiedUser user, AccountSshKey sshKey); - - AddKeySender create(IdentifiedUser user, List<String> gpgKey); - } - - private final IdentifiedUser callingUser; - private final IdentifiedUser user; - private final AccountSshKey sshKey; - private final List<String> gpgKeys; - - @AssistedInject - public AddKeySender(EmailArguments ea, - IdentifiedUser callingUser, - @Assisted IdentifiedUser user, - @Assisted AccountSshKey sshKey) { - super(ea, "addkey"); - this.callingUser = callingUser; - this.user = user; - this.sshKey = sshKey; - this.gpgKeys = null; - } - - @AssistedInject - public AddKeySender(EmailArguments ea, - IdentifiedUser callingUser, - @Assisted IdentifiedUser user, - @Assisted List<String> gpgKeys) { - super(ea, "addkey"); - this.callingUser = callingUser; - this.user = user; - this.sshKey = null; - this.gpgKeys = gpgKeys; - } - - @Override - protected void init() throws EmailException { - super.init(); - setHeader("Subject", - String.format("[Gerrit Code Review] New %s Keys Added", getKeyType())); - add(RecipientType.TO, new Address(getEmail())); - } - - @Override - protected boolean shouldSendMessage() { - /* - * Don't send an email if no keys are added, or an admin is adding a key to - * a user. - */ - return (sshKey != null || gpgKeys.size() > 0) && - (user.equals(callingUser) || - !callingUser.getCapabilities().canAdministrateServer()); - } - - @Override - protected void format() throws EmailException { - appendText(velocifyFile("AddKey.vm")); - } - - public String getEmail() { - return user.getAccount().getPreferredEmail(); - } - - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - - public String getKeyType() { - if (sshKey != null) { - return "SSH"; - } else if (gpgKeys != null) { - return "GPG"; - } - return "Unknown"; - } - - public String getSshKey() { - return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null; - } - - public String getGpgKeys() { - if (gpgKeys != null) { - return Joiner.on("\n").join(gpgKeys); - } - return null; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java deleted file mode 100644 index c9e42ad..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java +++ /dev/null
@@ -1,44 +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 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; - -/** Asks a user to review a change. */ -public class AddReviewerSender extends NewChangeSender { - public interface Factory { - AddReviewerSender create(Project.NameKey project, Change.Id id); - } - - @Inject - public AddReviewerSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccExistingReviewers(); - } -}
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/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java deleted file mode 100644 index badc706..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java +++ /dev/null
@@ -1,482 +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.mail; - -import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; - -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.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; -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.PatchSet; -import com.google.gerrit.reviewdb.client.PatchSetInfo; -import com.google.gerrit.reviewdb.client.Project; -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.patch.PatchList; -import com.google.gerrit.server.patch.PatchListEntry; -import com.google.gerrit.server.patch.PatchListNotAvailableException; -import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; -import com.google.gerrit.server.project.NoSuchChangeException; -import com.google.gerrit.server.project.ProjectState; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gwtorm.server.OrmException; - -import org.eclipse.jgit.diff.DiffFormatter; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.util.RawParseUtils; -import org.eclipse.jgit.util.TemporaryBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.sql.Timestamp; -import java.text.MessageFormat; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; - -/** Sends an email to one or more interested parties. */ -public abstract class ChangeEmail extends NotificationEmail { - private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class); - - protected static ChangeData newChangeData(EmailArguments ea, - Project.NameKey project, Change.Id id) { - return ea.changeDataFactory.create(ea.db.get(), project, id); - } - - protected final Change change; - protected final ChangeData changeData; - protected PatchSet patchSet; - protected PatchSetInfo patchSetInfo; - protected String changeMessage; - protected Timestamp timestamp; - - protected ProjectState projectState; - protected Set<Account.Id> authors; - protected boolean emailOnlyAuthors; - - protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) - throws OrmException { - super(ea, mc, cd.change().getDest()); - changeData = cd; - change = cd.change(); - emailOnlyAuthors = false; - } - - @Override - public void setFrom(final Account.Id id) { - super.setFrom(id); - - /** Is the from user in an email squelching group? */ - final IdentifiedUser user = args.identifiedUserFactory.create(id); - emailOnlyAuthors = !user.getCapabilities().canEmailReviewers(); - } - - public void setPatchSet(final PatchSet ps) { - patchSet = ps; - } - - public void setPatchSet(final PatchSet ps, final PatchSetInfo psi) { - patchSet = ps; - patchSetInfo = psi; - } - - @Deprecated - public void setChangeMessage(final ChangeMessage cm) { - setChangeMessage(cm.getMessage(), cm.getWrittenOn()); - } - - public void setChangeMessage(String cm, Timestamp t) { - changeMessage = cm; - timestamp = t; - } - - /** Format the message body by calling {@link #appendText(String)}. */ - @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); - } - formatFooter(); - } - - /** Format the message body by calling {@link #appendText(String)}. */ - protected abstract void formatChange() throws EmailException; - - /** - * Format the message footer by calling {@link #appendText(String)}. - * - * @throws EmailException if an error occurred. - */ - protected void formatFooter() throws EmailException { - } - - /** Setup the message headers and envelope (TO, CC, BCC). */ - @Override - protected void init() throws EmailException { - if (args.projectCache != null) { - projectState = args.projectCache.get(change.getProject()); - } else { - projectState = null; - } - - if (patchSet == null) { - try { - patchSet = changeData.currentPatchSet(); - } catch (OrmException err) { - patchSet = null; - } - } - - if (patchSet != null && patchSetInfo == null) { - try { - patchSetInfo = args.patchSetInfoFactory.get( - args.db.get(), changeData.notes(), patchSet.getId()); - } catch (PatchSetInfoNotAvailableException | OrmException err) { - patchSetInfo = null; - } - } - authors = getAuthors(); - - super.init(); - if (timestamp != null) { - setHeader("Date", new Date(timestamp.getTime())); - } - setChangeSubjectHeader(); - setHeader("X-Gerrit-Change-Id", "" + change.getKey().get()); - setChangeUrlHeader(); - setCommitIdHeader(); - } - - private void setChangeUrlHeader() { - final String u = getChangeUrl(); - if (u != null) { - setHeader("X-Gerrit-ChangeURL", "<" + u + ">"); - } - } - - private void setCommitIdHeader() { - if (patchSet != null && patchSet.getRevision() != null - && patchSet.getRevision().get() != null - && patchSet.getRevision().get().length() > 0) { - setHeader("X-Gerrit-Commit", patchSet.getRevision().get()); - } - } - - private void setChangeSubjectHeader() throws EmailException { - setHeader("Subject", velocifyFile("ChangeSubject.vm")); - } - - /** Get a link to the change; null if the server doesn't know its own address. */ - public String getChangeUrl() { - if (getGerritUrl() != null) { - final StringBuilder r = new StringBuilder(); - r.append(getGerritUrl()); - r.append(change.getChangeId()); - return r.toString(); - } - return null; - } - - public String getChangeMessageThreadId() throws EmailException { - return velocify("<gerrit.${change.createdOn.time}.$change.key.get()" + - "@$email.gerritHost>"); - } - - /** Format the sender's "cover letter", {@link #getCoverLetter()}. */ - protected void formatCoverLetter() { - final String cover = getCoverLetter(); - if (!"".equals(cover)) { - appendText(cover); - appendText("\n\n"); - } - } - - /** Get the text of the "cover letter". */ - public String getCoverLetter() { - if (changeMessage != null) { - return changeMessage.trim(); - } - return ""; - } - - /** Format the change message and the affected file list. */ - protected void formatChangeDetail() { - appendText(getChangeDetail()); - } - - /** Create the change message and the affected file list. */ - public String getChangeDetail() { - try { - StringBuilder detail = new StringBuilder(); - - if (patchSetInfo != null) { - detail.append(patchSetInfo.getMessage().trim()).append("\n"); - } else { - detail.append(change.getSubject().trim()).append("\n"); - } - - if (patchSet != null) { - detail.append("---\n"); - PatchList patchList = getPatchList(); - for (PatchListEntry p : patchList.getPatches()) { - if (Patch.COMMIT_MSG.equals(p.getNewName())) { - continue; - } - detail.append(p.getChangeType().getCode()) - .append(" ").append(p.getNewName()).append("\n"); - } - detail.append(MessageFormat.format("" // - + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " // - + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " // - + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" // - + "\n", patchList.getPatches().size() - 1, // - patchList.getInsertions(), // - patchList.getDeletions())); - detail.append("\n"); - } - return detail.toString(); - } catch (Exception err) { - log.warn("Cannot format change detail", err); - return ""; - } - } - - /** Get the patch list corresponding to this patch set. */ - protected PatchList getPatchList() throws PatchListNotAvailableException { - if (patchSet != null) { - return args.patchListCache.get(change, patchSet); - } - throw new PatchListNotAvailableException("no patchSet specified"); - } - - /** Get the project entity the change is in; null if its been deleted. */ - protected ProjectState getProjectState() { - return projectState; - } - - /** Get the groups which own the project. */ - protected Set<AccountGroup.UUID> getProjectOwners() { - final ProjectState r; - - r = args.projectCache.get(change.getProject()); - return r != null ? r.getOwners() : Collections.<AccountGroup.UUID> emptySet(); - } - - /** TO or CC all vested parties (change owner, patch set uploader, author). */ - protected void rcptToAuthors(final RecipientType rt) { - for (final Account.Id id : authors) { - add(rt, id); - } - } - - /** BCC any user who has starred this change. */ - protected void bccStarredBy() { - if (!NotifyHandling.ALL.equals(notify)) { - return; - } - - try { - // BCC anyone who has starred this change - // and remove anyone who has ignored this change. - // - Multimap<Account.Id, String> stars = - args.starredChangesUtil.byChangeFromIndex(change.getId()); - for (Map.Entry<Account.Id, Collection<String>> e : - stars.asMap().entrySet()) { - if (e.getValue().contains(StarredChangesUtil.DEFAULT_LABEL)) { - super.add(RecipientType.BCC, e.getKey()); - } - if (e.getValue().contains(StarredChangesUtil.IGNORE_LABEL)) { - AccountState accountState = args.accountCache.get(e.getKey()); - if (accountState != null) { - removeUser(accountState.getAccount()); - } - } - } - } catch (OrmException | NoSuchChangeException err) { - // Just don't BCC everyone. Better to send a partial message to those - // we already have queued up then to fail deliver entirely to people - // who have a lower interest in the change. - log.warn("Cannot BCC users that starred updated change", err); - } - } - - @Override - protected final Watchers getWatchers(NotifyType type) throws OrmException { - if (!NotifyHandling.ALL.equals(notify)) { - return new Watchers(); - } - - ProjectWatch watch = new ProjectWatch( - args, branch.getParentKey(), projectState, changeData); - return watch.getWatchers(type); - } - - /** Any user who has published comments on this change. */ - protected void ccAllApprovals() { - if (!NotifyHandling.ALL.equals(notify) - && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) { - return; - } - - try { - for (Account.Id id : changeData.reviewers().all()) { - add(RecipientType.CC, id); - } - } catch (OrmException err) { - log.warn("Cannot CC users that reviewed updated change", err); - } - } - - /** Users who have non-zero approval codes on the change. */ - protected void ccExistingReviewers() { - if (!NotifyHandling.ALL.equals(notify) - && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) { - return; - } - - try { - for (Account.Id id : changeData.reviewers().byState(REVIEWER)) { - add(RecipientType.CC, id); - } - } catch (OrmException err) { - log.warn("Cannot CC users that commented on updated change", err); - } - } - - @Override - protected void add(final RecipientType rt, final Account.Id to) { - if (! emailOnlyAuthors || authors.contains(to)) { - super.add(rt, to); - } - } - - @Override - protected boolean isVisibleTo(final Account.Id to) throws OrmException { - return projectState == null - || projectState.controlFor(args.identifiedUserFactory.create(to)) - .controlFor(args.db.get(), change).isVisible(args.db.get()); - } - - /** Find all users who are authors of any part of this change. */ - protected Set<Account.Id> getAuthors() { - Set<Account.Id> authors = new HashSet<>(); - - switch (notify) { - case NONE: - break; - case ALL: - default: - if (patchSet != null) { - authors.add(patchSet.getUploader()); - } - if (patchSetInfo != null) { - if (patchSetInfo.getAuthor().getAccount() != null) { - authors.add(patchSetInfo.getAuthor().getAccount()); - } - if (patchSetInfo.getCommitter().getAccount() != null) { - authors.add(patchSetInfo.getCommitter().getAccount()); - } - } - //$FALL-THROUGH$ - case OWNER_REVIEWERS: - case OWNER: - authors.add(change.getOwner()); - break; - } - - return authors; - } - - @Override - protected void setupVelocityContext() { - super.setupVelocityContext(); - velocityContext.put("change", change); - velocityContext.put("changeId", change.getKey()); - velocityContext.put("coverLetter", getCoverLetter()); - velocityContext.put("fromName", getNameFor(fromId)); - velocityContext.put("patchSet", patchSet); - velocityContext.put("patchSetInfo", patchSetInfo); - } - - public boolean getIncludeDiff() { - return args.settings.includeDiff; - } - - private static int HEAP_EST_SIZE = 32 * 1024; - - /** Show patch set as unified difference. */ - public String getUnifiedDiff() { - PatchList patchList; - try { - patchList = getPatchList(); - if (patchList.getOldId() == null) { - // Octopus merges are not well supported for diff output by Gerrit. - // Currently these always have a null oldId in the PatchList. - return "[Octopus merge; cannot be formatted as a diff.]\n"; - } - } catch (PatchListNotAvailableException e) { - log.error("Cannot format patch", e); - return ""; - } - - int maxSize = args.settings.maximumDiffSize; - TemporaryBuffer.Heap buf = - new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxSize), maxSize); - try (DiffFormatter fmt = new DiffFormatter(buf)) { - try (Repository git = args.server.openRepository(change.getProject())) { - try { - fmt.setRepository(git); - fmt.setDetectRenames(true); - fmt.format(patchList.getOldId(), patchList.getNewId()); - return RawParseUtils.decode(buf.toByteArray()); - } catch (IOException e) { - if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) { - return ""; - } - log.error("Cannot format patch", e); - return ""; - } - } catch (IOException e) { - log.error("Cannot open repository to format patch", e); - return ""; - } - } - } -}
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/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java deleted file mode 100644 index 2110e37..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java +++ /dev/null
@@ -1,86 +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.common.collect.Iterables; -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Notify interested parties of a brand new change. */ -public class CreateChangeSender extends NewChangeSender { - private static final Logger log = - LoggerFactory.getLogger(CreateChangeSender.class); - - public interface Factory { - CreateChangeSender create(Project.NameKey project, Change.Id id); - } - - @Inject - public CreateChangeSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - if (change.getStatus() == Change.Status.NEW) { - try { - // Try to mark interested owners with TO and CC or BCC line. - Watchers matching = getWatchers(NotifyType.NEW_CHANGES); - for (Account.Id user : Iterables.concat( - matching.to.accounts, - matching.cc.accounts, - matching.bcc.accounts)) { - if (isOwnerOfProjectOrBranch(user)) { - add(RecipientType.TO, user); - } - } - - // Add everyone else. Owners added above will not be duplicated. - add(RecipientType.TO, matching.to); - add(RecipientType.CC, matching.cc); - add(RecipientType.BCC, matching.bcc); - } catch (OrmException err) { - // Just don't CC everyone. Better to send a partial message to those - // we already have queued up then to fail deliver entirely to people - // who have a lower interest in the change. - log.warn("Cannot notify watchers for new change", err); - } - - includeWatchers(NotifyType.NEW_PATCHSETS); - } - } - - private boolean isOwnerOfProjectOrBranch(Account.Id user) { - return projectState != null - && projectState.controlFor(args.identifiedUserFactory.create(user)) - .controlForRef(change.getDest()) - .isOwner(); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java deleted file mode 100644 index 75f9f82..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java +++ /dev/null
@@ -1,81 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.mail; - -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** Let users know that a reviewer and possibly her review have - * been removed. */ -public class DeleteReviewerSender extends ReplyToChangeSender { - private final Set<Account.Id> reviewers = new HashSet<>(); - - public interface Factory extends - ReplyToChangeSender.Factory<DeleteReviewerSender> { - @Override - DeleteReviewerSender create(Project.NameKey project, Change.Id change); - } - - @Inject - public DeleteReviewerSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "deleteReviewer", newChangeData(ea, project, id)); - } - - public void addReviewers(Collection<Account.Id> cc) { - reviewers.addAll(cc); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - ccExistingReviewers(); - includeWatchers(NotifyType.ALL_COMMENTS); - add(RecipientType.TO, reviewers); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("DeleteReviewer.vm")); - } - - public List<String> getReviewerNames() { - if (reviewers.isEmpty()) { - return null; - } - List<String> names = new ArrayList<>(); - for (Account.Id id : reviewers) { - names.add(getNameFor(id)); - } - return names; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java deleted file mode 100644 index d861109..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java +++ /dev/null
@@ -1,54 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.mail; - -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -/** Send notice about a vote that was removed from a change. */ -public class DeleteVoteSender extends ReplyToChangeSender { - public interface Factory extends - ReplyToChangeSender.Factory<DeleteVoteSender> { - @Override - DeleteVoteSender create(Project.NameKey project, Change.Id change); - } - - @Inject - protected DeleteVoteSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "deleteVote", newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("DeleteVote.vm")); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java deleted file mode 100644 index 68e5e50..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java +++ /dev/null
@@ -1,138 +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.mail; - -import com.google.gerrit.common.Nullable; -import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.AnonymousUser; -import com.google.gerrit.server.ApprovalsUtil; -import com.google.gerrit.server.GerritPersonIdentProvider; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.IdentifiedUser.GenericFactory; -import com.google.gerrit.server.StarredChangesUtil; -import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.CapabilityControl; -import com.google.gerrit.server.account.GroupBackend; -import com.google.gerrit.server.account.GroupIncludeCache; -import com.google.gerrit.server.config.AllProjectsName; -import com.google.gerrit.server.config.AnonymousCowardName; -import com.google.gerrit.server.config.CanonicalWebUrl; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.index.account.AccountIndexCollection; -import com.google.gerrit.server.notedb.ChangeNotes; -import com.google.gerrit.server.patch.PatchListCache; -import com.google.gerrit.server.patch.PatchSetInfoFactory; -import com.google.gerrit.server.project.ProjectCache; -import com.google.gerrit.server.query.account.InternalAccountQuery; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.ChangeQueryBuilder; -import com.google.gerrit.server.ssh.SshAdvertisedAddresses; -import com.google.gerrit.server.validators.OutgoingEmailValidationListener; -import com.google.inject.Inject; -import com.google.inject.Provider; - -import org.apache.velocity.runtime.RuntimeInstance; -import org.eclipse.jgit.lib.PersonIdent; - -import java.util.List; - -public class EmailArguments { - final GitRepositoryManager server; - final ProjectCache projectCache; - final GroupBackend groupBackend; - final GroupIncludeCache groupIncludes; - final AccountCache accountCache; - final PatchListCache patchListCache; - final ApprovalsUtil approvalsUtil; - final FromAddressGenerator fromAddressGenerator; - final EmailSender emailSender; - final PatchSetInfoFactory patchSetInfoFactory; - final IdentifiedUser.GenericFactory identifiedUserFactory; - final CapabilityControl.Factory capabilityControlFactory; - final ChangeNotes.Factory changeNotesFactory; - final AnonymousUser anonymousUser; - final String anonymousCowardName; - final PersonIdent gerritPersonIdent; - final Provider<String> urlProvider; - final AllProjectsName allProjectsName; - final List<String> sshAddresses; - - final ChangeQueryBuilder queryBuilder; - final Provider<ReviewDb> db; - final ChangeData.Factory changeDataFactory; - final RuntimeInstance velocityRuntime; - final EmailSettings settings; - final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners; - final StarredChangesUtil starredChangesUtil; - final AccountIndexCollection accountIndexes; - final Provider<InternalAccountQuery> accountQueryProvider; - - @Inject - EmailArguments(GitRepositoryManager server, ProjectCache projectCache, - GroupBackend groupBackend, GroupIncludeCache groupIncludes, - AccountCache accountCache, - PatchListCache patchListCache, - ApprovalsUtil approvalsUtil, - FromAddressGenerator fromAddressGenerator, - EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory, - GenericFactory identifiedUserFactory, - CapabilityControl.Factory capabilityControlFactory, - ChangeNotes.Factory changeNotesFactory, - AnonymousUser anonymousUser, - @AnonymousCowardName String anonymousCowardName, - GerritPersonIdentProvider gerritPersonIdentProvider, - @CanonicalWebUrl @Nullable Provider<String> urlProvider, - AllProjectsName allProjectsName, - ChangeQueryBuilder queryBuilder, - Provider<ReviewDb> db, - ChangeData.Factory changeDataFactory, - RuntimeInstance velocityRuntime, - EmailSettings settings, - @SshAdvertisedAddresses List<String> sshAddresses, - DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners, - StarredChangesUtil starredChangesUtil, - AccountIndexCollection accountIndexes, - Provider<InternalAccountQuery> accountQueryProvider) { - this.server = server; - this.projectCache = projectCache; - this.groupBackend = groupBackend; - this.groupIncludes = groupIncludes; - this.accountCache = accountCache; - this.patchListCache = patchListCache; - this.approvalsUtil = approvalsUtil; - this.fromAddressGenerator = fromAddressGenerator; - this.emailSender = emailSender; - this.patchSetInfoFactory = patchSetInfoFactory; - this.identifiedUserFactory = identifiedUserFactory; - this.capabilityControlFactory = capabilityControlFactory; - this.changeNotesFactory = changeNotesFactory; - this.anonymousUser = anonymousUser; - this.anonymousCowardName = anonymousCowardName; - this.gerritPersonIdent = gerritPersonIdentProvider.get(); - this.urlProvider = urlProvider; - this.allProjectsName = allProjectsName; - this.queryBuilder = queryBuilder; - this.db = db; - this.changeDataFactory = changeDataFactory; - this.velocityRuntime = velocityRuntime; - this.settings = settings; - this.sshAddresses = sshAddresses; - this.outgoingEmailValidationListeners = outgoingEmailValidationListeners; - this.starredChangesUtil = starredChangesUtil; - this.accountIndexes = accountIndexes; - this.accountQueryProvider = accountQueryProvider; - } -}
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/EmailHeader.java deleted file mode 100644 index 6a964a3..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java +++ /dev/null
@@ -1,245 +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 java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.base.MoreObjects; - -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -public abstract class EmailHeader { - public abstract boolean isEmpty(); - - public abstract void write(Writer w) throws IOException; - - public static class String extends EmailHeader { - private final java.lang.String value; - - public String(java.lang.String v) { - value = v; - } - - public java.lang.String getString() { - return value; - } - - @Override - public boolean isEmpty() { - return value == null || value.length() == 0; - } - - @Override - public void write(Writer w) throws IOException { - if (needsQuotedPrintable(value)) { - w.write(quotedPrintable(value)); - } else { - w.write(value); - } - } - - @Override - public int hashCode() { - return Objects.hashCode(value); - } - - @Override - public boolean equals(Object o) { - return (o instanceof String) - && Objects.equals(value, ((String) o).value); - } - - @Override - public java.lang.String toString() { - return MoreObjects.toStringHelper(this).addValue(value).toString(); - } - } - - static boolean needsQuotedPrintable(java.lang.String value) { - for (int i = 0; i < value.length(); i++) { - if (value.charAt(i) < ' ' || '~' < value.charAt(i)) { - return true; - } - } - return false; - } - - static boolean needsQuotedPrintableWithinPhrase(final int cp) { - switch (cp) { - case '!': - case '*': - case '+': - case '-': - case '/': - case '=': - case '_': - return false; - default: - if (('a' <= cp && cp <= 'z') - || ('A' <= cp && cp <= 'Z') - || ('0' <= cp && cp <= '9')) { - return false; - } - return true; - } - } - - static java.lang.String quotedPrintable(java.lang.String value) { - final StringBuilder r = new StringBuilder(); - - r.append("=?UTF-8?Q?"); - for (int i = 0; i < value.length(); i++) { - final int cp = value.codePointAt(i); - if (cp == ' ') { - r.append('_'); - - } else if (needsQuotedPrintableWithinPhrase(cp)) { - byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes(UTF_8); - for (byte b: buf) { - r.append('='); - r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase()); - r.append(Integer.toHexString(b & 0x0f).toUpperCase()); - } - - } else { - r.append(Character.toChars(cp)); - } - } - r.append("?="); - - return r.toString(); - } - - public static class Date extends EmailHeader { - private final java.util.Date value; - - public Date(java.util.Date v) { - value = v; - } - - public java.util.Date getDate() { - return value; - } - - @Override - public boolean isEmpty() { - return value == null; - } - - @Override - public void write(Writer w) throws IOException { - final SimpleDateFormat fmt; - // Mon, 1 Jun 2009 10:49:44 -0700 - fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US); - w.write(fmt.format(value)); - } - - @Override - public int hashCode() { - return Objects.hashCode(value); - } - - @Override - public boolean equals(Object o) { - return (o instanceof Date) - && Objects.equals(value, ((Date) o).value); - } - - @Override - public java.lang.String toString() { - return MoreObjects.toStringHelper(this).addValue(value).toString(); - } - } - - public static class AddressList extends EmailHeader { - private final List<Address> list = new ArrayList<>(); - - public AddressList() { - } - - public AddressList(Address addr) { - add(addr); - } - - public List<Address> getAddressList() { - return Collections.unmodifiableList(list); - } - - public void add(Address addr) { - list.add(addr); - } - - void remove(java.lang.String email) { - for (Iterator<Address> i = list.iterator(); i.hasNext();) { - if (i.next().email.equals(email)) { - i.remove(); - } - } - } - - @Override - public boolean isEmpty() { - return list.isEmpty(); - } - - @Override - public void write(Writer w) throws IOException { - int len = 8; - boolean firstAddress = true; - boolean needComma = false; - for (final Address addr : list) { - java.lang.String s = addr.toHeaderString(); - if (firstAddress) { - firstAddress = false; - } else if (72 < len + s.length()) { - w.write(",\r\n\t"); - len = 8; - needComma = false; - } - - if (needComma) { - w.write(", "); - } - w.write(s); - len += s.length(); - needComma = true; - } - } - - @Override - public int hashCode() { - return Objects.hashCode(list); - } - - @Override - public boolean equals(Object o) { - return (o instanceof AddressList) - && Objects.equals(list, ((AddressList) o).list); - } - - @Override - public java.lang.String toString() { - return MoreObjects.toStringHelper(this).addValue(list).toString(); - } - } -}
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/Encryption.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java new file mode 100644 index 0000000..a557532 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Encryption.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail; + +public enum Encryption { + NONE, SSL, TLS +}
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/FromAddressGenerator.java deleted file mode 100644 index 9bcabc3..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java +++ /dev/null
@@ -1,24 +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.reviewdb.client.Account; - -/** Constructs an address to send email from. */ -public interface FromAddressGenerator { - boolean isGenericAddress(Account.Id fromId); - - Address from(Account.Id fromId); -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java deleted file mode 100644 index 51f7ad1..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java +++ /dev/null
@@ -1,189 +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 java.nio.charset.StandardCharsets.UTF_8; - -import com.google.gerrit.common.data.ParameterizedString; -import com.google.gerrit.reviewdb.client.Account; -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.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Singleton; - -import org.apache.commons.codec.binary.Base64; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.PersonIdent; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */ -@Singleton -public class FromAddressGeneratorProvider implements - Provider<FromAddressGenerator> { - private final FromAddressGenerator generator; - - @Inject - FromAddressGeneratorProvider(@GerritServerConfig final Config cfg, - @AnonymousCowardName final String anonymousCowardName, - @GerritPersonIdent final PersonIdent myIdent, - final AccountCache accountCache) { - - final String from = cfg.getString("sendemail", null, "from"); - final Address srvAddr = toAddress(myIdent); - - if (from == null || "MIXED".equalsIgnoreCase(from)) { - ParameterizedString name = new ParameterizedString("${user} (Code Review)"); - generator = - new PatternGen(srvAddr, accountCache, anonymousCowardName, name, - srvAddr.email); - - } else if ("USER".equalsIgnoreCase(from)) { - generator = new UserGen(accountCache, 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; - if (name == null || name.getParameterNames().isEmpty()) { - generator = new ServerGen(a); - } else { - generator = - new PatternGen(srvAddr, accountCache, anonymousCowardName, name, - a.email); - } - } - } - - private static Address toAddress(final PersonIdent myIdent) { - return new Address(myIdent.getName(), myIdent.getEmailAddress()); - } - - @Override - public FromAddressGenerator get() { - return generator; - } - - static final class UserGen implements FromAddressGenerator { - private final AccountCache accountCache; - private final Address srvAddr; - - UserGen(AccountCache accountCache, Address srvAddr) { - this.accountCache = accountCache; - this.srvAddr = srvAddr; - } - - @Override - public boolean isGenericAddress(Account.Id fromId) { - return false; - } - - @Override - public Address from(final Account.Id fromId) { - if (fromId != null) { - Account a = accountCache.get(fromId).getAccount(); - String userEmail = a.getPreferredEmail(); - return new Address( - a.getFullName(), - userEmail != null ? userEmail : srvAddr.getEmail()); - } - return srvAddr; - } - } - - static final class ServerGen implements FromAddressGenerator { - private final Address srvAddr; - - ServerGen(Address srvAddr) { - this.srvAddr = srvAddr; - } - - @Override - public boolean isGenericAddress(Account.Id fromId) { - return true; - } - - @Override - public Address from(final Account.Id fromId) { - return srvAddr; - } - } - - static final class PatternGen implements FromAddressGenerator { - private final ParameterizedString senderEmailPattern; - private final Address serverAddress; - private final AccountCache accountCache; - private final String anonymousCowardName; - private final ParameterizedString namePattern; - - PatternGen(final Address serverAddress, final AccountCache accountCache, - final String anonymousCowardName, - final ParameterizedString namePattern, final String senderEmail) { - this.senderEmailPattern = new ParameterizedString(senderEmail); - this.serverAddress = serverAddress; - this.accountCache = accountCache; - this.anonymousCowardName = anonymousCowardName; - this.namePattern = namePattern; - } - - @Override - public boolean isGenericAddress(Account.Id fromId) { - return false; - } - - @Override - public Address from(final Account.Id fromId) { - final String senderName; - - if (fromId != null) { - final Account account = accountCache.get(fromId).getAccount(); - String fullName = account.getFullName(); - if (fullName == null || "".equals(fullName)) { - fullName = anonymousCowardName; - } - senderName = namePattern.replace("user", fullName).toString(); - - } else { - senderName = serverAddress.name; - } - - String senderEmail; - if (senderEmailPattern.getParameterNames().isEmpty()) { - senderEmail = senderEmailPattern.getRawPattern(); - } else { - senderEmail = senderEmailPattern - .replace("userHash", hashOf(senderName)) - .toString(); - } - return new Address(senderName, senderEmail); - } - } - - private static String hashOf(String data) { - try { - MessageDigest hash = MessageDigest.getInstance("MD5"); - byte[] bytes = hash.digest(data.getBytes(UTF_8)); - return Base64.encodeBase64URLSafeString(bytes); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("No MD5 available", e); - } - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailFilter.java new file mode 100644 index 0000000..31c6641 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailFilter.java
@@ -0,0 +1,35 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; +import com.google.gerrit.server.mail.receive.MailMessage; + +/** + * Listener to filter incoming email. + * <p> + * Invoked by Gerrit for each incoming email. + */ +@ExtensionPoint +public interface MailFilter { + /** + * Determine if Gerrit should discard or further process the message. + * + * @param message MailMessage parsed by Gerrit. + * @return {@code true}, if Gerrit should process the message, {@code false} + * otherwise. + */ + boolean shouldProcessMessage(MailMessage message); +}
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/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java deleted file mode 100644 index f6c3d0f..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java +++ /dev/null
@@ -1,126 +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.common.collect.HashBasedTable; -import com.google.common.collect.Table; -import com.google.gerrit.common.data.LabelType; -import com.google.gerrit.common.data.LabelTypes; -import com.google.gerrit.common.data.LabelValue; -import com.google.gerrit.common.errors.EmailException; -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.PatchSetApproval; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -/** Send notice about a change successfully merged. */ -public class MergedSender extends ReplyToChangeSender { - public interface Factory { - MergedSender create(Project.NameKey project, Change.Id id); - } - - private final LabelTypes labelTypes; - - @Inject - public MergedSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "merged", newChangeData(ea, project, id)); - labelTypes = changeData.changeControl().getLabelTypes(); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS); - includeWatchers(NotifyType.SUBMITTED_CHANGES); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("Merged.vm")); - } - - public String getApprovals() { - try { - Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create(); - Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create(); - for (PatchSetApproval ca : args.approvalsUtil.byPatchSet( - args.db.get(), changeData.changeControl(), patchSet.getId())) { - LabelType lt = labelTypes.byLabel(ca.getLabelId()); - if (lt == null) { - continue; - } - if (ca.getValue() > 0) { - pos.put(ca.getAccountId(), lt.getName(), ca); - } else if (ca.getValue() < 0) { - neg.put(ca.getAccountId(), lt.getName(), ca); - } - } - - return format("Approvals", pos) + format("Objections", neg); - } catch (OrmException err) { - // Don't list the approvals - } - return ""; - } - - private String format(String type, - Table<Account.Id, String, PatchSetApproval> approvals) { - StringBuilder txt = new StringBuilder(); - if (approvals.isEmpty()) { - return ""; - } - txt.append(type).append(":\n"); - for (Account.Id id : approvals.rowKeySet()) { - txt.append(" "); - txt.append(getNameFor(id)); - txt.append(": "); - boolean first = true; - for (LabelType lt : labelTypes.getLabelTypes()) { - PatchSetApproval ca = approvals.get(id, lt.getName()); - if (ca == null) { - continue; - } - - if (first) { - first = false; - } else { - txt.append("; "); - } - - LabelValue v = lt.getValue(ca); - if (v != null) { - txt.append(v.getText()); - } else { - txt.append(lt.getName()); - txt.append('='); - txt.append(LabelValue.formatValue(ca.getValue())); - } - } - txt.append('\n'); - } - txt.append('\n'); - return txt.toString(); - } -}
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/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java deleted file mode 100644 index 62385d9..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java +++ /dev/null
@@ -1,83 +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 com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gwtorm.server.OrmException; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** Sends an email alerting a user to a new change for them to review. */ -public abstract class NewChangeSender extends ChangeEmail { - private final Set<Account.Id> reviewers = new HashSet<>(); - private final Set<Account.Id> extraCC = new HashSet<>(); - - protected NewChangeSender(EmailArguments ea, ChangeData cd) - throws OrmException { - super(ea, "newchange", cd); - } - - public void addReviewers(final Collection<Account.Id> cc) { - reviewers.addAll(cc); - } - - public void addExtraCC(final Collection<Account.Id> cc) { - extraCC.addAll(cc); - } - - @Override - protected void init() throws EmailException { - super.init(); - - setHeader("Message-ID", getChangeMessageThreadId()); - - switch (notify) { - case NONE: - case OWNER: - break; - case ALL: - default: - add(RecipientType.CC, extraCC); - //$FALL-THROUGH$ - case OWNER_REVIEWERS: - add(RecipientType.TO, reviewers); - break; - } - - rcptToAuthors(RecipientType.CC); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("NewChange.vm")); - } - - public List<String> getReviewerNames() { - if (reviewers.isEmpty()) { - return null; - } - List<String> names = new ArrayList<>(); - for (Account.Id id : reviewers) { - names.add(getNameFor(id)); - } - return names; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java deleted file mode 100644 index de338ec..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java +++ /dev/null
@@ -1,106 +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. - -package com.google.gerrit.server.mail; - -import com.google.common.collect.Iterables; -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Common class for notifications that are related to a project and branch - */ -public abstract class NotificationEmail extends OutgoingEmail { - private static final Logger log = - LoggerFactory.getLogger(NotificationEmail.class); - - protected Branch.NameKey branch; - - protected NotificationEmail(EmailArguments ea, - String mc, Branch.NameKey branch) { - super(ea, mc); - this.branch = branch; - } - - @Override - protected void init() throws EmailException { - super.init(); - setListIdHeader(); - } - - private void setListIdHeader() throws EmailException { - // Set a reasonable list id so that filters can be used to sort messages - setVHeader("List-Id", "<$email.listId.replace('@', '.')>"); - if (getSettingsUrl() != null) { - setVHeader("List-Unsubscribe", "<$email.settingsUrl>"); - } - } - - public String getListId() throws EmailException { - return velocify("gerrit-$projectName.replace('/', '-')@$email.gerritHost"); - } - - /** Include users and groups that want notification of events. */ - protected void includeWatchers(NotifyType type) { - try { - Watchers matching = getWatchers(type); - add(RecipientType.TO, matching.to); - add(RecipientType.CC, matching.cc); - add(RecipientType.BCC, matching.bcc); - } catch (OrmException err) { - // Just don't CC everyone. Better to send a partial message to those - // we already have queued up then to fail deliver entirely to people - // who have a lower interest in the change. - log.warn("Cannot BCC watchers for " + type, err); - } - } - - /** Returns all watchers that are relevant */ - protected abstract Watchers getWatchers(NotifyType type) throws OrmException; - - /** Add users or email addresses to the TO, CC, or BCC list. */ - protected void add(RecipientType type, Watchers.List list) { - for (Account.Id user : list.accounts) { - add(type, user); - } - for (Address addr : list.emails) { - add(type, addr); - } - } - - public String getSshHost() { - String host = Iterables.getFirst(args.sshAddresses, null); - if (host == null) { - return null; - } - if (host.startsWith("*:")) { - return getGerritHost() + host.substring(1); - } - return host; - } - - @Override - protected void setupVelocityContext() { - super.setupVelocityContext(); - velocityContext.put("projectName", branch.getParentKey().get()); - velocityContext.put("branch", branch); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java deleted file mode 100644 index 6200688..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java +++ /dev/null
@@ -1,507 +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.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.gerrit.common.errors.EmailException; -import com.google.gerrit.extensions.api.changes.NotifyHandling; -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.validators.OutgoingEmailValidationListener; -import com.google.gerrit.server.validators.ValidationException; -import com.google.gwtorm.server.OrmException; - -import org.apache.commons.lang.StringUtils; -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.context.InternalContextAdapterImpl; -import org.apache.velocity.runtime.RuntimeInstance; -import org.apache.velocity.runtime.parser.node.SimpleNode; -import org.eclipse.jgit.util.SystemReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.StringReader; -import java.io.StringWriter; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -/** Sends an email to one or more interested parties. */ -public abstract class OutgoingEmail { - private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class); - - private static final String HDR_TO = "To"; - private static final String HDR_CC = "CC"; - - protected String messageClass; - private final HashSet<Account.Id> rcptTo = new HashSet<>(); - private final Map<String, EmailHeader> headers; - private final Set<Address> smtpRcptTo = new HashSet<>(); - private Address smtpFromAddress; - private StringBuilder body; - protected VelocityContext velocityContext; - - protected final EmailArguments args; - protected Account.Id fromId; - protected NotifyHandling notify = NotifyHandling.ALL; - - protected OutgoingEmail(EmailArguments ea, String mc) { - args = ea; - messageClass = mc; - headers = new LinkedHashMap<>(); - } - - public void setFrom(final Account.Id id) { - fromId = id; - } - - public void setNotify(NotifyHandling notify) { - this.notify = notify; - } - - /** - * Format and enqueue the message for delivery. - * - * @throws EmailException - */ - public void send() throws EmailException { - if (NotifyHandling.NONE.equals(notify)) { - return; - } - - if (!args.emailSender.isEnabled()) { - // Server has explicitly disabled email sending. - // - return; - } - - init(); - format(); - appendText(velocifyFile("Footer.vm")); - if (shouldSendMessage()) { - if (fromId != null) { - final Account fromUser = args.accountCache.get(fromId).getAccount(); - GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferencesInfo(); - - if (senderPrefs != null - && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) { - // If we are impersonating a user, make sure they receive a CC of - // this message so they can always review and audit what we sent - // on their behalf to others. - // - add(RecipientType.CC, fromId); - } else if (rcptTo.remove(fromId)) { - // If they don't want a copy, but we queued one up anyway, - // drop them from the recipient lists. - // - removeUser(fromUser); - } - - // Check the preferences of all recipients. If any user has disabled - // his email notifications then drop him from recipients' list - for (Account.Id id : rcptTo) { - Account thisUser = args.accountCache.get(id).getAccount(); - GeneralPreferencesInfo prefs = thisUser.getGeneralPreferencesInfo(); - if (prefs == null || prefs.getEmailStrategy() == DISABLED) { - removeUser(thisUser); - } - if (smtpRcptTo.isEmpty()) { - return; - } - } - } - - OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); - va.messageClass = messageClass; - va.smtpFromAddress = smtpFromAddress; - va.smtpRcptTo = smtpRcptTo; - va.headers = headers; - va.body = body.toString(); - for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { - try { - validator.validateOutgoingEmail(va); - } catch (ValidationException e) { - return; - } - } - - args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body); - } - } - - /** Format the message body by calling {@link #appendText(String)}. */ - protected abstract void format() throws EmailException; - - /** - * Setup the message headers and envelope (TO, CC, BCC). - * - * @throws EmailException if an error occurred. - */ - protected void init() throws EmailException { - setupVelocityContext(); - - smtpFromAddress = args.fromAddressGenerator.from(fromId); - setHeader("Date", new Date()); - headers.put("From", new EmailHeader.AddressList(smtpFromAddress)); - headers.put(HDR_TO, new EmailHeader.AddressList()); - headers.put(HDR_CC, new EmailHeader.AddressList()); - setHeader("Message-ID", ""); - - 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 - // it is a generic header for this Gerrit server, include the - // 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); - } - } - - setHeader("X-Gerrit-MessageType", messageClass); - body = new StringBuilder(); - - if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { - appendText(getFromLine()); - } - } - - protected String getFromLine() { - final Account account = args.accountCache.get(fromId).getAccount(); - final String name = account.getFullName(); - final String email = account.getPreferredEmail(); - StringBuilder f = new StringBuilder(); - - if ((name != null && !name.isEmpty()) - || (email != null && !email.isEmpty())) { - f.append("From"); - if (name != null && !name.isEmpty()) { - f.append(" ").append(name); - } - if (email != null && !email.isEmpty()) { - f.append(" <").append(email).append(">"); - } - f.append(":\n\n"); - } - return f.toString(); - } - - public String getGerritHost() { - if (getGerritUrl() != null) { - try { - return new URL(getGerritUrl()).getHost(); - } catch (MalformedURLException e) { - // Try something else. - } - } - - // Fall back onto whatever the local operating system thinks - // this server is called. We hopefully didn't get here as a - // good admin would have configured the canonical url. - // - return SystemReader.getInstance().getHostname(); - } - - public String getSettingsUrl() { - if (getGerritUrl() != null) { - final StringBuilder r = new StringBuilder(); - r.append(getGerritUrl()); - r.append("settings"); - return r.toString(); - } - return null; - } - - public String getGerritUrl() { - return args.urlProvider.get(); - } - - /** Set a header in the outgoing message using a template. */ - protected void setVHeader(final String name, final String value) throws - EmailException { - setHeader(name, velocify(value)); - } - - /** Set a header in the outgoing message. */ - protected void setHeader(final String name, final String value) { - headers.put(name, new EmailHeader.String(value)); - } - - protected void setHeader(final String name, final Date date) { - headers.put(name, new EmailHeader.Date(date)); - } - - /** Append text to the outgoing email body. */ - protected void appendText(final String text) { - if (text != null) { - body.append(text); - } - } - - /** Lookup a human readable name for an account, usually the "full name". */ - protected String getNameFor(final Account.Id accountId) { - if (accountId == null) { - return args.gerritPersonIdent.getName(); - } - - final Account userAccount = args.accountCache.get(accountId).getAccount(); - String name = userAccount.getFullName(); - if (name == null) { - name = userAccount.getPreferredEmail(); - } - if (name == null) { - name = args.anonymousCowardName + " #" + accountId; - } - return name; - } - - /** - * Gets the human readable name and email for an account; - * if neither are available, returns the Anonymous Coward name. - * - * @param accountId user to fetch. - * @return name/email of account, or Anonymous Coward if unset. - */ - public String getNameEmailFor(Account.Id accountId) { - AccountState who = args.accountCache.get(accountId); - String name = who.getAccount().getFullName(); - String email = who.getAccount().getPreferredEmail(); - - if (name != null && email != null) { - return name + " <" + email + ">"; - - } else if (name != null) { - return name; - } else if (email != null) { - return email; - - } else /* (name == null && email == null) */ { - return args.anonymousCowardName + " #" + accountId; - } - } - - /** - * Gets the human readable name and email for an account; - * if both are unavailable, returns the username. If no - * username is set, this function returns null. - * - * @param accountId user to fetch. - * @return name/email of account, username, or null if unset. - */ - public String getUserNameEmailFor(Account.Id accountId) { - AccountState who = args.accountCache.get(accountId); - String name = who.getAccount().getFullName(); - String email = who.getAccount().getPreferredEmail(); - - if (name != null && email != null) { - return name + " <" + email + ">"; - } else if (email != null) { - return email; - } else if (name != null) { - return name; - } - String username = who.getUserName(); - if (username != null) { - return username; - } - return null; - } - - protected boolean shouldSendMessage() { - if (body.length() == 0) { - // If we have no message body, don't send. - return false; - } - - if (smtpRcptTo.isEmpty()) { - // If we have nobody to send this message to, then all of our - // selection filters previously for this type of message were - // unable to match a destination. Don't bother sending it. - return false; - } - - if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) { - // If the only recipient is also the sender, don't bother. - // - return false; - } - - return true; - } - - /** Schedule this message for delivery to the listed accounts. */ - protected void add(final RecipientType rt, final Collection<Account.Id> list) { - for (final Account.Id id : list) { - add(rt, id); - } - } - - protected void add(final RecipientType rt, final UserIdentity who) { - if (who != null && who.getAccount() != null) { - add(rt, who.getAccount()); - } - } - - /** Schedule delivery of this message to the given account. */ - protected void add(final RecipientType rt, final Account.Id to) { - try { - if (!rcptTo.contains(to) && isVisibleTo(to)) { - rcptTo.add(to); - add(rt, toAddress(to)); - } - } catch (OrmException e) { - log.error("Error reading database for account: " + to, e); - } - } - - /** - * @param to account. - * @throws OrmException - * @return whether this email is visible to the given account. - */ - protected boolean isVisibleTo(final Account.Id to) throws OrmException { - return true; - } - - /** Schedule delivery of this message to the given account. */ - protected void add(final RecipientType rt, final Address addr) { - 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)"); - } else if (smtpRcptTo.add(addr)) { - switch (rt) { - case TO: - ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr); - break; - case CC: - ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr); - break; - case BCC: - break; - } - } - } - } - - private Address toAddress(final Account.Id id) { - final Account a = args.accountCache.get(id).getAccount(); - final String e = a.getPreferredEmail(); - if (!a.isActive() || e == null) { - return null; - } - return new Address(a.getFullName(), e); - } - - protected void setupVelocityContext() { - velocityContext = new VelocityContext(); - - velocityContext.put("email", this); - velocityContext.put("messageClass", messageClass); - velocityContext.put("StringUtils", StringUtils.class); - } - - protected String velocify(String template) throws EmailException { - try { - RuntimeInstance runtime = args.velocityRuntime; - String templateName = "OutgoingEmail"; - SimpleNode tree = runtime.parse(new StringReader(template), templateName); - InternalContextAdapterImpl ica = new InternalContextAdapterImpl(velocityContext); - ica.pushCurrentTemplateName(templateName); - try { - tree.init(ica, runtime); - StringWriter w = new StringWriter(); - tree.render(ica, w); - return w.toString(); - } finally { - ica.popCurrentTemplateName(); - } - } catch (Exception e) { - throw new EmailException("Cannot format velocity template: " + template, e); - } - } - - protected String velocifyFile(String name) throws EmailException { - try { - RuntimeInstance runtime = args.velocityRuntime; - if (runtime.getLoaderNameForResource(name) == null) { - name = "com/google/gerrit/server/mail/" + name; - } - Template template = runtime.getTemplate(name, UTF_8.name()); - StringWriter w = new StringWriter(); - template.merge(velocityContext, w); - return w.toString(); - } catch (Exception e) { - throw new EmailException("Cannot format velocity template " + name, e); - } - } - - public String joinStrings(Iterable<Object> in, String joiner) { - return joinStrings(in.iterator(), joiner); - } - - public String joinStrings(Iterator<Object> in, String joiner) { - if (!in.hasNext()) { - return ""; - } - - Object first = in.next(); - if (!in.hasNext()) { - return safeToString(first); - } - - StringBuilder r = new StringBuilder(); - r.append(safeToString(first)); - while (in.hasNext()) { - r.append(joiner).append(safeToString(in.next())); - } - return r.toString(); - } - - protected void removeUser(Account user) { - String fromEmail = user.getPreferredEmail(); - for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext();) { - if (j.next().email.equals(fromEmail)) { - j.remove(); - } - } - for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) { - // Don't remove fromEmail from the "From" header though! - if (entry.getValue() instanceof AddressList - && !entry.getKey().equals("From")) { - ((AddressList) entry.getValue()).remove(fromEmail); - } - } - } - - private static String safeToString(Object obj) { - return obj != null ? obj.toString() : ""; - } -}
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/OutgoingEmailValidator.java deleted file mode 100644 index 5ab5f4e..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmailValidator.java +++ /dev/null
@@ -1,30 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.mail; - -import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS; - -import org.apache.commons.validator.routines.DomainValidator; -import org.apache.commons.validator.routines.EmailValidator; - -public class OutgoingEmailValidator { - static { - DomainValidator.updateTLDOverride(GENERIC_PLUS, new String[]{"local"}); - } - - public static boolean isValid(String addr) { - return EmailValidator.getInstance(true, true).isValid(addr); - } -}
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/ProjectWatch.java deleted file mode 100644 index f19b2a8..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java +++ /dev/null
@@ -1,283 +0,0 @@ -// Copyright (C) 2013 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.server.mail; - -import com.google.common.base.Strings; -import com.google.gerrit.common.data.GroupDescription; -import com.google.gerrit.common.data.GroupDescriptions; -import com.google.gerrit.common.data.GroupReference; -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.AccountProjectWatch; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; -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.account.AccountState; -import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; -import com.google.gerrit.server.git.NotifyConfig; -import com.google.gerrit.server.project.ProjectState; -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.SingleGroupUser; -import com.google.gwtorm.server.OrmException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class ProjectWatch { - private static final Logger log = LoggerFactory.getLogger(ProjectWatch.class); - - protected final EmailArguments args; - protected final ProjectState projectState; - protected final Project.NameKey project; - protected final ChangeData changeData; - - public ProjectWatch(EmailArguments args, Project.NameKey project, - ProjectState projectState, ChangeData changeData) { - this.args = args; - this.project = project; - this.projectState = projectState; - this.changeData = changeData; - } - - /** Returns all watchers that are relevant */ - public final Watchers getWatchers(NotifyType type) throws OrmException { - Watchers matching; - if (args.accountIndexes.getSearchIndex() != null) { - matching = getWatchersFromIndex(type); - } else { - matching = getWatchersFromDb(type); - } - - for (ProjectState state : projectState.tree()) { - for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) { - if (nc.isNotify(type)) { - try { - add(matching, nc); - } catch (QueryParseException e) { - log.warn("Project {} has invalid notify {} filter \"{}\": {}", - state.getProject().getName(), nc.getName(), - nc.getFilter(), e.getMessage()); - } - } - } - } - - return matching; - } - - private Watchers getWatchersFromIndex(NotifyType type) - throws OrmException { - Watchers matching = new Watchers(); - Set<Account.Id> projectWatchers = new HashSet<>(); - - for (AccountState a : args.accountQueryProvider.get() - .byWatchedProject(project)) { - Account.Id accountId = a.getAccount().getId(); - for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : - a.getProjectWatches().entrySet()) { - if (project.equals(e.getKey().project()) - && add(matching, accountId, e.getKey(), e.getValue(), type)) { - // We only want to prevent matching All-Projects if this filter hits - projectWatchers.add(accountId); - } - } - } - - for (AccountState a : args.accountQueryProvider.get() - .byWatchedProject(args.allProjectsName)) { - for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : - a.getProjectWatches().entrySet()) { - if (args.allProjectsName.equals(e.getKey().project())) { - Account.Id accountId = a.getAccount().getId(); - if (!projectWatchers.contains(accountId)) { - add(matching, accountId, e.getKey(), e.getValue(), type); - } - } - } - } - return matching; - } - - private Watchers getWatchersFromDb(NotifyType type) - throws OrmException { - Watchers matching = new Watchers(); - Set<Account.Id> projectWatchers = new HashSet<>(); - - for (AccountProjectWatch w : args.db.get().accountProjectWatches() - .byProject(project)) { - if (add(matching, w, type)) { - // We only want to prevent matching All-Projects if this filter hits - projectWatchers.add(w.getAccountId()); - } - } - - for (AccountProjectWatch w : args.db.get().accountProjectWatches() - .byProject(args.allProjectsName)) { - if (!projectWatchers.contains(w.getAccountId())) { - add(matching, w, type); - } - } - return matching; - } - - public static class Watchers { - static class List { - protected final Set<Account.Id> accounts = new HashSet<>(); - protected final Set<Address> emails = new HashSet<>(); - } - protected final List to = new List(); - protected final List cc = new List(); - protected final List bcc = new List(); - - List list(NotifyConfig.Header header) { - switch (header) { - case TO: - return to; - case CC: - return cc; - default: - case BCC: - return bcc; - } - } - } - - private void add(Watchers matching, NotifyConfig nc) - throws OrmException, QueryParseException { - for (GroupReference ref : nc.getGroups()) { - CurrentUser user = new SingleGroupUser(args.capabilityControlFactory, - ref.getUUID()); - if (filterMatch(user, nc.getFilter())) { - deliverToMembers(matching.list(nc.getHeader()), ref.getUUID()); - } - } - - if (!nc.getAddresses().isEmpty()) { - if (filterMatch(null, nc.getFilter())) { - matching.list(nc.getHeader()).emails.addAll(nc.getAddresses()); - } - } - } - - private void deliverToMembers( - Watchers.List matching, - AccountGroup.UUID startUUID) throws OrmException { - ReviewDb db = args.db.get(); - Set<AccountGroup.UUID> seen = new HashSet<>(); - List<AccountGroup.UUID> q = new ArrayList<>(); - - seen.add(startUUID); - q.add(startUUID); - - while (!q.isEmpty()) { - AccountGroup.UUID uuid = q.remove(q.size() - 1); - GroupDescription.Basic group = args.groupBackend.get(uuid); - if (!Strings.isNullOrEmpty(group.getEmailAddress())) { - // If the group has an email address, do not expand membership. - matching.emails.add(new Address(group.getEmailAddress())); - continue; - } - - AccountGroup ig = GroupDescriptions.toAccountGroup(group); - if (ig == null) { - // Non-internal groups cannot be expanded by the server. - continue; - } - - for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) { - matching.accounts.add(m.getAccountId()); - } - for (AccountGroup.UUID m : args.groupIncludes.subgroupsOf(uuid)) { - if (seen.add(m)) { - q.add(m); - } - } - } - } - - private boolean add(Watchers matching, Account.Id accountId, - ProjectWatchKey key, Set<NotifyType> watchedTypes, NotifyType type) - throws OrmException { - IdentifiedUser user = args.identifiedUserFactory.create(accountId); - - try { - if (filterMatch(user, key.filter())) { - // If we are set to notify on this type, add the user. - // Otherwise, still return true to stop notifications for this user. - if (watchedTypes.contains(type)) { - matching.bcc.accounts.add(accountId); - } - return true; - } - } catch (QueryParseException e) { - // Ignore broken filter expressions. - } - return false; - } - - private boolean add(Watchers matching, AccountProjectWatch w, NotifyType type) - throws OrmException { - IdentifiedUser user = args.identifiedUserFactory.create(w.getAccountId()); - - try { - if (filterMatch(user, w.getFilter())) { - // If we are set to notify on this type, add the user. - // Otherwise, still return true to stop notifications for this user. - if (w.isNotify(type)) { - matching.bcc.accounts.add(w.getAccountId()); - } - return true; - } - } catch (QueryParseException e) { - // Ignore broken filter expressions. - } - return false; - } - - private boolean filterMatch(CurrentUser user, String filter) - throws OrmException, QueryParseException { - ChangeQueryBuilder qb; - Predicate<ChangeData> p = null; - - if (user == null) { - qb = args.queryBuilder.asUser(args.anonymousUser); - } else { - qb = args.queryBuilder.asUser(user); - p = qb.is_visible(); - } - - if (filter != null) { - Predicate<ChangeData> filterPredicate = qb.parse(filter); - if (p == null) { - p = filterPredicate; - } else { - p = Predicate.and(filterPredicate, p); - } - } - return p == null || p.asMatchable().match(changeData); - } -}
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/RecipientType.java deleted file mode 100644 index ea0def0..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RecipientType.java +++ /dev/null
@@ -1,19 +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; - -public enum RecipientType { - TO, CC, BCC -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java deleted file mode 100644 index cfdeb8f..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java +++ /dev/null
@@ -1,72 +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.base.Preconditions.checkNotNull; - -import com.google.gerrit.common.errors.EmailException; -import com.google.gerrit.server.IdentifiedUser; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -public class RegisterNewEmailSender extends OutgoingEmail { - public interface Factory { - RegisterNewEmailSender create(String address); - } - - private final EmailTokenVerifier tokenVerifier; - private final IdentifiedUser user; - private final String addr; - private String emailToken; - - @Inject - public RegisterNewEmailSender(EmailArguments ea, - EmailTokenVerifier etv, - IdentifiedUser callingUser, - @Assisted final String address) { - super(ea, "registernewemail"); - tokenVerifier = etv; - user = callingUser; - addr = address; - } - - @Override - protected void init() throws EmailException { - super.init(); - setHeader("Subject", "[Gerrit Code Review] Email Verification"); - add(RecipientType.TO, new Address(addr)); - } - - @Override - protected void format() throws EmailException { - appendText(velocifyFile("RegisterNewEmail.vm")); - } - - public String getUserNameEmail() { - return getUserNameEmailFor(user.getAccountId()); - } - - public String getEmailRegistrationToken() { - if (emailToken == null) { - emailToken = checkNotNull( - tokenVerifier.encode(user.getAccountId(), addr), "token"); - } - return emailToken; - } - - public boolean isAllowed() { - return args.emailSender.canEmail(addr); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java deleted file mode 100644 index df9f20e..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java +++ /dev/null
@@ -1,88 +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 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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** Send notice of new patch sets for reviewers. */ -public class ReplacePatchSetSender extends ReplyToChangeSender { - public interface Factory { - ReplacePatchSetSender create(Project.NameKey project, Change.Id id); - } - - private final Set<Account.Id> reviewers = new HashSet<>(); - private final Set<Account.Id> extraCC = new HashSet<>(); - - @Inject - public ReplacePatchSetSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "newpatchset", newChangeData(ea, project, id)); - } - - public void addReviewers(final Collection<Account.Id> cc) { - reviewers.addAll(cc); - } - - public void addExtraCC(final Collection<Account.Id> cc) { - extraCC.addAll(cc); - } - - @Override - protected void init() throws EmailException { - super.init(); - - if (fromId != null) { - // Don't call yourself a reviewer of your own patch set. - // - reviewers.remove(fromId); - } - add(RecipientType.TO, reviewers); - add(RecipientType.CC, extraCC); - rcptToAuthors(RecipientType.CC); - bccStarredBy(); - includeWatchers(NotifyType.NEW_PATCHSETS); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("ReplacePatchSet.vm")); - } - - public List<String> getReviewerNames() { - if (reviewers.isEmpty()) { - return null; - } - List<String> names = new ArrayList<>(); - for (Account.Id id : reviewers) { - names.add(getNameFor(id)); - } - return names; - } -}
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/ReplyToChangeSender.java deleted file mode 100644 index dd922d3..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java +++ /dev/null
@@ -1,44 +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 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; - -/** Alert a user to a reply to a change, usually commentary made during review. */ -public abstract class ReplyToChangeSender extends ChangeEmail { - public interface Factory<T extends ReplyToChangeSender> { - T create(Project.NameKey project, Change.Id id); - } - - protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) - throws OrmException { - super(ea, mc, cd); - } - - @Override - protected void init() throws EmailException { - super.init(); - - final String threadId = getChangeMessageThreadId(); - setHeader("In-Reply-To", threadId); - setHeader("References", threadId); - - rcptToAuthors(RecipientType.TO); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java deleted file mode 100644 index d946eb2..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java +++ /dev/null
@@ -1,54 +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. - -package com.google.gerrit.server.mail; - -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -/** Send notice about a change being restored by its owner. */ -public class RestoredSender extends ReplyToChangeSender { - public interface Factory extends - ReplyToChangeSender.Factory<RestoredSender> { - @Override - RestoredSender create(Project.NameKey project, Change.Id id); - } - - @Inject - public RestoredSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "restore", newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("Restored.vm")); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java deleted file mode 100644 index 2c9c37e..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java +++ /dev/null
@@ -1,52 +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. - -package com.google.gerrit.server.mail; - -import com.google.gerrit.common.errors.EmailException; -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.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; - -/** Send notice about a change being reverted. */ -public class RevertedSender extends ReplyToChangeSender { - public interface Factory { - RevertedSender create(Project.NameKey project, Change.Id id); - } - - @Inject - public RevertedSender(EmailArguments ea, - @Assisted Project.NameKey project, - @Assisted Change.Id id) - throws OrmException { - super(ea, "revert", newChangeData(ea, project, id)); - } - - @Override - protected void init() throws EmailException { - super.init(); - - ccAllApprovals(); - bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS); - } - - @Override - protected void formatChange() throws EmailException { - appendText(velocifyFile("Reverted.vm")); - } -}
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/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java deleted file mode 100644 index e263c6a..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java +++ /dev/null
@@ -1,294 +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 java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.primitives.Ints; -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.inject.AbstractModule; -import com.google.inject.Inject; -import com.google.inject.Singleton; - -import org.apache.commons.net.smtp.AuthSMTPClient; -import org.apache.commons.net.smtp.SMTPClient; -import org.apache.commons.net.smtp.SMTPReply; -import org.eclipse.jgit.lib.Config; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** Sends email via a nearby SMTP server. */ -@Singleton -public class SmtpEmailSender implements EmailSender { - /** The socket's connect timeout (0 = infinite timeout) */ - private static final int DEFAULT_CONNECT_TIMEOUT = 0; - - public static class Module extends AbstractModule { - @Override - protected void configure() { - bind(EmailSender.class).to(SmtpEmailSender.class); - } - } - - public enum Encryption { - NONE, SSL, TLS - } - - private final boolean enabled; - private final int connectTimeout; - - private String smtpHost; - private int smtpPort; - private String smtpUser; - private String smtpPass; - private Encryption smtpEncryption; - private boolean sslVerify; - private Set<String> allowrcpt; - private String importance; - private int expiryDays; - - @Inject - SmtpEmailSender(@GerritServerConfig final Config cfg) { - enabled = cfg.getBoolean("sendemail", null, "enable", true); - connectTimeout = - Ints.checkedCast(ConfigUtil.getTimeUnit(cfg, "sendemail", null, - "connectTimeout", DEFAULT_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)); - - - smtpHost = cfg.getString("sendemail", null, "smtpserver"); - if (smtpHost == null) { - smtpHost = "127.0.0.1"; - } - - smtpEncryption = - cfg.getEnum("sendemail", null, "smtpencryption", Encryption.NONE); - sslVerify = cfg.getBoolean("sendemail", null, "sslverify", true); - - final int defaultPort; - switch (smtpEncryption) { - case SSL: - defaultPort = 465; - break; - - case NONE: - case TLS: - default: - defaultPort = 25; - break; - } - smtpPort = cfg.getInt("sendemail", null, "smtpserverport", defaultPort); - - smtpUser = cfg.getString("sendemail", null, "smtpuser"); - smtpPass = cfg.getString("sendemail", null, "smtppass"); - - Set<String> rcpt = new HashSet<>(); - for (String addr : cfg.getStringList("sendemail", null, "allowrcpt")) { - rcpt.add(addr); - } - allowrcpt = Collections.unmodifiableSet(rcpt); - importance = cfg.getString("sendemail", null, "importance"); - expiryDays = cfg.getInt("sendemail", null, "expiryDays", 0); - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public boolean canEmail(String address) { - if (!isEnabled()) { - return false; - } - - if (allowrcpt.isEmpty()) { - return true; - } - - if (allowrcpt.contains(address)) { - return true; - } - - String domain = address.substring(address.lastIndexOf('@') + 1); - if (allowrcpt.contains(domain) || allowrcpt.contains("@" + domain)) { - return true; - } - - return false; - } - - @Override - public void send(final Address from, Collection<Address> rcpt, - final Map<String, EmailHeader> callerHeaders, final String body) - throws EmailException { - if (!isEnabled()) { - throw new EmailException("Sending email is disabled"); - } - - 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()); - if (importance != null) { - setMissingHeader(hdrs, "Importance", importance); - } - if (expiryDays > 0) { - Date expiry = new Date(TimeUtil.nowMs() + - expiryDays * 24 * 60 * 60 * 1000L ); - setMissingHeader(hdrs, "Expiry-Date", - new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry)); - } - - StringBuffer rejected = new StringBuffer(); - try { - final SMTPClient client = open(); - try { - if (!client.setSender(from.email)) { - throw new EmailException("Server " + smtpHost - + " rejected from address " + from.email); - } - - /* Do not prevent the email from being sent to "good" users simply - * because some users get rejected. If not, a single rejected - * project watcher could prevent email for most actions on a project - * from being sent to any user! Instead, queue up the errors, and - * throw an exception after sending the email to get the rejected - * error(s) logged. - */ - for (Address addr : rcpt) { - if (!client.addRecipient(addr.email)) { - String error = client.getReplyString(); - rejected.append("Server ").append(smtpHost) - .append(" rejected recipient ").append(addr) - .append(": ").append(error); - } - } - - Writer messageDataWriter = client.sendMessageData(); - if (messageDataWriter == null) { - /* Include rejected recipient error messages here to not lose that - * information. That piece of the puzzle is vital if zero recipients - * are accepted and the server consequently rejects the DATA command. - */ - throw new EmailException(rejected + "Server " + smtpHost - + " rejected DATA command: " + client.getReplyString()); - } - try (Writer w = new BufferedWriter(messageDataWriter)) { - for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) { - if (!h.getValue().isEmpty()) { - w.write(h.getKey()); - w.write(": "); - h.getValue().write(w); - w.write("\r\n"); - } - } - - w.write("\r\n"); - w.write(body); - w.flush(); - } - - if (!client.completePendingCommand()) { - throw new EmailException("Server " + smtpHost - + " rejected message body: " + client.getReplyString()); - } - - client.logout(); - if (rejected.length() > 0) { - throw new EmailException(rejected.toString()); - } - } finally { - client.disconnect(); - } - } catch (IOException e) { - throw new EmailException("Cannot send outgoing email", e); - } - } - - private void setMissingHeader(final Map<String, EmailHeader> hdrs, - final String name, final String value) { - if (!hdrs.containsKey(name) || hdrs.get(name).isEmpty()) { - hdrs.put(name, new EmailHeader.String(value)); - } - } - - private SMTPClient open() throws EmailException { - final AuthSMTPClient client = new AuthSMTPClient(UTF_8.name()); - - if (smtpEncryption == Encryption.SSL) { - client.enableSSL(sslVerify); - } - - client.setConnectTimeout(connectTimeout); - try { - client.connect(smtpHost, smtpPort); - int replyCode = client.getReplyCode(); - String replyString = client.getReplyString(); - if (!SMTPReply.isPositiveCompletion(replyCode)) { - throw new EmailException( - String.format("SMTP server rejected connection: %d: %s", - replyCode, replyString)); - } - if (!client.login()) { - throw new EmailException( - "SMTP server rejected HELO/EHLO greeting: " + replyString); - } - - if (smtpEncryption == Encryption.TLS) { - if (!client.startTLS(smtpHost, smtpPort, sslVerify)) { - throw new EmailException("SMTP server does not support TLS"); - } - if (!client.login()) { - throw new EmailException("SMTP server rejected login: " + replyString); - } - } - - if (smtpUser != null && !client.auth(smtpUser, smtpPass)) { - throw new EmailException("SMTP server rejected auth: " + replyString); - } - return client; - } catch (IOException | EmailException e) { - if (client.isConnected()) { - try { - client.disconnect(); - } catch (IOException e2) { - //Ignored - } - } - if (e instanceof EmailException) { - throw (EmailException) e; - } - throw new EmailException(e.getMessage(), e); - } - } -}
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/VelocityRuntimeProvider.java deleted file mode 100644 index 3fdc550..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java +++ /dev/null
@@ -1,122 +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. - -package com.google.gerrit.server.mail; - -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 org.apache.velocity.runtime.RuntimeConstants; -import org.apache.velocity.runtime.RuntimeInstance; -import org.apache.velocity.runtime.RuntimeServices; -import org.apache.velocity.runtime.log.LogChute; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.file.Files; -import java.util.Properties; - -/** Configures Velocity template engine for sending email. */ -@Singleton -public class VelocityRuntimeProvider implements Provider<RuntimeInstance> { - private final SitePaths site; - - @Inject - VelocityRuntimeProvider(SitePaths site) { - this.site = site; - } - - @Override - public RuntimeInstance get() { - String rl = "resource.loader"; - String pkg = "org.apache.velocity.runtime.resource.loader"; - - Properties p = new Properties(); - p.setProperty(RuntimeConstants.VM_PERM_INLINE_LOCAL, "true"); - p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, - Slf4jLogChute.class.getName()); - p.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); - p.setProperty("runtime.log.logsystem.log4j.category", "velocity"); - - if (Files.isDirectory(site.mail_dir)) { - p.setProperty(rl, "file, class"); - p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader"); - p.setProperty("file." + rl + ".path", - site.mail_dir.toAbsolutePath().toString()); - p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader"); - } else { - p.setProperty(rl, "class"); - p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader"); - } - - RuntimeInstance ri = new RuntimeInstance(); - try { - ri.init(p); - } catch (Exception err) { - throw new ProvisionException("Cannot configure Velocity templates", err); - } - return ri; - } - - /** Connects Velocity to sfl4j. */ - public static class Slf4jLogChute implements LogChute { - private static final Logger log = LoggerFactory.getLogger("velocity"); - - @Override - public void init(RuntimeServices rs) { - } - - @Override - public boolean isLevelEnabled(int level) { - switch (level) { - default: - case DEBUG_ID: - return log.isDebugEnabled(); - case INFO_ID: - return log.isInfoEnabled(); - case WARN_ID: - return log.isWarnEnabled(); - case ERROR_ID: - return log.isErrorEnabled(); - } - } - - @Override - public void log(int level, String message) { - log(level, message, null); - } - - @Override - public void log(int level, String msg, Throwable err) { - switch (level) { - default: - case DEBUG_ID: - log.debug(msg, err); - break; - case INFO_ID: - log.info(msg, err); - break; - case WARN_ID: - log.warn(msg, err); - break; - case ERROR_ID: - log.error(msg, err); - break; - } - } - } -}
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..27d3052 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailMessage.java
@@ -0,0 +1,97 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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(); + 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(); + // Raw content as received over the wire + @Nullable + public abstract ImmutableList<Integer> rawContent(); + @Nullable + public abstract String rawContentUTF(); + + public static Builder builder() { + return new AutoValue_MailMessage.Builder(); + } + + public abstract Builder toBuilder(); + + @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 Builder rawContent(ImmutableList<Integer> val); + public abstract Builder rawContentUTF(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..eb084ca --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -0,0 +1,288 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.registration.DynamicMap; +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.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.mail.MailFilter; +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 DynamicMap<MailFilter> mailFilters; + 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, + DynamicMap<MailFilter> mailFilters, + @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.mailFilters = mailFilters; + 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 { + for (DynamicMap.Entry<MailFilter> filter : mailFilters) { + if (!filter.getProvider().get().shouldProcessMessage(message)) { + log.warn("Mail: Message " + message.id() + " filtered by plugin " + + filter.getPluginName() + " " + filter.getExportName() + + ". Will delete message."); + return; + } + } + + 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, + UnprocessableEntityException { + 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, + false, null); + comment.tag = tag; + if (c.inReplyTo != null) { + comment.parentUuid = c.inReplyTo.key.uuid; + comment.lineNbr = c.inReplyTo.lineNbr; + comment.range = c.inReplyTo.range; + comment.unresolved = c.inReplyTo.unresolved; + } + 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/receive/Protocol.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java new file mode 100644 index 0000000..e8311f1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Protocol.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.receive; + +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..52dc3ce --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/RawMailParser.java
@@ -0,0 +1,178 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharStreams; +import com.google.common.primitives.Ints; +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(); + messageBuilder.rawContentUTF(raw); + 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); + } + + MailMessage.Builder messageBuilder = parse(b.toString()).toBuilder(); + messageBuilder.rawContent(ImmutableList.copyOf(Ints.asList(chars))); + return messageBuilder.build(); + } + + /** + * 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/send/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AbandonedSender.java new file mode 100644 index 0000000..8ddfe5c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AbandonedSender.java
@@ -0,0 +1,63 @@ +// 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.errors.EmailException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a change being abandoned by its owner. */ +public class AbandonedSender extends ReplyToChangeSender { + public interface Factory extends + ReplyToChangeSender.Factory<AbandonedSender> { + @Override + AbandonedSender create(Project.NameKey project, Change.Id change); + } + + @Inject + public AbandonedSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "abandon", ChangeEmail.newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + includeWatchers(NotifyType.ABANDONED_CHANGES); + includeWatchers(NotifyType.ALL_COMMENTS); + } + + @Override + protected void formatChange() throws EmailException { + 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/send/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java new file mode 100644 index 0000000..47068eb --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -0,0 +1,133 @@ +// 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.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; + +import java.util.List; + +public class AddKeySender extends OutgoingEmail { + public interface Factory { + AddKeySender create(IdentifiedUser user, AccountSshKey sshKey); + + AddKeySender create(IdentifiedUser user, List<String> gpgKey); + } + + private final IdentifiedUser callingUser; + private final IdentifiedUser user; + private final AccountSshKey sshKey; + private final List<String> gpgKeys; + + @AssistedInject + public AddKeySender(EmailArguments ea, + IdentifiedUser callingUser, + @Assisted IdentifiedUser user, + @Assisted AccountSshKey sshKey) { + super(ea, "addkey"); + this.callingUser = callingUser; + this.user = user; + this.sshKey = sshKey; + this.gpgKeys = null; + } + + @AssistedInject + public AddKeySender(EmailArguments ea, + IdentifiedUser callingUser, + @Assisted IdentifiedUser user, + @Assisted List<String> gpgKeys) { + super(ea, "addkey"); + this.callingUser = callingUser; + this.user = user; + this.sshKey = null; + this.gpgKeys = gpgKeys; + } + + @Override + protected void init() throws EmailException { + super.init(); + setHeader("Subject", + String.format("[Gerrit Code Review] New %s Keys Added", getKeyType())); + add(RecipientType.TO, new Address(getEmail())); + } + + @Override + protected boolean shouldSendMessage() { + /* + * Don't send an email if no keys are added, or an admin is adding a key to + * a user. + */ + return (sshKey != null || gpgKeys.size() > 0) && + (user.equals(callingUser) || + !callingUser.getCapabilities().canAdministrateServer()); + } + + @Override + protected void format() throws EmailException { + appendText(textTemplate("AddKey")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("AddKeyHtml")); + } + } + + public String getEmail() { + return user.getAccount().getPreferredEmail(); + } + + public String getUserNameEmail() { + return getUserNameEmailFor(user.getAccountId()); + } + + public String getKeyType() { + if (sshKey != null) { + return "SSH"; + } else if (gpgKeys != null) { + return "GPG"; + } + return "Unknown"; + } + + public String getSshKey() { + return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null; + } + + public String getGpgKeys() { + if (gpgKeys != null) { + return Joiner.on("\n").join(gpgKeys); + } + 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/send/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddReviewerSender.java new file mode 100644 index 0000000..67577de --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddReviewerSender.java
@@ -0,0 +1,44 @@ +// 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.errors.EmailException; +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; + +/** Asks a user to review a change. */ +public class AddReviewerSender extends NewChangeSender { + public interface Factory { + AddReviewerSender create(Project.NameKey project, Change.Id id); + } + + @Inject + public AddReviewerSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccExistingReviewers(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java new file mode 100644 index 0000000..e867caf --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -0,0 +1,542 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.collect.ListMultimap; +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.reviewdb.client.Account; +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.Patch; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetInfo; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +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; +import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; + +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.Timestamp; +import java.text.MessageFormat; +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; +import java.util.TreeSet; + +/** Sends an email to one or more interested parties. */ +public abstract class ChangeEmail extends NotificationEmail { + private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class); + + protected static ChangeData newChangeData(EmailArguments ea, + Project.NameKey project, Change.Id id) { + return ea.changeDataFactory.create(ea.db.get(), project, id); + } + + protected final Change change; + protected final ChangeData changeData; + protected PatchSet patchSet; + protected PatchSetInfo patchSetInfo; + protected String changeMessage; + protected Timestamp timestamp; + + protected ProjectState projectState; + protected Set<Account.Id> authors; + protected boolean emailOnlyAuthors; + + protected ChangeEmail(EmailArguments ea, String mc, ChangeData cd) + throws OrmException { + super(ea, mc, cd.change().getDest()); + changeData = cd; + change = cd.change(); + emailOnlyAuthors = false; + } + + @Override + public void setFrom(final Account.Id id) { + super.setFrom(id); + + /** Is the from user in an email squelching group? */ + final IdentifiedUser user = args.identifiedUserFactory.create(id); + emailOnlyAuthors = !user.getCapabilities().canEmailReviewers(); + } + + public void setPatchSet(final PatchSet ps) { + patchSet = ps; + } + + public void setPatchSet(final PatchSet ps, final PatchSetInfo psi) { + patchSet = ps; + patchSetInfo = psi; + } + + @Deprecated + public void setChangeMessage(final ChangeMessage cm) { + setChangeMessage(cm.getMessage(), cm.getWrittenOn()); + } + + public void setChangeMessage(String cm, Timestamp t) { + changeMessage = cm; + timestamp = t; + } + + /** Format the message body by calling {@link #appendText(String)}. */ + @Override + protected void format() throws EmailException { + formatChange(); + appendText(textTemplate("ChangeFooter")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("ChangeFooterHtml")); + } + formatFooter(); + } + + /** Format the message body by calling {@link #appendText(String)}. */ + protected abstract void formatChange() throws EmailException; + + /** + * Format the message footer by calling {@link #appendText(String)}. + * + * @throws EmailException if an error occurred. + */ + protected void formatFooter() throws EmailException { + } + + /** Setup the message headers and envelope (TO, CC, BCC). */ + @Override + protected void init() throws EmailException { + if (args.projectCache != null) { + projectState = args.projectCache.get(change.getProject()); + } else { + projectState = null; + } + + if (patchSet == null) { + try { + patchSet = changeData.currentPatchSet(); + } catch (OrmException err) { + patchSet = 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(); + + super.init(); + if (timestamp != null) { + setHeader("Date", new Date(timestamp.getTime())); + } + setChangeSubjectHeader(); + setHeader("X-Gerrit-Change-Id", "" + change.getKey().get()); + setHeader("X-Gerrit-Change-Number", "" + change.getChangeId()); + setChangeUrlHeader(); + setCommitIdHeader(); + } + + private void setChangeUrlHeader() { + final String u = getChangeUrl(); + if (u != null) { + setHeader("X-Gerrit-ChangeURL", "<" + u + ">"); + } + } + + private void setCommitIdHeader() { + if (patchSet != null && patchSet.getRevision() != null + && patchSet.getRevision().get() != null + && patchSet.getRevision().get().length() > 0) { + setHeader("X-Gerrit-Commit", patchSet.getRevision().get()); + } + } + + private void setChangeSubjectHeader() throws EmailException { + setHeader("Subject", textTemplate("ChangeSubject")); + } + + /** Get a link to the change; null if the server doesn't know its own address. */ + public String getChangeUrl() { + if (getGerritUrl() != null) { + final StringBuilder r = new StringBuilder(); + r.append(getGerritUrl()); + r.append(change.getChangeId()); + return r.toString(); + } + return null; + } + + public String getChangeMessageThreadId() throws EmailException { + return velocify("<gerrit.${change.createdOn.time}.$change.key.get()" + + "@$email.gerritHost>"); + } + + /** Format the sender's "cover letter", {@link #getCoverLetter()}. */ + protected void formatCoverLetter() { + final String cover = getCoverLetter(); + if (!"".equals(cover)) { + appendText(cover); + appendText("\n\n"); + } + } + + /** Get the text of the "cover letter". */ + public String getCoverLetter() { + if (changeMessage != null) { + return changeMessage.trim(); + } + return ""; + } + + /** Format the change message and the affected file list. */ + protected void formatChangeDetail() { + appendText(getChangeDetail()); + } + + /** Create the change message and the affected file list. */ + public String getChangeDetail() { + try { + StringBuilder detail = new StringBuilder(); + + if (patchSetInfo != null) { + detail.append(patchSetInfo.getMessage().trim()).append("\n"); + } else { + detail.append(change.getSubject().trim()).append("\n"); + } + + if (patchSet != null) { + detail.append("---\n"); + PatchList patchList = getPatchList(); + for (PatchListEntry p : patchList.getPatches()) { + if (Patch.isMagic(p.getNewName())) { + continue; + } + detail.append(p.getChangeType().getCode()) + .append(" ").append(p.getNewName()).append("\n"); + } + detail.append(MessageFormat.format("" // + + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " // + + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " // + + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" // + + "\n", patchList.getPatches().size() - 1, // + patchList.getInsertions(), // + patchList.getDeletions())); + detail.append("\n"); + } + return detail.toString(); + } catch (Exception err) { + log.warn("Cannot format change detail", err); + return ""; + } + } + + /** Get the patch list corresponding to this patch set. */ + protected PatchList getPatchList() throws PatchListNotAvailableException { + if (patchSet != null) { + return args.patchListCache.get(change, patchSet); + } + throw new PatchListNotAvailableException("no patchSet specified"); + } + + /** Get the project entity the change is in; null if its been deleted. */ + protected ProjectState getProjectState() { + return projectState; + } + + /** Get the groups which own the project. */ + protected Set<AccountGroup.UUID> getProjectOwners() { + final ProjectState r; + + r = args.projectCache.get(change.getProject()); + return r != null ? r.getOwners() : Collections.<AccountGroup.UUID> emptySet(); + } + + /** TO or CC all vested parties (change owner, patch set uploader, author). */ + protected void rcptToAuthors(final RecipientType rt) { + for (final Account.Id id : authors) { + add(rt, id); + } + } + + /** BCC any user who has starred this change. */ + protected void bccStarredBy() { + if (!NotifyHandling.ALL.equals(notify)) { + return; + } + + try { + // BCC anyone who has starred this change + // and remove anyone who has ignored this change. + // + ListMultimap<Account.Id, String> stars = + args.starredChangesUtil.byChangeFromIndex(change.getId()); + for (Map.Entry<Account.Id, Collection<String>> e : + stars.asMap().entrySet()) { + if (e.getValue().contains(StarredChangesUtil.DEFAULT_LABEL)) { + super.add(RecipientType.BCC, e.getKey()); + } + if (e.getValue().contains(StarredChangesUtil.IGNORE_LABEL)) { + AccountState accountState = args.accountCache.get(e.getKey()); + if (accountState != null) { + removeUser(accountState.getAccount()); + } + } + } + } catch (OrmException err) { + // Just don't BCC everyone. Better to send a partial message to those + // we already have queued up then to fail deliver entirely to people + // who have a lower interest in the change. + log.warn("Cannot BCC users that starred updated change", err); + } + } + + @Override + protected final Watchers getWatchers(NotifyType type) throws OrmException { + if (!NotifyHandling.ALL.equals(notify)) { + return new Watchers(); + } + + ProjectWatch watch = new ProjectWatch( + args, branch.getParentKey(), projectState, changeData); + return watch.getWatchers(type); + } + + /** Any user who has published comments on this change. */ + protected void ccAllApprovals() { + if (!NotifyHandling.ALL.equals(notify) + && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) { + return; + } + + try { + for (Account.Id id : changeData.reviewers().all()) { + add(RecipientType.CC, id); + } + } catch (OrmException err) { + log.warn("Cannot CC users that reviewed updated change", err); + } + } + + /** Users who have non-zero approval codes on the change. */ + protected void ccExistingReviewers() { + if (!NotifyHandling.ALL.equals(notify) + && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) { + return; + } + + try { + for (Account.Id id : changeData.reviewers() + .byState(ReviewerStateInternal.REVIEWER)) { + add(RecipientType.CC, id); + } + } catch (OrmException err) { + log.warn("Cannot CC users that commented on updated change", err); + } + } + + @Override + protected void add(final RecipientType rt, final Account.Id to) { + if (! emailOnlyAuthors || authors.contains(to)) { + super.add(rt, to); + } + } + + @Override + protected boolean isVisibleTo(final Account.Id to) throws OrmException { + return projectState == null + || projectState.controlFor(args.identifiedUserFactory.create(to)) + .controlFor(args.db.get(), change).isVisible(args.db.get()); + } + + /** Find all users who are authors of any part of this change. */ + protected Set<Account.Id> getAuthors() { + Set<Account.Id> authors = new HashSet<>(); + + switch (notify) { + case NONE: + break; + case ALL: + default: + if (patchSet != null) { + authors.add(patchSet.getUploader()); + } + if (patchSetInfo != null) { + if (patchSetInfo.getAuthor().getAccount() != null) { + authors.add(patchSetInfo.getAuthor().getAccount()); + } + if (patchSetInfo.getCommitter().getAccount() != null) { + authors.add(patchSetInfo.getCommitter().getAccount()); + } + } + //$FALL-THROUGH$ + case OWNER_REVIEWERS: + case OWNER: + authors.add(change.getOwner()); + break; + } + + return authors; + } + + @Override + protected void setupVelocityContext() { + super.setupVelocityContext(); + velocityContext.put("change", change); + velocityContext.put("changeId", change.getKey()); + velocityContext.put("coverLetter", getCoverLetter()); + velocityContext.put("fromName", getNameFor(fromId)); + velocityContext.put("patchSet", patchSet); + 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 final int HEAP_EST_SIZE = 32 * 1024; + + /** Show patch set as unified difference. */ + public String getUnifiedDiff() { + PatchList patchList; + try { + patchList = getPatchList(); + if (patchList.getOldId() == null) { + // Octopus merges are not well supported for diff output by Gerrit. + // Currently these always have a null oldId in the PatchList. + return "[Octopus merge; cannot be formatted as a diff.]\n"; + } + } catch (PatchListNotAvailableException e) { + log.error("Cannot format patch", e); + return ""; + } + + int maxSize = args.settings.maximumDiffSize; + TemporaryBuffer.Heap buf = + new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxSize), maxSize); + try (DiffFormatter fmt = new DiffFormatter(buf)) { + try (Repository git = args.server.openRepository(change.getProject())) { + try { + fmt.setRepository(git); + fmt.setDetectRenames(true); + fmt.format(patchList.getOldId(), patchList.getNewId()); + return RawParseUtils.decode(buf.toByteArray()); + } catch (IOException e) { + if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) { + return ""; + } + log.error("Cannot format patch", e); + return ""; + } + } catch (IOException e) { + log.error("Cannot open repository to format patch", e); + return ""; + } + } + } +}
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..acb98cf --- /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.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.account.WatchConfig.NotifyType; +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; + 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 referred 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.debug(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/send/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java new file mode 100644 index 0000000..51f8cef --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -0,0 +1,87 @@ +// 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.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.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Notify interested parties of a brand new change. */ +public class CreateChangeSender extends NewChangeSender { + private static final Logger log = + LoggerFactory.getLogger(CreateChangeSender.class); + + public interface Factory { + CreateChangeSender create(Project.NameKey project, Change.Id id); + } + + @Inject + public CreateChangeSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + if (change.getStatus() == Change.Status.NEW) { + try { + // Try to mark interested owners with TO and CC or BCC line. + Watchers matching = getWatchers(NotifyType.NEW_CHANGES); + for (Account.Id user : Iterables.concat( + matching.to.accounts, + matching.cc.accounts, + matching.bcc.accounts)) { + if (isOwnerOfProjectOrBranch(user)) { + add(RecipientType.TO, user); + } + } + + // Add everyone else. Owners added above will not be duplicated. + add(RecipientType.TO, matching.to); + add(RecipientType.CC, matching.cc); + add(RecipientType.BCC, matching.bcc); + } catch (OrmException err) { + // Just don't CC everyone. Better to send a partial message to those + // we already have queued up then to fail deliver entirely to people + // who have a lower interest in the change. + log.warn("Cannot notify watchers for new change", err); + } + + includeWatchers(NotifyType.NEW_PATCHSETS); + } + } + + private boolean isOwnerOfProjectOrBranch(Account.Id user) { + return projectState != null + && projectState.controlFor(args.identifiedUserFactory.create(user)) + .controlForRef(change.getDest()) + .isOwner(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java new file mode 100644 index 0000000..dafa786 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -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. + +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.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Let users know that a reviewer and possibly her review have + * been removed. */ +public class DeleteReviewerSender extends ReplyToChangeSender { + private final Set<Account.Id> reviewers = new HashSet<>(); + + public interface Factory extends + ReplyToChangeSender.Factory<DeleteReviewerSender> { + @Override + DeleteReviewerSender create(Project.NameKey project, Change.Id change); + } + + @Inject + public DeleteReviewerSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "deleteReviewer", newChangeData(ea, project, id)); + } + + public void addReviewers(Collection<Account.Id> cc) { + reviewers.addAll(cc); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + ccExistingReviewers(); + includeWatchers(NotifyType.ALL_COMMENTS); + add(RecipientType.TO, reviewers); + } + + @Override + protected void formatChange() throws EmailException { + appendText(textTemplate("DeleteReviewer")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("DeleteReviewerHtml")); + } + } + + public List<String> getReviewerNames() { + if (reviewers.isEmpty()) { + return null; + } + List<String> names = new ArrayList<>(); + for (Account.Id id : reviewers) { + names.add(getNameFor(id)); + } + 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/send/DeleteVoteSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteVoteSender.java new file mode 100644 index 0000000..a9e8cc4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteVoteSender.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.mail.send; + +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a vote that was removed from a change. */ +public class DeleteVoteSender extends ReplyToChangeSender { + public interface Factory extends + ReplyToChangeSender.Factory<DeleteVoteSender> { + @Override + DeleteVoteSender create(Project.NameKey project, Change.Id change); + } + + @Inject + protected DeleteVoteSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "deleteVote", newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + includeWatchers(NotifyType.ALL_COMMENTS); + } + + @Override + protected void formatChange() throws EmailException { + 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/send/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java new file mode 100644 index 0000000..c870474 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -0,0 +1,143 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.extensions.registration.DynamicSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.AnonymousUser; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.GerritPersonIdentProvider; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.IdentifiedUser.GenericFactory; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.CapabilityControl; +import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.account.GroupIncludeCache; +import com.google.gerrit.server.config.AllProjectsName; +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.mail.EmailSettings; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.query.account.InternalAccountQuery; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.ssh.SshAdvertisedAddresses; +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; + +import java.util.List; + +public class EmailArguments { + final GitRepositoryManager server; + final ProjectCache projectCache; + final GroupBackend groupBackend; + final GroupIncludeCache groupIncludes; + final AccountCache accountCache; + final PatchListCache patchListCache; + final ApprovalsUtil approvalsUtil; + final FromAddressGenerator fromAddressGenerator; + final EmailSender emailSender; + final PatchSetInfoFactory patchSetInfoFactory; + final IdentifiedUser.GenericFactory identifiedUserFactory; + final CapabilityControl.Factory capabilityControlFactory; + final ChangeNotes.Factory changeNotesFactory; + final AnonymousUser anonymousUser; + final String anonymousCowardName; + final PersonIdent gerritPersonIdent; + 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; + final Provider<InternalAccountQuery> accountQueryProvider; + + @Inject + EmailArguments(GitRepositoryManager server, ProjectCache projectCache, + GroupBackend groupBackend, GroupIncludeCache groupIncludes, + AccountCache accountCache, + PatchListCache patchListCache, + ApprovalsUtil approvalsUtil, + FromAddressGenerator fromAddressGenerator, + EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory, + GenericFactory identifiedUserFactory, + CapabilityControl.Factory capabilityControlFactory, + ChangeNotes.Factory changeNotesFactory, + AnonymousUser anonymousUser, + @AnonymousCowardName String anonymousCowardName, + GerritPersonIdentProvider gerritPersonIdentProvider, + @CanonicalWebUrl @Nullable Provider<String> urlProvider, + AllProjectsName allProjectsName, + ChangeQueryBuilder queryBuilder, + Provider<ReviewDb> db, + ChangeData.Factory changeDataFactory, + RuntimeInstance velocityRuntime, + @MailTemplates SoyTofu soyTofu, + EmailSettings settings, + @SshAdvertisedAddresses List<String> sshAddresses, + SitePaths site, + DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners, + StarredChangesUtil starredChangesUtil, + Provider<InternalAccountQuery> accountQueryProvider) { + this.server = server; + this.projectCache = projectCache; + this.groupBackend = groupBackend; + this.groupIncludes = groupIncludes; + this.accountCache = accountCache; + this.patchListCache = patchListCache; + this.approvalsUtil = approvalsUtil; + this.fromAddressGenerator = fromAddressGenerator; + this.emailSender = emailSender; + this.patchSetInfoFactory = patchSetInfoFactory; + this.identifiedUserFactory = identifiedUserFactory; + this.capabilityControlFactory = capabilityControlFactory; + this.changeNotesFactory = changeNotesFactory; + this.anonymousUser = anonymousUser; + this.anonymousCowardName = anonymousCowardName; + this.gerritPersonIdent = gerritPersonIdentProvider.get(); + this.urlProvider = urlProvider; + this.allProjectsName = allProjectsName; + this.queryBuilder = queryBuilder; + 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.accountQueryProvider = accountQueryProvider; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java new file mode 100644 index 0000000..43d365c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java
@@ -0,0 +1,246 @@ +// 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 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; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public abstract class EmailHeader { + public abstract boolean isEmpty(); + + public abstract void write(Writer w) throws IOException; + + public static class String extends EmailHeader { + private final java.lang.String value; + + public String(java.lang.String v) { + value = v; + } + + public java.lang.String getString() { + return value; + } + + @Override + public boolean isEmpty() { + return value == null || value.length() == 0; + } + + @Override + public void write(Writer w) throws IOException { + if (needsQuotedPrintable(value)) { + w.write(quotedPrintable(value)); + } else { + w.write(value); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public boolean equals(Object o) { + return (o instanceof String) + && Objects.equals(value, ((String) o).value); + } + + @Override + public java.lang.String toString() { + return MoreObjects.toStringHelper(this).addValue(value).toString(); + } + } + + 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; + } + } + return false; + } + + static boolean needsQuotedPrintableWithinPhrase(final int cp) { + switch (cp) { + case '!': + case '*': + case '+': + case '-': + case '/': + case '=': + case '_': + return false; + default: + if (('a' <= cp && cp <= 'z') + || ('A' <= cp && cp <= 'Z') + || ('0' <= cp && cp <= '9')) { + return false; + } + return true; + } + } + + public static java.lang.String quotedPrintable(java.lang.String value) { + final StringBuilder r = new StringBuilder(); + + r.append("=?UTF-8?Q?"); + for (int i = 0; i < value.length(); i++) { + final int cp = value.codePointAt(i); + if (cp == ' ') { + r.append('_'); + + } else if (needsQuotedPrintableWithinPhrase(cp)) { + byte[] buf = new java.lang.String(Character.toChars(cp)).getBytes(UTF_8); + for (byte b: buf) { + r.append('='); + r.append(Integer.toHexString((b >>> 4) & 0x0f).toUpperCase()); + r.append(Integer.toHexString(b & 0x0f).toUpperCase()); + } + + } else { + r.append(Character.toChars(cp)); + } + } + r.append("?="); + + return r.toString(); + } + + public static class Date extends EmailHeader { + private final java.util.Date value; + + public Date(java.util.Date v) { + value = v; + } + + public java.util.Date getDate() { + return value; + } + + @Override + public boolean isEmpty() { + return value == null; + } + + @Override + public void write(Writer w) throws IOException { + final SimpleDateFormat fmt; + // Mon, 1 Jun 2009 10:49:44 -0700 + fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US); + w.write(fmt.format(value)); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public boolean equals(Object o) { + return (o instanceof Date) + && Objects.equals(value, ((Date) o).value); + } + + @Override + public java.lang.String toString() { + return MoreObjects.toStringHelper(this).addValue(value).toString(); + } + } + + public static class AddressList extends EmailHeader { + private final List<Address> list = new ArrayList<>(); + + public AddressList() { + } + + public AddressList(Address addr) { + add(addr); + } + + public List<Address> getAddressList() { + return Collections.unmodifiableList(list); + } + + public void add(Address addr) { + list.add(addr); + } + + void remove(java.lang.String email) { + for (Iterator<Address> i = list.iterator(); i.hasNext();) { + if (i.next().getEmail().equals(email)) { + i.remove(); + } + } + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public void write(Writer w) throws IOException { + int len = 8; + boolean firstAddress = true; + boolean needComma = false; + for (final Address addr : list) { + java.lang.String s = addr.toHeaderString(); + if (firstAddress) { + firstAddress = false; + } else if (72 < len + s.length()) { + w.write(",\r\n\t"); + len = 8; + needComma = false; + } + + if (needComma) { + w.write(", "); + } + w.write(s); + len += s.length(); + needComma = true; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(list); + } + + @Override + public boolean equals(Object o) { + return (o instanceof AddressList) + && Objects.equals(list, ((AddressList) o).list); + } + + @Override + public java.lang.String toString() { + return MoreObjects.toStringHelper(this).addValue(list).toString(); + } + } +}
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/send/FromAddressGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java new file mode 100644 index 0000000..2489063 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGenerator.java
@@ -0,0 +1,25 @@ +// 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.reviewdb.client.Account; +import com.google.gerrit.server.mail.Address; + +/** Constructs an address to send email from. */ +public interface FromAddressGenerator { + boolean isGenericAddress(Account.Id fromId); + + Address from(Account.Id fromId); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java new file mode 100644 index 0000000..3326b38 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -0,0 +1,245 @@ +// 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 java.nio.charset.StandardCharsets.UTF_8; + +import com.google.gerrit.common.data.ParameterizedString; +import com.google.gerrit.reviewdb.client.Account; +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.mail.Address; +import com.google.gerrit.server.mail.MailUtil; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.apache.commons.codec.binary.Base64; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.PersonIdent; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; + +/** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */ +@Singleton +public class FromAddressGeneratorProvider implements + Provider<FromAddressGenerator> { + private final FromAddressGenerator generator; + + @Inject + FromAddressGeneratorProvider(@GerritServerConfig final Config cfg, + @AnonymousCowardName final String anonymousCowardName, + @GerritPersonIdent final PersonIdent myIdent, + final AccountCache accountCache) { + + final String from = cfg.getString("sendemail", null, "from"); + final Address srvAddr = toAddress(myIdent); + + if (from == null || "MIXED".equalsIgnoreCase(from)) { + ParameterizedString name = new ParameterizedString("${user} (Code Review)"); + generator = + new PatternGen(srvAddr, accountCache, anonymousCowardName, name, + srvAddr.getEmail()); + } else if ("USER".equalsIgnoreCase(from)) { + 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.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.getEmail()); + } + } + } + + private static Address toAddress(final PersonIdent myIdent) { + return new Address(myIdent.getName(), myIdent.getEmailAddress()); + } + + @Override + public FromAddressGenerator get() { + return generator; + } + + static final class UserGen implements FromAddressGenerator { + private final AccountCache accountCache; + private final Pattern domainPattern; + private final String anonymousCowardName; + private final ParameterizedString nameRewriteTmpl; + private final Address serverAddress; + + /** + * 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.domainPattern = domainPattern; + this.anonymousCowardName = anonymousCowardName; + this.nameRewriteTmpl = nameRewriteTmpl; + this.serverAddress = serverAddress; + } + + @Override + public boolean isGenericAddress(Account.Id fromId) { + return false; + } + + @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(); + 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(); + } + + 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; + } + } + + static final class ServerGen implements FromAddressGenerator { + private final Address srvAddr; + + ServerGen(Address srvAddr) { + this.srvAddr = srvAddr; + } + + @Override + public boolean isGenericAddress(Account.Id fromId) { + return true; + } + + @Override + public Address from(final Account.Id fromId) { + return srvAddr; + } + } + + static final class PatternGen implements FromAddressGenerator { + private final ParameterizedString senderEmailPattern; + private final Address serverAddress; + private final AccountCache accountCache; + private final String anonymousCowardName; + private final ParameterizedString namePattern; + + PatternGen(final Address serverAddress, final AccountCache accountCache, + final String anonymousCowardName, + final ParameterizedString namePattern, final String senderEmail) { + this.senderEmailPattern = new ParameterizedString(senderEmail); + this.serverAddress = serverAddress; + this.accountCache = accountCache; + this.anonymousCowardName = anonymousCowardName; + this.namePattern = namePattern; + } + + @Override + public boolean isGenericAddress(Account.Id fromId) { + return false; + } + + @Override + public Address from(final Account.Id fromId) { + final String senderName; + + if (fromId != null) { + final Account account = accountCache.get(fromId).getAccount(); + String fullName = account.getFullName(); + if (fullName == null || "".equals(fullName)) { + fullName = anonymousCowardName; + } + senderName = namePattern.replace("user", fullName).toString(); + + } else { + senderName = serverAddress.getName(); + } + + String senderEmail; + if (senderEmailPattern.getParameterNames().isEmpty()) { + senderEmail = senderEmailPattern.getRawPattern(); + } else { + senderEmail = senderEmailPattern + .replace("userHash", hashOf(senderName)) + .toString(); + } + return new Address(senderName, senderEmail); + } + } + + private static String hashOf(String data) { + try { + MessageDigest hash = MessageDigest.getInstance("MD5"); + byte[] bytes = hash.digest(data.getBytes(UTF_8)); + return Base64.encodeBase64URLSafeString(bytes); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("No MD5 available", e); + } + } +}
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..c3cc701 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -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. + +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.shared.SoyAstCache; +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; + private final SoyAstCache cache; + + @Inject + MailSoyTofuProvider(SitePaths site, + SoyAstCache cache) { + this.site = site; + this.cache = cache; + } + + @Override + public SoyTofu get() throws ProvisionException { + SoyFileSet.Builder builder = SoyFileSet.builder(); + builder.setSoyAstCache(cache); + 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; + // TODO(davido): Consider using JGit's FileSnapshot to cache based on + // mtime. + 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/send/MailTemplates.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailTemplates.java new file mode 100644 index 0000000..b92567f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MailTemplates.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +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/send/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java new file mode 100644 index 0000000..38a2d5b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -0,0 +1,141 @@ +// 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.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelTypes; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a change successfully merged. */ +public class MergedSender extends ReplyToChangeSender { + public interface Factory { + MergedSender create(Project.NameKey project, Change.Id id); + } + + private final LabelTypes labelTypes; + + @Inject + public MergedSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "merged", newChangeData(ea, project, id)); + labelTypes = changeData.changeControl().getLabelTypes(); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + includeWatchers(NotifyType.ALL_COMMENTS); + includeWatchers(NotifyType.SUBMITTED_CHANGES); + } + + @Override + protected void formatChange() throws EmailException { + appendText(textTemplate("Merged")); + + if (useHtml()) { + appendHtml(soyHtmlTemplate("MergedHtml")); + } + } + + public String getApprovals() { + try { + Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create(); + Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create(); + for (PatchSetApproval ca : args.approvalsUtil.byPatchSet( + args.db.get(), changeData.changeControl(), patchSet.getId())) { + LabelType lt = labelTypes.byLabel(ca.getLabelId()); + if (lt == null) { + continue; + } + if (ca.getValue() > 0) { + pos.put(ca.getAccountId(), lt.getName(), ca); + } else if (ca.getValue() < 0) { + neg.put(ca.getAccountId(), lt.getName(), ca); + } + } + + return format("Approvals", pos) + format("Objections", neg); + } catch (OrmException err) { + // Don't list the approvals + } + return ""; + } + + private String format(String type, + Table<Account.Id, String, PatchSetApproval> approvals) { + StringBuilder txt = new StringBuilder(); + if (approvals.isEmpty()) { + return ""; + } + txt.append(type).append(":\n"); + for (Account.Id id : approvals.rowKeySet()) { + txt.append(" "); + txt.append(getNameFor(id)); + txt.append(": "); + boolean first = true; + for (LabelType lt : labelTypes.getLabelTypes()) { + PatchSetApproval ca = approvals.get(id, lt.getName()); + if (ca == null) { + continue; + } + + if (first) { + first = false; + } else { + txt.append("; "); + } + + LabelValue v = lt.getValue(ca); + if (v != null) { + txt.append(v.getText()); + } else { + txt.append(lt.getName()); + txt.append('='); + txt.append(LabelValue.formatValue(ca.getValue())); + } + } + txt.append('\n'); + } + 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/send/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java new file mode 100644 index 0000000..b4d6d01 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -0,0 +1,99 @@ +// 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.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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Sends an email alerting a user to a new change for them to review. */ +public abstract class NewChangeSender extends ChangeEmail { + private final Set<Account.Id> reviewers = new HashSet<>(); + private final Set<Account.Id> extraCC = new HashSet<>(); + + protected NewChangeSender(EmailArguments ea, ChangeData cd) + throws OrmException { + super(ea, "newchange", cd); + } + + public void addReviewers(final Collection<Account.Id> cc) { + reviewers.addAll(cc); + } + + public void addExtraCC(final Collection<Account.Id> cc) { + extraCC.addAll(cc); + } + + @Override + protected void init() throws EmailException { + super.init(); + + setHeader("Message-ID", getChangeMessageThreadId()); + + switch (notify) { + case NONE: + case OWNER: + break; + case ALL: + default: + add(RecipientType.CC, extraCC); + //$FALL-THROUGH$ + case OWNER_REVIEWERS: + add(RecipientType.TO, reviewers); + break; + } + + rcptToAuthors(RecipientType.CC); + } + + @Override + protected void formatChange() throws EmailException { + appendText(textTemplate("NewChange")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("NewChangeHtml")); + } + } + + public List<String> getReviewerNames() { + if (reviewers.isEmpty()) { + return null; + } + List<String> names = new ArrayList<>(); + for (Account.Id id : reviewers) { + names.add(getNameFor(id)); + } + return names; + } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContext.put("ownerName", getNameFor(change.getOwner())); + soyContextEmailData.put("reviewerNames", getReviewerNames()); + } + + @Override + protected boolean supportsHtml() { + return true; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java new file mode 100644 index 0000000..7d10a3f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -0,0 +1,130 @@ +// 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.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.Branch; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +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 + */ +public abstract class NotificationEmail extends OutgoingEmail { + private static final Logger log = + LoggerFactory.getLogger(NotificationEmail.class); + + protected Branch.NameKey branch; + + protected NotificationEmail(EmailArguments ea, + String mc, Branch.NameKey branch) { + super(ea, mc); + this.branch = branch; + } + + @Override + protected void init() throws EmailException { + super.init(); + setListIdHeader(); + } + + private void setListIdHeader() throws EmailException { + // Set a reasonable list id so that filters can be used to sort messages + setVHeader("List-Id", "<$email.listId.replace('@', '.')>"); + if (getSettingsUrl() != null) { + setVHeader("List-Unsubscribe", "<$email.settingsUrl>"); + } + } + + public String getListId() throws EmailException { + return velocify("gerrit-$projectName.replace('/', '-')@$email.gerritHost"); + } + + /** Include users and groups that want notification of events. */ + protected void includeWatchers(NotifyType type) { + try { + Watchers matching = getWatchers(type); + add(RecipientType.TO, matching.to); + add(RecipientType.CC, matching.cc); + add(RecipientType.BCC, matching.bcc); + } catch (OrmException err) { + // Just don't CC everyone. Better to send a partial message to those + // we already have queued up then to fail deliver entirely to people + // who have a lower interest in the change. + log.warn("Cannot BCC watchers for " + type, err); + } + } + + /** Returns all watchers that are relevant */ + protected abstract Watchers getWatchers(NotifyType type) throws OrmException; + + /** Add users or email addresses to the TO, CC, or BCC list. */ + protected void add(RecipientType type, Watchers.List list) { + for (Account.Id user : list.accounts) { + add(type, user); + } + for (Address addr : list.emails) { + add(type, addr); + } + } + + public String getSshHost() { + String host = Iterables.getFirst(args.sshAddresses, null); + if (host == null) { + return null; + } + if (host.startsWith("*:")) { + return getGerritHost() + host.substring(1); + } + return host; + } + + @Override + protected void setupVelocityContext() { + super.setupVelocityContext(); + 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/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java new file mode 100644 index 0000000..b8358c5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -0,0 +1,613 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.mail.send; + +import static com.google.common.base.Preconditions.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.ListMultimap; +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.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.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; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.context.InternalContextAdapterImpl; +import org.apache.velocity.runtime.RuntimeInstance; +import org.apache.velocity.runtime.parser.node.SimpleNode; +import org.eclipse.jgit.util.SystemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; +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; + +/** Sends an email to one or more interested parties. */ +public abstract class OutgoingEmail { + private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class); + + private static final String HDR_TO = "To"; + private static final String HDR_CC = "CC"; + + protected String messageClass; + private final HashSet<Account.Id> rcptTo = new HashSet<>(); + private final Map<String, EmailHeader> headers; + private final Set<Address> smtpRcptTo = new HashSet<>(); + private Address smtpFromAddress; + private StringBuilder textBody; + private StringBuilder htmlBody; + private ListMultimap<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; + + protected OutgoingEmail(EmailArguments ea, String mc) { + args = ea; + messageClass = mc; + headers = new LinkedHashMap<>(); + } + + public void setFrom(final Account.Id id) { + fromId = id; + } + + public void setNotify(NotifyHandling notify) { + this.notify = checkNotNull(notify); + } + + public void setAccountsToNotify( + ListMultimap<RecipientType, Account.Id> accountsToNotify) { + this.accountsToNotify = checkNotNull(accountsToNotify); + } + + /** + * Format and enqueue the message for delivery. + * + * @throws EmailException + */ + public void send() throws EmailException { + if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) { + return; + } + + if (!args.emailSender.isEnabled()) { + // Server has explicitly disabled email sending. + // + return; + } + + init(); + if (useHtml()) { + appendHtml(soyHtmlTemplate("HeaderHtml")); + } + format(); + appendText(textTemplate("Footer")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("FooterHtml")); + } + if (shouldSendMessage()) { + if (fromId != null) { + final Account fromUser = args.accountCache.get(fromId).getAccount(); + GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferencesInfo(); + + if (senderPrefs != null + && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) { + // If we are impersonating a user, make sure they receive a CC of + // this message so they can always review and audit what we sent + // on their behalf to others. + // + add(RecipientType.CC, 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. + // + removeUser(fromUser); + } + + // Check the preferences of all recipients. If any user has disabled + // his email notifications then drop him from recipients' list + for (Account.Id id : rcptTo) { + Account thisUser = args.accountCache.get(id).getAccount(); + GeneralPreferencesInfo prefs = thisUser.getGeneralPreferencesInfo(); + if (prefs == null || prefs.getEmailStrategy() == DISABLED) { + removeUser(thisUser); + } + if (smtpRcptTo.isEmpty()) { + return; + } + } + } + + String textPart = textBody.toString(); + OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); + va.messageClass = messageClass; + va.smtpFromAddress = smtpFromAddress; + va.smtpRcptTo = smtpRcptTo; + va.headers = headers; + + va.body = textPart; + if (useHtml()) { + va.htmlBody = htmlBody.toString(); + } else { + va.htmlBody = null; + } + + for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { + try { + validator.validateOutgoingEmail(va); + } catch (ValidationException e) { + return; + } + } + + args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, + va.body, va.htmlBody); + } + } + + /** Format the message body by calling {@link #appendText(String)}. */ + protected abstract void format() throws EmailException; + + /** + * Setup the message headers and envelope (TO, CC, BCC). + * + * @throws EmailException if an error occurred. + */ + protected void init() throws EmailException { + setupVelocityContext(); + setupSoyContext(); + + smtpFromAddress = args.fromAddressGenerator.from(fromId); + setHeader("Date", new Date()); + headers.put("From", new EmailHeader.AddressList(smtpFromAddress)); + headers.put(HDR_TO, new EmailHeader.AddressList()); + 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 + // it is a generic header for this Gerrit server, include the + // Reply-To header with the current user's email address. + // + final Address a = toAddress(fromId); + if (a != null && !smtpFromAddress.getEmail().equals(a.getEmail())) { + setHeader("Reply-To", a.getEmail()); + } + } + + setHeader("X-Gerrit-MessageType", messageClass); + textBody = new StringBuilder(); + htmlBody = new StringBuilder(); + + if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { + appendText(getFromLine()); + } + } + + protected String getFromLine() { + final Account account = args.accountCache.get(fromId).getAccount(); + final String name = account.getFullName(); + final String email = account.getPreferredEmail(); + StringBuilder f = new StringBuilder(); + + if ((name != null && !name.isEmpty()) + || (email != null && !email.isEmpty())) { + f.append("From"); + if (name != null && !name.isEmpty()) { + f.append(" ").append(name); + } + if (email != null && !email.isEmpty()) { + f.append(" <").append(email).append(">"); + } + f.append(":\n\n"); + } + return f.toString(); + } + + public String getGerritHost() { + if (getGerritUrl() != null) { + try { + return new URL(getGerritUrl()).getHost(); + } catch (MalformedURLException e) { + // Try something else. + } + } + + // Fall back onto whatever the local operating system thinks + // this server is called. We hopefully didn't get here as a + // good admin would have configured the canonical url. + // + return SystemReader.getInstance().getHostname(); + } + + public String getSettingsUrl() { + if (getGerritUrl() != null) { + final StringBuilder r = new StringBuilder(); + r.append(getGerritUrl()); + r.append("settings"); + return r.toString(); + } + return null; + } + + public String getGerritUrl() { + return args.urlProvider.get(); + } + + /** Set a header in the outgoing message using a template. */ + protected void setVHeader(final String name, final String value) throws + EmailException { + setHeader(name, velocify(value)); + } + + /** Set a header in the outgoing message. */ + protected void setHeader(final String name, final String value) { + headers.put(name, new EmailHeader.String(value)); + } + + protected void setHeader(final String name, final Date date) { + headers.put(name, new EmailHeader.Date(date)); + } + + /** Append text to the outgoing email body. */ + protected void appendText(final String text) { + if (text != null) { + textBody.append(text); + } + } + + /** Append html to the outgoing email body. */ + protected void appendHtml(String html) { + if (html != null) { + htmlBody.append(html); + } + } + + /** Lookup a human readable name for an account, usually the "full name". */ + protected String getNameFor(final Account.Id accountId) { + if (accountId == null) { + return args.gerritPersonIdent.getName(); + } + + final Account userAccount = args.accountCache.get(accountId).getAccount(); + String name = userAccount.getFullName(); + if (name == null) { + name = userAccount.getPreferredEmail(); + } + if (name == null) { + name = args.anonymousCowardName + " #" + accountId; + } + return name; + } + + /** + * Gets the human readable name and email for an account; + * if neither are available, returns the Anonymous Coward name. + * + * @param accountId user to fetch. + * @return name/email of account, or Anonymous Coward if unset. + */ + public String getNameEmailFor(Account.Id accountId) { + AccountState who = args.accountCache.get(accountId); + String name = who.getAccount().getFullName(); + String email = who.getAccount().getPreferredEmail(); + + if (name != null && email != null) { + return name + " <" + email + ">"; + + } else if (name != null) { + return name; + } else if (email != null) { + return email; + + } else /* (name == null && email == null) */ { + return args.anonymousCowardName + " #" + accountId; + } + } + + /** + * Gets the human readable name and email for an account; + * if both are unavailable, returns the username. If no + * username is set, this function returns null. + * + * @param accountId user to fetch. + * @return name/email of account, username, or null if unset. + */ + public String getUserNameEmailFor(Account.Id accountId) { + AccountState who = args.accountCache.get(accountId); + String name = who.getAccount().getFullName(); + String email = who.getAccount().getPreferredEmail(); + + if (name != null && email != null) { + return name + " <" + email + ">"; + } else if (email != null) { + return email; + } else if (name != null) { + return name; + } + String username = who.getUserName(); + if (username != null) { + return username; + } + return null; + } + + protected boolean shouldSendMessage() { + if (textBody.length() == 0) { + // If we have no message body, don't send. + return false; + } + + if (smtpRcptTo.isEmpty()) { + // If we have nobody to send this message to, then all of our + // selection filters previously for this type of message were + // unable to match a destination. Don't bother sending it. + return false; + } + + 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; + } + + return true; + } + + /** Schedule this message for delivery to the listed accounts. */ + protected void add(final RecipientType rt, final Collection<Account.Id> list) { + for (final Account.Id id : list) { + add(rt, id); + } + } + + protected void add(final RecipientType rt, final UserIdentity who) { + if (who != null && who.getAccount() != null) { + add(rt, who.getAccount()); + } + } + + /** Schedule delivery of this message to the given account. */ + protected void add(final RecipientType rt, final Account.Id to) { + try { + if (!rcptTo.contains(to) && isVisibleTo(to)) { + rcptTo.add(to); + add(rt, toAddress(to)); + } + } catch (OrmException e) { + log.error("Error reading database for account: " + to, e); + } + } + + /** + * @param to account. + * @throws OrmException + * @return whether this email is visible to the given account. + */ + protected boolean isVisibleTo(final Account.Id to) throws OrmException { + return true; + } + + /** Schedule delivery of this message to the given account. */ + protected void add(final RecipientType rt, final Address addr) { + 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: + ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr); + break; + case CC: + ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr); + break; + case BCC: + break; + } + } + } + } + + private Address toAddress(final Account.Id id) { + final Account a = args.accountCache.get(id).getAccount(); + final String e = a.getPreferredEmail(); + if (!a.isActive() || e == null) { + return null; + } + return new Address(a.getFullName(), e); + } + + protected void setupVelocityContext() { + velocityContext = new VelocityContext(); + + velocityContext.put("email", this); + velocityContext.put("messageClass", messageClass); + 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; + String templateName = "OutgoingEmail"; + SimpleNode tree = runtime.parse(new StringReader(template), templateName); + InternalContextAdapterImpl ica = new InternalContextAdapterImpl(velocityContext); + ica.pushCurrentTemplateName(templateName); + try { + tree.init(ica, runtime); + StringWriter w = new StringWriter(); + tree.render(ica, w); + return w.toString(); + } finally { + ica.popCurrentTemplateName(); + } + } catch (Exception e) { + throw new EmailException("Cannot format velocity template: " + template, e); + } + } + + protected String velocifyFile(String name) throws EmailException { + try { + RuntimeInstance runtime = args.velocityRuntime; + if (runtime.getLoaderNameForResource(name) == null) { + name = "com/google/gerrit/server/mail/" + name; + } + Template template = runtime.getTemplate(name, UTF_8.name()); + StringWriter w = new StringWriter(); + template.merge(velocityContext, w); + return w.toString(); + } catch (Exception e) { + throw new EmailException("Cannot format velocity template " + name, e); + } + } + + 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); + } + + public String joinStrings(Iterator<Object> in, String joiner) { + if (!in.hasNext()) { + return ""; + } + + Object first = in.next(); + if (!in.hasNext()) { + return safeToString(first); + } + + StringBuilder r = new StringBuilder(); + r.append(safeToString(first)); + while (in.hasNext()) { + r.append(joiner).append(safeToString(in.next())); + } + return r.toString(); + } + + protected void removeUser(Account user) { + String fromEmail = user.getPreferredEmail(); + for (Iterator<Address> j = smtpRcptTo.iterator(); j.hasNext();) { + if (j.next().getEmail().equals(fromEmail)) { + j.remove(); + } + } + for (Map.Entry<String, EmailHeader> entry : headers.entrySet()) { + // Don't remove fromEmail from the "From" header though! + if (entry.getValue() instanceof AddressList + && !entry.getKey().equals("From")) { + ((AddressList) entry.getValue()).remove(fromEmail); + } + } + } + + 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/send/OutgoingEmailValidator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java new file mode 100644 index 0000000..1e92c83 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.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.server.mail.send; + +import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS; + +import org.apache.commons.validator.routines.DomainValidator; +import org.apache.commons.validator.routines.EmailValidator; + +public class OutgoingEmailValidator { + static { + DomainValidator.updateTLDOverride(GENERIC_PLUS, new String[]{"local"}); + } + + public static boolean isValid(String addr) { + return EmailValidator.getInstance(true, true).isValid(addr); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java new file mode 100644 index 0000000..2269b66 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -0,0 +1,231 @@ +// 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.mail.send; + +import com.google.common.base.Strings; +import com.google.gerrit.common.data.GroupDescription; +import com.google.gerrit.common.data.GroupDescriptions; +import com.google.gerrit.common.data.GroupReference; +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.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.account.AccountState; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +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; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.query.change.SingleGroupUser; +import com.google.gwtorm.server.OrmException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ProjectWatch { + private static final Logger log = LoggerFactory.getLogger(ProjectWatch.class); + + protected final EmailArguments args; + protected final ProjectState projectState; + protected final Project.NameKey project; + protected final ChangeData changeData; + + public ProjectWatch(EmailArguments args, Project.NameKey project, + ProjectState projectState, ChangeData changeData) { + this.args = args; + this.project = project; + this.projectState = projectState; + this.changeData = changeData; + } + + /** Returns all watchers that are relevant */ + public final Watchers getWatchers(NotifyType type) throws OrmException { + Watchers matching = new Watchers(); + Set<Account.Id> projectWatchers = new HashSet<>(); + + for (AccountState a : args.accountQueryProvider.get() + .byWatchedProject(project)) { + Account.Id accountId = a.getAccount().getId(); + for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : + a.getProjectWatches().entrySet()) { + if (project.equals(e.getKey().project()) + && add(matching, accountId, e.getKey(), e.getValue(), type)) { + // We only want to prevent matching All-Projects if this filter hits + projectWatchers.add(accountId); + } + } + } + + for (AccountState a : args.accountQueryProvider.get() + .byWatchedProject(args.allProjectsName)) { + for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : + a.getProjectWatches().entrySet()) { + if (args.allProjectsName.equals(e.getKey().project())) { + Account.Id accountId = a.getAccount().getId(); + if (!projectWatchers.contains(accountId)) { + add(matching, accountId, e.getKey(), e.getValue(), type); + } + } + } + } + + for (ProjectState state : projectState.tree()) { + for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) { + if (nc.isNotify(type)) { + try { + add(matching, nc); + } catch (QueryParseException e) { + log.warn("Project {} has invalid notify {} filter \"{}\": {}", + state.getProject().getName(), nc.getName(), + nc.getFilter(), e.getMessage()); + } + } + } + } + + return matching; + } + + public static class Watchers { + static class List { + protected final Set<Account.Id> accounts = new HashSet<>(); + protected final Set<Address> emails = new HashSet<>(); + } + protected final List to = new List(); + protected final List cc = new List(); + protected final List bcc = new List(); + + List list(NotifyConfig.Header header) { + switch (header) { + case TO: + return to; + case CC: + return cc; + default: + case BCC: + return bcc; + } + } + } + + private void add(Watchers matching, NotifyConfig nc) + throws OrmException, QueryParseException { + for (GroupReference ref : nc.getGroups()) { + CurrentUser user = new SingleGroupUser(args.capabilityControlFactory, + ref.getUUID()); + if (filterMatch(user, nc.getFilter())) { + deliverToMembers(matching.list(nc.getHeader()), ref.getUUID()); + } + } + + if (!nc.getAddresses().isEmpty()) { + if (filterMatch(null, nc.getFilter())) { + matching.list(nc.getHeader()).emails.addAll(nc.getAddresses()); + } + } + } + + private void deliverToMembers( + Watchers.List matching, + AccountGroup.UUID startUUID) throws OrmException { + ReviewDb db = args.db.get(); + Set<AccountGroup.UUID> seen = new HashSet<>(); + List<AccountGroup.UUID> q = new ArrayList<>(); + + seen.add(startUUID); + q.add(startUUID); + + while (!q.isEmpty()) { + AccountGroup.UUID uuid = q.remove(q.size() - 1); + GroupDescription.Basic group = args.groupBackend.get(uuid); + if (!Strings.isNullOrEmpty(group.getEmailAddress())) { + // If the group has an email address, do not expand membership. + matching.emails.add(new Address(group.getEmailAddress())); + continue; + } + + AccountGroup ig = GroupDescriptions.toAccountGroup(group); + if (ig == null) { + // Non-internal groups cannot be expanded by the server. + continue; + } + + for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) { + matching.accounts.add(m.getAccountId()); + } + for (AccountGroup.UUID m : args.groupIncludes.subgroupsOf(uuid)) { + if (seen.add(m)) { + q.add(m); + } + } + } + } + + private boolean add(Watchers matching, Account.Id accountId, + ProjectWatchKey key, Set<NotifyType> watchedTypes, NotifyType type) + throws OrmException { + IdentifiedUser user = args.identifiedUserFactory.create(accountId); + + try { + if (filterMatch(user, key.filter())) { + // If we are set to notify on this type, add the user. + // Otherwise, still return true to stop notifications for this user. + if (watchedTypes.contains(type)) { + matching.bcc.accounts.add(accountId); + } + return true; + } + } catch (QueryParseException e) { + // Ignore broken filter expressions. + } + return false; + } + + private boolean filterMatch(CurrentUser user, String filter) + throws OrmException, QueryParseException { + ChangeQueryBuilder qb; + Predicate<ChangeData> p = null; + + if (user == null) { + qb = args.queryBuilder.asUser(args.anonymousUser); + } else { + qb = args.queryBuilder.asUser(user); + p = qb.is_visible(); + } + + if (filter != null) { + Predicate<ChangeData> filterPredicate = qb.parse(filter); + if (p == null) { + p = filterPredicate; + } else { + p = Predicate.and(filterPredicate, p); + } + } + return p == null || p.asMatchable().match(changeData); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java new file mode 100644 index 0000000..b665690 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RegisterNewEmailSender.java
@@ -0,0 +1,83 @@ +// 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.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; + +public class RegisterNewEmailSender extends OutgoingEmail { + public interface Factory { + RegisterNewEmailSender create(String address); + } + + private final EmailTokenVerifier tokenVerifier; + private final IdentifiedUser user; + private final String addr; + private String emailToken; + + @Inject + public RegisterNewEmailSender(EmailArguments ea, + EmailTokenVerifier etv, + IdentifiedUser callingUser, + @Assisted final String address) { + super(ea, "registernewemail"); + tokenVerifier = etv; + user = callingUser; + addr = address; + } + + @Override + protected void init() throws EmailException { + super.init(); + setHeader("Subject", "[Gerrit Code Review] Email Verification"); + add(RecipientType.TO, new Address(addr)); + } + + @Override + protected void format() throws EmailException { + appendText(textTemplate("RegisterNewEmail")); + } + + public String getUserNameEmail() { + return getUserNameEmailFor(user.getAccountId()); + } + + public String getEmailRegistrationToken() { + if (emailToken == null) { + emailToken = checkNotNull( + tokenVerifier.encode(user.getAccountId(), addr), "token"); + } + return emailToken; + } + + 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/send/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java new file mode 100644 index 0000000..d5ba4a8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -0,0 +1,106 @@ +// 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.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.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Send notice of new patch sets for reviewers. */ +public class ReplacePatchSetSender extends ReplyToChangeSender { + public interface Factory { + ReplacePatchSetSender create(Project.NameKey project, Change.Id id); + } + + private final Set<Account.Id> reviewers = new HashSet<>(); + private final Set<Account.Id> extraCC = new HashSet<>(); + + @Inject + public ReplacePatchSetSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "newpatchset", newChangeData(ea, project, id)); + } + + public void addReviewers(final Collection<Account.Id> cc) { + reviewers.addAll(cc); + } + + public void addExtraCC(final Collection<Account.Id> cc) { + extraCC.addAll(cc); + } + + @Override + protected void init() throws EmailException { + super.init(); + + if (fromId != null) { + // Don't call yourself a reviewer of your own patch set. + // + reviewers.remove(fromId); + } + add(RecipientType.TO, reviewers); + add(RecipientType.CC, extraCC); + rcptToAuthors(RecipientType.CC); + bccStarredBy(); + includeWatchers(NotifyType.NEW_PATCHSETS); + } + + @Override + protected void formatChange() throws EmailException { + appendText(textTemplate("ReplacePatchSet")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("ReplacePatchSetHtml")); + } + } + + public List<String> getReviewerNames() { + 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/send/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java new file mode 100644 index 0000000..a6e2fa7 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplyToChangeSender.java
@@ -0,0 +1,45 @@ +// 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.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; +import com.google.gwtorm.server.OrmException; + +/** Alert a user to a reply to a change, usually commentary made during review. */ +public abstract class ReplyToChangeSender extends ChangeEmail { + public interface Factory<T extends ReplyToChangeSender> { + T create(Project.NameKey project, Change.Id id); + } + + protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd) + throws OrmException { + super(ea, mc, cd); + } + + @Override + protected void init() throws EmailException { + super.init(); + + final String threadId = getChangeMessageThreadId(); + setHeader("In-Reply-To", threadId); + setHeader("References", threadId); + + rcptToAuthors(RecipientType.TO); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RestoredSender.java new file mode 100644 index 0000000..fe37984 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RestoredSender.java
@@ -0,0 +1,62 @@ +// 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. + +package com.google.gerrit.server.mail.send; + +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a change being restored by its owner. */ +public class RestoredSender extends ReplyToChangeSender { + public interface Factory extends + ReplyToChangeSender.Factory<RestoredSender> { + @Override + RestoredSender create(Project.NameKey project, Change.Id id); + } + + @Inject + public RestoredSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "restore", ChangeEmail.newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + includeWatchers(NotifyType.ALL_COMMENTS); + } + + @Override + protected void formatChange() throws EmailException { + 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/send/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RevertedSender.java new file mode 100644 index 0000000..bad72ab --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/RevertedSender.java
@@ -0,0 +1,60 @@ +// 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. + +package com.google.gerrit.server.mail.send; + +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/** Send notice about a change being reverted. */ +public class RevertedSender extends ReplyToChangeSender { + public interface Factory { + RevertedSender create(Project.NameKey project, Change.Id id); + } + + @Inject + public RevertedSender(EmailArguments ea, + @Assisted Project.NameKey project, + @Assisted Change.Id id) + throws OrmException { + super(ea, "revert", ChangeEmail.newChangeData(ea, project, id)); + } + + @Override + protected void init() throws EmailException { + super.init(); + + ccAllApprovals(); + bccStarredBy(); + includeWatchers(NotifyType.ALL_COMMENTS); + } + + @Override + protected void formatChange() throws EmailException { + 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/send/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java new file mode 100644 index 0000000..d5622bf --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -0,0 +1,356 @@ +// 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 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; + +import org.apache.commons.net.smtp.AuthSMTPClient; +import org.apache.commons.net.smtp.SMTPClient; +import org.apache.commons.net.smtp.SMTPReply; +import org.eclipse.jgit.lib.Config; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +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. */ +@Singleton +public class SmtpEmailSender implements EmailSender { + /** The socket's connect timeout (0 = infinite timeout) */ + private static final int DEFAULT_CONNECT_TIMEOUT = 0; + + public static class Module extends AbstractModule { + @Override + protected void configure() { + bind(EmailSender.class).to(SmtpEmailSender.class); + } + } + + private final boolean enabled; + private final int connectTimeout; + + private String smtpHost; + private int smtpPort; + private String smtpUser; + private String smtpPass; + private Encryption smtpEncryption; + private boolean sslVerify; + private Set<String> allowrcpt; + private String importance; + private int expiryDays; + + @Inject + SmtpEmailSender(@GerritServerConfig final Config cfg) { + enabled = cfg.getBoolean("sendemail", null, "enable", true); + connectTimeout = + Ints.checkedCast(ConfigUtil.getTimeUnit(cfg, "sendemail", null, + "connectTimeout", DEFAULT_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)); + + + smtpHost = cfg.getString("sendemail", null, "smtpserver"); + if (smtpHost == null) { + smtpHost = "127.0.0.1"; + } + + smtpEncryption = + cfg.getEnum("sendemail", null, "smtpencryption", Encryption.NONE); + sslVerify = cfg.getBoolean("sendemail", null, "sslverify", true); + + final int defaultPort; + switch (smtpEncryption) { + case SSL: + defaultPort = 465; + break; + + case NONE: + case TLS: + default: + defaultPort = 25; + break; + } + smtpPort = cfg.getInt("sendemail", null, "smtpserverport", defaultPort); + + smtpUser = cfg.getString("sendemail", null, "smtpuser"); + smtpPass = cfg.getString("sendemail", null, "smtppass"); + + Set<String> rcpt = new HashSet<>(); + for (String addr : cfg.getStringList("sendemail", null, "allowrcpt")) { + rcpt.add(addr); + } + allowrcpt = Collections.unmodifiableSet(rcpt); + importance = cfg.getString("sendemail", null, "importance"); + expiryDays = cfg.getInt("sendemail", null, "expiryDays", 0); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean canEmail(String address) { + if (!isEnabled()) { + return false; + } + + if (allowrcpt.isEmpty()) { + return true; + } + + if (allowrcpt.contains(address)) { + return true; + } + + String domain = address.substring(address.lastIndexOf('@') + 1); + if (allowrcpt.contains(domain) || allowrcpt.contains("@" + domain)) { + return true; + } + + return false; + } + + @Override + public void send(final Address from, Collection<Address> rcpt, + 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"); + } + + final Map<String, EmailHeader> hdrs = + new LinkedHashMap<>(callerHeaders); + setMissingHeader(hdrs, "MIME-Version", "1.0"); + setMissingHeader(hdrs, "Content-Transfer-Encoding", "8bit"); + setMissingHeader(hdrs, "Content-Disposition", "inline"); + setMissingHeader(hdrs, "User-Agent", "Gerrit/" + Version.getVersion()); + if (importance != null) { + setMissingHeader(hdrs, "Importance", importance); + } + if (expiryDays > 0) { + Date expiry = new Date(TimeUtil.nowMs() + + expiryDays * 24 * 60 * 60 * 1000L ); + setMissingHeader(hdrs, "Expiry-Date", + 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.getEmail())) { + throw new EmailException("Server " + smtpHost + + " rejected from address " + from.getEmail()); + } + + /* Do not prevent the email from being sent to "good" users simply + * because some users get rejected. If not, a single rejected + * project watcher could prevent email for most actions on a project + * from being sent to any user! Instead, queue up the errors, and + * throw an exception after sending the email to get the rejected + * error(s) logged. + */ + for (Address addr : rcpt) { + if (!client.addRecipient(addr.getEmail())) { + String error = client.getReplyString(); + rejected.append("Server ").append(smtpHost) + .append(" rejected recipient ").append(addr) + .append(": ").append(error); + } + } + + Writer messageDataWriter = client.sendMessageData(); + if (messageDataWriter == null) { + /* Include rejected recipient error messages here to not lose that + * information. That piece of the puzzle is vital if zero recipients + * are accepted and the server consequently rejects the DATA command. + */ + throw new EmailException(rejected + "Server " + smtpHost + + " rejected DATA command: " + client.getReplyString()); + } + try (Writer w = new BufferedWriter(messageDataWriter)) { + for (Map.Entry<String, EmailHeader> h : hdrs.entrySet()) { + if (!h.getValue().isEmpty()) { + w.write(h.getKey()); + w.write(": "); + h.getValue().write(w); + w.write("\r\n"); + } + } + + w.write("\r\n"); + w.write(encodedBody); + w.flush(); + } + + if (!client.completePendingCommand()) { + throw new EmailException("Server " + smtpHost + + " rejected message body: " + client.getReplyString()); + } + + client.logout(); + if (rejected.length() > 0) { + throw new EmailException(rejected.toString()); + } + } finally { + client.disconnect(); + } + } catch (IOException e) { + throw new EmailException("Cannot send outgoing email", e); + } + } + + 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()) { + hdrs.put(name, new EmailHeader.String(value)); + } + } + + private SMTPClient open() throws EmailException { + final AuthSMTPClient client = new AuthSMTPClient(UTF_8.name()); + + if (smtpEncryption == Encryption.SSL) { + client.enableSSL(sslVerify); + } + + client.setConnectTimeout(connectTimeout); + try { + client.connect(smtpHost, smtpPort); + int replyCode = client.getReplyCode(); + String replyString = client.getReplyString(); + if (!SMTPReply.isPositiveCompletion(replyCode)) { + throw new EmailException( + String.format("SMTP server rejected connection: %d: %s", + replyCode, replyString)); + } + if (!client.login()) { + throw new EmailException( + "SMTP server rejected HELO/EHLO greeting: " + replyString); + } + + if (smtpEncryption == Encryption.TLS) { + if (!client.startTLS(smtpHost, smtpPort, sslVerify)) { + throw new EmailException("SMTP server does not support TLS"); + } + if (!client.login()) { + throw new EmailException("SMTP server rejected login: " + replyString); + } + } + + if (smtpUser != null && !client.auth(smtpUser, smtpPass)) { + throw new EmailException("SMTP server rejected auth: " + replyString); + } + return client; + } catch (IOException | EmailException e) { + if (client.isConnected()) { + try { + client.disconnect(); + } catch (IOException e2) { + //Ignored + } + } + if (e instanceof EmailException) { + throw (EmailException) e; + } + throw new EmailException(e.getMessage(), e); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java new file mode 100644 index 0000000..03d4f7a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/VelocityRuntimeProvider.java
@@ -0,0 +1,122 @@ +// 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. + +package com.google.gerrit.server.mail.send; + +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 org.apache.velocity.runtime.RuntimeConstants; +import org.apache.velocity.runtime.RuntimeInstance; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.log.LogChute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.util.Properties; + +/** Configures Velocity template engine for sending email. */ +@Singleton +public class VelocityRuntimeProvider implements Provider<RuntimeInstance> { + private final SitePaths site; + + @Inject + VelocityRuntimeProvider(SitePaths site) { + this.site = site; + } + + @Override + public RuntimeInstance get() { + String rl = "resource.loader"; + String pkg = "org.apache.velocity.runtime.resource.loader"; + + Properties p = new Properties(); + p.setProperty(RuntimeConstants.VM_PERM_INLINE_LOCAL, "true"); + p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, + Slf4jLogChute.class.getName()); + p.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); + p.setProperty("runtime.log.logsystem.log4j.category", "velocity"); + + if (Files.isDirectory(site.mail_dir)) { + p.setProperty(rl, "file, class"); + p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader"); + p.setProperty("file." + rl + ".path", + site.mail_dir.toAbsolutePath().toString()); + p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader"); + } else { + p.setProperty(rl, "class"); + p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader"); + } + + RuntimeInstance ri = new RuntimeInstance(); + try { + ri.init(p); + } catch (Exception err) { + throw new ProvisionException("Cannot configure Velocity templates", err); + } + return ri; + } + + /** Connects Velocity to sfl4j. */ + public static class Slf4jLogChute implements LogChute { + private static final Logger log = LoggerFactory.getLogger("velocity"); + + @Override + public void init(RuntimeServices rs) { + } + + @Override + public boolean isLevelEnabled(int level) { + switch (level) { + default: + case DEBUG_ID: + return log.isDebugEnabled(); + case INFO_ID: + return log.isInfoEnabled(); + case WARN_ID: + return log.isWarnEnabled(); + case ERROR_ID: + return log.isErrorEnabled(); + } + } + + @Override + public void log(int level, String message) { + log(level, message, null); + } + + @Override + public void log(int level, String msg, Throwable err) { + switch (level) { + default: + case DEBUG_ID: + log.debug(msg, err); + break; + case INFO_ID: + log.info(msg, err); + break; + case WARN_ID: + log.warn(msg, err); + break; + case ERROR_ID: + log.error(msg, err); + break; + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java index 96486e9..a231693 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java
@@ -31,7 +31,7 @@ * or cannot be determined, {@link MimeUtil2#UNKNOWN_MIME_TYPE} which * is an alias for {@code application/octet-stream}. */ - MimeType getMimeType(final String path, final byte[] content); + MimeType getMimeType(String path, byte[] content); /** * Is this content type safe to transmit to a browser directly? @@ -42,6 +42,6 @@ * content type and wants it to be protected (typically by wrapping * the data in a ZIP archive). */ - boolean isSafeInline(final MimeType type); + boolean isSafeInline(MimeType type); }
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..c58d3fe 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,9 @@ 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.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -108,16 +111,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 +141,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(); @@ -165,7 +175,20 @@ return ref != null ? ref.getObjectId() : null; } - protected LoadHandle openHandle(Repository repo) throws IOException { + /** + * Open a handle for reading this entity from a repository. + * <p> + * Implementations may override this method to provide auto-rebuilding + * behavior. + * + * @param repo open repository. + * @return handle for reading the entity. + * + * @throws NoSuchChangeException change does not exist. + * @throws IOException a repo-level error occurred. + */ + protected LoadHandle openHandle(Repository repo) + throws NoSuchChangeException, IOException { return openHandle(repo, readRef(repo)); } @@ -173,7 +196,7 @@ return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id); } - public T reload() throws OrmException { + public T reload() throws NoSuchChangeException, OrmException { loaded = false; return load(); } @@ -206,7 +229,7 @@ /** Set up the metadata, parsing any state from the loaded revision. */ protected abstract void onLoad(LoadHandle handle) - throws IOException, ConfigInvalidException; + throws NoSuchChangeException, IOException, ConfigInvalidException; @SuppressWarnings("unchecked") protected final T self() {
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..a107bda 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
@@ -37,9 +37,9 @@ import com.google.common.collect.ImmutableSortedMap; 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.Multimap; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; @@ -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; @@ -619,7 +562,7 @@ // but easy to reason about. List<ChangeMessage> as = new LinkedList<>(bundleA.filterChangeMessages()); - Multimap<ChangeMessageCandidate, ChangeMessage> bs = + ListMultimap<ChangeMessageCandidate, ChangeMessage> bs = LinkedListMultimap.create(); for (ChangeMessage b : bundleB.filterChangeMessages()) { bs.put(ChangeMessageCandidate.create(b), b); @@ -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/notedb/ChangeBundleReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java new file mode 100644 index 0000000..9e7a1fe1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundleReader.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb; + +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.server.OrmException; + +public interface ChangeBundleReader { + ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java index 7b59a47..57d5dce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -15,13 +15,13 @@ package com.google.gerrit.server.notedb; import static com.google.common.base.MoreObjects.firstNonNull; -import static com.google.common.base.Preconditions.checkArgument; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import com.google.auto.value.AutoValue; import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; @@ -61,25 +61,34 @@ */ public class ChangeDraftUpdate extends AbstractChangeUpdate { public interface Factory { - ChangeDraftUpdate create(ChangeNotes notes, Account.Id accountId, - PersonIdent authorIdent, Date when); - ChangeDraftUpdate create(Change change, Account.Id accountId, - PersonIdent authorIdent, Date when); + ChangeDraftUpdate create( + ChangeNotes notes, + @Assisted("effective") Account.Id accountId, + @Assisted("real") Account.Id realAccountId, + PersonIdent authorIdent, + Date when); + + ChangeDraftUpdate create( + Change change, + @Assisted("effective") Account.Id accountId, + @Assisted("real") Account.Id realAccountId, + PersonIdent authorIdent, + Date when); } @AutoValue abstract static class Key { - abstract RevId revId(); - abstract PatchLineComment.Key key(); + abstract String revId(); + abstract Comment.Key key(); } - private static Key key(PatchLineComment c) { - return new AutoValue_ChangeDraftUpdate_Key(c.getRevId(), c.getKey()); + private static Key key(Comment c) { + return new AutoValue_ChangeDraftUpdate_Key(c.revId, c.key); } private final AllUsersName draftsProject; - private List<PatchLineComment> put = new ArrayList<>(); + private List<Comment> put = new ArrayList<>(); private Set<Key> delete = new HashSet<>(); @AssistedInject @@ -90,11 +99,12 @@ AllUsersName allUsers, ChangeNoteUtil noteUtil, @Assisted ChangeNotes notes, - @Assisted Account.Id accountId, + @Assisted("effective") Account.Id accountId, + @Assisted("real") Account.Id realAccountId, @Assisted PersonIdent authorIdent, @Assisted Date when) { super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null, - accountId, authorIdent, when); + accountId, realAccountId, authorIdent, when); this.draftsProject = allUsers; } @@ -106,51 +116,44 @@ AllUsersName allUsers, ChangeNoteUtil noteUtil, @Assisted Change change, - @Assisted Account.Id accountId, + @Assisted("effective") Account.Id accountId, + @Assisted("real") Account.Id realAccountId, @Assisted PersonIdent authorIdent, @Assisted Date when) { super(migration, noteUtil, serverIdent, anonymousCowardName, null, change, - accountId, authorIdent, when); + accountId, realAccountId, authorIdent, when); this.draftsProject = allUsers; } - public void putComment(PatchLineComment c) { + public void putComment(Comment c) { verifyComment(c); - checkArgument(c.getStatus() == PatchLineComment.Status.DRAFT, - "Cannot insert a published comment into a ChangeDraftUpdate"); put.add(c); } - public void deleteComment(PatchLineComment c) { + public void deleteComment(Comment c) { verifyComment(c); delete.add(key(c)); } - public void deleteComment(RevId revId, PatchLineComment.Key key) { + public void deleteComment(String revId, Comment.Key key) { delete.add(new AutoValue_ChangeDraftUpdate_Key(revId, key)); } - private void verifyComment(PatchLineComment comment) { - checkArgument(comment.getAuthor().equals(accountId), - "The author for the following comment does not match the author of" - + " this ChangeDraftUpdate (%s): %s", accountId, comment); - } - private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb) throws ConfigInvalidException, OrmException, IOException { - RevisionNoteMap rnm = getRevisionNoteMap(rw, curr); + RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr); Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size()); RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm); - for (PatchLineComment c : put) { + for (Comment c : put) { if (!delete.contains(key(c))) { - cache.get(c.getRevId()).putComment(c); + cache.get(new RevId(c.revId)).putComment(c); } } for (Key k : delete) { - cache.get(k.revId()).deleteComment(k.key()); + cache.get(new RevId(k.revId())).deleteComment(k.key()); } Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders(); @@ -159,7 +162,7 @@ for (Map.Entry<RevId, RevisionNoteBuilder> e : builders.entrySet()) { updatedRevs.add(e.getKey()); ObjectId id = ObjectId.fromString(e.getKey().get()); - byte[] data = e.getValue().build(noteUtil); + byte[] data = e.getValue().build(noteUtil, noteUtil.getWriteJson()); if (!Arrays.equals(data, e.getValue().baseRaw)) { touchedAnyRevs = true; } @@ -190,8 +193,8 @@ return cb; } - private RevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr) - throws ConfigInvalidException, OrmException, IOException { + private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, + ObjectId curr) throws ConfigInvalidException, OrmException, IOException { if (migration.readChanges()) { // If reading from changes is enabled, then the old DraftCommentNotes // already parsed the revision notes. We can reuse them as long as the ref @@ -203,7 +206,8 @@ if (draftNotes != null) { ObjectId idFromNotes = firstNonNull(draftNotes.getRevision(), ObjectId.zeroId()); - RevisionNoteMap rnm = draftNotes.getRevisionNoteMap(); + RevisionNoteMap<ChangeRevisionNote> rnm = + draftNotes.getRevisionNoteMap(); if (idFromNotes.equals(curr) && rnm != null) { return rnm; } @@ -219,7 +223,10 @@ // Even though reading from changes might not be enabled, we need to // parse any existing revision notes so we can merge them. return RevisionNoteMap.parse( - noteUtil, getId(), rw.getObjectReader(), noteMap, true); + noteUtil, getId(), + rw.getObjectReader(), + noteMap, + PatchLineComment.Status.DRAFT); } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java index 4c1a734..d47e462 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,30 +15,31 @@ 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; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; 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(ListMultimap<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..ee9f4f7 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,39 +19,41 @@ 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; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.metrics.Timer1; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.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.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; @@ -123,21 +118,27 @@ } public ChangeNotes createChecked(ReviewDb db, Change c) - throws OrmException, NoSuchChangeException { + throws OrmException { return createChecked(db, c.getProject(), c.getId()); } public ChangeNotes createChecked(ReviewDb db, Project.NameKey project, - Change.Id changeId) throws OrmException, NoSuchChangeException { - Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId); - if (change == null || !change.getProject().equals(project)) { + Change.Id changeId) throws OrmException { + Change change = readOneReviewDbChange(db, changeId); + if (change == null) { + if (!args.migration.readChanges()) { + throw new NoSuchChangeException(changeId); + } + // Change isn't in ReviewDb, but its primary storage might be in NoteDb. + // Prepopulate the change exists with proper noteDbState field. + change = newNoteDbOnlyChange(project, changeId); + } else if (!change.getProject().equals(project)) { throw new NoSuchChangeException(changeId); } return new ChangeNotes(args, change).load(); } - public ChangeNotes createChecked(Change.Id changeId) - throws OrmException, NoSuchChangeException { + public ChangeNotes createChecked(Change.Id changeId) throws OrmException { InternalChangeQuery query = queryProvider.get().noFields(); List<ChangeData> changes = query.byLegacyChangeId(changeId); if (changes.isEmpty()) { @@ -151,15 +152,33 @@ return changes.get(0).notes(); } + public static Change newNoteDbOnlyChange( + Project.NameKey project, Change.Id changeId) { + Change change = new Change( + null, changeId, null, + new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), + null); + change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + return change; + } + private Change loadChangeFromDb(ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException { - Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId); - checkNotNull(change, - "change %s not found in ReviewDb", changeId); - checkArgument(change.getProject().equals(project), - "passed project %s when creating ChangeNotes for %s, but actual" - + " project is %s", - project, changeId, change.getProject()); + checkArgument(project != null, "project is required"); + Change change = readOneReviewDbChange(db, changeId); + + if (change == null && args.migration.readChanges()) { + // Change isn't in ReviewDb, but its primary storage might be in NoteDb. + // Prepopulate the change exists with proper noteDbState field. + change = newNoteDbOnlyChange(project, changeId); + } else { + checkNotNull(change, "change %s not found in ReviewDb", changeId); + checkArgument(change.getProject().equals(project), + "passed project %s when creating ChangeNotes for %s, but actual" + + " project is %s", + project, changeId, change.getProject()); + } + // TODO: Throw NoSuchChangeException when the change is not found in the // database return change; @@ -174,7 +193,7 @@ public ChangeNotes createWithAutoRebuildingDisabled(ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException { return new ChangeNotes( - args, loadChangeFromDb(db, project, changeId), false, null).load(); + args, loadChangeFromDb(db, project, changeId), true, false, null).load(); } /** @@ -189,24 +208,14 @@ return new ChangeNotes(args, change); } - public ChangeNotes createForBatchUpdate(Change change) throws OrmException { - 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 createForBatchUpdate(Change change, boolean shouldExist) + throws OrmException { + return new ChangeNotes(args, change, shouldExist, false, null).load(); } public ChangeNotes createWithAutoRebuildingDisabled(Change change, RefCache refs) throws OrmException { - return new ChangeNotes(args, change, false, refs).load(); + return new ChangeNotes(args, change, true, false, refs).load(); } // TODO(ekempin): Remove when database backend is deleted @@ -243,12 +252,12 @@ public List<ChangeNotes> create(ReviewDb db, Project.NameKey project, Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate) - throws OrmException { + throws OrmException { List<ChangeNotes> notes = new ArrayList<>(); if (args.migration.enabled()) { for (Change.Id cid : changeIds) { ChangeNotes cn = create(db, project, cid); - if (cn.getChange() != null && predicate.apply(cn)) { + if (cn.getChange() != null && predicate.test(cn)) { notes.add(cn); } } @@ -258,7 +267,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 +277,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 +293,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,14 +328,18 @@ Project.NameKey project) throws OrmException, IOException { Set<Change.Id> ids = scan(repo); List<ChangeNotes> changeNotes = new ArrayList<>(ids.size()); - db = ReviewDbUtil.unwrapDb(db); + PrimaryStorage defaultStorage = args.migration.changePrimaryStorage(); 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", - id, project); - continue; + if (defaultStorage == PrimaryStorage.REVIEW_DB) { + log.warn("skipping change {} found in project {} " + + "but not in ReviewDb", + id, project); + continue; + } + // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext. + change = newNoteDbOnlyChange(project, id); } else if (!change.getProject().equals(project)) { log.error( "skipping change {} found in project {} " + @@ -354,6 +368,7 @@ } } + private final boolean shouldExist; private final RefCache refs; private Change change; @@ -361,20 +376,28 @@ // 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; + private ImmutableSet<Comment.Key> commentKeys; @VisibleForTesting public ChangeNotes(Args args, Change change) { - this(args, change, true, null); + this(args, change, true, true, null); } - private ChangeNotes(Args args, Change change, boolean autoRebuild, - @Nullable RefCache refs) { - super(args, change.getId(), autoRebuild); + private ChangeNotes(Args args, Change change, boolean shouldExist, + boolean autoRebuild, @Nullable RefCache refs) { + super(args, change.getId(), PrimaryStorage.of(change), autoRebuild); this.change = new Change(change); + this.shouldExist = shouldExist; this.refs = refs; } @@ -382,12 +405,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 +442,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,33 +487,42 @@ } /** @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 ImmutableSet<Comment.Key> getCommentKeys() { + if (commentKeys == null) { + ImmutableSet.Builder<Comment.Key> b = ImmutableSet.builder(); + for (Comment c : getComments().values()) { + b.add(new Comment.Key(c.key)); + } + commentKeys = b.build(); + } + return commentKeys; + } + + public ImmutableListMultimap<RevId, Comment> getDraftComments( Account.Id author) throws OrmException { - loadDraftComments(author); - final Multimap<RevId, PatchLineComment> 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( - 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())) { - return false; - } - } - return true; - } - }); + return getDraftComments(author, null); + } + + public ImmutableListMultimap<RevId, Comment> getDraftComments( + Account.Id author, @Nullable Ref ref) throws OrmException { + loadDraftComments(author, ref); + // Filter out any zombie draft comments. These are drafts that are also in + // the published map, and arise when the update to All-Users to delete them + // during the publish operation failed. return ImmutableListMultimap.copyOf( - filtered); + Multimaps.filterEntries( + draftCommentNotes.getComments(), + e -> !getCommentKeys().contains(e.getValue().key))); + } + + public ImmutableListMultimap<RevId, RobotComment> getRobotComments() + throws OrmException { + loadRobotComments(); + return robotCommentNotes.getComments(); } /** @@ -471,32 +531,44 @@ * comments have been loaded or if the caller would like the drafts for * another author. */ - private void loadDraftComments(Account.Id author) + private void loadDraftComments(Account.Id author, @Nullable Ref ref) throws OrmException { - if (draftCommentNotes == null || - !author.equals(draftCommentNotes.getAuthor())) { + if (draftCommentNotes == null + || !author.equals(draftCommentNotes.getAuthor()) + || ref != null) { draftCommentNotes = new DraftCommentNotes( - args, change, author, autoRebuild, rebuildResult); + args, change, author, autoRebuild, rebuildResult, ref); draftCommentNotes.load(); } } + 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(), null); 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,21 +576,26 @@ } @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()); } @Override protected void onLoad(LoadHandle handle) - throws IOException, ConfigInvalidException { + throws NoSuchChangeException, IOException, ConfigInvalidException { ObjectId rev = handle.id(); if (rev == null) { + if (args.migration.readChanges() + && PrimaryStorage.of(change) == PrimaryStorage.NOTE_DB + && shouldExist) { + throw new NoSuchChangeException(getChangeId()); + } loadDefaults(); return; } @@ -543,17 +620,22 @@ @Override protected ObjectId readRef(Repository repo) throws IOException { return refs != null - ? refs.get(getRefName()).orNull() + ? refs.get(getRefName()).orElse(null) : super.readRef(repo); } @Override - protected LoadHandle openHandle(Repository repo) throws IOException { + protected LoadHandle openHandle(Repository repo) + throws NoSuchChangeException, IOException { if (autoRebuild) { NoteDbChangeState state = NoteDbChangeState.parse(change); ObjectId id = readRef(repo); - if (state == null && id == null) { - return super.openHandle(repo, id); + if (id == null) { + if (state == null) { + return super.openHandle(repo, id); + } else if (shouldExist) { + throw new NoSuchChangeException(getChangeId()); + } } RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo); if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) { @@ -575,6 +657,7 @@ if (manager == null) { return super.openHandle(repo, oldId); // May be null in tests. } + manager.setRefLogMessage("Auto-rebuilding change"); r = manager.stageAndApplyDelta(change); try { rebuilder.execute(db, cid, manager);
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..07de733 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.ListMultimap; 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,19 +130,23 @@ 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 ListMultimap<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; + private final ListMultimap<PatchSet.Id, ChangeMessage> + changeMessagesByPatchSet; // Non-final private members filled in during the parsing process. 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 +156,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 +165,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 +196,7 @@ parseNotes(); allPastReviewers.addAll(reviewers.rowKeySet()); pruneReviewers(); + updatePatchSetStates(); checkMandatoryFooters(); } @@ -178,25 +204,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,16 +238,29 @@ comments); } - 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()); - } + 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 ListMultimap<PatchSet.Id, PatchSetApproval> buildApprovals() { + ListMultimap<PatchSet.Id, PatchSetApproval> result = + 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); } @@ -242,7 +284,7 @@ return Lists.reverse(allChangeMessages); } - private Multimap<PatchSet.Id, ChangeMessage> buildMessagesByPatchSet() { + private ListMultimap<PatchSet.Id, ChangeMessage> buildMessagesByPatchSet() { for (Collection<ChangeMessage> v : changeMessagesByPatchSet.asMap().values()) { Collections.reverse((List<ChangeMessage>) v); @@ -260,15 +302,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 +318,7 @@ if (accountId != null) { ownerId = accountId; } + Account.Id realAccountId = parseRealAccountId(commit, accountId); if (changeId == null) { changeId = parseChangeId(commit); @@ -296,12 +332,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 +349,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 +357,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 +378,8 @@ if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) { lastUpdatedOn = ts; } + + parseDescription(psId, commit); } private String parseSubmissionId(ChangeNotesCommit commit) @@ -357,6 +403,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 +496,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 +537,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 +582,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 +620,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 +703,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 +716,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 +789,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 +834,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 +868,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 +882,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 +933,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 +954,6 @@ break; case DELETED: - deleted = true; patchSets.remove(e.getKey()); break; @@ -825,35 +965,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 +1015,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..6c15c7e 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,28 +15,32 @@ 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.common.collect.ListMultimap; import com.google.gerrit.common.Nullable; 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.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,21 +90,24 @@ @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, + ListMultimap<PatchSet.Id, PatchSetApproval> approvals, ReviewerSet reviewers, List<Account.Id> allPastReviewers, List<ReviewerStatusUpdate> reviewerUpdates, List<SubmitRecord> submitRecords, List<ChangeMessage> allChangeMessages, - Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet, - Multimap<RevId, PatchLineComment> publishedComments) { + ListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet, + ListMultimap<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/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java deleted file mode 100644 index 679b5e2..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java +++ /dev/null
@@ -1,82 +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.notedb; - -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.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; - -public abstract class ChangeRebuilder { - public static class NoPatchSetsException extends OrmException { - private static final long serialVersionUID = 1L; - - NoPatchSetsException(Change.Id changeId) { - super("Change " + changeId - + " cannot be rebuilt because it has no patch sets"); - } - } - - private final SchemaFactory<ReviewDb> schemaFactory; - - protected ChangeRebuilder(SchemaFactory<ReviewDb> schemaFactory) { - this.schemaFactory = schemaFactory; - } - - public final ListenableFuture<Result> rebuildAsync( - final Change.Id id, ListeningExecutorService executor) { - return executor.submit(new Callable<Result>() { - @Override - public Result call() throws Exception { - try (ReviewDb db = schemaFactory.open()) { - return rebuild(db, id); - } - } - }); - } - - public abstract Result rebuild(ReviewDb db, Change.Id changeId) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException; - - public abstract Result rebuild(NoteDbUpdateManager manager, - ChangeBundle bundle) throws NoSuchChangeException, IOException, - OrmException, ConfigInvalidException; - - public abstract boolean rebuildProject(ReviewDb db, - ImmutableMultimap<Project.NameKey, Change.Id> allChanges, - Project.NameKey project, Repository allUsersRepo) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException; - - public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) - throws NoSuchChangeException, IOException, OrmException; - - public abstract Result execute(ReviewDb db, Change.Id changeId, - NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException, - IOException; -}
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/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java index 802359c..3d2d4fd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableSet; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -49,16 +50,23 @@ } private static final String NOTE_DB = "noteDb"; + + // All of these names must be reflected in the allowed set in checkConfig. + private static final String PRIMARY_STORAGE = "primaryStorage"; private static final String READ = "read"; - private static final String WRITE = "write"; private static final String SEQUENCE = "sequence"; + private static final String WRITE = "write"; private static void checkConfig(Config cfg) { Set<String> keys = new HashSet<>(); for (NoteDbTable t : NoteDbTable.values()) { - keys.add(t.key()); + keys.add(t.key().toLowerCase()); } - Set<String> allowed = ImmutableSet.of(READ, WRITE, SEQUENCE); + Set<String> allowed = ImmutableSet.of( + PRIMARY_STORAGE.toLowerCase(), + READ.toLowerCase(), + WRITE.toLowerCase(), + SEQUENCE.toLowerCase()); for (String t : cfg.getSubsections(NOTE_DB)) { checkArgument(keys.contains(t.toLowerCase()), "invalid NoteDb table: %s", t); @@ -81,6 +89,7 @@ private final boolean writeChanges; private final boolean readChanges; private final boolean readChangeSequence; + private final PrimaryStorage changePrimaryStorage; private final boolean writeAccounts; private final boolean readAccounts; @@ -98,6 +107,9 @@ // NoteDb. This decision for the default may be reevaluated later. readChangeSequence = cfg.getBoolean(NOTE_DB, CHANGES.key(), SEQUENCE, false); + changePrimaryStorage = cfg.getEnum( + NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB); + writeAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), WRITE, false); readAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), READ, false); } @@ -118,6 +130,11 @@ } @Override + public PrimaryStorage changePrimaryStorage() { + return changePrimaryStorage; + } + + @Override public boolean writeAccounts() { return writeAccounts; }
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..0408ffa 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
@@ -14,23 +14,28 @@ package com.google.gerrit.server.notedb; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; 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.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.common.Nullable; 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; @@ -39,6 +44,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.RevCommit; @@ -66,16 +72,17 @@ private final Change change; private final Account.Id author; private final NoteDbUpdateManager.Result rebuildResult; + private final Ref ref; - private ImmutableListMultimap<RevId, PatchLineComment> comments; - private RevisionNoteMap revisionNoteMap; + private ImmutableListMultimap<RevId, Comment> comments; + private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap; @AssistedInject DraftCommentNotes( Args args, @Assisted Change change, @Assisted Account.Id author) { - this(args, change, author, true, null); + this(args, change, author, true, null, null); } @AssistedInject @@ -83,10 +90,13 @@ 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; + this.ref = null; } DraftCommentNotes( @@ -94,14 +104,22 @@ Change change, Account.Id author, boolean autoRebuild, - NoteDbUpdateManager.Result rebuildResult) { - super(args, change.getId(), autoRebuild); + @Nullable NoteDbUpdateManager.Result rebuildResult, + @Nullable Ref ref) { + super(args, change.getId(), PrimaryStorage.of(change), autoRebuild); this.change = change; this.author = author; this.rebuildResult = rebuildResult; + this.ref = ref; + if (ref != null) { + checkArgument( + ref.getName().equals(getRefName()), + "draft ref not for change %s and account %s: %s", + getChangeId(), author, ref.getName()); + } } - RevisionNoteMap getRevisionNoteMap() { + RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap() { return revisionNoteMap; } @@ -109,13 +127,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; } } @@ -124,7 +142,15 @@ @Override protected String getRefName() { - return RefNames.refsDraftComments(getChangeId(), author); + return refsDraftComments(getChangeId(), author); + } + + @Override + protected ObjectId readRef(Repository repo) throws IOException { + if (ref != null) { + return ref.getObjectId(); + } + return super.readRef(repo); } @Override @@ -140,11 +166,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); + ListMultimap<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); @@ -161,7 +188,8 @@ } @Override - protected LoadHandle openHandle(Repository repo) throws IOException { + protected LoadHandle openHandle(Repository repo) + throws NoSuchChangeException, IOException { if (rebuildResult != null) { StagedResult sr = checkNotNull(rebuildResult.staged()); return LoadHandle.create( @@ -189,7 +217,8 @@ return null; } - private LoadHandle rebuildAndOpen(Repository repo) throws IOException { + private LoadHandle rebuildAndOpen(Repository repo) + throws NoSuchChangeException, IOException { Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES); try { Change.Id cid = getChangeId();
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..d488fcb 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,30 +16,38 @@ 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 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.common.primitives.Longs; import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.TimeUtil; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.server.ReviewDbUtil; import com.google.gerrit.server.git.RefCache; +import com.google.gwtorm.server.OrmRuntimeException; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import java.io.IOException; -import java.util.Collections; +import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; /** * The state of all relevant NoteDb refs across all repos corresponding to a @@ -48,16 +56,44 @@ * 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>R=[read-only-until],[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... + * <li>N + * <li>N=[read-only-until] + * </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, + @VisibleForTesting + public static Delta create(Change.Id changeId, + Optional<ObjectId> newChangeMetaId, Map<Account.Id, ObjectId> newDraftIds) { if (newDraftIds == null) { newDraftIds = ImmutableMap.of(); @@ -73,33 +109,124 @@ 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 -> !ObjectId.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))); + String first = parts.get(0); + Optional<Timestamp> readOnlyUntil = parseReadOnlyUntil(id, str, first); + + // Only valid NOTE_DB state is "N". + if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) { + return new NoteDbChangeState( + id, NOTE_DB, Optional.empty(), readOnlyUntil); } - 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, readOnlyUntil); + } + throw invalidState(id, str); } + private static Optional<Timestamp> parseReadOnlyUntil(Change.Id id, + String fullStr, String first) { + if (first.length() > 2 && first.charAt(1) == '=') { + Long ts = Longs.tryParse(first.substring(2)); + if (ts == null) { + throw invalidState(id, fullStr); + } + return Optional.of(new Timestamp(ts)); + } + return Optional.empty(); + } + + private static IllegalArgumentException invalidState(Change.Id id, + String str) { + return new IllegalArgumentException( + "invalid state string for change " + id + ": " + str); + } + + /** + * Apply a delta to the state stored in a change entity. + * <p> + * This method does not check whether the old state was read-only; it is up to + * the caller to not violate read-only semantics when storing the change back + * in ReviewDb. + * + * @param change change entity. The delta is applied against this entity's + * {@code noteDbState} and the new state is stored back in the entity as a + * side effect. + * @param delta delta to apply. + * @return new state, equivalent to what is stored in {@code change} as a side + * effect. + */ public static NoteDbChangeState applyDelta(Change change, Delta delta) { if (delta == null) { return null; @@ -112,6 +239,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 +252,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 +268,30 @@ } NoteDbChangeState state = new NoteDbChangeState( - change.getId(), changeMetaId, draftIds); + change.getId(), + oldState != null + ? oldState.getPrimaryStorage() + : REVIEW_DB, + Optional.of(RefState.create(changeMetaId, draftIds)), + // Copy old read-only deadline rather than advancing it; the caller is + // still responsible for finishing the rest of its work before the lease + // runs out. + oldState != null ? oldState.getReadOnlyUntil() : Optional.empty()); change.setNoteDbState(state.toString()); return state; } + // TODO(dborowitz): Ugly. Refactor these static methods into a Checker class + // or something. They do not belong in NoteDbChangeState itself because: + // - need to inject Config but don't want a whole Factory + // - can't be methods on NoteDbChangeState because state is nullable (though + // we could also solve this by inventing an empty-but-non-null state) + // Also we should clean up duplicated code between static/non-static methods. 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 +301,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 +311,103 @@ 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()); + public static long getReadOnlySkew(Config cfg) { + return cfg.getTimeUnit( + "notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS); + } + + private static Timestamp timeForReadOnlyCheck(long skewMs) { + // Subtract some slop in case the machine that set the change's read-only + // lease has a clock behind ours. + return new Timestamp(TimeUtil.nowMs() - skewMs); + } + + public static void checkNotReadOnly(@Nullable Change change, long skewMs) { + checkNotReadOnly(parse(change), skewMs); + } + + public static void checkNotReadOnly(@Nullable NoteDbChangeState state, + long skewMs) { + if (state == null) { + return; // No state means ReviewDb primary non-read-only. + } else if (state.isReadOnly(timeForReadOnlyCheck(skewMs))) { + throw new OrmRuntimeException( + "change " + state.getChangeId() + " is read-only until " + + state.getReadOnlyUntil().get()); } - return sb.toString(); } private final Change.Id changeId; - private final ObjectId changeMetaId; - private final ImmutableMap<Account.Id, ObjectId> draftIds; + private final PrimaryStorage primaryStorage; + private final Optional<RefState> refState; + private final Optional<Timestamp> readOnlyUntil; - public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId, - Map<Account.Id, ObjectId> draftIds) { + public NoteDbChangeState( + Change.Id changeId, + PrimaryStorage primaryStorage, + Optional<RefState> refState, + Optional<Timestamp> readOnlyUntil) { this.changeId = checkNotNull(changeId); - this.changeMetaId = checkNotNull(changeMetaId); - this.draftIds = ImmutableMap.copyOf(Maps.filterValues( - draftIds, Predicates.not(Predicates.equalTo(ObjectId.zeroId())))); + this.primaryStorage = checkNotNull(primaryStorage); + this.refState = checkNotNull(refState); + this.readOnlyUntil = checkNotNull(readOnlyUntil); + + 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); + } + } + + 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; } @@ -217,23 +415,76 @@ return true; } - @VisibleForTesting - Change.Id getChangeId() { + public boolean isReadOnly(Timestamp now) { + return readOnlyUntil.isPresent() && now.before(readOnlyUntil.get()); + } + + public Optional<Timestamp> getReadOnlyUntil() { + return readOnlyUntil; + } + + public NoteDbChangeState withReadOnlyUntil(Timestamp ts) { + return new NoteDbChangeState( + changeId, primaryStorage, refState, Optional.of(ts)); + } + + public Change.Id getChangeId() { return changeId; } - @VisibleForTesting public ObjectId getChangeMetaId() { - return changeMetaId; + return refState().changeMetaId(); } - @VisibleForTesting - ImmutableMap<Account.Id, ObjectId> getDraftIds() { - return draftIds; + public ImmutableMap<Account.Id, ObjectId> getDraftIds() { + return refState().draftIds(); + } + + public 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: + if (!readOnlyUntil.isPresent()) { + // Don't include enum field, just IDs (though parse would accept it). + return refState().toString(); + } + return primaryStorage.code + "=" + readOnlyUntil.get().getTime() + + "," + refState.get(); + case NOTE_DB: + if (!readOnlyUntil.isPresent()) { + return NOTE_DB_PRIMARY_STATE; + } + return primaryStorage.code + "=" + readOnlyUntil.get().getTime(); + default: + throw new IllegalArgumentException( + "Unsupported PrimaryStorage: " + primaryStorage); + } + } + + @Override + public int hashCode() { + return Objects.hash(changeId, primaryStorage, refState, readOnlyUntil); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NoteDbChangeState)) { + return false; + } + NoteDbChangeState s = (NoteDbChangeState) o; + return changeId.equals(s.changeId) + && primaryStorage.equals(s.primaryStorage) + && refState.equals(s.refState) + && readOnlyUntil.equals(s.readOnlyUntil); } }
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..7934884 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,17 @@ 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 +50,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()); @@ -69,16 +70,14 @@ } @Override - public Result rebuild(NoteDbUpdateManager manager, - ChangeBundle bundle) { + public Result rebuildEvenIfReadOnly(ReviewDb db, Id changeId) { return null; } @Override - public boolean rebuildProject(ReviewDb db, - ImmutableMultimap<NameKey, Id> allChanges, NameKey project, - Repository allUsersRepo) { - return false; + public Result rebuild(NoteDbUpdateManager manager, + ChangeBundle bundle) { + return null; } @Override @@ -91,6 +90,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..b5adea6 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; @@ -33,13 +33,16 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.ChainedReceiveCommands; 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.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; @@ -48,6 +51,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; @@ -58,6 +62,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -70,7 +75,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 +83,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 +125,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 +149,7 @@ this.close = close; } - Optional<ObjectId> getObjectId(String refName) throws IOException { + public Optional<ObjectId> getObjectId(String refName) throws IOException { return cmds.get(refName); } @@ -172,6 +178,7 @@ } } + private final Provider<PersonIdent> serverIdent; private final GitRepositoryManager repoManager; private final NotesMigration migration; private final AllUsersName allUsersName; @@ -179,26 +186,32 @@ 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; private OpenRepo allUsersRepo; private Map<Change.Id, StagedResult> staged; private boolean checkExpectedState = true; + private String refLogMessage; + private PersonIdent refLogIdent; @AssistedInject - NoteDbUpdateManager(GitRepositoryManager repoManager, + NoteDbUpdateManager(@GerritPersonIdent Provider<PersonIdent> serverIdent, + GitRepositoryManager repoManager, NotesMigration migration, AllUsersName allUsersName, NoteDbMetrics metrics, @Assisted Project.NameKey projectName) { + this.serverIdent = serverIdent; this.repoManager = repoManager; this.migration = migration; 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 +246,27 @@ return this; } - NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) { + public NoteDbUpdateManager setCheckExpectedState(boolean checkExpectedState) { this.checkExpectedState = checkExpectedState; return this; } - OpenRepo getChangeRepo() throws IOException { + public NoteDbUpdateManager setRefLogMessage(String message) { + this.refLogMessage = message; + return this; + } + + public NoteDbUpdateManager setRefLogIdent(PersonIdent ident) { + this.refLogIdent = ident; + return this; + } + + public OpenRepo getChangeRepo() throws IOException { initChangeRepo(); return changeRepo; } - OpenRepo getAllUsersRepo() throws IOException { + public OpenRepo getAllUsersRepo() throws IOException { initAllUsersRepo(); return allUsersRepo; } @@ -273,6 +296,7 @@ } return changeUpdates.isEmpty() && draftUpdates.isEmpty() + && robotCommentUpdates.isEmpty() && toDelete.isEmpty(); } @@ -294,6 +318,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 +382,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); @@ -425,12 +453,15 @@ } } - private static void execute(OpenRepo or) throws IOException { + private void execute(OpenRepo or) throws IOException { if (or == null || or.cmds.isEmpty()) { return; } or.flush(); BatchRefUpdate bru = or.repo.getRefDatabase().newBatchUpdate(); + bru.setRefLogMessage( + firstNonNull(refLogMessage, "Update NoteDb refs"), false); + bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get()); or.cmds.addTo(bru); bru.setAllowNonFastForwards(true); bru.execute(or.rw, NullProgressMonitor.INSTANCE); @@ -453,6 +484,9 @@ if (!draftUpdates.isEmpty()) { addUpdates(draftUpdates, allUsersRepo); } + if (!robotCommentUpdates.isEmpty()) { + addUpdates(robotCommentUpdates, changeRepo); + } for (Change.Id id : toDelete) { doDelete(id); } @@ -478,6 +512,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 +547,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 +565,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 +589,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/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java index 56b41d9..6afe87d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.notedb; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; + /** * Holds the current state of the NoteDb migration. * <p> @@ -71,6 +73,9 @@ */ public abstract boolean readChangeSequence(); + /** @return default primary storage for new changes. */ + public abstract PrimaryStorage changePrimaryStorage(); + public abstract boolean readAccounts(); public abstract boolean writeAccounts();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java new file mode 100644 index 0000000..0a5c014 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
@@ -0,0 +1,318 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.RepoRefCache; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; +import com.google.gwtorm.server.AtomicUpdate; +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; + +import com.github.rholder.retry.RetryException; +import com.github.rholder.retry.Retryer; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; +import com.github.rholder.retry.WaitStrategies; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.Timestamp; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Helper to migrate the {@link PrimaryStorage} of individual changes. */ +@Singleton +public class PrimaryStorageMigrator { + private static final Logger log = + LoggerFactory.getLogger(PrimaryStorageMigrator.class); + + private final Provider<ReviewDb> db; + private final GitRepositoryManager repoManager; + private final AllUsersName allUsers; + private final ChangeRebuilder rebuilder; + + private final long skewMs; + private final long timeoutMs; + private final Retryer<NoteDbChangeState> testEnsureRebuiltRetryer; + + @Inject + PrimaryStorageMigrator(@GerritServerConfig Config cfg, + Provider<ReviewDb> db, + GitRepositoryManager repoManager, + AllUsersName allUsers, + ChangeRebuilder rebuilder) { + this(cfg, db, repoManager, allUsers, rebuilder, null); + } + + @VisibleForTesting + public PrimaryStorageMigrator(Config cfg, + Provider<ReviewDb> db, + GitRepositoryManager repoManager, + AllUsersName allUsers, + ChangeRebuilder rebuilder, + @Nullable Retryer<NoteDbChangeState> testEnsureRebuiltRetryer) { + this.db = db; + this.repoManager = repoManager; + this.allUsers = allUsers; + this.rebuilder = rebuilder; + this.testEnsureRebuiltRetryer = testEnsureRebuiltRetryer; + skewMs = NoteDbChangeState.getReadOnlySkew(cfg); + + String s = "notedb"; + timeoutMs = cfg.getTimeUnit( + s, null, "primaryStorageMigrationTimeout", + MILLISECONDS.convert(60, SECONDS), MILLISECONDS); + } + + /** + * Migrate a change's primary storage from ReviewDb to NoteDb. + * <p> + * This method will return only if the primary storage of the change is NoteDb + * afterwards. (It may return early if the primary storage was already + * NoteDb.) + * <p> + * If this method throws an exception, then the primary storage of the change + * is probably not NoteDb. (It is possible that the primary storage of the + * change is NoteDb in this case, but there was an error reading the state.) + * Moreover, after an exception, the change may be read-only until a lease + * expires. If the caller chooses to retry, they should wait until the + * read-only lease expires; this method will fail relatively quickly if called + * on a read-only change. + * <p> + * Note that if the change is read-only after this method throws an exception, + * that does not necessarily guarantee that the read-only lease was acquired + * during that particular method invocation; this call may have in fact failed + * because another thread acquired the lease first. + * + * @param id change ID. + * + * @throws OrmException if a ReviewDb-level error occurs. + * @throws IOException if a repo-level error occurs. + */ + public void migrateToNoteDbPrimary(Change.Id id) + throws OrmException, IOException { + // Since there are multiple non-atomic steps in this method, we need to + // consider what happens when there is another writer concurrent with the + // thread executing this method. + // + // Let: + // * OR = other writer writes noteDbState & new data to ReviewDb (in one + // transaction) + // * ON = other writer writes to NoteDb + // * MRO = migrator sets state to read-only + // * MR = ensureRebuilt writes rebuilt noteDbState to ReviewDb (but does not + // otherwise update ReviewDb in this transaction) + // * MN = ensureRebuilt writes rebuilt state to NoteDb + // + // Consider all the interleavings of these operations. + // + // * OR,ON,MRO,... + // Other writer completes before migrator begins; this is not a concurrent + // write. + // * MRO,...,OR,... + // OR will fail, since it atomically checks that the noteDbState is not + // read-only before proceeding. This results in an exception, but not a + // concurrent write. + // + // Thus all the "interesting" interleavings start with OR,MRO, and differ on + // where ON falls relative to MR/MN. + // + // * OR,MRO,ON,MR,MN + // The other NoteDb write succeeds despite the noteDbState being + // read-only. Because the read-only state from MRO includes the update + // from OR, the change is up-to-date at this point. Thus MR,MN is a no-op. + // The end result is an up-to-date, read-only change. + // + // * OR,MRO,MR,ON,MN + // The change is out-of-date when ensureRebuilt begins, because OR + // succeeded but the corresponding ON has not happened yet. ON will + // succeed, because there have been no intervening NoteDb writes. MN will + // fail, because ON updated the state in NoteDb to something other than + // what MR claimed. This leaves the change in an out-of-date, read-only + // state. + // + // If this method threw an exception in this case, the change would + // eventually switch back to read-write when the read-only lease expires, + // so this situation is recoverable. However, it would be inconvenient for + // a change to be read-only for so long. + // + // Thus, as an optimization, we have a retry loop that attempts + // ensureRebuilt while still holding the same read-only lease. This + // effectively results in the interleaving OR,MR,ON,MR,MN; in contrast + // with the previous case, here, MR/MN actually rebuilds the change. In + // the case of a write failure, MR/MN might fail and get retried again. If + // it exceeds the maximum number of retries, an exception is thrown. + // + // * OR,MRO,MR,MN,ON + // The change is out-of-date when ensureRebuilt begins. The change is + // rebuilt, leaving a new state in NoteDb. ON will fail, because the old + // NoteDb state has changed since the ref state was read when the update + // began (prior to OR). This results in an exception from ON, but the end + // result is still an up-to-date, read-only change. The end user that + // initiated the other write observes an error, but this is no different + // from other errors that need retrying, e.g. due to a backend write + // failure. + + Stopwatch sw = Stopwatch.createStarted(); + Change readOnlyChange = setReadOnly(id); // MRO + if (readOnlyChange == null) { + return; // Already migrated. + } + + NoteDbChangeState rebuiltState; + try { + // MR,MN + rebuiltState = ensureRebuiltRetryer(sw).call( + () -> ensureRebuilt( + readOnlyChange.getProject(), id, + NoteDbChangeState.parse(readOnlyChange))); + } catch (RetryException | ExecutionException e) { + throw new OrmException(e); + } + + // At this point, the noteDbState in ReviewDb is read-only, and it is + // guaranteed to match the state actually in NoteDb. Now it is safe to set + // the primary storage to NoteDb. + + setPrimaryStorageNoteDb(id, rebuiltState); + log.info("Migrated change {} to NoteDb primary in {}ms", id, + sw.elapsed(MILLISECONDS)); + } + + private Change setReadOnly(Change.Id id) throws OrmException { + AtomicBoolean alreadyMigrated = new AtomicBoolean(false); + Change result = db().changes().atomicUpdate(id, new AtomicUpdate<Change>() { + @Override + public Change update(Change change) { + NoteDbChangeState state = NoteDbChangeState.parse(change); + if (state == null) { + // Could rebuild the change here, but that's more complexity, and this + // really shouldn't happen. + throw new OrmRuntimeException( + "change " + id + " has no note_db_state; rebuild it first"); + } + // If the change is already read-only, then the lease is held by another + // (likely failed) migrator thread. Fail early, as we can't take over + // the lease. + NoteDbChangeState.checkNotReadOnly(change, skewMs); + if (state.getPrimaryStorage() != PrimaryStorage.NOTE_DB) { + Timestamp now = TimeUtil.nowTs(); + Timestamp until = new Timestamp(now.getTime() + timeoutMs); + change.setNoteDbState(state.withReadOnlyUntil(until).toString()); + } else { + alreadyMigrated.set(true); + } + return change; + } + }); + return alreadyMigrated.get() ? null : result; + } + + private Retryer<NoteDbChangeState> ensureRebuiltRetryer(Stopwatch sw) { + if (testEnsureRebuiltRetryer != null) { + return testEnsureRebuiltRetryer; + } + // Retry the ensureRebuilt step with backoff until half the timeout has + // expired, leaving the remaining half for the rest of the steps. + long remainingNanos = + (MILLISECONDS.toNanos(timeoutMs) / 2) - sw.elapsed(NANOSECONDS); + remainingNanos = Math.max(remainingNanos, 0); + return RetryerBuilder.<NoteDbChangeState>newBuilder() + .retryIfException( + e -> (e instanceof IOException) || (e instanceof OrmException)) + .withWaitStrategy( + WaitStrategies.join( + WaitStrategies.exponentialWait(250, MILLISECONDS), + WaitStrategies.randomWait(50, MILLISECONDS))) + .withStopStrategy( + StopStrategies.stopAfterDelay(remainingNanos, NANOSECONDS)) + .build(); + } + + private NoteDbChangeState ensureRebuilt(Project.NameKey project, Change.Id id, + NoteDbChangeState readOnlyState) + throws IOException, OrmException, RepositoryNotFoundException { + try (Repository changeRepo = repoManager.openRepository(project); + Repository allUsersRepo = repoManager.openRepository(allUsers)) { + if (!readOnlyState.isUpToDate( + new RepoRefCache(changeRepo), new RepoRefCache(allUsersRepo))) { + NoteDbUpdateManager.Result r = + rebuilder.rebuildEvenIfReadOnly(db(), id); + checkState( + r.newState().getReadOnlyUntil() + .equals(readOnlyState.getReadOnlyUntil()), + "state after rebuilding has different read-only lease: %s != %s", + r.newState(), readOnlyState); + readOnlyState = r.newState(); + } + } + return readOnlyState; + } + + private void setPrimaryStorageNoteDb(Change.Id id, + NoteDbChangeState expectedState) throws OrmException { + db().changes().atomicUpdate(id, new AtomicUpdate<Change>() { + @Override + public Change update(Change change) { + NoteDbChangeState state = NoteDbChangeState.parse(change); + if (!Objects.equals(state, expectedState)) { + throw new OrmRuntimeException(badState(state, expectedState)); + } + Timestamp until = state.getReadOnlyUntil().get(); + if (TimeUtil.nowTs().after(until)) { + throw new OrmRuntimeException( + "read-only lease on change " + id + " expired at " + until); + } + change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); + return change; + } + }); + } + + private ReviewDb db() { + return ReviewDbUtil.unwrapDb(db.get()); + } + + private String badState(NoteDbChangeState actual, + NoteDbChangeState expected) { + return "state changed unexpectedly: " + actual + " != " + expected; + } +}
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..46a0802 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.ListMultimap; 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 ListMultimap<Integer, Comment> buildCommentMap() { + ListMultimap<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 { + ListMultimap<Integer, Comment> comments = buildCommentMap(); + if (comments.isEmpty() && pushCert == null) { + return; + } + + RevisionNoteData data = new RevisionNoteData(); + data.comments = COMMENT_ORDER.sortedCopy(comments.values()); + data.pushCert = pushCert; + + try (OutputStreamWriter osw = new OutputStreamWriter(out, UTF_8)) { + noteUtil.getGson().toJson(data, osw); + } + } + + private void buildNoteLegacy(ChangeNoteUtil noteUtil, OutputStream out) + throws IOException { if (pushCert != null) { byte[] certBytes = pushCert.getBytes(UTF_8); out.write(certBytes, 0, trimTrailingNewlines(certBytes)); out.write('\n'); } - - Multimap<PatchSet.Id, PatchLineComment> all = ArrayListMultimap.create(); - for (PatchLineComment c : baseComments) { - if (!delete.contains(c.getKey()) && !put.containsKey(c.getKey())) { - all.put(c.getPatchSetId(), c); - } - } - for (PatchLineComment c : put.values()) { - if (!delete.contains(c.getKey())) { - all.put(c.getPatchSetId(), c); - } - } - noteUtil.buildNote(all, out); - return out.toByteArray(); + noteUtil.buildNote(buildCommentMap(), out); } private static int trimTrailingNewlines(byte[] bytes) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java new file mode 100644 index 0000000..e0ee934 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteData.java
@@ -0,0 +1,28 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb; + +import com.google.gerrit.reviewdb.client.Comment; + +import java.util.List; + +/** + * Holds the raw data of a RevisionNote. + * <p>It is intended for (de)serialization to JSON only. + */ +class RevisionNoteData { + String pushCert; + List<Comment> comments; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java index cd70528..8a9f711 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -16,6 +16,8 @@ import com.google.common.collect.ImmutableMap; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.RevId; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -27,29 +29,45 @@ import java.util.HashMap; import java.util.Map; -class RevisionNoteMap { +class RevisionNoteMap<T extends RevisionNote<? extends Comment>> { final NoteMap noteMap; - final ImmutableMap<RevId, RevisionNote> revisionNotes; + final ImmutableMap<RevId, T> revisionNotes; - static RevisionNoteMap parse(ChangeNoteUtil noteUtil, + static RevisionNoteMap<ChangeRevisionNote> parse(ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap, - boolean draftsOnly) throws ConfigInvalidException, IOException { - Map<RevId, RevisionNote> result = new HashMap<>(); + PatchLineComment.Status status) + throws ConfigInvalidException, IOException { + Map<RevId, ChangeRevisionNote> result = new HashMap<>(); for (Note note : noteMap) { - RevisionNote rn = new RevisionNote( - noteUtil, changeId, reader, note.getData(), draftsOnly); + ChangeRevisionNote rn = new ChangeRevisionNote( + noteUtil, changeId, reader, note.getData(), status); + rn.parse(); result.put(new RevId(note.name()), rn); } - return new RevisionNoteMap(noteMap, ImmutableMap.copyOf(result)); + return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result)); } - static RevisionNoteMap emptyMap() { - return new RevisionNoteMap(NoteMap.newEmptyMap(), - ImmutableMap.<RevId, RevisionNote> of()); + static RevisionNoteMap<RobotCommentsRevisionNote> parseRobotComments( + ChangeNoteUtil noteUtil, ObjectReader reader, NoteMap noteMap) + throws ConfigInvalidException, IOException { + Map<RevId, RobotCommentsRevisionNote> result = new HashMap<>(); + for (Note note : noteMap) { + RobotCommentsRevisionNote rn = new RobotCommentsRevisionNote( + noteUtil, reader, note.getData()); + rn.parse(); + result.put(new RevId(note.name()), rn); + } + return new RevisionNoteMap<>(noteMap, ImmutableMap.copyOf(result)); + } + + static <T extends RevisionNote<? extends Comment>> RevisionNoteMap<T> + emptyMap() { + return new RevisionNoteMap<>(NoteMap.newEmptyMap(), + ImmutableMap.<RevId, T> of()); } private RevisionNoteMap(NoteMap noteMap, - ImmutableMap<RevId, RevisionNote> revisionNotes) { + ImmutableMap<RevId, T> revisionNotes) { this.noteMap = noteMap; this.revisionNotes = revisionNotes; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java new file mode 100644 index 0000000..ae2edd6 --- /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.ListMultimap; +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)); + ListMultimap<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/notedb/RobotCommentsRevisionNoteData.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java new file mode 100644 index 0000000..ea3a149 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentsRevisionNoteData.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb; + +import com.google.gerrit.reviewdb.client.RobotComment; + +import java.util.List; + +public class RobotCommentsRevisionNoteData { + List<RobotComment> comments; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java index c0bb8ab..ffe0fcb 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,16 @@ 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.project.NoSuchChangeException; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl; 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,12 +54,24 @@ @Override public Result rebuild(ReviewDb db, Change.Id changeId) - throws NoSuchChangeException, IOException, OrmException, - ConfigInvalidException { + throws IOException, OrmException { + return rebuild(db, changeId, true); + } + + @Override + public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId) + throws IOException, OrmException { + return rebuild(db, changeId, false); + } + + private Result rebuild(ReviewDb db, Change.Id changeId, + boolean checkReadOnly) throws IOException, OrmException { if (failNextUpdate.getAndSet(false)) { throw new IOException("Update failed"); } - Result result = delegate.rebuild(db, changeId); + Result result = checkReadOnly + ? delegate.rebuild(db, changeId) + : delegate.rebuildEvenIfReadOnly(db, changeId); if (stealNextUpdate.getAndSet(false)) { throw new IOException("Update stolen"); } @@ -72,8 +80,7 @@ @Override public Result rebuild(NoteDbUpdateManager manager, - ChangeBundle bundle) throws NoSuchChangeException, IOException, - OrmException, ConfigInvalidException { + ChangeBundle bundle) throws IOException, 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,33 +88,15 @@ } @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 { + throws IOException, OrmException { // Don't inspect stealNextUpdate; that happens in execute() below. return delegate.stage(db, changeId); } @Override public Result execute(ReviewDb db, Change.Id changeId, - NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException, - IOException { + NoteDbUpdateManager manager) throws OrmException, IOException { if (failNextUpdate.getAndSet(false)) { throw new IOException("Update failed"); } @@ -117,4 +106,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/notedb/rebuild/AbortUpdateException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java new file mode 100644 index 0000000..0e6d3e9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/AbortUpdateException.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.notedb.rebuild; + +import com.google.gwtorm.server.OrmRuntimeException; + +class AbortUpdateException extends OrmRuntimeException { + private static final long serialVersionUID = 1L; + + AbortUpdateException() { + super("aborted"); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java new file mode 100644 index 0000000..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/rebuild/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java new file mode 100644 index 0000000..ea456da --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.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.server.notedb.rebuild; + +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.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.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; + +import java.io.IOException; +import java.util.concurrent.Callable; + +public abstract class ChangeRebuilder { + public static class NoPatchSetsException extends OrmException { + private static final long serialVersionUID = 1L; + + NoPatchSetsException(Change.Id changeId) { + super("Change " + changeId + + " cannot be rebuilt because it has no patch sets"); + } + } + + private final SchemaFactory<ReviewDb> schemaFactory; + + protected ChangeRebuilder(SchemaFactory<ReviewDb> schemaFactory) { + this.schemaFactory = schemaFactory; + } + + public final ListenableFuture<Result> rebuildAsync( + final Change.Id id, ListeningExecutorService executor) { + return executor.submit(new Callable<Result>() { + @Override + public Result call() throws Exception { + try (ReviewDb db = schemaFactory.open()) { + return rebuild(db, id); + } + } + }); + } + + public abstract Result rebuild(ReviewDb db, Change.Id changeId) + throws IOException, OrmException; + + public abstract Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId) + throws IOException, OrmException; + + public abstract Result rebuild(NoteDbUpdateManager manager, + ChangeBundle bundle) throws IOException, OrmException; + + public abstract void buildUpdates(NoteDbUpdateManager manager, + ChangeBundle bundle) throws IOException, OrmException; + + public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) + throws IOException, OrmException; + + public abstract Result execute(ReviewDb db, Change.Id changeId, + NoteDbUpdateManager manager) throws OrmException, IOException; +}
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..ffd11a7 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -0,0 +1,626 @@ +// 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.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +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.GerritServerConfig; +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.Config; +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; + private final long skewMs; + + @Inject + ChangeRebuilderImpl(@GerritServerConfig Config cfg, + 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; + this.skewMs = NoteDbChangeState.getReadOnlySkew(cfg); + } + + @Override + public Result rebuild(ReviewDb db, Change.Id changeId) + throws IOException, OrmException { + return rebuild(db, changeId, true); + } + + @Override + public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId) + throws IOException, OrmException { + return rebuild(db, changeId, false); + } + + private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly) + throws IOException, OrmException { + db = ReviewDbUtil.unwrapDb(db); + // Read change just to get project; this instance is then discarded so we + // can read a consistent ChangeBundle inside a transaction. + 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, checkReadOnly); + } + } + + @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 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 OrmException, IOException { + return execute(db, changeId, manager, true); + } + + public Result execute(ReviewDb db, Change.Id changeId, + NoteDbUpdateManager manager, boolean checkReadOnly) + throws 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) { + if (checkReadOnly) { + NoteDbChangeState.checkNotReadOnly(change, skewMs); + } + 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) + .setRefLogMessage("Rebuilding change"); + 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<>(); + ListMultimap<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..44b3966 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,16 +24,15 @@ 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; import com.google.gerrit.server.edit.ChangeEditUtil; import com.google.gerrit.server.git.GitRepositoryManager; @@ -60,6 +57,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; @@ -88,8 +86,7 @@ private final Provider<PatchScriptBuilder> builderFactory; 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 @@ -118,8 +115,7 @@ Provider<PatchScriptBuilder> builderFactory, PatchListCache patchListCache, ReviewDb db, - AccountInfoCacheFactory.Factory aicFactory, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, ChangeEditUtil editReader, @Assisted ChangeControl control, @Assisted final String fileName, @@ -132,8 +128,7 @@ this.patchListCache = patchListCache; this.db = db; this.control = control; - this.aicFactory = aicFactory; - this.plcUtil = plcUtil; + this.commentsUtil = commentsUtil; this.editReader = editReader; this.fileName = fileName; @@ -151,8 +146,7 @@ Provider<PatchScriptBuilder> builderFactory, PatchListCache patchListCache, ReviewDb db, - AccountInfoCacheFactory.Factory aicFactory, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, ChangeEditUtil editReader, @Assisted ChangeControl control, @Assisted String fileName, @@ -165,8 +159,7 @@ this.patchListCache = patchListCache; this.db = db; this.control = control; - this.aicFactory = aicFactory; - this.plcUtil = plcUtil; + this.commentsUtil = commentsUtil; this.editReader = editReader; this.fileName = fileName; @@ -188,9 +181,8 @@ } @Override - public PatchScript call() throws OrmException, NoSuchChangeException, - LargeObjectException, AuthException, - InvalidChangeOperationException, IOException { + public PatchScript call() throws OrmException, LargeObjectException, + AuthException, InvalidChangeOperationException, IOException { if (parentNum < 0) { validatePatchSetId(psa); } @@ -214,8 +206,6 @@ bId = toObjectId(psEntityB); if (parentNum < 0) { aId = psEntityA != null ? toObjectId(psEntityA) : null; - } else { - aId = getParent(git, bId, parentNum); } try { @@ -247,7 +237,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 +253,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; } @@ -345,24 +338,23 @@ } if (loadComments && edit == null) { - AccountInfoCacheFactory aic = aicFactory.create(); comments = new CommentDetail(psa, psb); switch (changeType) { case ADDED: case MODIFIED: - loadPublished(byKey, aic, newName); + loadPublished(byKey, newName); break; case DELETED: - loadPublished(byKey, aic, newName); + loadPublished(byKey, newName); break; case COPIED: case RENAMED: if (psa != null) { - loadPublished(byKey, aic, oldName); + loadPublished(byKey, oldName); } - loadPublished(byKey, aic, newName); + loadPublished(byKey, newName); break; case REWRITE: @@ -375,57 +367,50 @@ switch (changeType) { case ADDED: case MODIFIED: - loadDrafts(byKey, aic, me, newName); + loadDrafts(byKey, me, newName); break; case DELETED: - loadDrafts(byKey, aic, me, newName); + loadDrafts(byKey, me, newName); break; case COPIED: case RENAMED: if (psa != null) { - loadDrafts(byKey, aic, me, oldName); + loadDrafts(byKey, me, oldName); } - loadDrafts(byKey, aic, me, newName); + loadDrafts(byKey, me, newName); break; case REWRITE: break; } } - - comments.setAccountInfoCache(aic.create()); } } - private void loadPublished(final Map<Patch.Key, Patch> byKey, - final AccountInfoCacheFactory aic, final String file) throws OrmException { + private void loadPublished(Map<Patch.Key, Patch> byKey, 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()); - } - - final Patch.Key pKey = c.getKey().getParentKey(); - final Patch p = byKey.get(pKey); + for (Comment c : commentsUtil.publishedByChangeFile(db, notes, changeId, file)) { + comments.include(change.getId(), c); + 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); } } } - 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)) { - aic.want(me); - } - - final Patch.Key pKey = c.getKey().getParentKey(); - final Patch p = byKey.get(pKey); + private void loadDrafts(Map<Patch.Key, Patch> byKey, Account.Id me, + String file) throws OrmException { + for (Comment c : + commentsUtil.draftByChangeFileAuthor(db, control.getNotes(), file, me)) { + comments.include(change.getId(), c); + 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/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java index 438add6..5d7e139 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -19,7 +19,7 @@ import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is; import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.annotations.Export; import com.google.gerrit.extensions.annotations.ExtensionPoint; import com.google.gerrit.extensions.annotations.Listen; @@ -54,7 +54,7 @@ private final ModuleGenerator httpGen; private Set<Class<?>> sysSingletons; - private Multimap<TypeLiteral<?>, Class<?>> sysListen; + private ListMultimap<TypeLiteral<?>, Class<?>> sysListen; private String initJs; Module sysModule;
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..81e7433 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.ListMultimap; 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,24 +46,16 @@ 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; import java.util.jar.JarFile; import java.util.jar.Manifest; -public class JarScanner implements PluginContentScanner { +public class JarScanner implements PluginContentScanner, AutoCloseable { 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(); + ListMultimap<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(); @@ -139,6 +131,11 @@ return findSubClassesOf(superClass.getName()); } + @Override + public void close() throws IOException { + jarFile.close(); + } + private List<String> findSubClassesOf(String superClass) throws IOException { String name = superClass.replace('.', '/'); @@ -199,10 +196,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 +218,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 +271,7 @@ private abstract static class AbstractAnnotationVisitor extends AnnotationVisitor { AbstractAnnotationVisitor() { - super(Opcodes.ASM4); + super(Opcodes.ASM5); } @Override @@ -294,10 +294,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 +308,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..5826c58 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; @@ -25,8 +24,8 @@ 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.Ordering; +import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import com.google.gerrit.extensions.annotations.PluginName; @@ -400,7 +399,7 @@ } public synchronized void rescan() { - Multimap<String, Path> pluginsFiles = prunePlugins(pluginsDir); + SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir); if (pluginsFiles.isEmpty()) { return; } @@ -479,7 +478,7 @@ return sortedPlugins; } - private void syncDisabledPlugins(Multimap<String, Path> jars) { + private void syncDisabledPlugins(SetMultimap<String, Path> jars) { stopRemovedPlugins(jars); dropRemovedDisabledPlugins(jars); } @@ -526,7 +525,7 @@ } } - private void stopRemovedPlugins(Multimap<String, Path> jars) { + private void stopRemovedPlugins(SetMultimap<String, Path> jars) { Set<String> unload = Sets.newHashSet(running.keySet()); for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) { for (Path path : entry.getValue()) { @@ -540,7 +539,7 @@ } } - private void dropRemovedDisabledPlugins(Multimap<String, Path> jars) { + private void dropRemovedDisabledPlugins(SetMultimap<String, Path> jars) { Set<String> unload = Sets.newHashSet(disabled.keySet()); for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) { for (Path path : entry.getValue()) { @@ -645,7 +644,7 @@ // Only one active plugin per plugin name can exist for each plugin name. // Filter out disabled plugins and transform the multimap to a map private static Map<String, Path> filterDisabled( - Multimap<String, Path> pluginPaths) { + SetMultimap<String, Path> pluginPaths) { Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize( pluginPaths.keys().size()); for (String name : pluginPaths.keys()) { @@ -668,9 +667,9 @@ // // NOTE: Bear in mind that the plugin name can be reassigned after load by the // Server plugin provider. - public Multimap<String, Path> prunePlugins(Path pluginsDir) { + public SetMultimap<String, Path> prunePlugins(Path pluginsDir) { List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir); - Multimap<String, Path> map; + SetMultimap<String, Path> map; map = asMultimap(pluginPaths); for (String plugin : map.keySet()) { Collection<Path> files = map.asMap().get(plugin); @@ -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) { @@ -739,8 +735,8 @@ return null; } - private Multimap<String, Path> asMultimap(List<Path> plugins) { - Multimap<String, Path> map = LinkedHashMultimap.create(); + private SetMultimap<String, Path> asMultimap(List<Path> plugins) { + SetMultimap<String, Path> map = LinkedHashMultimap.create(); for (Path srcPath : plugins) { map.put(getPluginName(srcPath), 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 40f1fea..7175685 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
@@ -31,7 +31,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -43,9 +42,9 @@ private final String pluginCanonicalWebUrl; private final ClassLoader classLoader; private final String metricsPrefix; - 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; @@ -63,14 +62,18 @@ ClassLoader classLoader, String metricsPrefix) 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); + this.manifest = scanner == null ? null : getPluginManifest(scanner); this.metricsPrefix = metricsPrefix; - loadGuiceModules(manifest, classLoader); + if (manifest != null) { + loadGuiceModules(manifest, classLoader); + } } public ServerPlugin(String name, @@ -107,7 +110,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; @@ -199,7 +202,7 @@ } if (env.hasSshModule()) { - List<Module> modules = new LinkedList<>(); + List<Module> modules = new ArrayList<>(); if (getApiType() == ApiType.PLUGIN) { modules.add(env.getSshModule()); } @@ -215,7 +218,7 @@ } if (env.hasHttpModule()) { - List<Module> modules = new LinkedList<>(); + List<Module> modules = new ArrayList<>(); if (getApiType() == ApiType.PLUGIN) { modules.add(env.getHttpModule()); }
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..68debb9 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
@@ -58,13 +58,12 @@ } public ChangeControl controlFor(ReviewDb db, Project.NameKey project, - Change.Id changeId, CurrentUser user) - throws NoSuchChangeException, OrmException { + Change.Id changeId, CurrentUser user) throws OrmException { return controlFor(notesFactory.create(db, project, changeId), user); } public ChangeControl controlFor(ReviewDb db, Change change, - CurrentUser user) throws NoSuchChangeException, OrmException { + CurrentUser user) throws OrmException { final Project.NameKey projectKey = change.getProject(); try { return projectControl.controlFor(projectKey, user) @@ -88,12 +87,12 @@ } public ChangeControl validateFor(ReviewDb db, Change.Id changeId, - CurrentUser user) throws NoSuchChangeException, OrmException { + CurrentUser user) throws OrmException { return validateFor(db, notesFactory.createChecked(changeId), user); } public ChangeControl validateFor(ReviewDb db, ChangeNotes notes, - CurrentUser user) throws NoSuchChangeException, OrmException { + CurrentUser user) throws OrmException { ChangeControl c = controlFor(notes, user); if (!c.isVisible(db)) { throw new NoSuchChangeException(c.getId()); @@ -261,15 +260,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 +364,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 +389,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 +440,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 +469,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/CommitIncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java new file mode 100644 index 0000000..297f138 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.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.project; + +import com.google.gerrit.extensions.api.changes.IncludedInInfo; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.change.IncludedIn; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.revwalk.RevCommit; + +import java.io.IOException; + +@Singleton +class CommitIncludedIn implements RestReadView<CommitResource> { + private IncludedIn includedIn; + + @Inject + CommitIncludedIn(IncludedIn includedIn) { + this.includedIn = includedIn; + } + + @Override + public IncludedInInfo apply(CommitResource rsrc) + throws RestApiException, OrmException, IOException { + RevCommit commit = rsrc.getCommit(); + Project.NameKey project = rsrc.getProject().getProject().getNameKey(); + return includedIn.apply(project, commit.getId().getName()); + } +}
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 fd6e225..4db1ccd 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
@@ -308,10 +308,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..adff11bb --- /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 { + 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..c0da5d1 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
@@ -76,11 +76,14 @@ child(PROJECT_KIND, "commits").to(CommitsCollection.class); get(COMMIT_KIND).to(GetCommit.class); + get(COMMIT_KIND, "in").to(CommitIncludedIn.class); child(COMMIT_KIND, "files").to(FilesInCommitCollection.class); 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 +94,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/NoSuchChangeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java index f766c7f..7946a3a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java
@@ -15,16 +15,17 @@ package com.google.gerrit.server.project; import com.google.gerrit.reviewdb.client.Change; +import com.google.gwtorm.server.OrmException; /** Indicates the change does not exist. */ -public class NoSuchChangeException extends Exception { +public class NoSuchChangeException extends OrmException { private static final long serialVersionUID = 1L; - public NoSuchChangeException(final Change.Id key) { + public NoSuchChangeException(Change.Id key) { this(key, null); } - public NoSuchChangeException(final Change.Id key, final Throwable why) { + public NoSuchChangeException(Change.Id key, Throwable why) { super(key.toString(), why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java index a862ac2..bb1b20e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -18,8 +18,10 @@ import static com.google.gerrit.server.project.RefPattern.isRE; import com.google.auto.value.AutoValue; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +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(); + ListMultimap<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..c2d7b7d 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; @@ -189,7 +180,7 @@ n.remove(p.getNameKey()); list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n)); } catch (ExecutionException e) { - log.warn("Cannot list avaliable projects", e); + log.warn("Cannot list available projects", e); } finally { listLock.unlock(); } @@ -204,7 +195,7 @@ n.add(newProjectName); list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n)); } catch (ExecutionException e) { - log.warn("Cannot list avaliable projects", e); + log.warn("Cannot list available projects", e); } finally { listLock.unlock(); } @@ -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/ProjectCacheWarmer.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java index 2cdb172..729aa9d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -25,8 +25,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; @Singleton public class ProjectCacheWarmer implements LifecycleListener { @@ -50,9 +53,10 @@ new ScheduledThreadPoolExecutor(config.getInt("cache", "projects", "loadThreads", cpus), new ThreadFactoryBuilder().setNameFormat( "ProjectCacheLoader-%d").build()); + ExecutorService scheduler = Executors.newFixedThreadPool(1); log.info("Loading project cache"); - pool.execute(new Runnable() { + scheduler.execute(new Runnable() { @Override public void run() { for (final Project.NameKey name : cache.all()) { @@ -64,6 +68,12 @@ }); } pool.shutdown(); + try { + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + log.info("Finished loading project cache"); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for project cache to load"); + } } }); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java index 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/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java index e74511a..9eafc4bc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -26,7 +26,7 @@ /** Node of a Project in a tree formatted by {@link ListProjects}. */ public class ProjectNode implements TreeNode, Comparable<ProjectNode> { public interface Factory { - ProjectNode create(final Project project, final boolean isVisible); + ProjectNode create(Project project, boolean isVisible); } private final AllProjectsName allProjectsName;
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/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java index 1e5a7c9..33e24ac 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -48,7 +48,8 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import java.io.IOException; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -223,11 +224,11 @@ private List<AccessSection> getAccessSections( Map<String, AccessSectionInfo> sectionInfos) throws UnprocessableEntityException { - List<AccessSection> sections = new LinkedList<>(); if (sectionInfos == null) { - return sections; + return Collections.emptyList(); } + List<AccessSection> sections = new ArrayList<>(sectionInfos.size()); for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) { AccessSection accessSection = new AccessSection(entry.getKey());
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/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java index 38411e3..87de6bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IsVisibleToPredicate.java
@@ -14,9 +14,23 @@ package com.google.gerrit.server.query; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.query.change.SingleGroupUser; + public abstract class IsVisibleToPredicate<T> extends OperatorPredicate<T> implements Matchable<T> { public IsVisibleToPredicate(String name, String value) { super(name, value); } + + protected static String describe(CurrentUser user) { + if (user.isIdentifiedUser()) { + return user.getAccountId().toString(); + } + if (user instanceof SingleGroupUser) { + return "group:" + user.getEffectiveGroups() + .getKnownGroups().iterator().next().toString(); + } + return user.toString(); + } }
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/AccountIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java index dc68a61..0d6f5ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -14,26 +14,13 @@ package com.google.gerrit.server.query.account; -import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.query.IsVisibleToPredicate; -import com.google.gerrit.server.query.change.SingleGroupUser; import com.google.gwtorm.server.OrmException; public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> { - private static String describe(CurrentUser user) { - if (user.isIdentifiedUser()) { - return user.getAccountId().toString(); - } - if (user instanceof SingleGroupUser) { - return "group:" + user.getEffectiveGroups().getKnownGroups() // - .iterator().next().toString(); - } - return user.toString(); - } - private final AccountControl accountControl; AccountIsVisibleToPredicate(AccountControl accountControl) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java index 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..1c945e3 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; @@ -35,10 +34,6 @@ * Parses a query string meant to be applied to account objects. */ public class AccountQueryBuilder extends QueryBuilder<AccountState> { - public interface ChangeOperatorFactory - extends OperatorFactory<AccountState, AccountQueryBuilder> { - } - public static final String FIELD_ACCOUNT = "account"; public static final String FIELD_EMAIL = "email"; public static final String FIELD_LIMIT = "limit"; @@ -124,13 +119,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/AssigneePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java new file mode 100644 index 0000000..38622ed --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.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.server.query.change; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gwtorm.server.OrmException; + +class AssigneePredicate extends ChangeIndexPredicate { + private final Account.Id id; + + AssigneePredicate(Account.Id id) { + super(ChangeField.ASSIGNEE, id.toString()); + this.id = id; + } + + @Override + 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 + public int getCost() { + return 1; + } +}
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..98f6cb5 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,54 +17,54 @@ 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.ImmutableMultimap; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.common.data.SubmitTypeRecord; 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 +87,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 +108,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 +305,7 @@ return cd; } + private boolean lazyLoad = true; private final ReviewDb db; private final GitRepositoryManager repoManager; private final ChangeControl.GenericFactory changeControlFactory; @@ -316,13 +315,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 +336,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 ImmutableListMultimap<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 +370,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -382,7 +388,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 +408,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -419,7 +425,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 +446,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -457,7 +463,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 +485,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -496,7 +502,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 +525,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, ChangeMessagesUtil cmUtil, - PatchLineCommentsUtil plcUtil, + CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache, NotesMigration notesMigration, @@ -538,7 +544,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 +554,11 @@ this.project = null; } + public ChangeData setLazyLoad(boolean load) { + lazyLoad = load; + return this; + } + public ReviewDb db() { return db; } @@ -568,10 +579,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 +591,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 +613,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 +674,7 @@ } public void setNoChangedLines() { - changedLines = Optional.absent(); + changedLines = Optional.empty(); } public Change.Id getId() { @@ -703,10 +714,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 +733,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 +763,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 +802,23 @@ public List<PatchSetApproval> currentApprovals() throws OrmException { if (currentApprovals == null) { + if (!lazyLoad) { + return Collections.emptyList(); + } Change c = change(); if (c == null) { currentApprovals = Collections.emptyList(); } else { - currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet( - db, changeControl(), c.currentPatchSetId())); + try { + currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet( + db, changeControl(), c.currentPatchSetId())); + } catch (OrmException e) { + if (e.getCause() instanceof NoSuchChangeException) { + currentApprovals = Collections.emptyList(); + } else { + throw e; + } + } } } return currentApprovals; @@ -866,17 +899,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 +938,9 @@ public ListMultimap<PatchSet.Id, PatchSetApproval> approvals() throws OrmException { if (allApprovals == null) { + if (!lazyLoad) { + return ImmutableListMultimap.of(); + } allApprovals = approvalsUtil.byChange(db, notes()); } return allApprovals; @@ -918,17 +951,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 +977,9 @@ public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException { if (reviewerUpdates == null) { + if (!lazyLoad) { + return Collections.emptyList(); + } reviewerUpdates = approvalsUtil.getReviewerUpdates(notes()); } return reviewerUpdates; @@ -957,10 +993,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 +1007,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 +1061,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 +1099,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 +1128,38 @@ } 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 + // Double-check that any drafts exist for this user after + // filtering out zombies. If some but not all drafts in the ref + // were zombies, the returned Ref still includes those zombies; + // this is suboptimal, but is ok for the purposes of + // draftsByUser(), and easier than trying to rebuild the change at + // this point. + && !notes().getDraftComments(account, ref).isEmpty()) { + draftsByUser.put(account, ref); + } + } + } else { + for (Comment sc : commentsUtil.draftByChange(db, notes())) { + draftsByUser.put(sc.author.getId(), null); + } } } return draftsByUser; @@ -1065,6 +1167,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 +1199,9 @@ public Set<String> hashtags() throws OrmException { if (hashtags == null) { + if (!lazyLoad) { + return Collections.emptySet(); + } hashtags = notes().getHashtags(); } return hashtags; @@ -1106,6 +1214,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); } @@ -1117,15 +1228,33 @@ this.starredByUser = starredByUser; } - public ImmutableMultimap<Account.Id, String> stars() throws OrmException { + public ImmutableListMultimap<Account.Id, String> stars() throws OrmException { if (stars == null) { - stars = checkNotNull(starredChangesUtil).byChange(legacyId); + if (!lazyLoad) { + return ImmutableListMultimap.of(); + } + ImmutableListMultimap.Builder<Account.Id, String> b = + ImmutableListMultimap.builder(); + for (Map.Entry<Account.Id, StarRef> e : starRefs().entrySet()) { + b.putAll(e.getKey(), e.getValue().labels()); + } + return b.build(); } return stars; } - public void setStars(Multimap<Account.Id, String> stars) { - this.stars = ImmutableMultimap.copyOf(stars); + public void setStars(ListMultimap<Account.Id, String> stars) { + this.stars = ImmutableListMultimap.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 @@ -1159,4 +1288,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/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java index 303c9f8..88499ec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -25,17 +25,6 @@ import com.google.inject.Provider; class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> { - private static String describe(CurrentUser user) { - if (user.isIdentifiedUser()) { - return user.getAccountId().toString(); - } - if (user instanceof SingleGroupUser) { - return "group:" + user.getEffectiveGroups().getKnownGroups() // - .iterator().next().toString(); - } - return user.toString(); - } - private final Provider<ReviewDb> db; private final ChangeNotes.Factory notesFactory; private final ChangeControl.GenericFactory changeControl;
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/LegacyReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java deleted file mode 100644 index cd93ed3..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyReviewerPredicate.java +++ /dev/null
@@ -1,43 +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.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 LegacyReviewerPredicate extends ChangeIndexPredicate { - private final Account.Id id; - - LegacyReviewerPredicate(Account.Id id) { - super(ChangeField.LEGACY_REVIEWER, id.toString()); - this.id = id; - } - - Account.Id getAccountId() { - return id; - } - - @Override - public boolean match(ChangeData object) throws OrmException { - return object.reviewers().all().contains(id); - } - - @Override - public int getCost() { - return 1; - } -}
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/SubmittablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java new file mode 100644 index 0000000..8782cfd --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.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.query.change; + +import com.google.gerrit.common.data.SubmitRecord; +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gwtorm.server.OrmException; + +class SubmittablePredicate extends ChangeIndexPredicate { + private final SubmitRecord.Status status; + + SubmittablePredicate(SubmitRecord.Status status) { + super(ChangeField.SUBMIT_RECORD, status.name()); + this.status = status; + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream() + .anyMatch(r -> r.status == status); + } + + @Override + public int getCost() { + return 1; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java new file mode 100644 index 0000000..939ece6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -0,0 +1,52 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.GroupControl; +import com.google.gerrit.server.query.IsVisibleToPredicate; +import com.google.gerrit.server.query.account.AccountQueryBuilder; +import com.google.gwtorm.server.OrmException; + +public class GroupIsVisibleToPredicate + extends IsVisibleToPredicate<AccountGroup> { + private final GroupControl.GenericFactory groupControlFactory; + private final CurrentUser user; + + GroupIsVisibleToPredicate(GroupControl.GenericFactory groupControlFactory, + CurrentUser user) { + super(AccountQueryBuilder.FIELD_VISIBLETO, describe(user)); + this.groupControlFactory = groupControlFactory; + this.user = user; + } + + @Override + public boolean match(AccountGroup group) throws OrmException { + try { + return groupControlFactory.controlFor(user, group.getGroupUUID()) + .isVisible(); + } catch (NoSuchGroupException e) { + // Ignored + return false; + } + } + + @Override + public int getCost() { + return 1; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java new file mode 100644 index 0000000..16f7e42 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -0,0 +1,83 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.index.FieldDef; +import com.google.gerrit.server.index.IndexPredicate; +import com.google.gerrit.server.index.group.GroupField; +import com.google.gerrit.server.query.Predicate; + +import java.util.List; +import java.util.Locale; + +public class GroupPredicates { + public static Predicate<AccountGroup> defaultPredicate(String query) { + // Adapt the capacity of this list when adding more default predicates. + List<Predicate<AccountGroup>> preds = Lists.newArrayListWithCapacity(5); + preds.add(uuid(new AccountGroup.UUID(query))); + preds.add(name(query)); + preds.add(inname(query)); + if (!Strings.isNullOrEmpty(query)) { + preds.add(description(query)); + } + preds.add(owner(query)); + return Predicate.or(preds); + } + + public static Predicate<AccountGroup> uuid(AccountGroup.UUID uuid) { + return new GroupPredicate(GroupField.UUID, + GroupQueryBuilder.FIELD_UUID, uuid.get()); + } + + public static Predicate<AccountGroup> description(String description) { + return new GroupPredicate(GroupField.DESCRIPTION, + GroupQueryBuilder.FIELD_DESCRIPTION, description); + } + + public static Predicate<AccountGroup> inname(String name) { + return new GroupPredicate(GroupField.NAME_PART, + GroupQueryBuilder.FIELD_INNAME, name.toLowerCase(Locale.US)); + } + + public static Predicate<AccountGroup> name(String name) { + return new GroupPredicate(GroupField.NAME, + GroupQueryBuilder.FIELD_NAME, name.toLowerCase(Locale.US)); + } + + public static Predicate<AccountGroup> owner(String owner) { + return new GroupPredicate(GroupField.OWNER_UUID, + GroupQueryBuilder.FIELD_OWNER, owner); + } + + public static Predicate<AccountGroup> isVisibleToAll() { + return new GroupPredicate(GroupField.IS_VISIBLE_TO_ALL, "1"); + } + + static class GroupPredicate extends IndexPredicate<AccountGroup> { + GroupPredicate(FieldDef<AccountGroup, ?> def, String value) { + super(def, value); + } + + GroupPredicate(FieldDef<AccountGroup, ?> def, String name, String value) { + super(def, name, value); + } + } + + private GroupPredicates() { + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java new file mode 100644 index 0000000..9105b99 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -0,0 +1,101 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.query.LimitPredicate; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryBuilder; +import com.google.gerrit.server.query.QueryParseException; +import com.google.inject.Inject; + +/** + * Parses a query string meant to be applied to group objects. + */ +public class GroupQueryBuilder extends QueryBuilder<AccountGroup> { + public static final String FIELD_UUID = "uuid"; + public static final String FIELD_DESCRIPTION = "description"; + public static final String FIELD_INNAME = "inname"; + public static final String FIELD_NAME = "name"; + public static final String FIELD_OWNER = "owner"; + public static final String FIELD_LIMIT = "limit"; + + private static final QueryBuilder.Definition<AccountGroup, GroupQueryBuilder> mydef = + new QueryBuilder.Definition<>(GroupQueryBuilder.class); + + @Inject + GroupQueryBuilder() { + super(mydef); + } + + @Operator + public Predicate<AccountGroup> uuid(String uuid) { + return GroupPredicates.uuid(new AccountGroup.UUID(uuid)); + } + + @Operator + public Predicate<AccountGroup> description(String description) + throws QueryParseException { + if (Strings.isNullOrEmpty(description)) { + throw error("description operator requires a value"); + } + + return GroupPredicates.description(description); + } + + @Operator + public Predicate<AccountGroup> inname(String namePart) { + if (namePart.isEmpty()) { + return name(namePart); + } + return GroupPredicates.inname(namePart); + } + + @Operator + public Predicate<AccountGroup> name(String name) { + return GroupPredicates.name(name); + } + + @Operator + public Predicate<AccountGroup> owner(String owner) { + return GroupPredicates.owner(owner); + } + + @Operator + public Predicate<AccountGroup> is(String value) throws QueryParseException { + if ("visibletoall".equalsIgnoreCase(value)) { + return GroupPredicates.isVisibleToAll(); + } + throw error("Invalid query"); + } + + @Override + protected Predicate<AccountGroup> defaultField(String query) { + return GroupPredicates.defaultPredicate(query); + } + + @Operator + public Predicate<AccountGroup> limit(String query) + throws QueryParseException { + Integer limit = Ints.tryParse(query); + if (limit == null) { + throw error("Invalid limit: " + query); + } + return new LimitPredicate<>(FIELD_LIMIT, limit); + } + +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java new file mode 100644 index 0000000..f7a94b4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -0,0 +1,63 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.query.group.GroupQueryBuilder.FIELD_LIMIT; + +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.GroupControl; +import com.google.gerrit.server.index.IndexConfig; +import com.google.gerrit.server.index.IndexPredicate; +import com.google.gerrit.server.index.group.GroupIndexCollection; +import com.google.gerrit.server.index.group.GroupIndexRewriter; +import com.google.gerrit.server.index.group.GroupSchemaDefinitions; +import com.google.gerrit.server.query.AndSource; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryProcessor; +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class GroupQueryProcessor extends QueryProcessor<AccountGroup> { + private final GroupControl.GenericFactory groupControlFactory; + + static { + // It is assumed that basic rewrites do not touch visibleto predicates. + checkState( + !GroupIsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class), + "GroupQueryProcessor assumes visibleto is not used by the index rewriter."); + } + + @Inject + protected GroupQueryProcessor(Provider<CurrentUser> userProvider, + Metrics metrics, + IndexConfig indexConfig, + GroupIndexCollection indexes, + GroupIndexRewriter rewriter, + GroupControl.GenericFactory groupControlFactory) { + super(userProvider, metrics, GroupSchemaDefinitions.INSTANCE, indexConfig, + indexes, rewriter, FIELD_LIMIT); + this.groupControlFactory = groupControlFactory; + } + + @Override + protected Predicate<AccountGroup> enforceVisibility( + Predicate<AccountGroup> pred) { + return new AndSource<>(pred, + new GroupIsVisibleToPredicate(groupControlFactory, userProvider.get()), + start); + } +}
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..a5c7d75 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
@@ -70,14 +70,15 @@ AllProjectsCreator( GitRepositoryManager mgr, AllProjectsName allProjectsName, + SystemGroupBackend systemGroupBackend, @GerritPersonIdent PersonIdent serverUser) { this.mgr = mgr; this.allProjectsName = allProjectsName; this.serverUser = serverUser; - this.anonymous = SystemGroupBackend.getGroup(ANONYMOUS_USERS); - this.registered = SystemGroupBackend.getGroup(REGISTERED_USERS); - this.owners = SystemGroupBackend.getGroup(PROJECT_OWNERS); + this.anonymous = systemGroupBackend.getGroup(ANONYMOUS_USERS); + this.registered = systemGroupBackend.getGroup(REGISTERED_USERS); + this.owners = systemGroupBackend.getGroup(PROJECT_OWNERS); } public AllProjectsCreator setAdministrators(GroupReference admin) { @@ -165,8 +166,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 +176,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/AllUsersCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java index b697519..626d258 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -54,11 +54,12 @@ AllUsersCreator( GitRepositoryManager mgr, AllUsersName allUsersName, + SystemGroupBackend systemGroupBackend, @GerritPersonIdent PersonIdent serverUser) { this.mgr = mgr; this.allUsersName = allUsersName; this.serverUser = serverUser; - this.registered = SystemGroupBackend.getGroup(REGISTERED_USERS); + this.registered = systemGroupBackend.getGroup(REGISTERED_USERS); } public AllUsersCreator setAdministrators(GroupReference admin) {
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/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java index 2581d56..7caea42 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -24,6 +24,8 @@ import com.google.gerrit.server.account.GroupUUID; import com.google.gerrit.server.config.SitePath; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.index.group.GroupIndex; +import com.google.gerrit.server.index.group.GroupIndexCollection; import com.google.gwtorm.jdbc.JdbcExecutor; import com.google.gwtorm.jdbc.JdbcSchema; import com.google.gwtorm.server.OrmException; @@ -46,6 +48,7 @@ private final AllUsersCreator allUsersCreator; private final PersonIdent serverUser; private final DataSourceType dataSourceType; + private final GroupIndexCollection indexCollection; private AccountGroup admin; private AccountGroup batch; @@ -55,20 +58,23 @@ AllProjectsCreator ap, AllUsersCreator auc, @GerritPersonIdent PersonIdent au, - DataSourceType dst) { - this(site.site_path, ap, auc, au, dst); + DataSourceType dst, + GroupIndexCollection ic) { + this(site.site_path, ap, auc, au, dst, ic); } public SchemaCreator(@SitePath Path site, AllProjectsCreator ap, AllUsersCreator auc, @GerritPersonIdent PersonIdent au, - DataSourceType dst) { + DataSourceType dst, + GroupIndexCollection ic) { site_path = site; allProjectsCreator = ap; allUsersCreator = auc; serverUser = au; dataSourceType = dst; + indexCollection = ic; } public void create(final ReviewDb db) throws OrmException, IOException, @@ -82,6 +88,7 @@ sVer.versionNbr = SchemaVersion.getBinaryVersion(); db.schemaVersion().insert(Collections.singleton(sVer)); + createDefaultGroups(db); initSystemConfig(db); allProjectsCreator .setAdministrators(GroupReference.forGroup(admin)) @@ -93,6 +100,30 @@ dataSourceType.getIndexScript().run(db); } + private void createDefaultGroups(ReviewDb db) + throws OrmException, IOException { + admin = newGroup(db, "Administrators", null); + admin.setDescription("Gerrit Site Administrators"); + db.accountGroups().insert(Collections.singleton(admin)); + db.accountGroupNames() + .insert(Collections.singleton(new AccountGroupName(admin))); + index(admin); + + batch = newGroup(db, "Non-Interactive Users", null); + batch.setDescription("Users who perform batch actions on Gerrit"); + batch.setOwnerGroupUUID(admin.getGroupUUID()); + db.accountGroups().insert(Collections.singleton(batch)); + db.accountGroupNames() + .insert(Collections.singleton(new AccountGroupName(batch))); + index(batch); + } + + private void index(AccountGroup group) throws IOException { + for (GroupIndex groupIndex : indexCollection.getWriteIndexes()) { + groupIndex.replace(group); + } + } + private AccountGroup newGroup(ReviewDb c, String name, AccountGroup.UUID uuid) throws OrmException { if (uuid == null) { @@ -104,27 +135,14 @@ uuid); } - private SystemConfig initSystemConfig(final ReviewDb c) throws OrmException { - admin = newGroup(c, "Administrators", null); - admin.setDescription("Gerrit Site Administrators"); - c.accountGroups().insert(Collections.singleton(admin)); - c.accountGroupNames().insert( - Collections.singleton(new AccountGroupName(admin))); - - batch = newGroup(c, "Non-Interactive Users", null); - batch.setDescription("Users who perform batch actions on Gerrit"); - batch.setOwnerGroupUUID(admin.getGroupUUID()); - c.accountGroups().insert(Collections.singleton(batch)); - c.accountGroupNames().insert( - Collections.singleton(new AccountGroupName(batch))); - - final SystemConfig s = SystemConfig.create(); + private SystemConfig initSystemConfig(ReviewDb db) throws OrmException { + SystemConfig s = SystemConfig.create(); try { s.sitePath = site_path.toRealPath().normalize().toString(); } catch (IOException e) { s.sitePath = site_path.toAbsolutePath().normalize().toString(); } - c.systemConfig().insert(Collections.singleton(s)); + db.systemConfig().insert(Collections.singleton(s)); return s; } }
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..fe14c52 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; @@ -24,6 +25,7 @@ import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.AbstractModule; @@ -81,6 +83,7 @@ AllUsersName.class, GitRepositoryManager.class, SitePaths.class, + SystemGroupBackend.class, }) { rebind(parent, Key.get(c)); } @@ -116,6 +119,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..2c81c56 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_141> C = Schema_141.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..d568cba 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.ListMultimap; +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; @@ -87,8 +86,7 @@ } private void updateProjectGroups(ReviewDb db, Repository repo, RevWalk rw, - Set<Change.Id> changes, UpdateUI ui) - throws OrmException, IOException, NoSuchChangeException { + Set<Change.Id> changes, UpdateUI ui) throws OrmException, IOException { // Match sorting in ReceiveCommits. rw.reset(); rw.sort(RevSort.TOPO); @@ -102,8 +100,10 @@ } } - Multimap<ObjectId, Ref> changeRefsBySha = ArrayListMultimap.create(); - Multimap<ObjectId, PatchSet.Id> patchSetsBySha = ArrayListMultimap.create(); + ListMultimap<ObjectId, Ref> changeRefsBySha = + MultimapBuilder.hashKeys().arrayListValues().build(); + ListMultimap<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) { @@ -132,8 +132,7 @@ } private static void updateGroups(ReviewDb db, GroupCollector collector, - Multimap<ObjectId, PatchSet.Id> patchSetsBySha) - throws OrmException, NoSuchChangeException { + ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha) throws OrmException { Map<PatchSet.Id, PatchSet> patchSets = db.patchSets().toMap(db.patchSets().get(patchSetsBySha.values())); for (Map.Entry<ObjectId, Collection<String>> e @@ -154,7 +153,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..95c0257 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.ListMultimap; +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(); + ListMultimap<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..3dd44cd 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.ListMultimap; +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(); + ListMultimap<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_125.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_125.java index 714eb69d..5adfef2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_125.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_125.java
@@ -58,6 +58,7 @@ private final GitRepositoryManager repoManager; private final AllUsersName allUsersName; private final AllProjectsName allProjectsName; + private final SystemGroupBackend systemGroupBackend; private final PersonIdent serverUser; @Inject @@ -65,11 +66,13 @@ GitRepositoryManager repoManager, AllUsersName allUsersName, AllProjectsName allProjectsName, + SystemGroupBackend systemGroupBackend, @GerritPersonIdent PersonIdent serverUser) { super(prior); this.repoManager = repoManager; this.allUsersName = allUsersName; this.allProjectsName = allProjectsName; + this.systemGroupBackend = systemGroupBackend; this.serverUser = serverUser; } @@ -82,7 +85,7 @@ config.getAccessSection(RefNames.REFS_USERS + "*", true) .remove(new Permission(Permission.READ)); - GroupReference registered = SystemGroupBackend.getGroup(REGISTERED_USERS); + GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS); AccessSection users = config.getAccessSection( RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true); grant(config, users, Permission.READ, true, registered);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_126.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_126.java index 50c518b..b26ce41 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_126.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_126.java
@@ -46,16 +46,19 @@ private final GitRepositoryManager repoManager; private final AllUsersName allUsersName; + private final SystemGroupBackend systemGroupBackend; private final PersonIdent serverUser; @Inject Schema_126(Provider<Schema_125> prior, GitRepositoryManager repoManager, AllUsersName allUsersName, + SystemGroupBackend systemGroupBackend, @GerritPersonIdent PersonIdent serverUser) { super(prior); this.repoManager = repoManager; this.allUsersName = allUsersName; + this.systemGroupBackend = systemGroupBackend; this.serverUser = serverUser; } @@ -70,7 +73,7 @@ RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}"; config.remove(config.getAccessSection(refsUsersShardedId)); - GroupReference registered = SystemGroupBackend.getGroup(REGISTERED_USERS); + GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS); AccessSection users = config.getAccessSection(refsUsersShardedId, true); grant(config, users, Permission.READ, false, true, registered); grant(config, users, Permission.PUSH, false, true, registered);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java index a7f57b6..d45d92c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
@@ -44,16 +44,19 @@ private final GitRepositoryManager repoManager; private final AllProjectsName allProjectsName; + private final SystemGroupBackend systemGroupBackend; private final PersonIdent serverUser; @Inject Schema_128(Provider<Schema_127> prior, GitRepositoryManager repoManager, AllProjectsName allProjectsName, + SystemGroupBackend systemGroupBackend, @GerritPersonIdent PersonIdent serverUser) { super(prior); this.repoManager = repoManager; this.allProjectsName = allProjectsName; + this.systemGroupBackend = systemGroupBackend; this.serverUser = serverUser; } @@ -64,7 +67,7 @@ allProjectsName, git)) { ProjectConfig config = ProjectConfig.read(md); - GroupReference registered = SystemGroupBackend.getGroup(REGISTERED_USERS); + GroupReference registered = systemGroupBackend.getGroup(REGISTERED_USERS); AccessSection refsFor = config.getAccessSection("refs/for/*", true); grant(config, refsFor, Permission.ADD_PATCH_SET, false, false, registered);
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-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java new file mode 100644 index 0000000..7c1cde8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_132.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_132 extends SchemaVersion { + @Inject + Schema_132(Provider<Schema_131> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java new file mode 100644 index 0000000..31d330b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_133 extends SchemaVersion { + @Inject + Schema_133(Provider<Schema_132> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java new file mode 100644 index 0000000..fa01ff3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_134.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_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..08b2366 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.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.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 SystemGroupBackend systemGroupBackend; + private final PersonIdent serverUser; + + @Inject + Schema_135(Provider<Schema_134> prior, + GitRepositoryManager repoManager, + AllProjectsName allProjectsName, + SystemGroupBackend systemGroupBackend, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.allProjectsName = allProjectsName; + this.systemGroupBackend = systemGroupBackend; + 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-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java new file mode 100644 index 0000000..a4b1c82 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_136.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_136 extends SchemaVersion { + @Inject + Schema_136(Provider<Schema_135> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.java new file mode 100644 index 0000000..1b4102f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_137.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.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/* 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/schema/Schema_138.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_138.java new file mode 100644 index 0000000..f824ee1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_138.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.server.schema; + +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/schema/Schema_139.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_139.java new file mode 100644 index 0000000..614320b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_139.java
@@ -0,0 +1,187 @@ +//Copyright (C) 2016 The Android Open Source Project +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.auto.value.AutoValue; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.WatchConfig; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.gwtorm.server.OrmDuplicateKeyException; +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.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Schema_139 extends SchemaVersion { + private static final String MSG = "Migrate project watches to git"; + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final PersonIdent serverUser; + + @Inject + Schema_139(Provider<Schema_138> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverUser = serverUser; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) + throws OrmException, SQLException { + ListMultimap<Account.Id, ProjectWatch> imports = + MultimapBuilder.hashKeys().arrayListValues().build(); + try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT " + + "account_id, " + + "project_name, " + + "filter, " + + "notify_abandoned_changes, " + + "notify_all_comments, " + + "notify_new_changes, " + + "notify_new_patch_sets, " + + "notify_submitted_changes " + + "FROM account_project_watches")) { + while (rs.next()) { + Account.Id accountId = new Account.Id(rs.getInt(1)); + ProjectWatch.Builder b = ProjectWatch.builder() + .project(new Project.NameKey(rs.getString(2))) + .filter(rs.getString(3)) + .notifyAbandonedChanges(rs.getBoolean(4)) + .notifyAllComments(rs.getBoolean(5)) + .notifyNewChanges(rs.getBoolean(6)) + .notifyNewPatchSets(rs.getBoolean(7)) + .notifySubmittedChanges(rs.getBoolean(8)); + imports.put(accountId, b.build()); + } + } + + if (imports.isEmpty()) { + return; + } + + try (Repository git = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(git)) { + BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate(); + bru.setRefLogIdent(serverUser); + bru.setRefLogMessage(MSG, false); + + for (Map.Entry<Account.Id, Collection<ProjectWatch>> e : imports.asMap() + .entrySet()) { + Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>(); + for (ProjectWatch projectWatch : e.getValue()) { + ProjectWatchKey key = ProjectWatchKey.create(projectWatch.project(), + projectWatch.filter()); + if (projectWatches.containsKey(key)) { + throw new OrmDuplicateKeyException( + "Duplicate key for watched project: " + key.toString()); + } + Set<NotifyType> notifyValues = EnumSet.noneOf(NotifyType.class); + if (projectWatch.notifyAbandonedChanges()) { + notifyValues.add(NotifyType.ABANDONED_CHANGES); + } + if (projectWatch.notifyAllComments()) { + notifyValues.add(NotifyType.ALL_COMMENTS); + } + if (projectWatch.notifyNewChanges()) { + notifyValues.add(NotifyType.NEW_CHANGES); + } + if (projectWatch.notifyNewPatchSets()) { + notifyValues.add(NotifyType.NEW_PATCHSETS); + } + if (projectWatch.notifySubmittedChanges()) { + notifyValues.add(NotifyType.SUBMITTED_CHANGES); + } + projectWatches.put(key, notifyValues); + } + + try (MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, + allUsersName, git, bru)) { + md.getCommitBuilder().setAuthor(serverUser); + md.getCommitBuilder().setCommitter(serverUser); + md.setMessage(MSG); + + WatchConfig watchConfig = new WatchConfig(e.getKey()); + watchConfig.load(md); + watchConfig.setProjectWatches(projectWatches); + watchConfig.commit(md); + } + } + bru.execute(rw, NullProgressMonitor.INSTANCE); + } catch (IOException | ConfigInvalidException ex) { + throw new OrmException(ex); + } + } + + @AutoValue + abstract static class ProjectWatch { + abstract Project.NameKey project(); + abstract @Nullable String filter(); + abstract boolean notifyAbandonedChanges(); + abstract boolean notifyAllComments(); + abstract boolean notifyNewChanges(); + abstract boolean notifyNewPatchSets(); + abstract boolean notifySubmittedChanges(); + + static Builder builder() { + return new AutoValue_Schema_139_ProjectWatch.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder project(Project.NameKey project); + abstract Builder filter(@Nullable String filter); + abstract Builder notifyAbandonedChanges(boolean notifyAbandonedChanges); + abstract Builder notifyAllComments(boolean notifyAllComments); + abstract Builder notifyNewChanges(boolean notifyNewChanges); + abstract Builder notifyNewPatchSets(boolean notifyNewPatchSets); + abstract Builder notifySubmittedChanges(boolean notifySubmittedChanges); + abstract ProjectWatch build(); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_140.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_140.java new file mode 100644 index 0000000..bdc5f55 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_140.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Remove ChangeMessage sequence. */ +public class Schema_140 extends SchemaVersion { + @Inject + Schema_140(Provider<Schema_139> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_141.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_141.java new file mode 100644 index 0000000..c081ea9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_141.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Add status field to account. */ +public class Schema_141 extends SchemaVersion { + @Inject + Schema_141(Provider<Schema_140> 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/ManualRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java index 900bb42..3115fd1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -26,14 +26,20 @@ * providers. */ public class ManualRequestContext implements RequestContext, AutoCloseable { - private final CurrentUser user; + private final Provider<CurrentUser> userProvider; private final Provider<ReviewDb> db; private final ThreadLocalRequestContext requestContext; private final RequestContext old; public ManualRequestContext(CurrentUser user, SchemaFactory<ReviewDb> schemaFactory, ThreadLocalRequestContext requestContext) throws OrmException { - this.user = user; + this(Providers.of(user), schemaFactory, requestContext); + } + + public ManualRequestContext(Provider<CurrentUser> userProvider, + SchemaFactory<ReviewDb> schemaFactory, + ThreadLocalRequestContext requestContext) throws OrmException { + this.userProvider = userProvider; this.db = Providers.of(schemaFactory.open()); this.requestContext = requestContext; old = requestContext.setContext(this); @@ -41,7 +47,7 @@ @Override public CurrentUser getUser() { - return user; + return userProvider.get(); } @Override
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..f3b44e0 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. } } @@ -178,7 +178,7 @@ /** * @see #wrap(Callable) */ - protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable); + protected abstract <T> Callable<T> wrapImpl(Callable<T> callable); protected <T> Callable<T> context(final RequestContext context, final Callable<T> callable) {
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/BUCK b/gerrit-server/src/main/prolog/BUCK deleted file mode 100644 index 09a6553..0000000 --- a/gerrit-server/src/main/prolog/BUCK +++ /dev/null
@@ -1,8 +0,0 @@ -include_defs('//lib/prolog/prolog.defs') - -prolog_cafe_library( - name = 'common', - srcs = ['gerrit_common.pl'], - deps = ['//gerrit-server:server'], - visibility = ['PUBLIC'], -)
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..61feb57 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.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 change + * @param email + */ +{template .ChangeFooterHtml autoescape="strict" kind="html"} + {if $email.changeUrl or $email.settingsUrl} + <p> + {if $email.changeUrl} + To view, visit{sp} + <a href="{$email.changeUrl}">change {$change.changeNumber}</a>. + {/if} + {if $email.changeUrl and $email.settingsUrl}{sp}{/if} + {if $email.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..9f7429f --- /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 ownerName + * @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} + {$ownerName} has uploaded this 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..559bb26 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param email + * @param fromName + * @param ownerName + * @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} + {$ownerName} has 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/ChangeUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ChangeUtilTest.java new file mode 100644 index 0000000..92d3a1c --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/ChangeUtilTest.java
@@ -0,0 +1,36 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +import java.util.regex.Pattern; + +public class ChangeUtilTest { + @Test + public void changeMessageUuid() throws Exception { + Pattern pat = Pattern.compile("^[0-9a-f]{8}_[0-9a-f]{8}$"); + assertThat("abcd1234_0987fedc").matches(pat); + + String id1 = ChangeUtil.messageUuid(); + assertThat(id1).matches(pat); + + String id2 = ChangeUtil.messageUuid(); + assertThat(id2).isNotEqualTo(id1); + assertThat(id2).matches(pat); + } +}
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..660edc9 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,20 +35,16 @@ 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.eclipse.jgit.lib.Config; import org.junit.Before; import org.junit.Test; 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"); @@ -62,19 +58,19 @@ user = createNiceMock(IdentifiedUser.class); replay(user); backends = new DynamicSet<>(); - backends.add(new SystemGroupBackend()); + backends.add(new SystemGroupBackend(new Config())); backend = new UniversalGroupBackend(backends); } @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 +79,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 +94,7 @@ } @Test - public void testKnownGroups() { + public void knownGroups() { GroupMembership checker = backend.membershipsOf(user); Set<UUID> knownGroups = checker.getKnownGroups(); assertEquals(2, knownGroups.size()); @@ -107,7 +103,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/account/WatchConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java index 0619a78..c40ca83 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/WatchConfigTest.java
@@ -17,8 +17,8 @@ import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.NotifyValue; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import com.google.gerrit.server.git.ValidationError;
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/GroupCollectorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java index ba0599d..ff9b81b 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -17,9 +17,8 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimap; +import com.google.common.collect.SortedSetMultimap; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -47,7 +46,7 @@ RevCommit branchTip = tr.commit().create(); RevCommit a = tr.commit().parent(branchTip).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(a, branchTip), patchSets(), groups()); @@ -62,7 +61,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit b = tr.commit().parent(a).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(b, branchTip), patchSets(), groups()); @@ -79,7 +78,7 @@ RevCommit b = tr.commit().parent(a).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group)); @@ -95,7 +94,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit b = tr.commit().parent(a).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups()); @@ -111,7 +110,7 @@ RevCommit b = tr.commit().parent(branchTip).create(); RevCommit m = tr.commit().parent(a).parent(b).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets(), groups()); @@ -129,7 +128,7 @@ RevCommit m = tr.commit().parent(a).parent(b).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets().put(b, psId(1, 1)), groups().put(psId(1, 1), group)); @@ -150,7 +149,7 @@ String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; String group2 = "1234567812345678123456781234567812345678"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets() .put(a, psId(1, 1)) @@ -176,7 +175,7 @@ String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; String group2a = "1234567812345678123456781234567812345678"; String group2b = "ef123456ef123456ef123456ef123456ef123456"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets() .put(a, psId(1, 1)) @@ -202,7 +201,7 @@ RevCommit m = tr.commit().parent(branchTip).parent(a).create(); String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group)); @@ -218,7 +217,7 @@ RevCommit a = tr.commit().parent(branchTip).create(); RevCommit m = tr.commit().parent(branchTip).parent(a).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets(), groups()); @@ -237,7 +236,7 @@ RevCommit m1 = tr.commit().parent(b).parent(c).create(); RevCommit m2 = tr.commit().parent(a).parent(m1).create(); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m2, branchTip), patchSets(), groups()); @@ -259,7 +258,7 @@ assertThat(m.getParentCount()).isEqualTo(2); assertThat(m.getParent(0)).isEqualTo(m.getParent(1)); - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets(), groups()); @@ -279,7 +278,7 @@ String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; String group2 = "1234567812345678123456781234567812345678"; - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( newWalk(m, branchTip), patchSets() .put(a, psId(1, 1)) @@ -307,7 +306,7 @@ rw.markStart(rw.parseCommit(d)); // Schema upgrade case: all commits are existing patch sets, but none have // groups assigned yet. - Multimap<ObjectId, String> groups = collectGroups( + SortedSetMultimap<ObjectId, String> groups = collectGroups( rw, patchSets() .put(branchTip, psId(1, 1)) @@ -339,9 +338,9 @@ return rw; } - private static Multimap<ObjectId, String> collectGroups( + private static SortedSetMultimap<ObjectId, String> collectGroups( RevWalk rw, - ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha, + ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha, ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup) throws Exception { GroupCollector gc = @@ -355,8 +354,9 @@ // Helper methods for constructing various map arguments, to avoid lots of // type specifications. - private static ImmutableMultimap.Builder<ObjectId, PatchSet.Id> patchSets() { - return ImmutableMultimap.builder(); + private static ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> + patchSets() { + return ImmutableListMultimap.builder(); } private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() {
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/SchemaUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/SchemaUtilTest.java index b623ae8..e6447da 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/SchemaUtilTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/SchemaUtilTest.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.index; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.index.SchemaUtil.getNameParts; import static com.google.gerrit.server.index.SchemaUtil.getPersonParts; import static com.google.gerrit.server.index.SchemaUtil.schema; @@ -68,4 +69,11 @@ "ba-z@example.com", "ba-z", "ba", "z", "example.com", "example", "com"); } + + @Test + public void getNamePartsExtractsParts() { + assertThat(getNameParts("")).isEmpty(); + assertThat(getNameParts("foO-bAr_Baz a.b@c/d")) + .containsExactly("foo", "bar", "baz", "a", "b", "c", "d"); + } }
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/FakeChangeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeChangeIndex.java index ea13ec4..43039f8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeChangeIndex.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -108,8 +108,4 @@ public void markReady(boolean ready) { throw new UnsupportedOperationException(); } - - @Override - public void stop() { - } }
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..adfd1fe --- /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.ImmutableListMultimap; +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())), + ImmutableListMultimap.of())) + .isFalse(); + + // Wrong ref value. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, SHA1), + P2, RefState.create(ref2, id2.name())), + ImmutableListMultimap.of())) + .isTrue(); + + // Swapped repos. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id2.name()), + P2, RefState.create(ref2, id1.name())), + ImmutableListMultimap.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())), + ImmutableListMultimap.of())) + .isFalse(); + + // Ignore ref not mentioned. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name())), + ImmutableListMultimap.of())) + .isFalse(); + + // One ref wrong. + assertThat( + refsAreStale( + repoManager, C, + ImmutableSetMultimap.of( + P1, RefState.create(ref1, id1.name()), + P1, RefState.create(ref3, SHA1)), + ImmutableListMultimap.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())), + ImmutableListMultimap.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())), + ImmutableListMultimap.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())), + ImmutableListMultimap.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())), + ImmutableListMultimap.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())), + ImmutableListMultimap.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())), + ImmutableListMultimap.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..33e34b6 --- /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.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.WatchConfig.NotifyType; +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..3d7abb0 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,36 +19,35 @@ 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; -import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; -import com.google.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 +75,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 +97,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 +159,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 +179,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 +201,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 +271,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 +348,10 @@ update.commit(); notes = newNotes(c); - assertThat(notes.getApprovals()).isEmpty(); + assertThat(notes.getApprovals()).containsExactlyEntriesIn( + ImmutableListMultimap.of( + psa.getPatchSetId(), + new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen()))); } @Test @@ -344,7 +373,10 @@ update.commit(); notes = newNotes(c); - assertThat(notes.getApprovals()).isEmpty(); + assertThat(notes.getApprovals()).containsExactlyEntriesIn( + ImmutableListMultimap.of( + psa.getPatchSetId(), + new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen()))); // Add back approval on same label. update = newUpdate(c, otherUser); @@ -368,13 +400,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 +417,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 +656,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 +893,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 +999,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 +1010,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 +1027,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 +1035,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 +1086,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 +1112,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 +1121,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 +1134,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 +1215,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,16 +1442,16 @@ 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); assertThat(notes.getComments()) - .isEqualTo(ImmutableMultimap.of(revId, comment)); + .isEqualTo(ImmutableListMultimap.of(revId, comment)); } @Test @@ -1293,16 +1462,16 @@ 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); assertThat(notes.getComments()) - .isEqualTo(ImmutableMultimap.of(revId, comment)); + .isEqualTo(ImmutableListMultimap.of(revId, comment)); } @Test @@ -1313,16 +1482,16 @@ 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); assertThat(notes.getComments()) - .isEqualTo(ImmutableMultimap.of(revId, comment)); + .isEqualTo(ImmutableListMultimap.of(revId, comment)); } @Test @@ -1333,16 +1502,16 @@ 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); assertThat(notes.getComments()) - .isEqualTo(ImmutableMultimap.of(revId, comment)); + .isEqualTo(ImmutableListMultimap.of(revId, comment)); } @Test @@ -1361,29 +1530,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 +1566,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 +1616,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 +1643,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 +1741,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 +1755,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,45 +1784,104 @@ 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( + ImmutableListMultimap.of( revId, comment1, revId, comment2, revId, comment3)); } @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(ImmutableListMultimap.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 +1896,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 +1915,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(ImmutableListMultimap.of(new RevId(comment.revId), comment)); } @Test @@ -1642,25 +1951,24 @@ 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( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev1), commentForBase, new RevId(rev2), commentForPS)); } @@ -1679,23 +1987,23 @@ 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( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev), comment1, new RevId(rev), comment2)).inOrder(); } @@ -1714,23 +2022,23 @@ 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( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev), comment1, new RevId(rev), comment2)).inOrder(); } @@ -1748,11 +2056,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,15 +2068,15 @@ 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( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev1), comment1, new RevId(rev2), comment2)); } @@ -1785,28 +2093,27 @@ 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); assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn( - ImmutableMultimap.of(new RevId(rev), comment1)); + ImmutableListMultimap.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); assertThat(notes.getDraftComments(otherUserId)).isEmpty(); assertThat(notes.getComments()).containsExactlyEntriesIn( - ImmutableMultimap.of(new RevId(rev), comment1)); + ImmutableListMultimap.of(new RevId(rev), comment1)); } @Test @@ -1826,19 +2133,19 @@ // 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); assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev), comment1, new RevId(rev), comment2)).inOrder(); assertThat(notes.getComments()).isEmpty(); @@ -1846,15 +2153,14 @@ // 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); assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn( - ImmutableMultimap.of(new RevId(rev), comment2)); + ImmutableListMultimap.of(new RevId(rev), comment2)); assertThat(notes.getComments()).containsExactlyEntriesIn( - ImmutableMultimap.of(new RevId(rev), comment1)); + ImmutableListMultimap.of(new RevId(rev), comment1)); } @Test @@ -1874,20 +2180,20 @@ // 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); assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev1), baseComment, new RevId(rev2), psComment)); assertThat(notes.getComments()).isEmpty(); @@ -1896,16 +2202,14 @@ 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); assertThat(notes.getDraftComments(otherUserId)).isEmpty(); assertThat(notes.getComments()).containsExactlyEntriesIn( - ImmutableMultimap.of( + ImmutableListMultimap.of( new RevId(rev1), baseComment, new RevId(rev2), psComment)); } @@ -1923,11 +2227,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 +2266,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 +2278,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 +2314,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 +2336,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 +2347,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,15 +2365,14 @@ 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( - ImmutableMultimap.of(new RevId(rev), comment)); + ImmutableListMultimap.of(new RevId(rev), comment)); } @Test @@ -2084,15 +2385,14 @@ 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( - ImmutableMultimap.of(new RevId(rev), comment)); + ImmutableListMultimap.of(new RevId(rev), comment)); } @Test @@ -2112,14 +2412,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 +2428,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 +2448,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 +2465,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 +2500,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 +2515,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 +2525,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 +2537,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 +2546,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 +2560,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 +2580,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 +2694,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..a7ccfc8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -16,29 +16,34 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.common.TimeUtil.nowTs; 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.server.notedb.NoteDbChangeState.RefState; +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 com.google.gerrit.testutil.TestTimeUtil; import org.eclipse.jgit.lib.ObjectId; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -/** Unit tests for {@link NoteDbChangeState}. */ -public class NoteDbChangeStateTest { - static { - KeyUtil.setEncoderImpl(new StandardKeyEncoder()); - } +import java.sql.Timestamp; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +/** Unit tests for {@link NoteDbChangeState}. */ +public class NoteDbChangeStateTest extends GerritBaseTests { ObjectId SHA1 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); ObjectId SHA2 = @@ -46,47 +51,98 @@ ObjectId SHA3 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"); - @Test - public void parseWithoutDrafts() { - NoteDbChangeState state = parse(new Change.Id(1), SHA1.name()); + @Before + public void setUp() { + TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS); + } + @After + public void tearDown() { + TestTimeUtil.useSystemTime(); + } + + @Test + 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.getReadOnlyUntil().isPresent()).isFalse(); + 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.getReadOnlyUntil().isPresent()).isFalse(); 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.getReadOnlyUntil().isPresent()).isFalse(); + 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.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.toString()).isEqualTo(expected); } @Test - public void applyDeltaToNullWithNoNewMetaId() { + public void parseReadOnlyUntil() { + Timestamp ts = new Timestamp(12345); + String str = "R=12345," + SHA1.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.getReadOnlyUntil().get()).isEqualTo(ts); + assertThat(state.toString()).isEqualTo(str); + + str = "N=12345"; + state = parse(new Change.Id(1), str); + assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); + assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); + assertThat(state.getRefState().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil().get()).isEqualTo(ts); + assertThat(state.toString()).isEqualTo(str); + } + + @Test + public void applyDeltaToNullWithNoNewMetaId() throws Exception { Change c = newChange(); assertThat(c.getNoteDbState()).isNull(); applyDelta(c, Delta.create(c.getId(), noMetaId(), noDrafts())); assertThat(c.getNoteDbState()).isNull(); - applyDelta(c, Delta.create(c.getId(), noMetaId(), - drafts(new Account.Id(1001), zeroId()))); + applyDelta( + c, + Delta.create( + c.getId(), noMetaId(), + drafts(new Account.Id(1001), zeroId()))); assertThat(c.getNoteDbState()).isNull(); } @Test - public void applyDeltaToMetaId() { + public void applyDeltaToMetaId() throws Exception { Change c = newChange(); applyDelta(c, Delta.create(c.getId(), metaId(SHA1), noDrafts())); assertThat(c.getNoteDbState()).isEqualTo(SHA1.name()); @@ -104,28 +160,79 @@ } @Test - public void applyDeltaToDrafts() { + public void applyDeltaToDrafts() throws Exception { Change c = newChange(); - applyDelta(c, Delta.create(c.getId(), metaId(SHA1), - drafts(new Account.Id(1001), SHA2))); + applyDelta( + c, + Delta.create( + c.getId(), metaId(SHA1), + drafts(new Account.Id(1001), SHA2))); assertThat(c.getNoteDbState()).isEqualTo( SHA1.name() + ",1001=" + SHA2.name()); - applyDelta(c, Delta.create(c.getId(), noMetaId(), - drafts(new Account.Id(2003), SHA3))); + applyDelta( + c, + Delta.create( + c.getId(), noMetaId(), + drafts(new Account.Id(2003), SHA3))); assertThat(c.getNoteDbState()).isEqualTo( SHA1.name() + ",1001=" + SHA2.name() + ",2003=" + SHA3.name()); - applyDelta(c, Delta.create(c.getId(), noMetaId(), - drafts(new Account.Id(2003), zeroId()))); + applyDelta( + c, + Delta.create( + c.getId(), noMetaId(), + drafts(new Account.Id(2003), zeroId()))); assertThat(c.getNoteDbState()).isEqualTo( SHA1.name() + ",1001=" + SHA2.name()); - applyDelta(c, Delta.create(c.getId(), metaId(SHA3), noDrafts())); + applyDelta( + c, Delta.create(c.getId(), metaId(SHA3), noDrafts())); assertThat(c.getNoteDbState()).isEqualTo( SHA3.name() + ",1001=" + SHA2.name()); } + @Test + public void applyDeltaToReadOnly() throws Exception { + Timestamp ts = nowTs(); + Change c = newChange(); + NoteDbChangeState state = new NoteDbChangeState(c.getId(), + REVIEW_DB, + Optional.of(RefState.create(SHA1, ImmutableMap.of())), + Optional.of(new Timestamp(ts.getTime() + 10000))); + c.setNoteDbState(state.toString()); + Delta delta = Delta.create(c.getId(), metaId(SHA2), noDrafts()); + applyDelta(c, delta); + assertThat(NoteDbChangeState.parse(c)).isEqualTo( + new NoteDbChangeState( + state.getChangeId(), + state.getPrimaryStorage(), + Optional.of(RefState.create(SHA2, ImmutableMap.of())), + state.getReadOnlyUntil())); + } + + @Test + public void parseNoteDbPrimary() { + NoteDbChangeState state = parse(new Change.Id(1), "N"); + assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); + assertThat(state.getRefState().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void parseInvalidPrimaryStorage() { + parse(new Change.Id(1), "X"); + } + + @Test + public void applyDeltaToNoteDbPrimaryIsNoOp() throws Exception { + 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 +241,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..e2efc4a 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,8 @@ 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.index.SingleVersionModule.SingleVersionListener; +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 +139,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(); } @@ -239,8 +241,10 @@ @Inject private CapabilityCollection.Factory capabilityCollectionFactory; @Inject private CapabilityControl.Factory capabilityControlFactory; @Inject private SchemaCreator schemaCreator; + @Inject private SingleVersionListener singleVersionListener; @Inject private InMemoryDatabase schemaFactory; @Inject private ThreadLocalRequestContext requestContext; + @Inject private Provider<InternalChangeQuery> queryProvider; @Before public void setUp() throws Exception { @@ -315,7 +319,12 @@ } db = schemaFactory.open(); - schemaCreator.create(db); + singleVersionListener.start(); + try { + schemaCreator.create(db); + } finally { + singleVersionListener.stop(); + } Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c = CacheBuilder.newBuilder().build(); @@ -355,14 +364,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 +379,7 @@ } @Test - public void testBlockOwnerProject() { + public void blockOwnerProject() { allow(local, OWNER, ADMIN, "refs/*"); block(local, OWNER, DEVS, "refs/*"); @@ -378,7 +387,7 @@ } @Test - public void testBranchDelegation1() { + public void branchDelegation1() { allow(local, OWNER, ADMIN, "refs/*"); allow(local, OWNER, DEVS, "refs/heads/x/*"); @@ -395,7 +404,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 +433,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 +447,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 +457,7 @@ } @Test - public void testBlockPushDraftsUnblockAdmin() { + public void blockPushDraftsUnblockAdmin() { block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*"); allow(parent, PUSH, ADMIN, "refs/drafts/*"); @@ -459,7 +468,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 +480,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 +493,7 @@ } @Test - public void testInheritRead_OverrideWithDeny() { + public void inheritRead_OverrideWithDeny() { allow(parent, READ, REGISTERED_USERS, "refs/*"); deny(local, READ, REGISTERED_USERS, "refs/*"); @@ -492,7 +501,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 +513,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 +526,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 +538,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 +549,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 +566,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 +576,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 +586,7 @@ } @Test - public void testSortWithRegex() { + public void sortWithRegex() { allow(local, READ, DEVS, "^refs/heads/.*"); allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*"); @@ -588,7 +597,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 +605,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 +615,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 +629,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 +646,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 +656,7 @@ } @Test - public void testUnblockNoForce() { + public void unblockNoForce() { block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); allow(local, PUSH, DEVS, "refs/heads/*"); @@ -656,7 +665,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 +675,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 +685,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 +694,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 +740,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 +749,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 +759,7 @@ } @Test - public void testUnblockVisibilityByRegisteredUsers() { + public void unblockVisibilityByRegisteredUsers() { block(local, READ, ANONYMOUS_USERS, "refs/heads/*"); allow(local, READ, REGISTERED_USERS, "refs/heads/*"); @@ -724,7 +770,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 +781,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 +792,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 +803,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 +814,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 +825,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 +836,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 +849,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 +860,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 +871,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 +898,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 +936,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..64a71db 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,17 @@ 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.DraftHandling; +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,9 +55,12 @@ 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.PatchSetUtil; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.account.AccountManager; @@ -62,12 +68,19 @@ import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ChangeTriplet; import com.google.gerrit.server.change.PatchSetInserter; +import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.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.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.RequestContext; @@ -87,6 +100,8 @@ import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.SystemReader; import org.junit.After; @@ -94,13 +109,16 @@ 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; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; @Ignore @@ -113,6 +131,7 @@ } @Inject protected AccountManager accountManager; + @Inject protected AllUsersName allUsersName; @Inject protected BatchUpdate.Factory updateFactory; @Inject protected ChangeInserter.Factory changeFactory; @Inject protected ChangeQueryBuilder queryBuilder; @@ -120,17 +139,20 @@ @Inject protected IdentifiedUser.GenericFactory userFactory; @Inject protected ChangeIndexCollection indexes; @Inject protected ChangeIndexer indexer; + @Inject protected IndexConfig indexConfig; @Inject protected InMemoryDatabase schemaFactory; @Inject protected InMemoryRepositoryManager repoManager; @Inject protected InternalChangeQuery internalChangeQuery; @Inject protected ChangeNotes.Factory notesFactory; @Inject protected PatchSetInserter.Factory patchSetFactory; + @Inject protected PatchSetUtil psUtil; @Inject protected ChangeControl.GenericFactory changeControlFactory; @Inject protected ChangeQueryProcessor queryProcessor; @Inject protected SchemaCreator schemaCreator; @Inject protected Sequences seq; @Inject protected ThreadLocalRequestContext requestContext; + protected Injector injector; protected LifecycleManager lifecycle; protected ReviewDb db; protected Account.Id userId; @@ -143,13 +165,17 @@ @Before public void setUpInjector() throws Exception { lifecycle = new LifecycleManager(); - Injector injector = createInjector(); + injector = createInjector(); lifecycle.add(injector); injector.injectMembers(this); lifecycle.start(); + setUpDatabase(); + } + protected void setUpDatabase() throws Exception { db = schemaFactory.open(); schemaCreator.create(db); + userId = accountManager.authenticate(AuthRequest.forUser("user")) .getAccountId(); Account userAccount = db.accounts().get(userId); @@ -189,7 +215,7 @@ @Before public void setTimeForTesting() { - resetTimeWithClockStep(1, MILLISECONDS); + resetTimeWithClockStep(1, SECONDS); } private void resetTimeWithClockStep(long clockStep, TimeUnit clockStepUnit) { @@ -244,7 +270,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 +369,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 +399,9 @@ assertQuery("owner:" + userId.get(), change1); assertQuery("owner:" + user2, change2); + + String nameEmail = user.asIdentifiedUser().getNameEmail(); + assertQuery("owner: \"" + nameEmail + "\"", change1); } @Test @@ -619,6 +651,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 +796,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 +999,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 +1027,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 +1051,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"); @@ -1244,6 +1313,59 @@ } @Test + public void byDraftByExcludesZombieDrafts() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + Project.NameKey project = new Project.NameKey("repo"); + TestRepository<Repo> repo = createProject(project.get()); + Change change = insert(repo, newChange(repo)); + Change.Id id = change.getId(); + + DraftInput in = new DraftInput(); + in.line = 1; + in.message = "nit: trailing whitespace"; + in.path = Patch.COMMIT_MSG; + gApi.changes().id(id.get()).current().createDraft(in); + + assertQuery("draftby:" + userId, change); + assertQuery("commentby:" + userId); + + TestRepository<Repo> allUsers = + new TestRepository<>(repoManager.openRepository(allUsersName)); + + Ref draftsRef = allUsers.getRepository().exactRef( + RefNames.refsDraftComments(id, userId)); + assertThat(draftsRef).isNotNull(); + + ReviewInput rin = ReviewInput.dislike(); + rin.drafts = DraftHandling.PUBLISH_ALL_REVISIONS; + gApi.changes().id(id.get()).current().review(rin); + + assertQuery("draftby:" + userId); + assertQuery("commentby:" + userId, change); + assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNull(); + + // Re-add drafts ref and ensure it gets filtered out during indexing. + allUsers.update(draftsRef.getName(), draftsRef.getObjectId()); + assertThat(allUsers.getRepository().exactRef(draftsRef.getName())) + .isNotNull(); + + if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB) { + // Record draft ref in noteDbState as well. + ReviewDb db = ReviewDbUtil.unwrapDb(this.db); + change = db.changes().get(id); + NoteDbChangeState.applyDelta(change, + NoteDbChangeState.Delta.create( + id, Optional.empty(), + ImmutableMap.of(userId, draftsRef.getObjectId()))); + db.changes().update(Collections.singleton(change)); + } + + indexer.index(db, project, id); + assertQuery("draftby:" + userId); + } + + @Test public void byStarredBy() throws Exception { TestRepository<Repo> repo = createProject("repo"); Change change1 = insert(repo, newChange(repo)); @@ -1417,6 +1539,76 @@ } @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)); + String changeId1 = change1.getKey().get(); + Change change2 = insert(repo, newChange(repo)); + String changeId2 = change2.getKey().get(); + + requestContext.setContext(newRequestContext(user1)); + assertQuery("has:edit"); + gApi.changes() + .id(changeId1) + .edit() + .create(); + gApi.changes() + .id(changeId2) + .edit() + .create(); + + requestContext.setContext(newRequestContext(user2)); + assertQuery("has:edit"); + gApi.changes() + .id(changeId2) + .edit() + .create(); + + 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 +1628,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 +1692,133 @@ 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)); + String changeId = change.getKey().get(); + ChangeNotes notes = + notesFactory.create(db, change.getProject(), change.getId()); + PatchSet ps = psUtil.get(db, notes, change.currentPatchSetId()); + + requestContext.setContext(newRequestContext(user)); + gApi.changes() + .id(changeId) + .edit() + .create(); + 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 { + // This test method manages primary storage manually. + assume().that(notesMigration.changePrimaryStorage()) + .isEqualTo(PrimaryStorage.REVIEW_DB); + 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(); + String changeId = change.getKey().get(); + 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 + gApi.changes() + .id(changeId) + .edit() + .create(); + + // 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 +1866,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 +1901,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 +1915,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 +1960,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 +1984,7 @@ .append("status=").append(c.status).append(", ") .append("lastUpdated=").append(c.updated.getTime()) .append("}"); - if (it.hasNext()) { + if (changeIds.hasNext()) { b.append(", "); } } @@ -1674,23 +1993,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/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java new file mode 100644 index 0000000..0ecc125 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -0,0 +1,453 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.api.GerritApi; +import com.google.gerrit.extensions.api.groups.GroupInput; +import com.google.gerrit.extensions.api.groups.Groups.QueryRequest; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.common.GroupInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.lifecycle.LifecycleManager; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.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.AuthRequest; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.query.account.InternalAccountQuery; +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; +import com.google.gerrit.testutil.GerritServerTests; +import com.google.gerrit.testutil.InMemoryDatabase; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.util.Providers; + +import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +@Ignore +public abstract class AbstractQueryGroupsTest extends GerritServerTests { + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + cfg.setInt("index", null, "maxPages", 10); + return cfg; + } + + @Rule + public final TestName testName = new TestName(); + + @Inject + protected AccountCache accountCache; + + @Inject + protected AccountManager accountManager; + + @Inject + protected GerritApi gApi; + + @Inject + protected IdentifiedUser.GenericFactory userFactory; + + @Inject + private Provider<AnonymousUser> anonymousUser; + + @Inject + protected InMemoryDatabase schemaFactory; + + @Inject + protected SchemaCreator schemaCreator; + + @Inject + protected ThreadLocalRequestContext requestContext; + + @Inject + protected OneOffRequestContext oneOffRequestContext; + + @Inject + protected InternalAccountQuery internalAccountQuery; + + @Inject + protected AllProjectsName allProjects; + + @Inject + protected GroupCache groupCache; + + protected LifecycleManager lifecycle; + protected ReviewDb db; + protected AccountInfo currentUserInfo; + protected CurrentUser user; + + protected abstract Injector createInjector(); + + @Before + public void setUpInjector() throws Exception { + lifecycle = new LifecycleManager(); + Injector injector = createInjector(); + lifecycle.add(injector); + injector.injectMembers(this); + lifecycle.start(); + + db = schemaFactory.open(); + schemaCreator.create(db); + + Account.Id userId = createAccount("user", "User", "user@example.com", true); + user = userFactory.create(userId); + requestContext.setContext(newRequestContext(userId)); + currentUserInfo = gApi.accounts().id(userId.get()).get(); + } + + protected RequestContext newRequestContext(Account.Id requestUserId) { + final CurrentUser requestUser = + userFactory.create(requestUserId); + return new RequestContext() { + @Override + public CurrentUser getUser() { + return requestUser; + } + + @Override + public Provider<ReviewDb> getReviewDbProvider() { + return Providers.of(db); + } + }; + } + + protected void setAnonymous() { + requestContext.setContext(new RequestContext() { + @Override + public CurrentUser getUser() { + return anonymousUser.get(); + } + + @Override + public Provider<ReviewDb> getReviewDbProvider() { + return Providers.of(db); + } + }); + } + + @After + public void tearDownInjector() { + if (lifecycle != null) { + lifecycle.stop(); + } + requestContext.setContext(null); + if (db != null) { + db.close(); + } + InMemoryDatabase.drop(schemaFactory); + } + + @Test + public void byUuid() throws Exception { + assertQuery("uuid:6d70856bc40ded50f2585c4c0f7e179f3544a272"); + assertQuery("uuid:non-existing"); + + GroupInfo group = createGroup(name("group")); + assertQuery("uuid:" + group.id, group); + + GroupInfo admins = gApi.groups().id("Administrators").get(); + assertQuery("uuid:" + admins.id, admins); + } + + @Test + public void byName() throws Exception { + assertQuery("name:non-existing"); + + GroupInfo group = createGroup(name("group")); + assertQuery("name:" + group.name, group); + assertQuery("name:" + group.name.toUpperCase(Locale.US), group); + + // only exact match + GroupInfo groupWithHyphen = createGroup(name("group-with-hyphen")); + createGroup(name("group-no-match-with-hyphen")); + assertQuery("name:" + groupWithHyphen.name, groupWithHyphen); + } + + @Test + public void byInname() throws Exception { + String namePart = testName.getMethodName(); + GroupInfo group1 = createGroup("group-" + namePart); + GroupInfo group2 = createGroup("group-" + namePart + "-2"); + GroupInfo group3 = createGroup("group-" + namePart + "3"); + assertQuery("inname:" + namePart, group1, group2, group3); + assertQuery("inname:" + namePart.toUpperCase(Locale.US), group1, group2, + group3); + assertQuery("inname:" + namePart.toLowerCase(Locale.US), group1, group2, + group3); + } + + @Test + public void byDescription() throws Exception { + GroupInfo group1 = + createGroupWithDescription(name("group1"), "This is a test group."); + GroupInfo group2 = + createGroupWithDescription(name("group2"), "ANOTHER TEST GROUP."); + createGroupWithDescription(name("group3"), "Maintainers of project foo."); + assertQuery("description:test", group1, group2); + + assertQuery("description:non-existing"); + + exception.expect(BadRequestException.class); + exception.expectMessage("description operator requires a value"); + assertQuery("description:\"\""); + } + + @Test + public void byOwner() throws Exception { + assertQuery("owner:non-existing"); + + GroupInfo ownerGroup = createGroup(name("owner-group")); + GroupInfo group = createGroupWithOwner(name("group"), ownerGroup); + createGroup(name("group2")); + + // ownerGroup owns itself + assertQuery("owner:" + ownerGroup.id, group, ownerGroup); + } + + @Test + public void byIsVisibleToAll() throws Exception { + assertQuery("is:visibletoall"); + + GroupInfo groupThatIsVisibleToAll = + createGroupThatIsVisibleToAll(name("group-that-is-visible-to-all")); + createGroup(name("group")); + + assertQuery("is:visibletoall", groupThatIsVisibleToAll); + } + + @Test + public void byDefaultField() throws Exception { + GroupInfo group1 = createGroup(name("foo-group")); + GroupInfo group2 = createGroup(name("group2")); + GroupInfo group3 = createGroupWithDescription(name("group3"), + "decription that contains foo and the UUID of group2: " + group2.id); + + assertQuery("non-existing"); + assertQuery("foo", group1, group3); + assertQuery(group2.id, group2, group3); + } + + @Test + public void withLimit() throws Exception { + GroupInfo group1 = createGroup(name("group1")); + GroupInfo group2 = createGroup(name("group2")); + GroupInfo group3 = createGroup(name("group3")); + + String query = + "uuid:" + group1.id + " OR uuid:" + group2.id + " OR uuid:" + group3.id; + List<GroupInfo> result = assertQuery(query, group1, group2, group3); + assertThat(result.get(result.size() - 1)._moreGroups).isNull(); + + result = assertQuery(newQuery(query).withLimit(2), result.subList(0, 2)); + assertThat(result.get(result.size() - 1)._moreGroups).isTrue(); + } + + @Test + public void withStart() throws Exception { + GroupInfo group1 = createGroup(name("group1")); + GroupInfo group2 = createGroup(name("group2")); + GroupInfo group3 = createGroup(name("group3")); + + String query = + "uuid:" + group1.id + " OR uuid:" + group2.id + " OR uuid:" + group3.id; + List<GroupInfo> result = assertQuery(query, group1, group2, group3); + + assertQuery(newQuery(query).withStart(1), result.subList(1, 3)); + } + + @Test + public void asAnonymous() throws Exception { + GroupInfo group = createGroup(name("group")); + + setAnonymous(); + assertQuery("uuid:" + group.id); + } + + // reindex permissions are tested by {@link GroupsIT#reindexPermissions} + @Test + public void reindex() throws Exception { + GroupInfo group1 = createGroupWithDescription(name("group"), "barX"); + + // update group in the database so that group index is stale + String newDescription = "barY"; + AccountGroup group = + db.accountGroups().get(new AccountGroup.Id(group1.groupId)); + group.setDescription(newDescription); + db.accountGroups().update(Collections.singleton(group)); + + assertQuery("description:" + group1.description, group1); + assertQuery("description:" + newDescription); + + gApi.groups().id(group1.id).index(); + assertQuery("description:" + group1.description); + assertQuery("description:" + newDescription, group1); + } + + private Account.Id createAccount(String username, String fullName, + String email, boolean active) throws Exception { + try (ManualRequestContext ctx = oneOffRequestContext.open()) { + Account.Id id = + accountManager.authenticate(AuthRequest.forUser(username)).getAccountId(); + if (email != null) { + accountManager.link(id, AuthRequest.forEmail(email)); + } + 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; + } + } + + protected GroupInfo createGroup(String name, AccountInfo... members) + throws Exception { + return createGroupWithDescription(name, null, members); + } + + protected GroupInfo createGroupWithDescription(String name, + String description, AccountInfo... members) throws Exception { + GroupInput in = new GroupInput(); + in.name = name; + in.description = description; + in.members = Arrays.asList(members).stream() + .map(a -> String.valueOf(a._accountId)).collect(toList()); + return gApi.groups().create(in).get(); + } + + protected GroupInfo createGroupWithOwner(String name, GroupInfo ownerGroup) + throws Exception { + GroupInput in = new GroupInput(); + in.name = name; + in.ownerId = ownerGroup.id; + return gApi.groups().create(in).get(); + } + + protected GroupInfo createGroupThatIsVisibleToAll(String name) + throws Exception { + GroupInput in = new GroupInput(); + in.name = name; + in.visibleToAll = true; + return gApi.groups().create(in).get(); + } + + protected GroupInfo getGroup(AccountGroup.UUID uuid) throws Exception { + return gApi.groups().id(uuid.get()).get(); + } + + protected List<GroupInfo> assertQuery(Object query, GroupInfo... groups) + throws Exception { + return assertQuery(newQuery(query), groups); + } + + protected List<GroupInfo> assertQuery(QueryRequest query, + GroupInfo... groups) throws Exception { + return assertQuery(query, Arrays.asList(groups)); + } + + protected List<GroupInfo> assertQuery(QueryRequest query, + List<GroupInfo> groups) throws Exception { + List<GroupInfo> result = query.get(); + Iterable<String> uuids = uuids(result); + assertThat(uuids).named(format(query, result, groups)) + .containsExactlyElementsIn(uuids(groups)); + return result; + } + + protected QueryRequest newQuery(Object query) { + return gApi.groups().query(query.toString()); + } + + protected String format(QueryRequest query, List<GroupInfo> actualGroups, + List<GroupInfo> expectedGroups) { + StringBuilder b = new StringBuilder(); + b.append("query '").append(query.getQuery()) + .append("' with expected groups "); + b.append(format(expectedGroups)); + b.append(" and result "); + b.append(format(actualGroups)); + return b.toString(); + } + + protected String format(Iterable<GroupInfo> groups) { + StringBuilder b = new StringBuilder(); + b.append("["); + Iterator<GroupInfo> it = groups.iterator(); + while (it.hasNext()) { + GroupInfo g = it.next(); + b.append("{").append(g.id).append(", ").append("name=").append(g.name) + .append(", ").append("groupId=").append(g.groupId).append(", ") + .append("url=").append(g.url).append(", ").append("ownerId=") + .append(g.ownerId).append(", ").append("owner=").append(g.owner) + .append(", ").append("description=").append(g.description) + .append(", ").append("visibleToAll=") + .append(toBoolean(g.options.visibleToAll)).append("}"); + if (it.hasNext()) { + b.append(", "); + } + } + b.append("]"); + return b.toString(); + } + + protected static boolean toBoolean(Boolean b) { + return b == null ? false : b; + } + + protected static Iterable<String> ids(GroupInfo... groups) { + return uuids(Arrays.asList(groups)); + } + + protected static Iterable<String> uuids(List<GroupInfo> groups) { + return groups.stream().map(g -> g.id).collect(toList()); + } + + protected String name(String name) { + if (name == null) { + return null; + } + return name + "_" + testName.getMethodName().toLowerCase(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java new file mode 100644 index 0000000..d8deca6 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
@@ -0,0 +1,31 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.query.group; + +import com.google.gerrit.testutil.InMemoryModule; +import com.google.inject.Guice; +import com.google.inject.Injector; + +import org.eclipse.jgit.lib.Config; + +public class LuceneQueryGroupsTest extends AbstractQueryGroupsTest { + @Override + protected Injector createInjector() { + Config luceneConfig = new Config(config); + InMemoryModule.setDefaults(luceneConfig); + return Guice.createInjector( + new InMemoryModule(luceneConfig, notesMigration)); + } +}
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..dbb7db6 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; @@ -29,6 +29,7 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.testutil.InMemoryDatabase; import com.google.gerrit.testutil.InMemoryH2Type; import com.google.gerrit.testutil.InMemoryRepositoryManager; @@ -36,6 +37,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 +73,7 @@ } @Test - public void testUpdate() throws OrmException, FileNotFoundException, + public void update() throws OrmException, FileNotFoundException, IOException { db.create(); @@ -108,9 +110,26 @@ .toProvider(AnonymousCowardNameProvider.class); bind(DataSourceType.class).to(InMemoryH2Type.class); + + bind(SystemGroupBackend.class); } }).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 +156,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/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java index 11d7ad0..885a1f5 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -22,7 +22,6 @@ import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess; import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess; import com.google.gerrit.reviewdb.server.AccountGroupNameAccess; -import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess; import com.google.gerrit.reviewdb.server.ChangeAccess; import com.google.gerrit.reviewdb.server.ChangeMessageAccess; import com.google.gerrit.reviewdb.server.PatchLineCommentAccess; @@ -115,11 +114,6 @@ } @Override - public AccountProjectWatchAccess accountProjectWatches() { - throw new Disabled(); - } - - @Override public ChangeAccess changes() { throw new Disabled(); } @@ -168,9 +162,4 @@ public int nextChangeId() { throw new Disabled(); } - - @Override - public int nextChangeMessageId() { - throw new Disabled(); - } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java deleted file mode 100644 index c3bfe1e..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java +++ /dev/null
@@ -1,55 +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.testutil; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.Sets; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.account.AccountByEmailCache; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** Fake implementation of {@link AccountByEmailCache} for testing. */ -public class FakeAccountByEmailCache implements AccountByEmailCache { - private final SetMultimap<String, Account.Id> byEmail; - private final Set<Account.Id> anyEmail; - - public FakeAccountByEmailCache() { - byEmail = HashMultimap.create(); - anyEmail = new HashSet<>(); - } - - @Override - public synchronized Set<Account.Id> get(String email) { - return Collections.unmodifiableSet( - Sets.union(byEmail.get(email), anyEmail)); - } - - @Override - public synchronized void evict(String email) { - // Do nothing. - } - - public synchronized void put(String email, Account.Id id) { - byEmail.put(email, id); - } - - public synchronized void putAny(Account.Id id) { - anyEmail.add(id); - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java index 07cd63e..76f24df 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
@@ -19,9 +19,9 @@ 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.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; import java.util.HashMap;
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/FilesystemLoggingMockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java deleted file mode 100644 index bbcb6a9..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java +++ /dev/null
@@ -1,178 +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.testutil; - -import com.google.common.base.Strings; - -import org.eclipse.jgit.util.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -public abstract class FilesystemLoggingMockingTestCase extends LoggingMockingTestCase { - - private Collection<File> toCleanup = new ArrayList<>(); - - /** - * Asserts that a given file exists. - * - * @param file The file to test. - */ - protected void assertExists(File file) { - assertTrue("File '" + file.getAbsolutePath() + "' does not exist", - file.exists()); - } - - /** - * Asserts that a given file does not exist. - * - * @param file The file to test. - */ - protected void assertDoesNotExist(File file) { - assertFalse("File '" + file.getAbsolutePath() + "' exists", file.exists()); - } - - /** - * Asserts that a given file exists and is a directory. - * - * @param file The file to test. - */ - protected void assertDirectory(File file) { - // Although isDirectory includes checking for existence, we nevertheless - // explicitly check for existence, to get more appropriate error messages - assertExists(file); - assertTrue("File '" + file.getAbsolutePath() + "' is not a directory", - file.isDirectory()); - } - - /** - * Asserts that creating a directory from the given file worked - * - * @param file The directory to create - */ - protected void assertMkdirs(File file) { - assertTrue("Could not create directory '" + file.getAbsolutePath() + "'", - file.mkdirs()); - } - - /** - * Asserts that creating a directory from the specified file worked - * - * @param parent The parent of the directory to create - * @param name The name of the directoryto create (relative to {@code parent} - * @return The created directory - */ - protected File assertMkdirs(File parent, String name) { - File file = new File(parent, name); - assertMkdirs(file); - return file; - } - - /** - * Asserts that creating a file worked - * - * @param file The file to create - */ - protected void assertCreateFile(File file) throws IOException { - assertTrue("Could not create file '" + file.getAbsolutePath() + "'", - file.createNewFile()); - } - - /** - * Asserts that creating a file worked - * - * @param parent The parent of the file to create - * @param name The name of the file to create (relative to {@code parent} - * @return The created file - */ - protected File assertCreateFile(File parent, String name) throws IOException { - File file = new File(parent, name); - assertCreateFile(file); - return file; - } - - /** - * Creates a file in the system's default folder for temporary files. - * - * The file/directory automatically gets removed during tearDown. - * - * The name of the created file begins with 'gerrit_test_', and is located - * in the system's default folder for temporary files. - * - * @param suffix Trailing part of the file name. - * @return The temporary file. - * @throws IOException If a file could not be created. - */ - private File createTempFile(String suffix) throws IOException { - String prefix = "gerrit_test_"; - if (!Strings.isNullOrEmpty(getName())) { - prefix += getName() + "_"; - } - File tmp = File.createTempFile(prefix, suffix); - toCleanup.add(tmp); - return tmp; - } - - /** - * Creates a file in the system's default folder for temporary files. - * - * The file/directory automatically gets removed during tearDown. - * - * The name of the created file begins with 'gerrit_test_', and is located - * in the system's default folder for temporary files. - * - * @return The temporary file. - * @throws IOException If a file could not be created. - */ - protected File createTempFile() throws IOException { - return createTempFile(""); - } - - /** - * Creates a directory in the system's default folder for temporary files. - * - * The directory (and all it's contained files/directory) automatically get - * removed during tearDown. - * - * The name of the created directory begins with 'gerrit_test_', and is be - * located in the system's default folder for temporary files. - * - * @return The temporary directory. - * @throws IOException If a file could not be created. - */ - protected File createTempDir() throws IOException { - File tmp = createTempFile(".dir"); - if (!tmp.delete()) { - throw new IOException("Cannot delete temporary file '" + tmp.getPath() - + "'"); - } - tmp.mkdir(); - return tmp; - } - - private void cleanupCreatedFiles() throws IOException { - for (File file : toCleanup) { - FileUtils.delete(file, FileUtils.RECURSIVE); - } - } - - @Override - public void tearDown() throws Exception { - cleanupCreatedFiles(); - super.tearDown(); - } -}
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/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java index 458e100..ae0a23a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -17,15 +17,20 @@ import static com.google.common.truth.Truth.assertThat; import com.google.gerrit.lifecycle.LifecycleManager; +import com.google.gerrit.pgm.init.index.elasticsearch.ElasticIndexModuleOnInit; +import com.google.gerrit.pgm.init.index.lucene.LuceneIndexModuleOnInit; import com.google.gerrit.reviewdb.client.CurrentSchemaVersion; import com.google.gerrit.reviewdb.client.SystemConfig; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.index.IndexModule; +import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.schema.SchemaVersion; import com.google.gwtorm.jdbc.Database; import com.google.gwtorm.jdbc.SimpleDataSource; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; @@ -71,15 +76,44 @@ } private final SchemaCreator schemaCreator; + private final SingleVersionListener singleVersionListener; + private Connection openHandle; private Database<ReviewDb> database; private boolean created; @Inject - InMemoryDatabase(SchemaCreator schemaCreator) throws OrmException { - this.schemaCreator = schemaCreator; + InMemoryDatabase(Injector injector) throws OrmException { + Injector childInjector = injector.createChildInjector(new AbstractModule() { + @Override + protected void configure() { + switch (IndexModule.getIndexType(injector)) { + case LUCENE: + install(new LuceneIndexModuleOnInit()); + break; + case ELASTICSEARCH: + install(new ElasticIndexModuleOnInit()); + break; + default: + throw new IllegalStateException("unsupported index.type"); + } + } + }); + this.schemaCreator = childInjector.getInstance(SchemaCreator.class); + this.singleVersionListener = + childInjector.getInstance(SingleVersionListener.class); + initDatabase(); + } + InMemoryDatabase(SchemaCreator schemaCreator, + SingleVersionListener singleVersionListener) throws OrmException { + this.schemaCreator = schemaCreator; + this.singleVersionListener = singleVersionListener; + initDatabase(); + } + + private void initDatabase() throws OrmException { try { DataSource dataSource = newDataSource(); @@ -112,9 +146,12 @@ if (!created) { created = true; try (ReviewDb c = open()) { + singleVersionListener.start(); schemaCreator.create(c); } catch (IOException | ConfigInvalidException e) { throw new OrmException("Cannot create in-memory database", e); + } finally { + singleVersionListener.stop(); } } return this;
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..254c7e7 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; @@ -48,8 +49,14 @@ import com.google.gerrit.server.git.SearchingChangeCacheImpl; import com.google.gerrit.server.git.SendEmailExecutor; import com.google.gerrit.server.index.IndexModule.IndexType; +import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener; +import com.google.gerrit.server.index.account.AllAccountsIndexer; +import com.google.gerrit.server.index.change.AllChangesIndexer; import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.server.index.group.AllGroupsIndexer; import com.google.gerrit.server.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; @@ -70,6 +77,7 @@ import com.google.inject.Singleton; import com.google.inject.TypeLiteral; import com.google.inject.servlet.RequestScoped; +import com.google.inject.util.Providers; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; @@ -148,6 +156,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 +185,7 @@ .to(InMemoryH2Type.class); bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}) .to(InMemoryDatabase.class); + bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class); bind(SecureStore.class).to(DefaultSecureStore.class); @@ -203,6 +214,10 @@ install(new GpgModule(cfg)); install(new H2AccountPatchReviewStore.InMemoryModule()); + bind(AllAccountsIndexer.class).toProvider(Providers.of(null)); + bind(AllChangesIndexer.class).toProvider(Providers.of(null)); + bind(AllGroupsIndexer.class).toProvider(Providers.of(null)); + IndexType indexType = null; try { indexType = cfg.getEnum("index", null, "type", IndexType.LUCENE); @@ -214,6 +229,9 @@ case LUCENE: install(luceneIndexModule()); break; + case ELASTICSEARCH: + install(elasticIndexModule()); + break; default: throw new ProvisionException( "index type unsupported in tests: " + indexType); @@ -230,20 +248,27 @@ @Provides @Singleton - InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator) - throws OrmException { - return new InMemoryDatabase(schemaCreator); + InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator, + SingleVersionListener singleVersionListener) throws OrmException { + return new InMemoryDatabase(schemaCreator, singleVersionListener); } 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 e7bd8f8..b6e59dd 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/LoggingMockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java deleted file mode 100644 index d7140ec..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/LoggingMockingTestCase.java +++ /dev/null
@@ -1,134 +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.testutil; - -import com.google.gerrit.testutil.log.LogUtil; - -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; -import org.apache.log4j.spi.LoggingEvent; -import org.junit.After; - -import java.util.ArrayList; -import java.util.Iterator; - -/** - * Testcase capturing associated logs and allowing to assert on them. - * - * For a test case SomeNameTest, the log for SomeName gets captured. Assertions - * on logs run against the coptured log events from this logger. After the - * tests, the logger are set back to their original settings. - */ -public abstract class LoggingMockingTestCase extends MockingTestCase { - private String loggerName; - private LogUtil.LoggerSettings loggerSettings; - private java.util.Collection<LoggingEvent> loggedEvents; - - /** - * Assert a logged event with a given string. - * <p> - * If such a event is found, it is removed from the captured logs. - * - * @param needle The string to look for. - */ - protected final void assertLogMessageContains(String needle) { - LoggingEvent hit = null; - Iterator<LoggingEvent> iter = loggedEvents.iterator(); - while (hit == null && iter.hasNext()) { - LoggingEvent event = iter.next(); - if (event.getRenderedMessage().contains(needle)) { - hit = event; - } - } - assertNotNull("Could not find log message containing '" + needle + "'", - hit); - assertTrue("Could not remove log message containing '" + needle + "'", - loggedEvents.remove(hit)); - } - - /** - * Assert a logged event whose throwable contains a given string - * <p> - * If such a event is found, it is removed from the captured logs. - * - * @param needle The string to look for. - */ - protected final void assertLogThrowableMessageContains(String needle) { - LoggingEvent hit = null; - Iterator<LoggingEvent> iter = loggedEvents.iterator(); - while (hit == null && iter.hasNext()) { - LoggingEvent event = iter.next(); - if (event.getThrowableInformation().getThrowable().toString() - .contains(needle)) { - hit = event; - } - } - assertNotNull("Could not find log message with a Throwable containing '" - + needle + "'", hit); - assertTrue("Could not remove log message with a Throwable containing '" - + needle + "'", loggedEvents.remove(hit)); - } - - /** - * Assert that all logged events have been asserted - */ - // As the PowerMock runner does not pass through runTest, we inject log - // verification through @After - @After - public final void assertNoUnassertedLogEvents() { - if (loggedEvents.size() > 0) { - LoggingEvent event = loggedEvents.iterator().next(); - String msg = "Found untreated logged events. First one is:\n"; - msg += event.getRenderedMessage(); - if (event.getThrowableInformation() != null) { - msg += "\n" + event.getThrowableInformation().getThrowable(); - } - fail(msg); - } - } - - @Override - public void setUp() throws Exception { - super.setUp(); - loggedEvents = new ArrayList<>(); - - // The logger we're interested is class name without the trailing "Test". - // While this is not the most general approach it is sufficient for now, - // and we can improve later to allow tests to specify which loggers are - // to check. - loggerName = this.getClass().getCanonicalName(); - loggerName = loggerName.substring(0, loggerName.length() - 4); - loggerSettings = LogUtil.logToCollection(loggerName, loggedEvents); - } - - @Override - protected void runTest() throws Throwable { - super.runTest(); - // Plain JUnit runner does not pick up @After, so we add it here - // explicitly. Note, that we cannot put this into tearDown, as failure - // to verify mocks would bail out and might leave open resources from - // subclasses open. - assertNoUnassertedLogEvents(); - } - - @Override - public void tearDown() throws Exception { - if (loggerName != null && loggerSettings != null) { - Logger logger = LogManager.getLogger(loggerName); - loggerSettings.pushOntoLogger(logger); - } - super.tearDown(); - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java deleted file mode 100644 index 569a57f..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/MockingTestCase.java +++ /dev/null
@@ -1,155 +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.testutil; - -import junit.framework.TestCase; - -import org.easymock.EasyMock; -import org.easymock.IMocksControl; -import org.junit.After; -import org.junit.runner.RunWith; -import org.powermock.api.easymock.PowerMock; -import org.powermock.modules.junit4.PowerMockRunner; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * Test case with some support for automatically verifying mocks. - * - * This test case works transparently with EasyMock and PowerMock. - */ -public abstract class MockingTestCase extends TestCase { - private Collection<Object> mocks; - private Collection<IMocksControl> mockControls; - private boolean mocksReplayed; - private boolean usePowerMock; - - /** - * Create and register a mock control. - * - * @return The mock control instance. - */ - protected final IMocksControl createMockControl() { - IMocksControl mockControl = EasyMock.createControl(); - assertTrue("Adding mock control failed", mockControls.add(mockControl)); - return mockControl; - } - - /** - * Create and register a mock. - * - * Creates a mock and registers it in the list of created mocks, so it gets - * treated automatically upon {@code replay} and {@code verify}; - * @param toMock The class to create a mock for. - * @return The mock instance. - */ - protected final <T> T createMock(Class<T> toMock) { - return createMock(toMock, null); - } - - /** - * Create a mock for a mock control and register a mock. - * - * Creates a mock and registers it in the list of created mocks, so it gets - * treated automatically upon {@code replay} and {@code verify}; - * @param toMock The class to create a mock for. - * @param control The mock control to create the mock on. If null, do not use - * a specific control. - * @return The mock instance. - */ - protected final <T> T createMock(Class<T> toMock, IMocksControl control) { - assertFalse("Mocks have already been set to replay", mocksReplayed); - final T mock; - if (control == null) { - if (usePowerMock) { - mock = PowerMock.createMock(toMock); - } else { - mock = EasyMock.createMock(toMock); - } - assertTrue("Adding " + toMock.getName() + " mock failed", - mocks.add(mock)); - } else { - mock = control.createMock(toMock); - } - return mock; - } - - /** - * Set all registered mocks to replay - */ - protected final void replayMocks() { - assertFalse("Mocks have already been set to replay", mocksReplayed); - if (usePowerMock) { - PowerMock.replayAll(); - } else { - EasyMock.replay(mocks.toArray()); - } - for (IMocksControl mockControl : mockControls) { - mockControl.replay(); - } - mocksReplayed = true; - } - - /** - * Verify all registered mocks - * - * This method is called automatically at the end of a test. Nevertheless, - * it is safe to also call it beforehand, if this better meets the - * verification part of a test. - */ - // As the PowerMock runner does not pass through runTest, we inject mock - // verification through @After - @After - public final void verifyMocks() { - if (!mocks.isEmpty() || !mockControls.isEmpty()) { - assertTrue("Created mocks have not been set to replay. Call replayMocks " - + "within the test", mocksReplayed); - if (usePowerMock) { - PowerMock.verifyAll(); - } else { - EasyMock.verify(mocks.toArray()); - } - for (IMocksControl mockControl : mockControls) { - mockControl.verify(); - } - } - } - - @Override - public void setUp() throws Exception { - super.setUp(); - - usePowerMock = false; - RunWith runWith = this.getClass().getAnnotation(RunWith.class); - if (runWith != null) { - usePowerMock = PowerMockRunner.class.isAssignableFrom(runWith.value()); - } - - mocks = new ArrayList<>(); - mockControls = new ArrayList<>(); - mocksReplayed = false; - } - - @Override - protected void runTest() throws Throwable { - super.runTest(); - // Plain JUnit runner does not pick up @After, so we add it here - // explicitly. Note, that we cannot put this into tearDown, as failure - // to verify mocks would bail out and might leave open resources from - // subclasses open. - verifyMocks(); - } -}
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..ae0e515 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,31 +15,38 @@ 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; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Repository; +import org.junit.runner.Description; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; @Singleton public class NoteDbChecker { @@ -48,37 +55,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 +115,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 +125,45 @@ } } - private List<ChangeBundle> readExpected(Iterable<Change.Id> changeIds) - throws Exception { + public void assertNoReviewDbChanges(Description desc) throws Exception { ReviewDb db = getUnwrappedDb(); + assertThat(db.changes().all().toList()) + .named("Changes in " + desc.getTestClass()) + .isEmpty(); + assertThat(db.changeMessages().all().toList()) + .named("ChangeMessages in " + desc.getTestClass()) + .isEmpty(); + assertThat(db.patchSets().all().toList()) + .named("PatchSets in " + desc.getTestClass()) + .isEmpty(); + assertThat(db.patchSetApprovals().all().toList()) + .named("PatchSetApprovals in " + desc.getTestClass()) + .isEmpty(); + assertThat(db.patchComments().all().toList()) + .named("PatchLineComments in " + desc.getTestClass()) + .isEmpty(); + } + + private List<ChangeBundle> readExpected(Stream<Change.Id> changeIds) + throws Exception { 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 +177,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..7db76fd 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. */ @@ -29,38 +29,39 @@ /** Reading and writing all data to NoteDb is enabled. */ READ_WRITE, + /** Changes are created with their primary storage as NoteDb. */ + PRIMARY, + /** * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check * that the results match. */ CHECK; - private static final String VAR = "GERRIT_NOTEDB"; + private static final String ENV_VAR = "GERRIT_NOTEDB"; + private static final String SYS_PROP = "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(ENV_VAR); + if (Strings.isNullOrEmpty(value)) { + value = System.getProperty(SYS_PROP); } - 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)); + NoteDbMode mode = Enums.getIfPresent(NoteDbMode.class, value).orNull(); + if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) { + checkArgument(mode != null, "Invalid value for env variable %s: %s", + ENV_VAR, System.getenv(ENV_VAR)); + } else { + checkArgument(mode != null, "Invalid value for system property %s: %s", + SYS_PROP, System.getProperty(SYS_PROP)); } - return mode.get(); + 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); + return get() == READ_WRITE || get() == PRIMARY; } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java deleted file mode 100644 index e008b78..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/PassThroughKeyUtilEncoder.java +++ /dev/null
@@ -1,29 +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.testutil; - -import com.google.gwtorm.client.KeyUtil.Encoder; - -public class PassThroughKeyUtilEncoder extends Encoder { - @Override - public String encode(String e) { - return e; - } - - @Override - public String decode(String e) { - return e; - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java deleted file mode 100644 index f1e7b7b..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/SetMatcher.java +++ /dev/null
@@ -1,55 +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.testutil; - -import com.google.common.collect.Sets; - -import org.easymock.EasyMock; -import org.easymock.IArgumentMatcher; - -import java.util.Set; - -/** - * Match for Iterables via set equals - * - * Converts both expected and actual parameter to a set and compares those two - * sets via equals to determine whether or not they match. - */ -public class SetMatcher<T> implements IArgumentMatcher { - public static <S extends Iterable<T>,T> S setEq(S expected) { - EasyMock.reportMatcher(new SetMatcher<>(expected)); - return null; - } - - Set<T> expected; - - public SetMatcher(Iterable<T> expected) { - this.expected = Sets.newHashSet(expected); - } - - @Override - public boolean matches(Object actual) { - if (actual instanceof Iterable<?>) { - Set<?> actualSet = Sets.newHashSet((Iterable<?>)actual); - return expected.equals(actualSet); - } - return false; - } - - @Override - public void appendTo(StringBuffer buffer) { - buffer.append(expected); - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java new file mode 100644 index 0000000..bc4e4e9 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.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.testutil; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Enums; +import com.google.common.base.Strings; + +public enum SshMode { + /** Tests annotated with UseSsh will be disabled. */ + NO, + + /** Tests annotated with UseSsh will be enabled. */ + YES; + + private static final String ENV_VAR = "GERRIT_USE_SSH"; + private static final String SYS_PROP = "gerrit.use.ssh"; + + public static SshMode get() { + String value = System.getenv(ENV_VAR); + if (Strings.isNullOrEmpty(value)) { + value = System.getProperty(SYS_PROP); + } + if (Strings.isNullOrEmpty(value)) { + return YES; + } + value = value.toUpperCase(); + SshMode mode = Enums.getIfPresent(SshMode.class, value).orNull(); + if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) { + checkArgument(mode != null, "Invalid value for env variable %s: %s", + ENV_VAR, System.getenv(ENV_VAR)); + } else { + checkArgument(mode != null, "Invalid value for system property %s: %s", + SYS_PROP, System.getProperty(SYS_PROP)); + } + return mode; + } + + public static boolean useSsh() { + return get() == YES; + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java index 2f9d67f..b11b2cd 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -14,6 +14,9 @@ package com.google.gerrit.testutil; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.NotesMigration; import com.google.inject.Singleton; @@ -22,6 +25,8 @@ public class TestNotesMigration extends NotesMigration { private volatile boolean readChanges; private volatile boolean writeChanges; + private volatile PrimaryStorage changePrimaryStorage = + PrimaryStorage.REVIEW_DB; private volatile boolean failOnLoad; @Override @@ -36,6 +41,11 @@ return readChanges; } + @Override + public PrimaryStorage changePrimaryStorage() { + return changePrimaryStorage; + } + // Increase visbility from superclass, as tests may want to check whether // NoteDb data is written in specific migration scenarios. @Override @@ -68,6 +78,12 @@ return this; } + public TestNotesMigration setChangePrimaryStorage( + PrimaryStorage changePrimaryStorage) { + this.changePrimaryStorage = checkNotNull(changePrimaryStorage); + return this; + } + public TestNotesMigration setFailOnLoad(boolean failOnLoad) { this.failOnLoad = failOnLoad; return this; @@ -82,16 +98,24 @@ case READ_WRITE: setWriteChanges(true); setReadChanges(true); + setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); break; case WRITE: setWriteChanges(true); setReadChanges(false); + setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); + break; + case PRIMARY: + setWriteChanges(true); + setReadChanges(true); + setChangePrimaryStorage(PrimaryStorage.NOTE_DB); break; case CHECK: case OFF: default: setWriteChanges(false); setReadChanges(false); + setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); break; } return this;
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-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java deleted file mode 100644 index 05f52c0..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/log/CollectionAppender.java +++ /dev/null
@@ -1,58 +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.testutil.log; - -import com.google.common.collect.Lists; - -import org.apache.log4j.AppenderSkeleton; -import org.apache.log4j.spi.LoggingEvent; - -import java.util.Collection; -import java.util.LinkedList; - -/** - * Log4j appender that logs into a list - */ -public class CollectionAppender extends AppenderSkeleton { - private Collection<LoggingEvent> events; - - public CollectionAppender() { - events = new LinkedList<>(); - } - - public CollectionAppender(Collection<LoggingEvent> events) { - this.events = events; - } - - @Override - public boolean requiresLayout() { - return false; - } - - @Override - protected void append(LoggingEvent event) { - if (! events.add(event)) { - throw new RuntimeException("Could not append event " + event); - } - } - - @Override - public void close() { - } - - public Collection<LoggingEvent> getLoggedEvents() { - return Lists.newLinkedList(events); - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java deleted file mode 100644 index e3c83ca..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/log/LogUtil.java +++ /dev/null
@@ -1,88 +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.testutil.log; - -import org.apache.log4j.Appender; -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; -import org.apache.log4j.spi.LoggingEvent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Enumeration; -import java.util.List; - -public class LogUtil { - /** - * Change logger's setting so it only logs to a collection. - * - * @param logName Name of the logger to modify. - * @param collection The collection to log into. - * @return The logger's original settings. - */ - public static LoggerSettings logToCollection(String logName, - Collection<LoggingEvent> collection) { - Logger logger = LogManager.getLogger(logName); - LoggerSettings loggerSettings = new LoggerSettings(logger); - logger.removeAllAppenders(); - logger.setAdditivity(false); - CollectionAppender listAppender = new CollectionAppender(collection); - logger.addAppender(listAppender); - return loggerSettings; - } - - /** - * Capsule for a logger's settings that get mangled by rerouting logging to a collection - */ - public static class LoggerSettings { - private final boolean additive; - private final List<Appender> appenders; - - /** - * Read off logger settings from an instance. - * - * @param logger The logger to read the settings off from. - */ - private LoggerSettings(Logger logger) { - this.additive = logger.getAdditivity(); - - Enumeration<?> appenders = logger.getAllAppenders(); - this.appenders = new ArrayList<>(); - while (appenders.hasMoreElements()) { - Object appender = appenders.nextElement(); - if (appender instanceof Appender) { - this.appenders.add((Appender)appender); - } else { - throw new RuntimeException("getAllAppenders of " + logger - + " contained an object that is not an Appender"); - } - } - } - - /** - * Pushes this settings back onto a logger. - * - * @param logger the logger on which to push the settings. - */ - public void pushOntoLogger(Logger logger) { - logger.setAdditivity(additive); - - logger.removeAllAppenders(); - for (Appender appender : appenders) { - logger.addAppender(appender); - } - } - } -}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK deleted file mode 100644 index 54b83e2..0000000 --- a/gerrit-sshd/BUCK +++ /dev/null
@@ -1,60 +0,0 @@ -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/auto:auto-value', - '//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', - ], - provided_deps = [ - '//lib/bouncycastle:bcprov', - '//lib:servlet-api-3_1', - ], - visibility = ['PUBLIC'], -) - -java_sources( - name = 'sshd-src', - srcs = SRCS, - visibility = ['PUBLIC'], -) - -java_test( - name = 'sshd_tests', - srcs = glob( - ['src/test/java/**/*.java'], - ), - deps = [ - ':sshd', - '//gerrit-extension-api:api', - '//gerrit-server:server', - '//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/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java index 8b468a7..effed2c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -18,7 +18,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.LinkedListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; import com.google.gerrit.extensions.annotations.Export; import com.google.gerrit.server.plugins.InvalidPluginException; import com.google.gerrit.server.plugins.ModuleGenerator; @@ -36,7 +36,8 @@ extends AbstractModule implements ModuleGenerator { private final Map<String, Class<Command>> commands = new HashMap<>(); - private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create(); + private final ListMultimap<TypeLiteral<?>, Class<?>> listeners = + LinkedListMultimap.create(); private CommandName command; @Override
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java index fed8226..74f2017 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -14,8 +14,8 @@ package com.google.gerrit.sshd; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; import com.google.gerrit.audit.AuditService; import com.google.gerrit.audit.SshAuditEvent; import com.google.gerrit.common.TimeUtil; @@ -156,14 +156,15 @@ audit(context.get(), status, dcmd); } - private Multimap<String, ?> extractParameters(DispatchCommand dcmd) { + private ListMultimap<String, ?> extractParameters(DispatchCommand dcmd) { if (dcmd == null) { - return ArrayListMultimap.create(0, 0); + return MultimapBuilder.hashKeys(0).arrayListValues(0).build(); } String[] cmdArgs = dcmd.getArguments(); String paramName = null; int argPos = 0; - Multimap<String, String> parms = ArrayListMultimap.create(); + ListMultimap<String, String> parms = + MultimapBuilder.hashKeys().arrayListValues().build(); for (int i = 2; i < cmdArgs.length; i++) { String arg = cmdArgs[i]; // -- stop parameters parsing @@ -258,7 +259,8 @@ audit(ctx, result, extractWhat(cmd), extractParameters(cmd)); } - private void audit(Context ctx, Object result, String cmd, Multimap<String, ?> params) { + private void audit(Context ctx, Object result, String cmd, + ListMultimap<String, ?> params) { String sessionId; CurrentUser currentUser; long created;
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/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java index c8ebb6c..fa20219 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -28,6 +28,8 @@ import org.kohsuke.args4j.Argument; +import java.io.IOException; + @CommandMetaData(name = "rename-group", description = "Rename an account group") public class RenameGroupCommand extends SshCommand { @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed") @@ -50,7 +52,8 @@ PutName.Input input = new PutName.Input(); input.name = newGroupName; putName.apply(rsrc, input); - } catch (RestApiException | OrmException | NoSuchGroupException e) { + } catch (RestApiException | OrmException | IOException + | NoSuchGroupException e) { throw die(e); } }
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/CommitInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/CommitInfoSubject.java new file mode 100644 index 0000000..9af2e1f --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/CommitInfoSubject.java
@@ -0,0 +1,66 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.truth.ListSubject; + +public class CommitInfoSubject extends Subject<CommitInfoSubject, CommitInfo> { + + private static final SubjectFactory<CommitInfoSubject, CommitInfo> + COMMIT_INFO_SUBJECT_FACTORY = + new SubjectFactory<CommitInfoSubject, CommitInfo>() { + @Override + public CommitInfoSubject getSubject(FailureStrategy failureStrategy, + CommitInfo commitInfo) { + return new CommitInfoSubject(failureStrategy, commitInfo); + } + }; + + public static CommitInfoSubject assertThat(CommitInfo commitInfo) { + return assertAbout(COMMIT_INFO_SUBJECT_FACTORY) + .that(commitInfo); + } + + private CommitInfoSubject(FailureStrategy failureStrategy, + CommitInfo commitInfo) { + super(failureStrategy, commitInfo); + } + + public StringSubject commit() { + isNotNull(); + CommitInfo commitInfo = actual(); + return Truth.assertThat(commitInfo.commit).named("commit"); + } + + public ListSubject<CommitInfoSubject, CommitInfo> parents() { + isNotNull(); + CommitInfo commitInfo = actual(); + return ListSubject.assertThat(commitInfo.parents, + CommitInfoSubject::assertThat).named("parents"); + } + + public GitPersonSubject committer() { + isNotNull(); + CommitInfo commitInfo = actual(); + return GitPersonSubject.assertThat(commitInfo.committer).named("committer"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/EditInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/EditInfoSubject.java new file mode 100644 index 0000000..1471411 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/EditInfoSubject.java
@@ -0,0 +1,66 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.truth.OptionalSubject; + +import java.util.Optional; + +public class EditInfoSubject extends Subject<EditInfoSubject, EditInfo> { + + private static final SubjectFactory<EditInfoSubject, EditInfo> + EDIT_INFO_SUBJECT_FACTORY = + new SubjectFactory<EditInfoSubject, EditInfo>() { + @Override + public EditInfoSubject getSubject(FailureStrategy failureStrategy, + EditInfo editInfo) { + return new EditInfoSubject(failureStrategy, editInfo); + } + }; + + public static EditInfoSubject assertThat(EditInfo editInfo) { + return assertAbout(EDIT_INFO_SUBJECT_FACTORY) + .that(editInfo); + } + + public static OptionalSubject<EditInfoSubject, EditInfo> assertThat( + Optional<EditInfo> editInfoOptional) { + return OptionalSubject.assertThat(editInfoOptional, + EditInfoSubject::assertThat); + } + + private EditInfoSubject(FailureStrategy failureStrategy, EditInfo editInfo) { + super(failureStrategy, editInfo); + } + + public CommitInfoSubject commit() { + isNotNull(); + EditInfo editInfo = actual(); + return CommitInfoSubject.assertThat(editInfo.commit).named("commit"); + } + + public StringSubject baseRevision() { + isNotNull(); + EditInfo editInfo = actual(); + return Truth.assertThat(editInfo.baseRevision).named("baseRevision"); + } +}
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/GitPersonSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/GitPersonSubject.java new file mode 100644 index 0000000..3782e94 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/GitPersonSubject.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.common; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.ComparableSubject; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; + +import java.sql.Timestamp; + +public class GitPersonSubject extends Subject<GitPersonSubject, GitPerson> { + + private static final SubjectFactory<GitPersonSubject, GitPerson> + GIT_PERSON_SUBJECT_FACTORY = + new SubjectFactory<GitPersonSubject, GitPerson>() { + @Override + public GitPersonSubject getSubject(FailureStrategy failureStrategy, + GitPerson gitPerson) { + return new GitPersonSubject(failureStrategy, gitPerson); + } + }; + + public static GitPersonSubject assertThat(GitPerson gitPerson) { + return assertAbout(GIT_PERSON_SUBJECT_FACTORY) + .that(gitPerson); + } + + private GitPersonSubject(FailureStrategy failureStrategy, + GitPerson gitPerson) { + super(failureStrategy, gitPerson); + } + + public ComparableSubject<?, Timestamp> creationDate() { + isNotNull(); + GitPerson gitPerson = actual(); + return Truth.assertThat(gitPerson.date).named("creationDate"); + } +}
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/extensions/restapi/BinaryResultSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java new file mode 100644 index 0000000..daaf283 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java
@@ -0,0 +1,71 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.restapi; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.PrimitiveByteArraySubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.truth.OptionalSubject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Optional; + +public class BinaryResultSubject + extends Subject<BinaryResultSubject, BinaryResult> { + + private static final SubjectFactory<BinaryResultSubject, BinaryResult> + BINARY_RESULT_SUBJECT_FACTORY = + new SubjectFactory<BinaryResultSubject, BinaryResult>() { + @Override + public BinaryResultSubject getSubject(FailureStrategy failureStrategy, + BinaryResult binaryResult) { + return new BinaryResultSubject(failureStrategy, + binaryResult); + } + }; + + public static BinaryResultSubject assertThat(BinaryResult binaryResult) { + return assertAbout(BINARY_RESULT_SUBJECT_FACTORY) + .that(binaryResult); + } + + public static OptionalSubject<BinaryResultSubject, BinaryResult> assertThat( + Optional<BinaryResult> binaryResultOptional) { + return OptionalSubject.assertThat(binaryResultOptional, + BinaryResultSubject::assertThat); + } + + private BinaryResultSubject(FailureStrategy failureStrategy, + BinaryResult binaryResult) { + super(failureStrategy, binaryResult); + } + + public PrimitiveByteArraySubject bytes() throws IOException { + isNotNull(); + // We shouldn't close the BinaryResult within this method as it might still + // be used afterwards. Besides, closing it doesn't have an effect for most + // implementations of a BinaryResult. + BinaryResult binaryResult = actual(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + binaryResult.writeTo(byteArrayOutputStream); + byte[] bytes = byteArrayOutputStream.toByteArray(); + return Truth.assertThat(bytes); + } +}
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-test-util/src/main/java/com/google/gerrit/truth/OptionalSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/truth/OptionalSubject.java new file mode 100644 index 0000000..daece87 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/truth/OptionalSubject.java
@@ -0,0 +1,103 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.truth; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.DefaultSubject; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; + +import java.util.Optional; +import java.util.function.Function; + +public class OptionalSubject<S extends Subject<S, ? super T>, T> + extends Subject<OptionalSubject<S, T>, Optional<T>> { + + private final Function<? super T, ? extends S> valueAssertThatFunction; + + public static <S extends Subject<S, ? super T>, T> OptionalSubject<S, T> + assertThat(Optional<T> optional, + Function<? super T, ? extends S> elementAssertThatFunction) { + OptionalSubjectFactory<S, T> optionalSubjectFactory = + new OptionalSubjectFactory<>(elementAssertThatFunction); + return assertAbout(optionalSubjectFactory).that(optional); + } + + public static OptionalSubject<DefaultSubject, ?> assertThat( + Optional<?> optional) { + // Unfortunately, we need to cast to DefaultSubject as Truth.assertThat() + // only returns Subject<DefaultSubject, Object>. There shouldn't be a way + // for that method not to return a DefaultSubject because the generic type + // definitions of a Subject are quite strict. + Function<Object, DefaultSubject> valueAssertThatFunction = + value -> (DefaultSubject) Truth.assertThat(value); + return assertThat(optional, valueAssertThatFunction); + } + + private OptionalSubject(FailureStrategy failureStrategy, Optional<T> optional, + Function<? super T, ? extends S> valueAssertThatFunction) { + super(failureStrategy, optional); + this.valueAssertThatFunction = valueAssertThatFunction; + } + + public void isPresent() { + isNotNull(); + Optional<T> optional = actual(); + if (!optional.isPresent()) { + fail("has a value"); + } + } + + public void isAbsent() { + isNotNull(); + Optional<T> optional = actual(); + if (optional.isPresent()) { + fail("does not have a value"); + } + } + + public void isEmpty() { + isAbsent(); + } + + public S value() { + isNotNull(); + isPresent(); + Optional<T> optional = actual(); + return valueAssertThatFunction.apply(optional.get()); + } + + private static class OptionalSubjectFactory<S extends Subject<S, ? super T>, + T> extends SubjectFactory<OptionalSubject<S, T>, Optional<T>> { + + private Function<? super T, ? extends S> valueAssertThatFunction; + + OptionalSubjectFactory( + Function<? super T, ? extends S> valueAssertThatFunction) { + this.valueAssertThatFunction = valueAssertThatFunction; + } + + @Override + public OptionalSubject<S, T> getSubject(FailureStrategy failureStrategy, + Optional<T> optional) { + return new OptionalSubject<>(failureStrategy, optional, + valueAssertThatFunction); + } + + } +}
diff --git a/gerrit-util-cli/BUCK b/gerrit-util-cli/BUCK deleted file mode 100644 index 8cdc2dc..0000000 --- a/gerrit-util-cli/BUCK +++ /dev/null
@@ -1,13 +0,0 @@ -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 = ['PUBLIC'], -)
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-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java index b5888b50..75f11f2 100644 --- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java +++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -35,9 +35,9 @@ package com.google.gerrit.util.cli; import com.google.common.base.Strings; -import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; -import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -224,7 +224,8 @@ public void parseOptionMap(Map<String, String[]> parameters) throws CmdLineException { - Multimap<String, String> map = LinkedHashMultimap.create(); + ListMultimap<String, String> map = + MultimapBuilder.hashKeys().arrayListValues().build(); for (Map.Entry<String, String[]> ent : parameters.entrySet()) { for (String val : ent.getValue()) { map.put(ent.getKey(), val); @@ -233,7 +234,7 @@ parseOptionMap(map); } - public void parseOptionMap(Multimap<String, String> params) + public void parseOptionMap(ListMultimap<String, String> params) throws CmdLineException { List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size()); for (final String key : params.keySet()) {
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK deleted file mode 100644 index cfab096..0000000 --- a/gerrit-util-http/BUCK +++ /dev/null
@@ -1,40 +0,0 @@ -java_library( - name = 'http', - srcs = glob(['src/main/java/**/*.java']), - provided_deps = ['//lib:servlet-api-3_1'], - visibility = ['PUBLIC'], -) - -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 = ['PUBLIC'], -) - -java_test( - name = 'http_tests', - srcs = glob( - ['src/test/java/**/*.java'], - excludes = TESTUTIL_SRCS, - ), - deps = [ - ':http', - ':testutil', - '//lib:junit', - '//lib:servlet-api-3_1', - '//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/BUCK b/gerrit-util-ssl/BUCK deleted file mode 100644 index 068f34c..0000000 --- a/gerrit-util-ssl/BUCK +++ /dev/null
@@ -1,5 +0,0 @@ -java_library( - name = 'ssl', - srcs = glob(['src/main/java/**/*.java']), - visibility = ['PUBLIC'], -)
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 deleted file mode 100644 index 6d74a83..0000000 --- a/gerrit-war/BUCK +++ /dev/null
@@ -1,76 +0,0 @@ -include_defs('//tools/git.defs') - -java_library( - name = 'init', - srcs = glob(['src/main/java/**/*.java']), - deps = [ - '//gerrit-cache-h2:cache-h2', - '//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/guice:guice', - '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', - '//lib/log:api', - ], - provided_deps = ['//lib:servlet-api-3_1'], - visibility = [ - '//:', - '//gerrit-gwtdebug:gwtdebug', - '//tools/eclipse:classpath', - ], -) - -genrule( - name = 'webapp_assets', - cmd = 'cd src/main/webapp; zip -qr $OUT .', - srcs = glob(['src/main/webapp/**/*']), - out = 'webapp_assets.zip', - visibility = ['//:'], -) - -genrule( - name = 'log4j-config__jar', - cmd = 'jar cf $OUT -C src/main/resources .', - srcs = ['src/main/resources/log4j.properties'], - out = 'log4j-config.jar', -) - -prebuilt_jar( - name = 'log4j-config', - binary_jar = ':log4j-config__jar', - visibility = [ - '//:', - '//tools/eclipse:classpath', - ], -) - -prebuilt_jar( - name = 'version', - binary_jar = ':gen_version', - visibility = ['//:'], -) - -genrule( - name = 'gen_version', - cmd = ';'.join([ - 'cd $TMP', - 'mkdir -p com/google/gerrit/common', - 'echo "%s" >com/google/gerrit/common/Version' % git_describe(), - 'zip -9Dqr $OUT .', - ]), - out = 'version.jar', -)
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 38ca054..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.5</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 fb7d0e9..6a8cd2f 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,8 @@ 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.StartupChecks; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner; @@ -38,6 +41,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 +55,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; @@ -309,11 +314,13 @@ 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()); modules.add(new RestCacheAdminModule()); modules.add(new GpgModule(config)); + modules.add(new StartupChecks.Module()); // Index module shutdown must happen before work queue shutdown, otherwise // work queue can get stuck waiting on index futures that will never return. @@ -336,6 +343,7 @@ }); modules.add(new GarbageCollectionModule()); modules.add(new ChangeCleanupRunner.Module()); + modules.addAll(LibModuleLoader.loadModules(cfgInjector)); return cfgInjector.createChildInjector(modules); } @@ -343,6 +351,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 deleted file mode 100644 index 380a3ef..0000000 --- a/lib/BUCK +++ /dev/null
@@ -1,275 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/GUAVA_VERSION') - -define_license(name = 'antlr') -define_license(name = 'Apache1.1') -define_license(name = 'Apache2.0') -define_license(name = 'args4j') -define_license(name = 'asciidoctor') -define_license(name = 'automaton') -define_license(name = 'bouncycastle') -define_license(name = 'CC-BY3.0-unported') -define_license(name = 'clippy') -define_license(name = 'codemirror-minified') -define_license(name = 'codemirror-original') -define_license(name = 'diffy') -define_license(name = 'es6-promise') -define_license(name = 'fetch') -define_license(name = 'h2') -define_license(name = 'highlightjs') -define_license(name = 'jgit') -define_license(name = 'jsch') -define_license(name = 'MPL1.1') -define_license(name = 'moment') -define_license(name = 'OFL1.1') -define_license(name = 'ow2') -define_license(name = 'page.js') -define_license(name = 'polymer') -define_license(name = 'postgresql') -define_license(name = 'prologcafe') -define_license(name = 'promise-polyfill') -define_license(name = 'protobuf') -define_license(name = 'PublicDomain') -define_license(name = 'silk_icons') -define_license(name = 'slf4j') -define_license(name = 'xz') - -define_license(name = 'DO_NOT_DISTRIBUTE') - -maven_jar( - name = 'gwtorm_client', - id = 'com.google.gerrit:gwtorm:1.15', - bin_sha1 = '26a2459f543ed78977535f92e379dc0d6cdde8bb', - src_sha1 = '9524088d6e46e299b12791cb1a63c4ba6a478b96', - license = 'Apache2.0', -) - -java_library( - name = 'gwtorm', - exported_deps = [':gwtorm_client'], - deps = [':protobuf'], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'gwtjsonrpc', - id = 'com.google.gerrit:gwtjsonrpc:1.9', - bin_sha1 = '458f55e92584fbd9ab91a89fa1c37654922a0f2b', - src_sha1 = 'ba539361c80a26f0d30a2f56068f6d83f44062d8', - license = 'Apache2.0', -) - -maven_jar( - name = 'gson', - id = 'com.google.code.gson:gson:2.7', - sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e', - license = 'Apache2.0', -) - -maven_jar( - name = 'guava', - id = 'com.google.guava:guava:' + GUAVA_VERSION, - sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9', - license = 'Apache2.0', -) - -maven_jar( - name = 'guava-retrying', - id = 'com.github.rholder:guava-retrying:2.0.0', - sha1 = '974bc0a04a11cc4806f7c20a34703bd23c34e7f4', - license = 'Apache2.0', - deps = [':jsr305'], -) - -maven_jar( - name = 'jsr305', - id = 'com.google.code.findbugs:jsr305:3.0.1', - sha1 = 'f7be08ec23c21485b9b5a1cf1654c2ec8c58168d', - license = 'Apache2.0', - attach_source = False, - # 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'], -) - -maven_jar( - name = 'velocity', - id = 'org.apache.velocity:velocity:1.7', - sha1 = '2ceb567b8f3f21118ecdec129fe1271dbc09aa7a', - license = 'Apache2.0', - deps = [ - '//lib/commons:collections', - '//lib/commons:lang', - '//lib/commons:oro', - ], - exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'], -) - -maven_jar( - name = 'jsch', - id = 'com.jcraft:jsch:0.1.53', - sha1 = '658b682d5c817b27ae795637dfec047c63d29935', - license = 'jsch', -) - -maven_jar( - name = 'servlet-api-3_1', - id = 'org.apache.tomcat:tomcat-servlet-api:8.0.24', - sha1 = '5d9e2e895e3111622720157d0aa540066d5fce3a', - license = 'Apache2.0', - exclude = ['META-INF/NOTICE', 'META-INF/LICENSE'], -) - -maven_jar( - name = 'args4j', - id = 'args4j:args4j:2.0.26', - sha1 = '01ebb18ebb3b379a74207d5af4ea7c8338ebd78b', - license = 'args4j', -) - -maven_jar( - name = 'mime-util', - id = 'eu.medsea.mimeutil:mime-util:2.1.3', - sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47', - license = 'Apache2.0', - exclude = ['LICENSE.txt', 'README.txt'], - attach_source = False, -) - -maven_jar( - name = 'juniversalchardet', - id = 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3', - sha1 = 'cd49678784c46aa8789c060538e0154013bb421b', - license = 'MPL1.1', -) - -maven_jar( - name = 'automaton', - id = 'dk.brics.automaton:automaton:1.11-8', - sha1 = '6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f', - license = 'automaton', -) - -maven_jar( - name = 'pegdown', - id = 'org.pegdown:pegdown:1.4.2', - sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7', - license = 'Apache2.0', - deps = [':grappa'], -) - -maven_jar( - name = 'grappa', - id = 'com.github.parboiled1:grappa:1.0.4', - sha1 = 'ad4b44b9c305dad7aa1e680d4b5c8eec9c4fd6f5', - license = 'Apache2.0', - deps = [ - ':jitescript', - '//lib/ow2:ow2-asm', - '//lib/ow2:ow2-asm-analysis', - '//lib/ow2:ow2-asm-tree', - '//lib/ow2:ow2-asm-util', - ], -) - -maven_jar( - name = 'jitescript', - id = 'me.qmx.jitescript:jitescript:0.4.0', - sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54', - license = 'Apache2.0', - visibility = ['//lib:grappa'], -) - -maven_jar( - name = 'derby', - id = 'org.apache.derby:derby:10.11.1.1', - sha1 = 'df4b50061e8e4c348ce243b921f53ee63ba9bbe1', - license = 'Apache2.0', - attach_source = False, -) - -maven_jar( - name = 'h2', - id = 'com.h2database:h2:1.3.176', - sha1 = 'fd369423346b2f1525c413e33f8cf95b09c92cbd', - license = 'h2', -) - -maven_jar( - name = 'postgresql', - id = 'org.postgresql:postgresql:9.4.1211.jre7', - sha1 = '56b01e9e667f408818a6ef06a89598dbab80687d', - license = 'postgresql', - attach_source = False, -) - -maven_jar( - name = 'protobuf', - # Must match version in gwtorm/pom.xml. - id = 'com.google.protobuf:protobuf-java:2.5.0', - bin_sha1 = 'a10732c76bfacdbd633a7eb0f7968b1059a65dfa', - src_sha1 = '7a27a7fc815e481b367ead5df19b4a71ace4a419', - license = 'protobuf', -) - -# Test-only dependencies below. - -maven_jar( - name = 'jimfs', - id = 'com.google.jimfs:jimfs:1.0', - sha1 = 'edd65a2b792755f58f11134e76485a928aab4c97', - license = 'DO_NOT_DISTRIBUTE', - deps = [':guava'], -) - -maven_jar( - name = 'junit', - id = 'junit:junit:4.11', - sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0', - license = 'DO_NOT_DISTRIBUTE', - exported_deps = [':hamcrest-core'], -) - -maven_jar( - name = 'hamcrest-core', - id = 'org.hamcrest:hamcrest-core:1.3', - sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0', - license = 'DO_NOT_DISTRIBUTE', - visibility = ['//lib:junit'], -) - -maven_jar( - name = 'truth', - id = 'com.google.truth:truth:0.28', - sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4', - license = 'DO_NOT_DISTRIBUTE', - exported_deps = [ - ':guava', - ':junit', - ], -) - -maven_jar( - name = 'tukaani-xz', - id = 'org.tukaani:xz:1.4', - sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3', - license = 'xz', - attach_source = False, - visibility = ['//gerrit-server:server'], -) - -maven_jar( - name = 'javassist', - id = 'org.javassist:javassist:3.20.0-GA', - sha1 = 'a9cbcdfb7e9f86fbc74d3afae65f2248bfbf82a0', - license = 'DO_NOT_DISTRIBUTE', -) - -maven_jar( - name = 'blame-cache', - id = 'com/google/gitiles:blame-cache:0.1-9', - sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826', - license = 'Apache2.0', - repository = GERRIT, -)
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..87a625f 100644 --- a/lib/JGIT_VERSION +++ b/lib/JGIT_VERSION
@@ -1,6 +1,4 @@ +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 # 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/BUCK b/lib/antlr/BUCK deleted file mode 100644 index edf153c..0000000 --- a/lib/antlr/BUCK +++ /dev/null
@@ -1,48 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '3.5.2' - -maven_jar( - name = 'java_runtime', - id = 'org.antlr:antlr-runtime:' + VERSION, - sha1 = 'cd9cd41361c155f3af0f653009dcecb08d8b4afd', - license = 'antlr', -) - -java_binary( - name = 'antlr-tool', - main_class = 'org.antlr.Tool', - deps = [':tool'], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'stringtemplate', - id = 'org.antlr:stringtemplate:4.0.2', - sha1 = 'e28e09e2d44d60506a7bcb004d6c23ff35c6ac08', - license = 'antlr', - attach_source = False, - visibility = [], -) - -maven_jar( - name = 'tool', - id = 'org.antlr:antlr:' + VERSION, - sha1 = 'c4a65c950bfc3e7d04309c515b2177c00baf7764', - license = 'antlr', - deps = [ - ':java_runtime', - ':stringtemplate', - ':antlr27', - ], - visibility = [], -) - -maven_jar( - name = 'antlr27', - id = 'antlr:antlr:2.7.7', - sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0', - license = 'antlr', - attach_source = False, - visibility = [], -)
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 deleted file mode 100644 index 733c670..0000000 --- a/lib/asciidoctor/BUCK +++ /dev/null
@@ -1,61 +0,0 @@ -include_defs('//lib/maven.defs') - -java_binary( - name = 'asciidoc', - main_class = 'AsciiDoctor', - deps = [':asciidoc_lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'asciidoc_lib', - srcs = ['java/AsciiDoctor.java'], - deps = [ - ':asciidoctor', - ':jruby', - '//lib:args4j', - '//lib:guava', - '//lib/log:api', - '//lib/log:nop', - ], - visibility = ['//tools/eclipse:classpath'], -) - -java_binary( - name = 'doc_indexer', - main_class = 'DocIndexer', - deps = [':doc_indexer_lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'doc_indexer_lib', - srcs = ['java/DocIndexer.java'], - deps = [ - ':asciidoc_lib', - '//gerrit-server:constants', - '//lib:args4j', - '//lib:guava', - '//lib/lucene:lucene-analyzers-common', - '//lib/lucene:lucene-core-and-backward-codecs', - ], - visibility = ['//tools/eclipse:classpath'], -) - -maven_jar( - name = 'asciidoctor', - id = 'org.asciidoctor:asciidoctorj:1.5.4.1', - sha1 = 'f7ddfb2bbed2f8da3f9ad0d1a5514f04b4274a5a', - license = 'asciidoctor', - visibility = [], - attach_source = False, -) - -maven_jar( - name = 'jruby', - id = 'org.jruby:jruby-complete:1.7.25', - sha1 = '8eb234259ec88edc05eedab05655f458a84bfcab', - 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 deleted file mode 100644 index 6197e34..0000000 --- a/lib/auto/BUCK +++ /dev/null
@@ -1,9 +0,0 @@ -include_defs('//lib/maven.defs') - -maven_jar( - name = 'auto-value', - id = 'com.google.auto.value:auto-value:1.3-rc1', - sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179', - 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/auto/auto_value.defs b/lib/auto/auto_value.defs deleted file mode 100644 index 4405747..0000000 --- a/lib/auto/auto_value.defs +++ /dev/null
@@ -1,21 +0,0 @@ -# NOTE: Do not use this file in your build rules; automatically supported by -# our implementation of java_library. - -AUTO_VALUE_DEP = '//lib/auto:auto-value' - -# Annotation processor classpath requires transitive dependencies. -# TODO(dborowitz): Clean this up when buck issue is closed and there is a -# better supported interface: -# https://github.com/facebook/buck/issues/85 -AUTO_VALUE_PROCESSOR_DEPS = [ - '//lib:velocity', - '//lib/auto:auto-value', - '//lib/commons:collections', - '//lib/commons:lang', - '//lib/commons:oro', -] - -AUTO_VALUE_PROCESSORS = [ - 'com.google.auto.value.processor.AutoAnnotationProcessor', - 'com.google.auto.value.processor.AutoValueProcessor', -]
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK deleted file mode 100644 index 68fa006..0000000 --- a/lib/bouncycastle/BUCK +++ /dev/null
@@ -1,28 +0,0 @@ -include_defs('//lib/maven.defs') - -# 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' - -maven_jar( - name = 'bcprov', - id = 'org.bouncycastle:bcprov-jdk15on:' + VERSION, - sha1 = '88a941faf9819d371e3174b5ed56a3f3f7d73269', - license = 'DO_NOT_DISTRIBUTE', #'bouncycastle' -) - -maven_jar( - name = 'bcpg', - id = 'org.bouncycastle:bcpg-jdk15on:' + VERSION, - sha1 = 'ff4665a4b5633ff6894209d5dd10b7e612291858', - license = 'DO_NOT_DISTRIBUTE', #'bouncycastle' - deps = [':bcprov'], -) - -maven_jar( - name = 'bcpkix', - id = 'org.bouncycastle:bcpkix-jdk15on:' + VERSION, - sha1 = 'b8ffac2bbc6626f86909589c8cc63637cc936504', - 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 deleted file mode 100644 index a0e0e9a..0000000 --- a/lib/codemirror/BUCK +++ /dev/null
@@ -1,141 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/codemirror/cm.defs') - -VERSION = '5.17.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', - attach_source = False, - license = 'codemirror-minified', - visibility = [], -) - -maven_jar( - name = 'codemirror-original', - id = 'org.webjars.npm:codemirror:' + VERSION, - sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85', - 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) - -maven_jar( - name = 'diff-match-patch', - id = 'org.webjars:google-diff-match-patch:' + DIFF_MATCH_PATCH_VERSION, - sha1 = '0cf1782dbcb8359d95070da9176059a5a9d37709', - license = 'Apache2.0', - attach_source = False, -) - -for archive, suffix, top in [('codemirror-original', '', TOP), ('codemirror-minified', '_r', TOP_MINIFIED)]: - # Main JavaScript and addons - genrule( - name = 'cm' + suffix, - cmd = ';'.join([ - "echo '/** @license' >$OUT", - 'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top), - "echo '*/' >>$OUT", - ] + - ['unzip -p $(location :%s) %s/%s >>$OUT' % (archive, top, n) for n in CM_JS] + - ['unzip -p $(location :%s) %s/addon/%s >>$OUT' % (archive, top, n) - for n in CM_ADDONS] - ), - out = 'cm%s.js' % suffix, - ) - - # Main CSS - genrule( - name = 'css' + suffix, - cmd = ';'.join([ - "echo '/** @license' >$OUT", - 'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top), - "echo '*/' >>$OUT", - ] + - ['unzip -p $(location :%s) %s/%s >>$OUT' % (archive, top, n) - for n in CM_CSS] - ), - out = 'cm%s.css' % suffix, - ) - - # Modes - for n in CM_MODES: - genrule ( - name = 'mode_%s%s' % (n, suffix), - cmd = ';'.join([ - "echo '/** @license' >$OUT", - 'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top), - "echo '*/' >>$OUT", - 'unzip -p $(location :%s) %s/mode/%s/%s.js >>$OUT' % (archive, top, n, n), - ] - ), - out = 'mode_%s%s.js' % (n, suffix), - ) - - # Themes - for n in CM_THEMES: - genrule( - name = 'theme_%s%s' % (n, suffix), - cmd = ';'.join([ - "echo '/** @license' >$OUT", - 'unzip -p $(location :%s) %s/LICENSE >>$OUT' % (archive, top), - "echo '*/' >>$OUT", - 'unzip -p $(location :%s) %s/theme/%s.css >>$OUT' % (archive, top, n) - ] - ), - out = 'theme_%s%s.css' % (n, suffix), - ) - - # Merge Addon bundled with diff-match-patch - genrule( - name = 'addon_merge%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 '/** @license' >>$OUT", - 'cat $(location //lib:LICENSE-Apache2.0) >>$OUT', - "echo '*/' >>$OUT", - 'unzip -p $(location :diff-match-patch) %s/diff_match_patch.js >>$OUT' % DIFF_MATCH_PATCH_TOP, - "echo ';' >> $OUT", - 'unzip -p $(location :%s) %s/addon/merge/merge.js >>$OUT' % (archive, top) - ] - ), - out = 'addon_merge%s.js' % suffix, - ) - - # Jar packaging - genrule( - name = 'jar' + suffix, - cmd = ';'.join([ - 'cd $TMP', - 'mkdir -p net/codemirror/{addon,lib,mode,theme}', - 'cp $(location :css%s) net/codemirror/lib/cm.css' % suffix, - 'cp $(location :cm%s) net/codemirror/lib/cm.js' % suffix] - + ['cp $(location :mode_%s%s) net/codemirror/mode/%s.js' % (n, suffix, n) - 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] - + ['zip -qr $OUT net/codemirror/{addon,lib,mode,theme}']), - out = 'codemirror%s.jar' % suffix, - ) - - prebuilt_jar( - name = 'codemirror' + suffix, - binary_jar = ':jar%s' % suffix, - deps = [ - ':jar' + suffix, - '//lib:LICENSE-' + archive, - ], - visibility = ['PUBLIC'], - ) -
diff --git a/lib/codemirror/BUILD b/lib/codemirror/BUILD new file mode 100644 index 0000000..f8f6c0b --- /dev/null +++ b/lib/codemirror/BUILD
@@ -0,0 +1,11 @@ +load("//lib/codemirror:cm.bzl", "pkg_cm") + +# This library is only used to insert a license statement into +# js_licenses.txt. +java_library( + name = "diff-match-patch", + runtime_deps = [ "@diff_match_patch//jar" ], + data = [ "//lib:LICENSE-Apache2.0" ], +) + +pkg_cm()
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl new file mode 100644 index 0000000..168ab33 --- /dev/null +++ b/lib/codemirror/cm.bzl
@@ -0,0 +1,355 @@ +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/client/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", + "duotone-dark", + "duotone-light", + "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", +] + +CM_VERSION = "5.22.0" + +TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION + +TOP_MINIFIED = "META-INF/resources/webjars/codemirror-minified/%s" % CM_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', + # dependency just for license tracking. + ':diff-match-patch', + 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 deleted file mode 100644 index baf2ce5..0000000 --- a/lib/codemirror/cm.defs +++ /dev/null
@@ -1,211 +0,0 @@ -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', - 'jade', - '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', - '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', -]
diff --git a/lib/commons/BUCK b/lib/commons/BUCK deleted file mode 100644 index 7c27477..0000000 --- a/lib/commons/BUCK +++ /dev/null
@@ -1,86 +0,0 @@ -include_defs('//lib/maven.defs') - -maven_jar( - name = 'codec', - id = 'commons-codec:commons-codec:1.4', - sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], -) - -maven_jar( - name = 'collections', - id = 'commons-collections:commons-collections:3.2.2', - sha1 = '8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], - attach_source = False, -) - -maven_jar( - name = 'compress', - id = 'org.apache.commons:commons-compress:1.7', - sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], -) - -maven_jar( - name = 'dbcp', - id = 'commons-dbcp:commons-dbcp:1.4', - sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39', - license = 'Apache2.0', - deps = [':pool'], - exclude = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', - 'testpool.jocl' - ], -) - -maven_jar( - name = 'lang', - id = 'commons-lang:commons-lang:2.6', - sha1 = '0ce1edb914c94ebc388f086c6827e8bdeec71ac2', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], -) - -maven_jar( - name = 'net', - id = 'commons-net:commons-net:3.5', - sha1 = '342fc284019f590e1308056990fdb24a08f06318', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], -) - -maven_jar( - name = 'pool', - id = 'commons-pool:commons-pool:1.5.5', - sha1 = '7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b', - license = 'Apache2.0', - attach_source = False, - exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'], -) - -maven_jar( - name = 'oro', - id = 'oro:oro:2.0.8', - sha1 = '5592374f834645c4ae250f4c9fbb314c9369d698', - license = 'Apache1.1', - attach_source = False, - exclude = ['META-INF/LICENSE'], -) - -# When updating the version of commons-validator, also update the -# list of supported TLDs in: -# gerrit-server/src/test/resources/com/google/gerrit/server/mail/tlds-alpha-by-domain.txt -# from: -# http://data.iana.org/TLD/tlds-alpha-by-domain.txt -maven_jar( - name = 'validator', - id = 'commons-validator:commons-validator:1.5.1', - sha1 = '86d05a46e8f064b300657f751b5a98c62807e2a0', - license = 'Apache2.0', -) -
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/BUCK b/lib/dropwizard/BUCK deleted file mode 100644 index de73e13..0000000 --- a/lib/dropwizard/BUCK +++ /dev/null
@@ -1,8 +0,0 @@ -include_defs('//lib/maven.defs') - -maven_jar( - name = 'dropwizard-core', - id = 'io.dropwizard.metrics:metrics-core:3.1.2', - sha1 = '224f03afd2521c6c94632f566beb1bb5ee32cf07', - license = 'Apache2.0', -)
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 deleted file mode 100644 index 93640a0..0000000 --- a/lib/easymock/BUCK +++ /dev/null
@@ -1,31 +0,0 @@ -include_defs('//lib/maven.defs') - -maven_jar( - name = 'easymock', - id = 'org.easymock:easymock:3.4', # When bumping the version - # number, make sure to also move powermock to a compatible version - sha1 = '9fdeea183a399f25c2469497612cad131e920fa3', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':cglib-2_2', - ':objenesis', - ], -) - -maven_jar( - name = 'cglib-2_2', - id = 'cglib:cglib-nodep:2.2.2', - sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941', - license = 'DO_NOT_DISTRIBUTE', - attach_source = False, -) - -maven_jar( - name = 'objenesis', - id = 'org.objenesis:objenesis:2.2', - sha1 = '3fb533efdaa50a768c394aa4624144cf8df17845', - 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/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 deleted file mode 100644 index c5b78eb..0000000 --- a/lib/fonts/BUCK +++ /dev/null
@@ -1,30 +0,0 @@ -# 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 -genrule( - name = 'sourcecodepro', - cmd = 'zip -rq $OUT .', - srcs = [ - 'SourceCodePro-Regular.woff', - 'SourceCodePro-Regular.woff2' - ], - out = 'sourcecodepro.zip', - 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/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 deleted file mode 100644 index 867b521..0000000 --- a/lib/guice/BUCK +++ /dev/null
@@ -1,65 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '4.1.0' -EXCLUDE = [ - 'META-INF/DEPENDENCIES', - 'META-INF/LICENSE', - 'META-INF/NOTICE', -] - -java_library( - name = 'guice', - exported_deps = [ - ':guice_library', - ':javax-inject', - ], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'guice_library', - id = 'com.google.inject:guice:' + VERSION, - sha1 = 'eeb69005da379a10071aa4948c48d89250febb07', - license = 'Apache2.0', - deps = [':aopalliance'], - exclude_java_sources = True, - exclude = EXCLUDE + [ - 'META-INF/maven/com.google.guava/guava/pom.properties', - 'META-INF/maven/com.google.guava/guava/pom.xml', - ], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'guice-assistedinject', - id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION, - sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb', - license = 'Apache2.0', - deps = [':guice'], - exclude = EXCLUDE, -) - -maven_jar( - name = 'guice-servlet', - id = 'com.google.inject.extensions:guice-servlet:' + VERSION, - sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb', - license = 'Apache2.0', - deps = [':guice'], - exclude = EXCLUDE, -) - -maven_jar( - name = 'aopalliance', - id = 'aopalliance:aopalliance:1.0', - sha1 = '0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8', - license = 'PublicDomain', - visibility = ['//lib/guice:guice'], -) - -maven_jar( - name = 'javax-inject', - id = 'javax.inject:javax.inject:1', - sha1 = '6975da39a7040257bd51d21a231b76c915872d38', - 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 deleted file mode 100644 index ebd58f1..0000000 --- a/lib/gwt/BUCK +++ /dev/null
@@ -1,31 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '2.7.0' - -maven_jar( - name = 'user', - id = 'com.google.gwt:gwt-user:' + VERSION, - sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b', - license = 'Apache2.0', - attach_source = False, - exclude = ['javax/servlet/*'], -) - -maven_jar( - name = 'dev', - id = 'com.google.gwt:gwt-dev:' + VERSION, - sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982', - license = 'Apache2.0', - 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 = ['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/BUCK b/lib/highlightjs/BUCK deleted file mode 100644 index 9940136..0000000 --- a/lib/highlightjs/BUCK +++ /dev/null
@@ -1,5 +0,0 @@ -export_file( - name = 'highlightjs', - src = 'highlight.min.js', - visibility = ['PUBLIC'], -)
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 deleted file mode 100644 index 03669f2..0000000 --- a/lib/httpcomponents/BUCK +++ /dev/null
@@ -1,41 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '4.4.1' - -maven_jar( - name = 'fluent-hc', - id = 'org.apache.httpcomponents:fluent-hc:' + VERSION, - bin_sha1 = '96fb842b68a44cc640c661186828b60590c71261', - src_sha1 = '702515612b2b94ce3374ed5b579d38cbd308eb4f', - license = 'Apache2.0', - deps = [':httpclient'] -) - -maven_jar( - name = 'httpclient', - id = 'org.apache.httpcomponents:httpclient:' + VERSION, - bin_sha1 = '016d0bc512222f1253ee6b64d389c84e22f697f0', - src_sha1 = '30cb4791019c7280227e027b01814f4964a02482', - license = 'Apache2.0', - deps = [ - '//lib/commons:codec', - ':httpcore', - '//lib/log:jcl-over-slf4j', - ], -) - -maven_jar( - name = 'httpcore', - id = 'org.apache.httpcomponents:httpcore:' + VERSION, - bin_sha1 = 'f5aa318bda4c6c8d688c9d00b90681dcd82ce636', - src_sha1 = '9700be0d0a331691654a8e901943c9a74e33c5fc', - license = 'Apache2.0', -) - -maven_jar( - name = 'httpmime', - id = 'org.apache.httpcomponents:httpmime:' + VERSION, - bin_sha1 = '2f8757f5ac5e38f46c794e5229d1f3c522e9b1df', - src_sha1 = '5394d3715181a87009032335a55b0a9789f6e26f', - license = 'Apache2.0', -)
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/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 deleted file mode 100644 index cc22b80..0000000 --- a/lib/jetty/BUCK +++ /dev/null
@@ -1,95 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '9.2.14.v20151106' -EXCLUDE = ['about.html'] - -maven_jar( - name = 'servlet', - id = 'org.eclipse.jetty:jetty-servlet:' + VERSION, - sha1 = '3a2cd4d8351a38c5d60e0eee010fee11d87483ef', - license = 'Apache2.0', - deps = [':security'], - exclude = EXCLUDE, -) - -maven_jar( - name = 'security', - id = 'org.eclipse.jetty:jetty-security:' + VERSION, - sha1 = '2d36974323fcb31e54745c1527b996990835db67', - license = 'Apache2.0', - deps = [':server'], - exclude = EXCLUDE, - visibility = [], -) - -maven_jar( - name = 'servlets', - id = 'org.eclipse.jetty:jetty-servlets:' + VERSION, - sha1 = 'a75c78a0ee544073457ca5ee9db20fdc6ed55225', - license = 'Apache2.0', - exclude = EXCLUDE, - visibility = [ - '//tools/eclipse:classpath', - '//gerrit-gwtdebug:gwtdebug', - ], -) - -maven_jar( - name = 'server', - id = 'org.eclipse.jetty:jetty-server:' + VERSION, - sha1 = '70b22c1353e884accf6300093362b25993dac0f5', - license = 'Apache2.0', - exported_deps = [ - ':continuation', - ':http', - ], - exclude = EXCLUDE, -) - -maven_jar( - name = 'jmx', - id = 'org.eclipse.jetty:jetty-jmx:' + VERSION, - sha1 = '617edc5e966b4149737811ef8b289cd94b831bab', - license = 'Apache2.0', - exported_deps = [ - ':continuation', - ':http', - ], - exclude = EXCLUDE, -) - -maven_jar( - name = 'continuation', - id = 'org.eclipse.jetty:jetty-continuation:' + VERSION, - sha1 = '8909d62fd7e28351e2da30de6fb4105539b949c0', - license = 'Apache2.0', - exclude = EXCLUDE, -) - -maven_jar( - name = 'http', - id = 'org.eclipse.jetty:jetty-http:' + VERSION, - sha1 = '699ad1f2fa6fb0717e1b308a8c9e1b8c69d81ef6', - license = 'Apache2.0', - exported_deps = [':io'], - exclude = EXCLUDE, -) - -maven_jar( - name = 'io', - id = 'org.eclipse.jetty:jetty-io:' + VERSION, - sha1 = 'dfa4137371a3f08769820138ca1a2184dacda267', - license = 'Apache2.0', - exported_deps = [':util'], - exclude = EXCLUDE, - visibility = [], -) - -maven_jar( - name = 'util', - id = 'org.eclipse.jetty:jetty-util:' + VERSION, - sha1 = '0057e00b912ae0c35859ac81594a996007706a0b', - 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..99f0432 --- /dev/null +++ b/lib/jgit/jgit.bzl
@@ -0,0 +1,5 @@ +JGIT_VERS = "4.6.0.201612231935-r.30-gd3148f300" + +DOC_VERS = "4.6.0.201612231935-r" # Set to JGIT_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 deleted file mode 100644 index 7c967b3..0000000 --- a/lib/jgit/org.eclipse.jgit.archive/BUCK +++ /dev/null
@@ -1,16 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/JGIT_VERSION') - -maven_jar( - name = 'jgit-archive', - id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS, - sha1 = '2db2e7666672a31fa41b7e1dadcba51df6d30954', - license = 'jgit', - repository = REPO, - deps = ['//lib/jgit/org.eclipse.jgit:jgit'], - unsign = True, - exclude = [ - 'about.html', - 'plugin.properties', - ], - )
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 deleted file mode 100644 index 06865cb..0000000 --- a/lib/jgit/org.eclipse.jgit.http.server/BUCK +++ /dev/null
@@ -1,16 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/JGIT_VERSION') - -maven_jar( - name = 'jgit-servlet', - id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS, - sha1 = '6e36638888918d9941dddec7e2abe1f162cc74d9', - license = 'jgit', - repository = REPO, - deps = ['//lib/jgit/org.eclipse.jgit:jgit'], - unsign = True, - exclude = [ - 'about.html', - 'plugin.properties', - ], -)
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 deleted file mode 100644 index 77b637a..0000000 --- a/lib/jgit/org.eclipse.jgit.junit/BUCK +++ /dev/null
@@ -1,12 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/JGIT_VERSION') - -maven_jar( - name = 'junit', - id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS, - sha1 = 'e8fb1d81f588c3174a9730bdecdbde9faa04140a', - license = 'DO_NOT_DISTRIBUTE', - repository = REPO, - unsign = True, - deps = ['//lib/jgit/org.eclipse.jgit:jgit'], -)
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 deleted file mode 100644 index 458703c..0000000 --- a/lib/jgit/org.eclipse.jgit/BUCK +++ /dev/null
@@ -1,25 +0,0 @@ -include_defs('//lib/maven.defs') -include_defs('//lib/JGIT_VERSION') - -maven_jar( - name = 'jgit', - id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS, - bin_sha1 = '3e3d0b73dcf4ad649f37758ea8502d92f3d299de', - src_sha1 = 'fc352952db91a4046e4b832145eb2dc8afce8db1', - license = 'jgit', - repository = REPO, - unsign = True, - deps = [':ewah'], - exclude = [ - 'META-INF/eclipse.inf', - 'about.html', - 'plugin.properties', - ], -) - -maven_jar( - name = 'ewah', - id = 'com.googlecode.javaewah:JavaEWAH:0.7.9', - sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a', - 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/BUCK b/lib/joda/BUCK deleted file mode 100644 index d78c456..0000000 --- a/lib/joda/BUCK +++ /dev/null
@@ -1,25 +0,0 @@ -include_defs('//lib/maven.defs') - -EXCLUDE = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', -] - -maven_jar( - name = 'joda-time', - id = 'joda-time:joda-time:2.9.4', - sha1 = '1c295b462f16702ebe720bbb08f62e1ba80da41b', - deps = [':joda-convert'], - license = 'Apache2.0', - exclude = EXCLUDE, - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'joda-convert', - id = 'org.joda:joda-convert:1.8.1', - sha1 = '675642ac208e0b741bc9118dcbcae44c271b992a', - license = 'Apache2.0', - exclude = EXCLUDE, - visibility = ['//lib/joda:joda-time'], -)
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 deleted file mode 100644 index c9a4256..0000000 --- a/lib/js.defs +++ /dev/null
@@ -1,171 +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. - -NPMJS = 'NPMJS' -GERRIT = 'GERRIT' - -# NOTE: npm_binary rules do not get their licenses checked by gen_licenses.py, -# as we would have to cut too many edges. DO NOT include these binaries in -# build outputs. Using them in the build _process_ is ok. -def npm_binary( - name, - version, - sha1 = '', - repository = NPMJS, - visibility = ['PUBLIC']): - - dir = '%s-%s' % (name, version) - filename = '%s.tgz' % dir - dest = '%s@%s.npm_binary.tgz' % (name, version) - 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: - raise ValueError('invalid repository: %s' % repository) - cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', url] - if sha1: - cmd.extend(['-v', sha1]) - genrule( - name = name, - cmd = ' '.join(cmd), - out = dest, - visibility = visibility, - ) - - -def run_npm_binary(target): - return '$(location //tools/js:run_npm_binary) $(location %s)' % target - - -def bower_component( - name, - package, - version, - license, - deps = [], - semver = None, - sha1 = '', - visibility = ['PUBLIC']): - download_name = '%s__download_bower' % name - genrule( - name = download_name, - cmd = ' '.join([ - '$(exe //tools/js:download_bower)', - '-b', '"%s"' % run_npm_binary('//lib/js:bower'), - '-n', name, - '-p', package, - '-v', version, - '-s', sha1, - '-o', '$OUT', - ]), - out = '%s.zip' % download_name, - license = license, - visibility = [], - ) - - renamed_name = '%s__renamed' % name - genrule( - name = renamed_name, - cmd = ' && '.join([ - 'cd $TMP', - 'mkdir bower_components', - 'cd bower_components', - 'unzip $(location :%s)' % download_name, - 'cd ..', - 'zip -r $OUT bower_components', - ]), - out = '%s.zip' % renamed_name, - visibility = [], - ) - - genrule( - name = name, - cmd = _combine_components([':%s' % renamed_name] + deps), - out = '%s-%s.zip' % (name, version), - visibility = visibility, - ) - - version_name = '%s__bower_version' % name - dep_version = semver if semver is not None else version - deps_json = '{"%s": "%s#%s"}' % (name, package, dep_version) - genrule( - name = version_name, - cmd = "echo '%s' > $OUT" % deps_json, - out = version_name, - visibility = visibility, - ) - - -def bower_components( - name, - deps, - visibility = ['PUBLIC']): - genrule( - name = name, - cmd = _combine_components(deps), - out = '%s.bower_components.zip' % name, - visibility = visibility, - ) - - -def _combine_components(deps): - cmds = ['cd $TMP'] - for d in deps: - cmds.append('unzip -qo $(location %s)' % d) - cmds.append('zip -r $OUT bower_components') - return ' && '.join(cmds) - - -VULCANIZE_FLAGS = [ - '--inline-scripts', - '--inline-css', - '--strip-comments', -] - -def vulcanize( - name, - app, - srcs, - components, - extra_flags = [], - visibility = ['PUBLIC']): - genrule( - name = '%s__vulcanized' % name, - cmd = ' '.join([ - 'unzip', '-qd', '$TMP', '$(location %s)' % components, - '&&', 'rm', '-rf', '$SRCDIR/bower_components', - '&&', 'ln', '-s', '-f', '$TMP/bower_components', '.', - '&&', run_npm_binary('//lib/js:vulcanize') - ] + VULCANIZE_FLAGS + extra_flags + [ - '--out-html', '$OUT', - '$SRCDIR/%s' % app, - ]), - srcs = srcs, - out = '%s.vulcanized.html' % name, - visibility = visibility, - ) - - genrule( - name = name, - cmd = ' '.join([ - 'cd', '$TMP', - '&&', run_npm_binary('//lib/js:crisper'), '--always-write-script', - '--source', '$(location :%s__vulcanized)' % name, - '--html', '%s.html' % name, - '--js', '%s.js' % name, - '&&', 'zip', '$OUT', '%s.html' % name, '%s.js' % name, - ]), - out = '%s.vulcanized.zip' % name, - )
diff --git a/lib/js/BUCK b/lib/js/BUCK deleted file mode 100644 index 1c46d35..0000000 --- a/lib/js/BUCK +++ /dev/null
@@ -1,427 +0,0 @@ -include_defs('//lib/js.defs') - -# WHEN REVIEWING NEW NPM_BINARY RULES: -# -# You must check licenses in the transitive closure of dependencies to ensure -# they can be used by Gerrit. (npm binaries are not distributed with Gerrit -# releases, so we are less restrictive in our selection of licenses, but we -# still need to do a sanity check.) -# -# To do this: -# npm install -g license-checker -# mkdir /tmp/npmtmp -# cd /tmp/npmtmp -# npm install <package>@<version> -# license-checker -# (Piping to grep -o 'licenses:.*' and/or sort -u may make the output saner.) - -npm_binary( - name = 'bower', - version = '1.7.9', - sha1 = 'b7296c2393e0d75edaa6ca39648132dd255812b0', -) - -npm_binary( - name = 'crisper', - version = '2.0.2', - sha1 = '7183c58cea33632fb036c91cefd1b43e390d22a2', - repository = GERRIT, -) - -npm_binary( - name = 'vulcanize', - version = '1.14.8', - sha1 = '679107f251c19ab7539529b1e3fdd40829e6fc63', - repository = GERRIT, -) - -# ## Adding Bower component dependencies -# -# 1. Add a dummy bower_component rule to this file, specifying the semantic -# version you want to use. The actual version will be filled in by Bower, -# after evaluating the full dependency tree. -# -# bower_component( -# name = 'somepackage', -# package = 'someauthor/somepackage', -# version = 'TODO', -# semver = '~1.0.0', -# license = 'DO_NOT_DISTRIBUTE' -# ) -# -# 2. Add your bower_component as a dep to a bower_components rule. -# -# bower_components( -# name = 'polygerrit_components', -# deps = [ -# '//lib/js:foo', -# '//lib/js:somepackage', # NEW -# ], -# ) -# -# 3. Run bower2buck.py. -# -# buck run //tools/js:bower2buck -- -o /tmp/newbuck -# -# 4. Use your favorite diff tool to merge the output in newbuck with this file. -# bower2buck reevaluates semantic versions and may upgrade some packages, so -# you may need to make changes beyond the new component that was added. -# -# meld /tmp/newbuck lib/js/BUCK -# -# -# ## Updating Bower component dependencies -# -# Use the same procedure as for adding dependencies, except just change the -# version number of the existing bower_component rather than adding a new rule. - -bower_component( - name = 'accessibility-developer-tools', - package = 'accessibility-developer-tools', - version = '2.10.0', - license = 'DO_NOT_DISTRIBUTE', - sha1 = 'bc1a5e56ff1bed7a7a6ef22a4b4e8300e4822aa5', -) - -bower_component( - name = 'async', - package = 'async', - version = '1.5.2', - license = 'DO_NOT_DISTRIBUTE', - sha1 = '1ec975d3b3834646a7e3d4b7e68118b90ed72508', -) - -bower_component( - name = 'chai', - package = 'chai', - version = '3.5.0', - license = 'DO_NOT_DISTRIBUTE', - sha1 = '849ad3ee7c77506548b7b5db603a4e150b9431aa', -) - -bower_component( - name = 'es6-promise', - package = 'stefanpenner/es6-promise', - version = '3.3.0', - license = 'es6-promise', - sha1 = 'a3a797bb22132f1ef75f9a2556173f81870c2e53', -) - -bower_component( - name = 'fetch', - package = 'fetch', - version = '1.0.0', - license = 'fetch', - sha1 = '1b05a2bb40c73232c2909dc196de7519fe4db7a9', -) - -bower_component( - name = 'iron-a11y-announcer', - package = 'polymerelements/iron-a11y-announcer', - version = '1.0.4', - deps = [':polymer'], - license = 'polymer', - sha1 = '9a915711b35092fa2f86ff6e904c4f3e43aa5234', -) - -bower_component( - name = 'iron-a11y-keys-behavior', - package = 'polymerelements/iron-a11y-keys-behavior', - version = '1.1.2', - deps = [':polymer'], - license = 'polymer', - sha1 = '57fd39ee153ce37ed719ba3f7a405afb987d54f9', -) - -bower_component( - name = 'iron-autogrow-textarea', - package = 'polymerelements/iron-autogrow-textarea', - version = '1.0.12', - deps = [ - ':iron-behaviors', - ':iron-flex-layout', - ':iron-form-element-behavior', - ':iron-validatable-behavior', - ':polymer', - ], - license = 'polymer', - sha1 = 'b9b6874c9a2b5be435557a827ff8bd6661672ee3', -) - -bower_component( - name = 'iron-behaviors', - package = 'polymerelements/iron-behaviors', - version = '1.0.16', - deps = [ - ':iron-a11y-keys-behavior', - ':polymer', - ], - license = 'polymer', - sha1 = 'bd70636a2c0a78c50d1a76f9b8ca1ffd815478a3', -) - -bower_component( - name = 'iron-dropdown', - package = 'polymerelements/iron-dropdown', - version = '1.4.0', - deps = [ - ':iron-a11y-keys-behavior', - ':iron-behaviors', - ':iron-overlay-behavior', - ':iron-resizable-behavior', - ':neon-animation', - ':polymer', - ], - license = 'polymer', - sha1 = '63e3d669a09edaa31c4f05afc76b53b919ef0595', -) - -bower_component( - name = 'iron-fit-behavior', - package = 'polymerelements/iron-fit-behavior', - version = '1.2.2', - deps = [':polymer'], - license = 'polymer', - sha1 = 'bc53e9bab36b21f086ab8fac8c53cc7214aa1890', -) - -bower_component( - name = 'iron-flex-layout', - package = 'polymerelements/iron-flex-layout', - version = '1.3.1', - deps = [':polymer'], - license = 'polymer', - sha1 = 'ba696394abff5e799fc06eb11bff4720129a1b52', -) - -bower_component( - name = 'iron-form-element-behavior', - package = 'polymerelements/iron-form-element-behavior', - version = '1.0.6', - deps = [':polymer'], - license = 'polymer', - sha1 = '8d9e6530edc1b99bec1a5c34853911fba3701220', -) - -bower_component( - name = 'iron-input', - package = 'polymerelements/iron-input', - version = '1.0.10', - deps = [ - ':iron-a11y-announcer', - ':iron-validatable-behavior', - ':polymer', - ], - license = 'polymer', - sha1 = '9bc0c8e81de2527125383cbcf74dd9f27e7fa9ac', -) - -bower_component( - name = 'iron-meta', - package = 'polymerelements/iron-meta', - version = '1.1.1', - deps = [':polymer'], - license = 'polymer', - sha1 = 'e06281b6ddb3355ceca44975a167381b1fd72ce5', -) - -bower_component( - name = 'iron-overlay-behavior', - package = 'polymerelements/iron-overlay-behavior', - version = '1.7.6', - deps = [ - ':iron-a11y-keys-behavior', - ':iron-fit-behavior', - ':iron-resizable-behavior', - ':polymer', - ], - license = 'polymer', - sha1 = '83181085fda59446ce74fd0d5ca30c223f38ee4a', -) - -bower_component( - name = 'iron-resizable-behavior', - package = 'polymerelements/iron-resizable-behavior', - version = '1.0.3', - deps = [':polymer'], - license = 'polymer', - sha1 = '5982a3e19af7ed3e3de276a9b7bd266b3a144002', -) - -bower_component( - name = 'iron-selector', - package = 'polymerelements/iron-selector', - version = '1.5.2', - deps = [':polymer'], - license = 'polymer', - sha1 = 'c57235dfda7fbb987c20ad0e97aac70babf1a1bf', -) - -bower_component( - name = 'iron-test-helpers', - package = 'polymerelements/iron-test-helpers', - version = '1.2.5', - deps = [':polymer'], - license = 'DO_NOT_DISTRIBUTE', - sha1 = '433b03b106f5ff32049b84150cd70938e18b67ac', -) - -bower_component( - name = 'iron-validatable-behavior', - package = 'polymerelements/iron-validatable-behavior', - version = '1.1.1', - deps = [ - ':iron-meta', - ':polymer', - ], - license = 'polymer', - sha1 = '480423380be0536f948735d91bc472f6e7ced5b4', -) - -bower_component( - name = 'lodash', - package = 'lodash', - version = '3.10.1', - license = 'DO_NOT_DISTRIBUTE', - sha1 = '2f207a8293c4c554bf6cf071241f7a00dc513d3a', -) - -bower_component( - name = 'mocha', - package = 'mocha', - version = '2.5.1', - license = 'DO_NOT_DISTRIBUTE', - sha1 = 'cb29bdd1047cfd9304659ecf10ec263f9c888c99', -) - -bower_component( - name = 'moment', - package = 'moment/moment', - version = '2.13.0', - license = 'moment', - sha1 = 'fc8ce2c799bab21f6ced7aff928244f4ca8880aa', -) - -bower_component( - name = 'neon-animation', - package = 'polymerelements/neon-animation', - version = '1.2.3', - deps = [ - ':iron-meta', - ':iron-resizable-behavior', - ':iron-selector', - ':polymer', - ':web-animations-js', - ], - license = 'polymer', - sha1 = '71cc0d3e0afdf8b8563e87d2ff03a6fa19183bd9', -) - -bower_component( - name = 'page', - package = 'visionmedia/page.js', - version = '1.7.1', - license = 'page.js', - sha1 = '51a05428dd4f68fae1df5f12d0e2b61ba67f7757', -) - -bower_component( - name = 'polymer', - package = 'polymer/polymer', - version = '1.4.0', - deps = [':webcomponentsjs'], - license = 'polymer', - sha1 = 'b84725939ead7c7bdf9917b065f68ef8dc790d06', -) - -bower_component( - name = 'promise-polyfill', - package = 'polymerlabs/promise-polyfill', - version = '1.0.0', - deps = [':polymer'], - license = 'promise-polyfill', - sha1 = 'a3b598c06cbd7f441402e666ff748326030905d6', -) - -bower_component( - name = 'sinon-chai', - package = 'sinon-chai', - version = '2.8.0', - license = 'DO_NOT_DISTRIBUTE', - sha1 = '0464b5d944fdf8116bb23e0b02ecfbac945b3517', -) - -bower_component( - name = 'sinonjs', - package = 'sinonjs', - version = '1.17.1', - license = 'DO_NOT_DISTRIBUTE', - sha1 = 'a26a6aab7358807de52ba738770f6ac709afd240', -) - -bower_component( - name = 'stacky', - package = 'stacky', - version = '1.3.2', - license = 'DO_NOT_DISTRIBUTE', - sha1 = 'd6c07a0112ab2e9677fe085933744466a89232fb', -) - -bower_component( - name = 'test-fixture', - package = 'polymerelements/test-fixture', - version = '1.1.1', - license = 'DO_NOT_DISTRIBUTE', - sha1 = 'e373bd21c069163c3a754e234d52c07c77b20d3c', -) - -bower_component( - name = 'web-animations-js', - package = 'web-animations/web-animations-js', - version = '2.2.1', - license = 'Apache2.0', - sha1 = '0e73b263a86aa6764ad35c273eb12055f83d7eda', -) - -bower_component( - name = 'web-component-tester', - package = 'web-component-tester', - version = '4.2.2', - deps = [ - ':accessibility-developer-tools', - ':async', - ':chai', - ':lodash', - ':mocha', - ':sinon-chai', - ':sinonjs', - ':stacky', - ':test-fixture', - ], - license = 'DO_NOT_DISTRIBUTE', - sha1 = '54556000c33d9ed7949aa546c1b4a1531491a5f0', -) - -bower_component( - name = 'webcomponentsjs', - package = 'webcomponentsjs', - version = '0.7.22', - license = 'polymer', - sha1 = '8ba97a4a279ec6973a19b171c462a7b5cf454fb9', -) - -# Zip highlightjs so that it can be imported as though it were a -# bower_component and also attach the library license to the Buck dependency -# graph. -HLJS_DIR = 'bower_components/highlightjs' -genrule( - name = 'highlightjs', - cmd = ' && '.join([ - 'mkdir -p %s' % HLJS_DIR, - 'cp $(location //lib/highlightjs:highlightjs) %s/highlight.min.js' % HLJS_DIR, - 'zip -r $OUT bower_components', - ]), - out = 'highlightjs.zip', - license = 'highlightjs', - visibility = ['PUBLIC'], -)
diff --git a/lib/js/BUILD b/lib/js/BUILD new file mode 100644 index 0000000..93b321f --- /dev/null +++ b/lib/js/BUILD
@@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools/bzl:js.bzl", "bower_component", "js_component") + +# For importing new versions of existing bower packages, +# +# 1) edit the versions of 'seed' components in WORKSPACE as desired +# +# 2) Run: 'python tools/js/bower2bazel.py -w lib/js/bower_archives.bzl -b lib/js/bower_components.bzl', to update dependency versions. +# + +# 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..bb047bd70 --- /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-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "async", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "chai", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "es6-promise", + license = "//lib:LICENSE-es6-promise", + 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-DO_NOT_DISTRIBUTE", + 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-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "mocha", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + 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-page.js", + seed = True, + ) + bower_component( + name = "polymer", + license = "//lib:LICENSE-polymer", + deps = [ ":webcomponentsjs" ], + seed = True, + ) + bower_component( + name = "promise-polyfill", + license = "//lib:LICENSE-promise-polyfill", + deps = [ ":polymer" ], + seed = True, + ) + bower_component( + name = "sinon-chai", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "sinonjs", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "stacky", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + ) + bower_component( + name = "test-fixture", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + seed = True, + ) + bower_component( + name = "web-animations-js", + license = "//lib:LICENSE-Apache2.0", + ) + bower_component( + name = "web-component-tester", + license = "//lib:LICENSE-DO_NOT_DISTRIBUTE", + 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/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/BUCK b/lib/log/BUCK deleted file mode 100644 index a5201f3..0000000 --- a/lib/log/BUCK +++ /dev/null
@@ -1,56 +0,0 @@ -include_defs('//lib/maven.defs') - -VER = '1.7.7' - -maven_jar( - name = 'api', - id = 'org.slf4j:slf4j-api:' + VER, - sha1 = '2b8019b6249bb05d81d3a3094e468753e2b21311', - license = 'slf4j', -) - -maven_jar( - name = 'nop', - id = 'org.slf4j:slf4j-nop:' + VER, - sha1 = '6cca9a3b999ff28b7a35ca762b3197cd7e4c2ad1', - license = 'slf4j', - deps = [':api'], -) - -maven_jar( - name = 'impl_log4j', - id = 'org.slf4j:slf4j-log4j12:' + VER, - sha1 = '58f588119ffd1702c77ccab6acb54bfb41bed8bd', - license = 'slf4j', - deps = [':log4j'], -) - -maven_jar( - name = 'jcl-over-slf4j', - id = 'org.slf4j:jcl-over-slf4j:' + VER, - sha1 = '56003dcd0a31deea6391b9e2ef2f2dc90b205a92', - license = 'slf4j', -) - -maven_jar( - name = 'log4j', - id = 'log4j:log4j:1.2.17', - sha1 = '5af35056b4d257e4b64b9e8069c0746e8b08629f', - license = 'Apache2.0', - exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'], -) - -maven_jar( - name = 'jsonevent-layout', - id = 'net.logstash.log4j:jsonevent-layout:1.7', - sha1 = '507713504f0ddb75ba512f62763519c43cf46fde', - license = 'Apache2.0', - deps = [':json-smart', '//lib/commons:lang'] -) - -maven_jar( - name = 'json-smart', - id = 'net.minidev:json-smart:1.1.1', - sha1 = '24a2f903d25e004de30ac602c5b47f2d4e420a59', - license = 'Apache2.0', -)
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 deleted file mode 100644 index c4a9872..0000000 --- a/lib/lucene/BUCK +++ /dev/null
@@ -1,75 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '5.5.0' - -# 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', - ], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'lucene-core', - id = 'org.apache.lucene:lucene-core:' + VERSION, - sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164', - license = 'Apache2.0', - exclude = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', - ], - visibility = [], -) - -maven_jar( - name = 'lucene-analyzers-common', - id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION, - sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc', - license = 'Apache2.0', - deps = [':lucene-core-and-backward-codecs'], - exclude = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', - ], -) - -maven_jar( - name = 'backward-codecs_jar', - id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION, - sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805', - license = 'Apache2.0', - deps = [':lucene-core'], - exclude = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', - ], - visibility = [], -) - -maven_jar( - name = 'lucene-misc', - id = 'org.apache.lucene:lucene-misc:' + VERSION, - sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca', - license = 'Apache2.0', - deps = [':lucene-core-and-backward-codecs'], - exclude = [ - 'META-INF/LICENSE.txt', - 'META-INF/NOTICE.txt', - ], -) - -maven_jar( - name = 'lucene-queryparser', - id = 'org.apache.lucene:lucene-queryparser:' + VERSION, - sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4', - license = 'Apache2.0', - deps = [':lucene-core-and-backward-codecs'], - exclude = [ - 'META-INF/LICENSE.txt', - '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/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/maven.defs b/lib/maven.defs deleted file mode 100644 index 913be35..0000000 --- a/lib/maven.defs +++ /dev/null
@@ -1,188 +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. - -GERRIT = 'GERRIT:' -GERRIT_API = 'GERRIT_API:' -MAVEN_CENTRAL = 'MAVEN_CENTRAL:' -MAVEN_LOCAL = 'MAVEN_LOCAL:' -MAVEN_SNAPSHOT = 'MAVEN_SNAPSHOT:' - -def define_license(name): - n = 'LICENSE-' + name - genrule( - name = n, - cmd = 'ln -s $SRCS $OUT', - srcs = [n], - out = n, - visibility = ['PUBLIC'], - ) - -def maven_jar( - name, - id, - license, - exclude = [], - exclude_java_sources = False, - unsign = False, - deps = [], - exported_deps = [], - sha1 = '', bin_sha1 = '', src_sha1 = '', - repository = MAVEN_CENTRAL, - attach_source = True, - visibility = ['PUBLIC'], - local_license = False): - from os import path - - def maven_snapshot(parts): - if len(parts) != 4: - raise NameError('%s:\nexpected id="groupId:artifactId:version:snapshot]"' - % id) - group, artifact, version, snapshot = parts - jar = path.join(name, - version + '-SNAPSHOT', - '-'.join([artifact.lower(), version, snapshot])) - url = '/'.join([ - repository, - group.replace('.', '/'), - artifact, - version + '-SNAPSHOT', - '-'.join([artifact.lower(), version, snapshot])]) - return jar, url - - def maven_release(parts): - if len(parts) not in [3, 4]: - raise NameError('%s:\nexpected id="groupId:artifactId:version[:classifier]"' - % id) - if len(parts) == 4: - group, artifact, version, classifier = parts - file_version = version + '-' + classifier - else: - group, artifact, version = parts - file_version = version - - jar = path.join(name, artifact.lower() + '-' + file_version) - url = '/'.join([ - repository, - group.replace('.', '/'), - artifact, - version, - artifact + '-' + file_version]) - - return jar, url - - parts = id.split(':') - if repository.startswith(MAVEN_SNAPSHOT): - jar, url = maven_snapshot(parts) - else: - jar, url = maven_release(parts) - - binjar = jar + '.jar' - binurl = url + '.jar' - - srcjar = jar + '-src.jar' - srcurl = url + '-sources.jar' - - cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', binurl] - if sha1: - cmd.extend(['-v', sha1]) - elif bin_sha1: - cmd.extend(['-v', bin_sha1]) - for x in exclude: - cmd.extend(['-x', x]) - if exclude_java_sources: - cmd.append('--exclude_java_sources') - if unsign: - cmd.append('--unsign') - - genrule( - name = '%s__download_bin' % name, - cmd = ' '.join(cmd), - out = binjar, - ) - license = ':LICENSE-' + license - if not local_license: - license = '//lib' + license - license = [license] - - if src_sha1 or attach_source: - cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', srcurl] - if src_sha1: - cmd.extend(['-v', src_sha1]) - genrule( - name = '%s__download_src' % name, - cmd = ' '.join(cmd), - out = srcjar, - ) - prebuilt_jar( - name = '%s_src' % name, - binary_jar = ':%s__download_src' % name, - deps = license, - visibility = visibility, - ) - else: - srcjar = None - genrule( - name = '%s_src' % name, - cmd = ':>$OUT', - out = '__%s__no_src' % name, - ) - - if exported_deps: - prebuilt_jar( - name = '%s__jar' % name, - deps = deps + license, - binary_jar = ':%s__download_bin' % name, - source_jar = ':%s__download_src' % name if srcjar else None, - ) - java_library( - name = name, - exported_deps = exported_deps + [':' + name + '__jar'], - visibility = visibility, - ) - else: - prebuilt_jar( - name = name, - deps = deps + license, - binary_jar = ':%s__download_bin' % name, - source_jar = ':%s__download_src' % name if srcjar else None, - visibility = visibility, - ) - - -def merge_maven_jars( - name, - srcs, - visibility = []): - - def cmd(jars): - return ('$(location //tools:merge_jars) $OUT ' - + ' '.join(['$(location %s)' % j for j in jars])) - - genrule( - name = '%s__merged_bin' % name, - cmd = cmd(['%s__download_bin' % s for s in srcs]), - out = '%s__merged.jar' % name, - ) - genrule( - name = '%s__merged_src' % name, - cmd = cmd(['%s__download_src' % s for s in srcs]), - # tools/eclipse/project.py requires -src.jar suffix. - out = '%s__merged-src.jar' % name, - ) - prebuilt_jar( - name = name, - binary_jar = ':%s__merged_bin' % name, - source_jar = ':%s__merged_src' % name, - visibility = visibility, - )
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/BUCK b/lib/mina/BUCK deleted file mode 100644 index f22a710..0000000 --- a/lib/mina/BUCK +++ /dev/null
@@ -1,26 +0,0 @@ -include_defs('//lib/maven.defs') - -EXCLUDE = [ - 'META-INF/DEPENDENCIES', - 'META-INF/LICENSE', - 'META-INF/NOTICE', -] - -maven_jar( - name = 'sshd', - id = 'org.apache.sshd:sshd-core:1.2.0', - sha1 = '4bc24a8228ba83dac832680366cf219da71dae8e', - src_sha1 = '490e3f03d7628ecf1cbb8317563fdbf06e68e29f', - license = 'Apache2.0', - deps = [':core'], - exclude = EXCLUDE, -) - -maven_jar( - name = 'core', - id = 'org.apache.mina:mina-core:2.0.10', - sha1 = 'a1cb1136b104219d6238de886bf5a3ea4554eb58', - src_sha1 = 'b70ff94ba379b4e825caca1af4ec83193fac4b10', - license = 'Apache2.0', - exclude = EXCLUDE, -)
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/BUCK b/lib/openid/BUCK deleted file mode 100644 index 728698b..0000000 --- a/lib/openid/BUCK +++ /dev/null
@@ -1,35 +0,0 @@ -include_defs('//lib/maven.defs') - -maven_jar( - name = 'consumer', - id = 'org.openid4java:openid4java:0.9.8', - sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160', - license = 'Apache2.0', - deps = [ - ':nekohtml', - ':xerces', - '//lib/httpcomponents:httpclient', - '//lib/log:jcl-over-slf4j', - '//lib/guice:guice', - ], - visibility = ['PUBLIC'], -) - -maven_jar( - name = 'nekohtml', - id = 'net.sourceforge.nekohtml:nekohtml:1.9.10', - sha1 = '14052461031a7054aa094f5573792feb6686d3de', - license = 'Apache2.0', - deps = [':xerces'], - attach_source = False, - visibility = [], -) - -maven_jar( - name = 'xerces', - id = 'xerces:xercesImpl:2.8.1', - sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1', - license = 'Apache2.0', - attach_source = False, - visibility = [], -)
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 deleted file mode 100644 index fabcb25..0000000 --- a/lib/ow2/BUCK +++ /dev/null
@@ -1,40 +0,0 @@ -include_defs('//lib/maven.defs') - -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-commons', - id = 'org.ow2.asm:asm-commons:' + VERSION, - sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c', - deps = [':ow2-asm-tree'], - 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/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 deleted file mode 100644 index b642457..0000000 --- a/lib/powermock/BUCK +++ /dev/null
@@ -1,73 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '1.6.4' # 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', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':powermock-module-junit4-common', - '//lib:junit', - ], -) - -maven_jar( - name = 'powermock-module-junit4-common', - id = 'org.powermock:powermock-module-junit4-common:' + VERSION, - sha1 = 'b0b578da443794ceb8224bd5f5f852aaf40f1b81', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':powermock-reflect', - '//lib:junit', - ], -) - -maven_jar( - name = 'powermock-reflect', - id = 'org.powermock:powermock-reflect:' + VERSION, - sha1 = '5532f4e7c42db4bca4778bc9f1afcd4b0ee0b893', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - '//lib:junit', - '//lib/easymock:objenesis', - ], -) - -maven_jar( - name = 'powermock-api-easymock', - id = 'org.powermock:powermock-api-easymock:' + VERSION, - sha1 = '5c385a0d8c13f84b731b75c6e90319c532f80b45', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':powermock-api-support', - '//lib/easymock:easymock', - ], -) - -maven_jar( - name = 'powermock-api-support', - id = 'org.powermock:powermock-api-support:' + VERSION, - sha1 = '314daafb761541293595630e10a3699ebc07881d', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':powermock-core', - ':powermock-reflect', - '//lib:junit', - ], -) - -maven_jar( - name = 'powermock-core', - id = 'org.powermock:powermock-core:' + VERSION, - sha1 = '85fb32e9ccba748d569fc36aef92e0b9e7f40b87', - license = 'DO_NOT_DISTRIBUTE', - deps = [ - ':powermock-reflect', - '//lib:javassist', - '//lib:junit', - ], -) -
diff --git a/lib/powermock/BUILD b/lib/powermock/BUILD index 8dc7d23..7353b56 100644 --- a/lib/powermock/BUILD +++ b/lib/powermock/BUILD
@@ -1,60 +1,67 @@ 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", + "@powermock_core//jar", + ], )
diff --git a/lib/prolog/BUCK b/lib/prolog/BUCK deleted file mode 100644 index 77fe5ac..0000000 --- a/lib/prolog/BUCK +++ /dev/null
@@ -1,64 +0,0 @@ -include_defs('//lib/maven.defs') - -VERSION = '1.4.1' -REPO = GERRIT - -maven_jar( - name = 'runtime', - id = 'com.googlecode.prolog-cafe:prolog-runtime:' + VERSION, - sha1 = 'c5d9f92e49c485969dcd424dfc0c08125b5f8246', - license = 'prologcafe', - repository = REPO, -) - -maven_jar( - name = 'compiler', - id = 'com.googlecode.prolog-cafe:prolog-compiler:' + VERSION, - sha1 = 'ac24044c6ec166fdcb352b78b80d187ead3eff41', - license = 'prologcafe', - repository = REPO, - deps = [ - ':io', - ':runtime', - ], -) - -maven_jar( - name = 'io', - id = 'com.googlecode.prolog-cafe:prolog-io:' + VERSION, - sha1 = 'b072426a4b1b8af5e914026d298ee0358a8bb5aa', - license = 'prologcafe', - repository = REPO, - deps = [':runtime'], - visibility = [], -) - -maven_jar( - name = 'cafeteria', - id = 'com.googlecode.prolog-cafe:prolog-cafeteria:' + VERSION, - sha1 = '8cbc3b0c19e7167c42d3f11667b21cb21ddec641', - license = 'prologcafe', - repository = REPO, - deps = [ - ':io', - ':runtime', - ], - visibility = ['//gerrit-pgm:'], -) - -java_binary( - name = 'compiler_bin', - main_class = 'BuckPrologCompiler', - deps = [':compiler_lib'], - visibility = ['PUBLIC'], -) - -java_library( - name = 'compiler_lib', - srcs = ['java/BuckPrologCompiler.java'], - deps = [ - ':compiler', - ':runtime', - ], - visibility = ['//tools/eclipse:classpath'], -)
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/lib/prolog/prolog.defs b/lib/prolog/prolog.defs deleted file mode 100644 index e74c21d..0000000 --- a/lib/prolog/prolog.defs +++ /dev/null
@@ -1,42 +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. - -def prolog_cafe_library( - name, - srcs, - deps = [], - visibility = []): - genrule( - name = name + '__pl2j', - cmd = '$(exe //lib/prolog:compiler_bin)' + - ' $TMP $OUT ' + - ' '.join(srcs), - srcs = srcs, - out = name + '.src.zip', - ) - java_library( - name = name + '__lib', - srcs = [':' + name + '__pl2j'], - deps = ['//lib/prolog:runtime'] + deps, - ) - genrule( - name = name + '__ln', - cmd = 'ln -s $(location :%s__lib) $OUT' % name, - out = name + '.jar', - ) - prebuilt_jar( - name = name, - binary_jar = ':%s__ln' % name, - visibility = visibility, - )
diff --git a/plugins/BUCK b/plugins/BUCK deleted file mode 100644 index c6bb7f1..0000000 --- a/plugins/BUCK +++ /dev/null
@@ -1,42 +0,0 @@ -BASE = get_base_path() -CORE = [ - 'commit-message-length-validator', - 'download-commands', - 'hooks', - 'replication', - 'reviewnotes', - 'singleusergroup' -] -CUSTOM = [ - # Add custom core plugins here -] - -# buck audit parses and resolves all deps even if not reachable -# from the root(s) passed to audit. Filter dependencies to only -# the ones that currently exist to allow buck to parse cleanly. -# TODO(sop): buck should more lazily resolve deps -def core_plugins(names): - from os import path - h, n = [], [] - for p in names: - if path.exists(path.join(BASE, p, 'BUCK')): - h.append(p) - else: - n.append(p) - return h, n -HAVE, NEED = core_plugins(CORE + CUSTOM) - -genrule( - name = 'core', - cmd = '' + - ';'.join(['echo >&2 plugins/'+n+' is required.' for n in NEED]) + - (';echo >&2;exit 1;' if NEED else '') + - 'mkdir -p $TMP/WEB-INF/plugins;' + - 'for s in ' + - ' '.join(['$(location //%s/%s:%s)' % (BASE, n, n) for n in HAVE]) + - ';do ln -s $s $TMP/WEB-INF/plugins;done;' + - 'cd $TMP;' + - 'zip -qr $OUT .', - out = 'core.zip', - visibility = ['//:release'], -)
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..b9b67ac 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin
@@ -1 +1 @@ -Subproject commit 536beda3ab4f6f8d8d8c5be1e3cf0e6b4e9b10d5 +Subproject commit b9b67ac2938795413b0768387d6014b45a455f26
diff --git a/plugins/download-commands b/plugins/download-commands index 5615076..46a8f7c 160000 --- a/plugins/download-commands +++ b/plugins/download-commands
@@ -1 +1 @@ -Subproject commit 5615076bcf114723d1744f7d8944f0df72dbbf2b +Subproject commit 46a8f7c67d190beabd9cbe6399d6bf85c304f7ee
diff --git a/plugins/external_plugin_deps.bzl b/plugins/external_plugin_deps.bzl new file mode 100644 index 0000000..391f920 --- /dev/null +++ b/plugins/external_plugin_deps.bzl
@@ -0,0 +1,2 @@ +def external_plugin_deps(): + pass \ No newline at end of file
diff --git a/plugins/hooks b/plugins/hooks index dc8d1c1..66741e2 160000 --- a/plugins/hooks +++ b/plugins/hooks
@@ -1 +1 @@ -Subproject commit dc8d1c18b3d140dd1b2fc7ffe4f4a53d39a1cf28 +Subproject commit 66741e2c77b92574f7dd7012e5326029eca783f5
diff --git a/plugins/replication b/plugins/replication index 39bf331..e5099ce 160000 --- a/plugins/replication +++ b/plugins/replication
@@ -1 +1 @@ -Subproject commit 39bf331502ec3f6d6143c99905f8d5565946ff41 +Subproject commit e5099cec8dbce52054640a9d0710071b1aa18e2b
diff --git a/plugins/reviewnotes b/plugins/reviewnotes index 3f3d572..a333627 160000 --- a/plugins/reviewnotes +++ b/plugins/reviewnotes
@@ -1 +1 @@ -Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c +Subproject commit a3336277c828d70c88de615e6f135018e1cd5e21
diff --git a/plugins/singleusergroup b/plugins/singleusergroup index 3ca1167..69a0553 160000 --- a/plugins/singleusergroup +++ b/plugins/singleusergroup
@@ -1 +1 @@ -Subproject commit 3ca1167edda713f4bfdcecd9c0e2626797d7027f +Subproject commit 69a0553ee0608f3057ec4e3dd17eb93401203181
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK deleted file mode 100644 index 80f9f29..0000000 --- a/polygerrit-ui/BUCK +++ /dev/null
@@ -1,33 +0,0 @@ -include_defs('//lib/js.defs') - -bower_components( - name = 'polygerrit_components', - deps = [ - '//lib/js:es6-promise', - '//lib/js:fetch', - '//lib/js:highlightjs', - '//lib/js:iron-autogrow-textarea', - '//lib/js:iron-dropdown', - '//lib/js:iron-input', - '//lib/js:iron-overlay-behavior', - '//lib/js:iron-selector', - '//lib/js:moment', - '//lib/js:page', - '//lib/js:polymer', - '//lib/js:promise-polyfill', - ], -) - -genrule( - name = 'fonts', - cmd = ' && '.join([ - 'cd $TMP', - 'for file in $SRCS; do unzip -q $file; done', - 'zip -q $OUT *', - ]), - srcs = [ - '//lib/fonts:sourcecodepro', - ], - out = 'fonts.zip', - visibility = ['PUBLIC'], -)
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..76979f1 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 [Bazel](https://bazel.build/) -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-bazel.html#_installation) +to get and install Bazel. + +## 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-bazel.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). @@ -58,8 +68,8 @@ that serves PolyGerrit: ```sh -buck build polygerrit && \ -java -jar buck-out/gen/polygerrit/polygerrit.war daemon --polygerrit-dev \ +bazel build polygerrit && \ +java -jar bazel-bin/polygerrit.war daemon --polygerrit-dev \ -d ../gerrit_testsite --console-log --show-stack-trace ``` @@ -81,16 +91,13 @@ Run all web tests: ```sh -buck test --no-results-cache --include web +./polygerrit-ui/app/run_test.sh ``` -The `--no-results-cache` flag prevents flaky test failures from being -cached. - If you need to pass additional arguments to `wct`: ```sh -WCT_ARGS='-p --some-flag="foo bar"' buck test --no-results-cache --include web +WCT_ARGS='-p --some-flag="foo bar"' ./polygerrit-ui/app/run_test.sh ``` For interactively working on a single test file, do the following:
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK deleted file mode 100644 index d03acf2..0000000 --- a/polygerrit-ui/app/BUCK +++ /dev/null
@@ -1,98 +0,0 @@ -include_defs('//lib/js.defs') - -WCT_TEST_PATTERNS = [ - 'test/*.js', - 'test/*.html', - '**/*_test.html', -] -PY_TEST_PATTERNS = ['polygerrit_wct_tests.py'] -APP_SRCS = glob( - ['**'], - excludes = [ - 'BUCK', - 'index.html', - 'test/**', - ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS) - -# List libraries to be copied statically into the build. (i.e. Libraries not -# expected to be Vulcanized.) -WEB_JS_LIBS = [ - ('bower_components/webcomponentsjs', 'webcomponents-lite.js'), - ('bower_components/highlightjs', 'highlight.min.js'), -] - -# Map the static libraries to commands for the polygerrit_ui rule. -JS_LIBS_MKDIR_CMDS = [] -JS_LIBS_UNZIP_CMDS = [] -for lib in WEB_JS_LIBS: - JS_LIBS_MKDIR_CMDS.append('mkdir -p ' + lib[0]) - path = lib[0] + '/' + lib[1] - cmd = 'unzip -p $(location //polygerrit-ui:polygerrit_components) %s>%s' % (path, path) - JS_LIBS_UNZIP_CMDS.append(cmd) - -# TODO(dborowitz): Putting these rules in this package avoids having to handle -# the app/ prefix like we would have to if this were in the parent directory. -# The only reason for the app subdirectory in the first place was convenience -# when witing server.go; when that goes away, we can just move all the files and -# these rules up one directory. -genrule( - name = 'polygerrit_ui', - cmd = ' && '.join([ - 'mkdir $TMP/polygerrit_ui', - 'cd $TMP/polygerrit_ui', - 'mkdir -p {fonts,elements}', - ' && '.join(JS_LIBS_MKDIR_CMDS), - 'unzip -qd fonts $(location //polygerrit-ui:fonts)', - 'unzip -qd elements $(location :gr-app)', - 'cp -rp $SRCDIR/* .', - ' && '.join(JS_LIBS_UNZIP_CMDS), - 'cd $TMP', - 'zip -9qr $OUT .', - ]), - srcs = glob([ - 'favicon.ico', - 'index.html', - 'styles/**/*.css' - ]), - out = 'polygerrit_ui.zip', - visibility = ['PUBLIC'], -) - -vulcanize( - name = 'gr-app', - app = 'elements/gr-app.html', - srcs = APP_SRCS, - components = '//polygerrit-ui:polygerrit_components', -) - -bower_components( - name = 'test_components', - deps = [ - '//polygerrit-ui:polygerrit_components', - '//lib/js:iron-test-helpers', - '//lib/js:test-fixture', - '//lib/js:web-component-tester', - ], -) - -genrule( - name = 'test_resources', - cmd = ' && '.join([ - 'cd $TMP', - 'unzip -q $(location :test_components)', - 'cp -r $SRCDIR/* .', - 'zip -r $OUT .', - ]), - srcs = APP_SRCS + glob(WCT_TEST_PATTERNS), - out = 'test_resources.zip', -) - -python_test( - name = 'polygerrit_tests', - srcs = glob(PY_TEST_PATTERNS), - resources = [':test_resources'], - labels = [ - 'manual', - 'web', - ], -)
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-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html new file mode 100644 index 0000000..aee9d7c --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
@@ -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. +--> +<script> +(function(window) { + 'use strict'; + + /** @polymerBehavior Gerrit.ChangeTableBehavior */ + var ChangeTableBehavior = { + CHANGE_TABLE_COLUMNS: [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ], + + /** + * Returns the complement to the given column array + * @param {Array} columns + */ + getComplementColumns: function(columns) { + return this.CHANGE_TABLE_COLUMNS.filter(function(column) { + return columns.indexOf(column) === -1; + }); + }, + + isColumnHidden: function(columnToCheck, columnsToDisplay) { + return columnsToDisplay.indexOf(columnToCheck) === -1; + }, + }; + + window.Gerrit = window.Gerrit || {}; + window.Gerrit.ChangeTableBehavior = ChangeTableBehavior; +})(window); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html new file mode 100644 index 0000000..71d831d --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -0,0 +1,106 @@ +<!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="gr-change-table-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('gr-change-table-behavior tests', function() { + var element; + var overlay; + + suiteSetup(function() { + // Define a Polymer element that uses this behavior. + Polymer({ + is: 'test-element', + behaviors: [Gerrit.ChangeTableBehavior], + }); + }); + + setup(function() { + element = fixture('basic'); + overlay = fixture('within-overlay'); + }); + + test('getComplementColumns', function() { + var columns = [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]; + assert.deepEqual(element.getComplementColumns(columns), []); + + columns = [ + 'Subject', + 'Status', + 'Project', + 'Branch', + 'Size', + ]; + assert.deepEqual(element.getComplementColumns(columns), + ['Owner', 'Updated']); + }); + + test('isColumnHidden', function() { + var columnToCheck = 'Project'; + var columnsToDisplay = [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]; + assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay)); + + var columnsToDisplay = [ + 'Subject', + 'Status', + 'Owner', + 'Branch', + 'Updated', + 'Size', + ]; + assert.isTrue(element.isColumnHidden(columnToCheck, columnsToDisplay)); + }); + }); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html 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/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html new file mode 100644 index 0000000..0dee091 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -0,0 +1,52 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-admin-view"> + <template> + <style> + main { + margin: 2em auto; + max-width: 46em; + } + h1 { + margin-bottom: .1em; + } + @media only screen and (max-width: 67em) { + main { + margin: 2em 0 2em 15em; + } + } + @media only screen and (max-width: 53em) { + .loading { + padding: 0 var(--default-horizontal-margin); + } + main { + margin: 2em 1em; + } + </style> + <main> + <h1>Admin</h1> + <section> + This page is not yet implemented in PolyGerrit. View it in the + <a id="gwtLink" href$="/?polygerrit=0#[[path]]" rel="external"> + Old UI</a> + </section> + </main> + </template> + <script src="gr-admin-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js new file mode 100644 index 0000000..cb248e1 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -0,0 +1,24 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-admin-view', + + properties: { + path: String, + }, + }); +})();
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..0df017b 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
@@ -13,10 +13,11 @@ See the License for the specific language governing permissions and limitations under the License. --> - +<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../behaviors/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"> @@ -25,7 +26,7 @@ <template> <style> :host { - display: flex; + display: table-row; border-bottom: 1px solid #eee; } :host([selected]) { @@ -34,12 +35,18 @@ :host([needs-review]) { font-weight: bold; } + :host([assigned]) { + background-color: #fcfad6; + } + :host([selected][assigned]) { + background-color: #fcfaa6; + } .cell { - flex-shrink: 0; padding: .3em .5em; } a { color: var(--default-text-color); + display: block; text-decoration: none; } a:hover { @@ -63,32 +70,58 @@ .u-gray-background { background-color: #F5F5F5; } + @media only screen and (max-width: 50em) { + :host { + display: flex; + } + } </style> <style include="gr-change-list-styles"></style> - <span class="cell keyboard"> + <td class="cell keyboard"> <span class="positionIndicator">▶</span> - </span> - <span class="cell star" hidden$="[[!showStar]]" hidden> + </td> + <td class="cell star" hidden$="[[!showStar]]" hidden> <gr-change-star change="{{change}}"></gr-change-star> - </span> - <a class="cell number" href$="[[changeURL]]" hidden$="[[!showNumber]]" hidden> - [[change._number]] - </a> - <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a> - <span class="cell status">[[changeStatusString(change)]]</span> - <span class="cell owner"> + </td> + <td class="cell number" hidden$="[[!showNumber]]" hidden> + <a href$="[[changeURL]]"> [[change._number]]</a> + </td> + <td class="cell subject" + hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"> + <a href$="[[changeURL]]">[[change.subject]]</a> + </td> + <td class="cell status" + hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"> + [[changeStatusString(change)]] + </td> + <td class="cell owner" + hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"> <gr-account-link account="[[change.owner]]"></gr-account-link> - </span> - <a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a> - <a class="cell branch" href$="[[_computeProjectBranchURL(change.project, change.branch)]]">[[change.branch]]</a> - <gr-date-formatter class="cell updated" date-str="[[change.updated]]"></gr-date-formatter> - <span class="cell size u-monospace"> + </td> + <td class="cell project" + hidden$="[[isColumnHidden('Project', visibleChangeTableColumns)]]"> + <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a> + </td> + <td class="cell branch" + hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"> + <a href$="[[_computeProjectBranchURL(change.project, change.branch)]]"> + [[change.branch]] + </a> + </td> + <td class="cell updated" + hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"> + <gr-date-formatter date-str="[[change.updated]]"></gr-date-formatter> + </td> + <td class="cell size u-monospace" + hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"> <span class="u-green"><span>+</span>[[change.insertions]]</span>, <span class="u-red"><span>-</span>[[change.deletions]]</span> - </span> + </td> <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <span title$="[[_computeLabelTitle(change, labelName)]]" - class$="[[_computeLabelClass(change, labelName)]]">[[_computeLabelValue(change, labelName)]]</span> + <td title$="[[_computeLabelTitle(change, labelName)]]" + class$="[[_computeLabelClass(change, labelName)]]"> + [[_computeLabelValue(change, labelName)]] + </td> </template> </template> <script src="gr-change-list-item.js"></script>
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..280de86 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
@@ -18,16 +18,7 @@ is: 'gr-change-list-item', properties: { - selected: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - needsReview: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, + visibleChangeTableColumns: Array, labelNames: { type: Array, }, @@ -43,7 +34,9 @@ }, behaviors: [ + Gerrit.ChangeTableBehavior, Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, ], _computeChangeURL: function(changeNum) { @@ -108,11 +101,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..3df613f 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
@@ -22,6 +22,7 @@ <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-change-list-item.html"> <test-fixture id="basic"> @@ -35,13 +36,17 @@ var element; setup(function() { + stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, + getLoggedIn: function() { return Promise.resolve(false); }, + }); element = fixture('basic'); }); test('change status', function() { var getStatusForChange = function(change) { element.change = change; - return element.$$('.cell.status').textContent; + return element.$$('.cell.status').textContent.trim(); }; assert.equal(getStatusForChange({mergeable: true}), ''); @@ -120,12 +125,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/'); @@ -133,5 +138,75 @@ assert.equal(element.changeURL, '/c/43/'); }); + test('no hidden columns', function() { + element.visibleChangeTableColumns = [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]; + + flushAsynchronousOperations(); + + element.CHANGE_TABLE_COLUMNS.forEach(function(column) { + var elementClass = '.' + column.toLowerCase(); + assert.isFalse(element.$$(elementClass).hidden); + }); + }); + + test('no hidden columns', function() { + element.visibleChangeTableColumns = [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]; + + flushAsynchronousOperations(); + + element.CHANGE_TABLE_COLUMNS.forEach(function(column) { + var elementClass = '.' + column.toLowerCase(); + assert.isFalse(element.$$(elementClass).hidden); + }); + }); + + test('project column hidden', function() { + element.visibleChangeTableColumns = [ + 'Subject', + 'Status', + 'Owner', + 'Branch', + 'Updated', + 'Size', + ]; + + flushAsynchronousOperations(); + + element.CHANGE_TABLE_COLUMNS.forEach(function(column) { + var elementClass = '.' + column.toLowerCase(); + if (column === 'Project') { + assert.isTrue(element.$$(elementClass).hidden); + } else { + assert.isFalse(element.$$(elementClass).hidden); + } + }); + }); + + test('random column does not exist', function() { + element.visibleChangeTableColumns = [ + 'Bad', + ]; + + flushAsynchronousOperations(); + var elementClass = '.bad'; + assert.isNotOk(element.$$(elementClass)); + }); + }); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html index 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..38b1db6 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. @@ -75,6 +76,11 @@ }, }, + listeners: { + 'next-page': '_handleNextPage', + 'previous-page': '_handlePreviousPage', + }, + attached: function() { this.fire('title-change', {title: this._query}); }, @@ -116,7 +122,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; } @@ -130,5 +137,17 @@ _hideNextArrow: function(loading, changesPerPage) { return loading || !this._changes || this._changes.length < changesPerPage; }, + + _handleNextPage() { + if (this._hideNextArrow(this._offset)) { return; } + page.show(this._computeNavLink( + this._query, this._offset, 1, this._changesPerPage)); + }, + + _handlePreviousPage() { + if (this._hidePrevArrow(this._offset)) { return; } + page.show(this._computeNavLink( + this._query, this._offset, -1, this._changesPerPage)); + }, }); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html 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..c9a8d64 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,8 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-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"> @@ -24,46 +25,66 @@ <dom-module id="gr-change-list"> <template> <style> - :host { - display: flex; - flex-direction: column; + #changeList { + border-collapse: collapse; + width: 100%; + } + .cell { + padding: .3em .5em; + } + th { + text-align: left; } </style> <style include="gr-change-list-styles"></style> - <div class="headerRow"> - <span class="topHeader keyboard"></span> <!-- keyboard position indicator --> - <span class="topHeader star" hidden$="[[!showStar]]" hidden></span> - <span class="topHeader number" hidden$="[[!showNumber]]" hidden>#</span> - <span class="topHeader subject">Subject</span> - <span class="topHeader status">Status</span> - <span class="topHeader owner">Owner</span> - <span class="topHeader project">Project</span> - <span class="topHeader branch">Branch</span> - <span class="topHeader updated">Updated</span> - <span class="topHeader size">Size</span> - <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <span class="topHeader label" title$="[[labelName]]"> - [[_computeLabelShortcut(labelName)]] - </span> + <table id="changeList"> + <tr class="headerRow"> + <th class="topHeader keyboard"></th> + <th class="topHeader star" hidden$="[[!showStar]]" hidden></th> + <th class="topHeader number" hidden$="[[!showNumber]]" hidden>#</th> + <template is="dom-repeat" items="[[changeTableColumns]]" as="item"> + <th class$="[[_lowerCase(item)]] topHeader" + hidden$="[[isColumnHidden(item, visibleChangeTableColumns)]]"> + [[item]] + </th> + </template> + <template is="dom-repeat" items="[[labelNames]]" as="labelName"> + <th class="topHeader label" title$="[[labelName]]"> + [[_computeLabelShortcut(labelName)]] + </th> + </template> + </tr> + <template is="dom-repeat" items="[[groups]]" as="changeGroup" + index-as="groupIndex"> + <template is="dom-if" if="[[_groupTitle(groupIndex)]]"> + <tr class="groupHeader"> + <td class="cell" + colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> + [[_groupTitle(groupIndex)]] + </td> + </tr> + </template> + <template is="dom-if" if="[[!changeGroup.length]]"> + <tr class="noChanges"> + <td class="cell" + colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> + No changes + </td> + </tr> + </template> + <template is="dom-repeat" items="[[changeGroup]]" as="change"> + <gr-change-list-item + selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]" + assigned$="[[_computeItemAssigned(account, change)]]" + needs-review$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" + change="[[change]]" + visible-change-table-columns="[[visibleChangeTableColumns]]" + show-number="[[showNumber]]" + show-star="[[showStar]]" + label-names="[[labelNames]]"></gr-change-list-item> + </template> </template> - </div> - <template is="dom-repeat" items="{{groups}}" as="changeGroup" index-as="groupIndex"> - <template is="dom-if" if="[[_groupTitle(groupIndex)]]"> - <div class="groupHeader">[[_groupTitle(groupIndex)]]</div> - </template> - <template is="dom-if" if="[[!changeGroup.length]]"> - <div class="noChanges">No changes</div> - </template> - <template is="dom-repeat" items="[[changeGroup]]" as="change"> - <gr-change-list-item - selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]" - needs-review="[[_computeItemNeedsReview(account, change, showReviewedState)]]" - change="[[change]]" - show-number="[[showNumber]]" - show-star="[[showStar]]" - label-names="[[labelNames]]"></gr-change-list-item> - </template> - </template> + </table> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-change-list.js"></script>
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..9f943dc 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -14,9 +14,23 @@ (function() { 'use strict'; + var NUMBER_FIXED_COLUMNS = 3; + Polymer({ is: 'gr-change-list', + /** + * Fired when next page key shortcut was pressed. + * + * @event next-page + */ + + /** + * Fired when previous page key shortcut was pressed. + * + * @event previous-page + */ + hostAttributes: { tabindex: 0, }, @@ -74,23 +88,41 @@ }, behaviors: [ + Gerrit.ChangeTableBehavior, Gerrit.KeyboardShortcutBehavior, Gerrit.RESTClientBehavior, ], + keyBindings: { + 'j': '_handleJKey', + 'k': '_handleKKey', + 'n ]': '_handleNKey', + 'o enter': '_handleEnterKey', + 'p [': '_handlePKey', + }, + attached: function() { this._loadPreferences(); }, + _lowerCase: function(column) { + return column.toLowerCase(); + }, + _loadPreferences: function() { return this._getLoggedIn().then(function(loggedIn) { + this.changeTableColumns = this.CHANGE_TABLE_COLUMNS; + if (!loggedIn) { this.showNumber = false; + this.visibleChangeTableColumns = this.CHANGE_TABLE_COLUMNS; return; } return this._getPreferences().then(function(preferences) { this.showNumber = !!(preferences && preferences.legacycid_in_change_table); + this.visibleChangeTableColumns = preferences.change_table.length > 0 ? + preferences.change_table : this.CHANGE_TABLE_COLUMNS; }.bind(this)); }.bind(this)); }, @@ -103,6 +135,11 @@ return this.$.restAPI.getPreferences(); }, + _computeColspan: function(changeTableColumns, labelNames) { + return changeTableColumns.length + labelNames.length + + NUMBER_FIXED_COLUMNS; + }, + _computeLabelNames: function(groups) { if (!groups) { return []; } var labels = []; @@ -149,31 +186,59 @@ account._account_id != change.owner._account_id; }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } + _computeItemAssigned: function(account, change) { + if (!change.assignee) { return false; } + return account._account_id === change.assignee._account_id; + }, - 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)); + }, + + _handleNKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.fire('next-page'); + }, + + _handlePKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.fire('previous-page'); }, _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..0ac850c 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"> @@ -71,9 +70,10 @@ suite('test show change number preference enabled', function() { setup(function(done) { - return stubRestAPI( - {legacycid_in_change_table: true, time_format: 'HHMM_12'} - ).then(function() { + return stubRestAPI({legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [], + }).then(function() { element = fixture('basic'); element._loadPreferences().then(function() { done(); }); }); @@ -87,9 +87,8 @@ suite('test show change number preference disabled', function() { setup(function(done) { // legacycid_in_change_table is not set when false. - return stubRestAPI( - {time_format: 'HHMM_12'} - ).then(function() { + return stubRestAPI({time_format: 'HHMM_12', change_table: []}).then( + function() { element = fixture('basic'); element._loadPreferences().then(function() { done(); }); }); @@ -118,6 +117,16 @@ 'Some-Special-Label-7'), 'SSL7'); }); + test('colspans', function() { + var thItemCount = Polymer.dom(element.root).querySelectorAll( + 'th').length; + + var changeTableColumns = []; + var labelNames = []; + assert.equal(thItemCount, element._computeColspan( + changeTableColumns, labelNames)); + }); + test('keyboard shortcuts', function(done) { element.selectedIndex = 0; element.changes = [ @@ -131,26 +140,26 @@ assert.equal(elementItems.length, 3); flush(function() { - assert.isTrue(elementItems[0].selected); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.isTrue(elementItems[0].hasAttribute('selected')); + 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(); @@ -236,6 +245,120 @@ '.noChanges'); assert.equal(noChangesMsg.length, 2); }); + + suite('empty column preference', function() { + var element; + + setup(function(done) { + return stubRestAPI({ + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [], + } + ).then(function() { + element = fixture('basic'); + element._loadPreferences().then(function() { done(); }); + }); + }); + + test('show number enabled', function() { + assert.isTrue(element.showNumber); + }); + + test('all columns visible', function() { + element.CHANGE_TABLE_COLUMNS.forEach(function(column) { + var elementClass = '.' + element._lowerCase(column); + assert.isFalse(element.$$(elementClass).hidden); + }); + }); + }); + + suite('full column preference', function() { + var element; + + setup(function(done) { + return stubRestAPI({ + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ], + }).then(function() { + element = fixture('basic'); + element._loadPreferences().then(function() { done(); }); + }); + }); + + test('all columns visible', function() { + element.changeTableColumns.forEach(function(column) { + var elementClass = '.' + element._lowerCase(column); + assert.isFalse(element.$$(elementClass).hidden); + }); + }); + }); + + suite('partial column preference', function() { + var element; + + setup(function(done) { + return stubRestAPI({ + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Branch', + 'Updated', + 'Size', + ], + }).then(function() { + element = fixture('basic'); + element._loadPreferences().then(function() { done(); }); + }); + }); + + test('all columns except project visible', function() { + element.changeTableColumns.forEach(function(column) { + var elementClass = '.' + column.toLowerCase(); + if (column === 'Project') { + assert.isTrue(element.$$(elementClass).hidden); + } else { + assert.isFalse(element.$$(elementClass).hidden); + } + }); + }); + }); + + suite('random column does not exist', function() { + var element; + + /* This would only exist if somebody manually updated the config + file. */ + setup(function(done) { + return stubRestAPI({ + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Bad', + ], + }).then(function() { + element = fixture('basic'); + element._loadPreferences().then(function() { done(); }); + }); + }); + + test('bad column does not exist', function() { + var elementClass = '.bad'; + assert.isNotOk(element.$$(elementClass)); + }); + }); }); suite('gr-change-list groups', function() { @@ -296,5 +419,32 @@ showStub.restore(); }); + test('assigned attribute set in each item', function() { + element.changes = [ + { + _number: 0, + status: 'NEW', + owner: {_account_id: 0}, + }, + { + _number: 1, + status: 'DRAFT', + owner: {_account_id: 42}, + }, + { + _number: 2, + status: 'ABANDONED', + owner: {_account_id: 0}, + }, + ]; + element.account = {_account_id: 42}; + flushAsynchronousOperations(); + var items = element._getListItems(); + assert.equal(items.length, 3); + for (var i = 0; i < items.length; i++) { + assert.equal(items[i].hasAttribute('assigned'), + items[i]._account_id === element.account._account_id); + } + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js index 3ac6463..e8ca5bf 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -29,6 +29,10 @@ value: function() { return {}; }, }, viewState: Object, + params: { + type: Object, + observer: '_paramsChanged', + }, _results: Array, _groupTitles: { @@ -51,7 +55,12 @@ attached: function() { this.fire('title-change', {title: 'My Reviews'}); + }, + /** + * Allows a refresh if menu item is selected again. + */ + _paramsChanged: function() { this._loading = true; this._getDashboardChanges().then(function(results) { this._results = results;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html new file mode 100644 index 0000000..6be8f7b --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-dashboard-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-dashboard-view.html"> + +<test-fixture id="basic"> + <template> + <gr-dashboard-view></gr-dashboard-view> + </template> +</test-fixture> + +<script> + suite('gr-dashboard-view tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('content is refreshed with same dropdown selected twice', function() { + var getChangesStub = sinon.stub(element, '_getDashboardChanges', + function() { + return Promise.resolve(); + }); + + element.params = {view: 'gr-dashboard-view'}; + + assert.equal(getChangesStub.callCount, 1); + element.params = {view: 'gr-dashboard-view'}; + assert.equal(getChangesStub.callCount, 2); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js index c5827d0..46f2ed2 100644 --- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js +++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -28,6 +28,16 @@ change: Object, filter: Function, placeholder: String, + /** + * When true, account-entry uses the account suggest API endpoint, which + * suggests any account in that Gerrit instance (and does not suggest + * groups). + * + * When false/undefined, account-entry uses the suggest_reviewers API + * endpoint, which suggests any account or group in that Gerrit instance + * that is not already a reviewer (or is not CCed) on that change. + */ + allowAnyUser: Boolean, suggestFrom: { type: Number, @@ -59,29 +69,41 @@ }, _makeSuggestion: function(reviewer) { + var name; + var value; + var generateStatusStr = function(account) { + return account.status ? ' (' + account.status + ')' : ''; + }; if (reviewer.account) { - return { - name: reviewer.account.name + ' (' + reviewer.account.email + ')', - value: reviewer, - }; + // Reviewer is an account suggestion from getChangeSuggestedReviewers. + name = reviewer.account.name + ' <' + reviewer.account.email + '>' + + generateStatusStr(reviewer.account); + value = reviewer; } else if (reviewer.group) { - return { - name: reviewer.group.name + ' (group)', - value: reviewer, - }; + // Reviewer is a group suggestion from getChangeSuggestedReviewers. + name = reviewer.group.name + ' (group)'; + value = reviewer; + } else if (reviewer._account_id) { + // Reviewer is an account suggestion from getSuggestedAccounts. + name = reviewer.name + ' <' + reviewer.email + '>' + + generateStatusStr(reviewer); + value = {account: reviewer, count: 1}; } + return {name: name, value: value}; }, _getReviewerSuggestions: function(input) { - var xhr = this.$.restAPI.getChangeSuggestedReviewers( - this.change._number, input); + var api = this.$.restAPI; + var xhr = this.allowAnyUser ? + api.getSuggestedAccounts(input) : + api.getChangeSuggestedReviewers(this.change._number, input); return xhr.then(function(reviewers) { if (!reviewers) { return []; } if (!this.filter) { return reviewers.map(this._makeSuggestion); } return reviewers .filter(this.filter) - .map(this._makeSuggestion); + .map(this._makeSuggestion.bind(this)); }.bind(this)); }, });
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html index 94db890..71e84d0 100644 --- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html +++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -33,13 +33,15 @@ <script> suite('gr-account-entry tests', function() { + var sandbox; var _nextAccountId = 0; - var makeAccount = function() { + var makeAccount = function(opt_status) { var accountId = ++_nextAccountId; return { _account_id: accountId, name: 'name ' + accountId, email: 'email ' + accountId, + status: opt_status, }; }; @@ -72,49 +74,99 @@ REVIEWER: [existingReviewer2], }, }; - - stub('gr-rest-api-interface', { - getChangeSuggestedReviewers: function() { - var redundantSuggestion1 = {account: existingReviewer1}; - var redundantSuggestion2 = {account: existingReviewer2}; - var redundantSuggestion3 = {account: owner}; - return Promise.resolve([redundantSuggestion1, redundantSuggestion2, - redundantSuggestion3, suggestion1, suggestion2, suggestion3]); - }, - }); + sandbox = sinon.sandbox.create(); }); - test('_makeSuggestion formats account or group accordingly', function() { - var account = makeAccount(); - var suggestion = element._makeSuggestion({account: account}); - assert.deepEqual(suggestion, { - name: account.name + ' (' + account.email + ')', - value: {account: account}, - }); - - var group = {name: 'test'}; - suggestion = element._makeSuggestion({group: group}); - assert.deepEqual(suggestion, { - name: group.name + ' (group)', - value: {group: group}, - }); + teardown(function() { + sandbox.restore(); }); - test('_getReviewerSuggestions excludes owner+reviewers', function(done) { - element._getReviewerSuggestions().then(function(reviewers) { - // Default is no filtering. - assert.equal(reviewers.length, 6); + suite('stubbed values for _getReviewerSuggestions', function() { + setup(function() { + stub('gr-rest-api-interface', { + getChangeSuggestedReviewers: function() { + var redundantSuggestion1 = {account: existingReviewer1}; + var redundantSuggestion2 = {account: existingReviewer2}; + var redundantSuggestion3 = {account: owner}; + return Promise.resolve([redundantSuggestion1, redundantSuggestion2, + redundantSuggestion3, suggestion1, suggestion2, suggestion3]); + }, + }); + }); - // Set up filter that only accepts suggestion1. - var accountId = suggestion1.account._account_id; - element.filter = function(suggestion) { - return suggestion.account && - suggestion.account._account_id === accountId; - }; + test('_makeSuggestion formats account or group accordingly', function() { + var account = makeAccount(); + var suggestion = element._makeSuggestion({account: account}); + assert.deepEqual(suggestion, { + name: account.name + ' <' + account.email + '>', + value: {account: account}, + }); + var group = {name: 'test'}; + suggestion = element._makeSuggestion({group: group}); + assert.deepEqual(suggestion, { + name: group.name + ' (group)', + value: {group: group}, + }); + + suggestion = element._makeSuggestion(account); + assert.deepEqual(suggestion, { + name: account.name + ' <' + account.email + '>', + value: {account: account, count: 1}, + }); + + account = makeAccount('OOO'); + + suggestion = element._makeSuggestion({account: account}); + assert.deepEqual(suggestion, { + name: account.name + ' <' + account.email + '> (OOO)', + value: {account: account}, + }); + + suggestion = element._makeSuggestion(account); + assert.deepEqual(suggestion, { + name: account.name + ' <' + account.email + '> (OOO)', + value: {account: account, count: 1}, + }); + }); + + test('_getReviewerSuggestions excludes owner+reviewers', function(done) { element._getReviewerSuggestions().then(function(reviewers) { - assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]); - }).then(done); + // Default is no filtering. + assert.equal(reviewers.length, 6); + + // Set up filter that only accepts suggestion1. + var accountId = suggestion1.account._account_id; + element.filter = function(suggestion) { + return suggestion.account && + suggestion.account._account_id === accountId; + }; + + element._getReviewerSuggestions().then(function(reviewers) { + assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]); + }).then(done); + }); + }); + }); + + test('allowAnyUser', function(done) { + var suggestReviewerStub = + sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers') + .returns(Promise.resolve([])); + var suggestAccountStub = + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts') + .returns(Promise.resolve([])); + + element._getReviewerSuggestions('').then(function() { + assert.isTrue(suggestReviewerStub.calledOnce); + assert.isFalse(suggestAccountStub.called); + element.allowAnyUser = true; + + element._getReviewerSuggestions('').then(function() { + assert.isTrue(suggestReviewerStub.calledOnce); + assert.isTrue(suggestAccountStub.calledOnce); + done(); + }); }); }); });
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..810658c 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,17 +42,21 @@ 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 borderless - hidden$="[[readonly]]" + hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" id="entry" change="[[change]]" filter="[[filter]]" placeholder="[[placeholder]]" - on-add="_handleAdd"> + on-add="_handleAdd" + on-input-keydown="_handleInputKeydown" + allow-any-user="[[allowAnyUser]]"> </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..35311f9 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
@@ -21,6 +21,7 @@ accounts: { type: Array, value: function() { return []; }, + notify: true, }, change: Object, filter: Function, @@ -30,13 +31,42 @@ value: null, notify: true, }, - readonly: Boolean, + readonly: { + type: Boolean, + value: false, + }, + /** + * When true, the account-entry autocomplete uses the account suggest API + * endpoint, which suggests any account in that Gerrit instance (and does + * not suggest groups). + * + * When false/undefined, account-entry uses the suggest_reviewers API + * endpoint, which suggests any account or group in that Gerrit instance + * that is not already a reviewer (or is not CCed) on that change. + */ + allowAnyUser: { + type: Boolean, + value: false, + }, + /** + * Array of values (groups/accounts) that are removable. When this prop is + * undefined, all values are removable. + */ + removableValues: Array, + maxCount: { + type: Number, + value: 0, + }, }, listeners: { 'remove': '_handleRemove', }, + get accountChips() { + return Polymer.dom(this.root).querySelectorAll('gr-account-chip'); + }, + get focusStart() { return this.$.entry.focusStart; }, @@ -81,11 +111,26 @@ }, _computeRemovable: function(account) { - return !this.readonly && !!account._pendingAdd; + if (this.readonly) { return false; } + if (this.removableValues) { + for (var i = 0; i < this.removableValues.length; i++) { + if (this.removableValues[i]._account_id === account._account_id) { + return true; + } + } + return !!account._pendingAdd; + } + return true; }, _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 +141,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 +214,10 @@ return {account: account}; } }); - return result; + }, + + _computeEntryHidden: function(maxCount, accountsRecord, readonly) { + return (maxCount && maxCount <= accountsRecord.base.length) || readonly; }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html index bb55d08..a334b03 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); @@ -155,9 +157,16 @@ var newAccount = makeAccount(); newAccount._pendingAdd = true; element.readonly = false; + element.removableValues = []; assert.isFalse(element._computeRemovable(existingReviewer1)); assert.isTrue(element._computeRemovable(newAccount)); + + element.removableValues = [existingReviewer1]; + assert.isTrue(element._computeRemovable(existingReviewer1)); + assert.isTrue(element._computeRemovable(newAccount)); + assert.isFalse(element._computeRemovable(existingReviewer2)); + element.readonly = true; assert.isFalse(element._computeRemovable(existingReviewer1)); assert.isFalse(element._computeRemovable(newAccount)); @@ -232,5 +241,86 @@ }, ]); }); + + 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); + }); + + test('max-count', function() { + element.maxCount = 1; + var acct = makeAccount(); + element._handleAdd({ + detail: { + value: { + account: acct, + }, + }, + }); + flushAsynchronousOperations(); + assert.isTrue(element.$.entry.hasAttribute('hidden')); + }); + + 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..982e400 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
@@ -19,6 +19,7 @@ <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-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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"> @@ -32,75 +33,92 @@ <template> <style> :host { - display: block; + display: inline-block; + font-family: var(--font-family); } section { - margin-top: 1em; + display: inline-block; } - .groupLabel { - color: #666; - margin-bottom: .15em; - text-align: center; - } - gr-button { - display: block; - margin-bottom: .5em; + gr-button, + gr-dropdown { + margin-left: .5em; } gr-button:before { content: attr(data-label); } - gr-button[loading]:before { - content: attr(data-loading-label); + #actionLoadingMessage { + color: #777; } @media screen and (max-width: 50em) { + :host, + section, + gr-button, + gr-dropdown { + display: block; + } + gr-button, + gr-dropdown { + margin-bottom: .5em; + margin-left: 0; + } .confirmDialog { width: 90vw; } + #actionLoadingMessage { + display: block; + margin: .5em; + text-align: center; + } } </style> <div> - <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]"> - <div class="groupLabel">Change</div> - <template is="dom-repeat" items="[[_changeActionValues]]" as="action"> + <span + id="actionLoadingMessage" + hidden$="[[!_actionLoadingMessage]]"> + [[_actionLoadingMessage]]</span> + <gr-dropdown + id="moreActions" + down-arrow + vertical-offset="32" + horizontal-align="right" + on-tap-item-abandon="_handleAbandonTap" + on-tap-item-cherrypick="_handleCherrypickTap" + on-tap-item-delete="_handleDeleteTap" + hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]" + items="[[_menuActions]]">More</gr-dropdown> + <section hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"> + <template + is="dom-repeat" + items="[[_topLevelActions]]" + as="action"> <gr-button title$="[[action.title]]" primary$="[[action.__primary]]" - hidden$="[[!action.enabled]]" data-action-key$="[[action.__key]]" data-action-type$="[[action.__type]]" data-label$="[[action.label]]" - data-loading-label$="[[_computeLoadingLabel(action.__key)]]" - on-tap="_handleActionTap"></gr-button> - </template> - </section> - <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]"> - <div class="groupLabel">Revision</div> - <template is="dom-repeat" items="[[_revisionActionValues]]" as="action"> - <gr-button title$="[[action.title]]" - primary$="[[action.__primary]]" disabled$="[[!action.enabled]]" - data-action-key$="[[action.__key]]" - data-action-type$="[[action.__type]]" - data-label$="[[action.label]]" - data-loading-label$="[[_computeLoadingLabel(action.__key)]]" on-tap="_handleActionTap"></gr-button> </template> </section> + <gr-button hidden$="[[!_loading]]" disabled>Loading actions...</gr-button> </div> <gr-overlay id="overlay" with-backdrop> <gr-confirm-rebase-dialog id="confirmRebase" + rebase-on-current="[[rebaseOnCurrent]]" class="confirmDialog" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" 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 +127,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..69f4f57 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,25 @@ var ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; + var QUICK_APPROVE_ACTION = { + __key: 'review', + __type: 'change', + enabled: true, + key: 'review', + label: 'Quick Approve', + method: 'POST', + }; + + /** + * Keys for actions to appear in the overflow menu rather than the top-level + * set of action buttons. + */ + var MENU_ACTION_KEYS = [ + 'abandon', + 'cherrypick', + '/', // '/' is the key for the delete action. + ]; + Polymer({ is: 'gr-change-actions', @@ -58,6 +106,12 @@ * @event reload-change */ + /** + * Fired when an action is tapped. + * + * @event <action key>-tap + */ + properties: { change: Object, actions: { @@ -74,31 +128,49 @@ }, }, changeNum: String, + changeStatus: String, + commitNum: String, patchNum: String, - commitInfo: Object, + rebaseOnCurrent: Boolean, + commitMessage: { + type: String, + value: '', + }, + revisionActions: { + type: Object, + value: function() { return {}; }, + }, _loading: { type: Boolean, value: true, }, - _revisionActions: { - type: Object, - value: function() { return {}; }, + _actionLoadingMessage: { + type: String, + value: null, }, - _revisionActionValues: { + _allActionValues: { type: Array, - computed: '_computeRevisionActionValues(_revisionActions.*, ' + - 'primaryActionKeys.*, _additionalActions.*)', + computed: '_computeAllActions(actions.*, revisionActions.*,' + + 'primaryActionKeys.*, _additionalActions.*, change)', }, - _changeActionValues: { + _topLevelActions: { type: Array, - computed: '_computeChangeActionValues(actions.*, ' + - 'primaryActionKeys.*, _additionalActions.*)', + computed: '_computeTopLevelActions(_allActionValues.*, ' + + '_hiddenActions.*)', + }, + _menuActions: { + type: Array, + computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*)', }, _additionalActions: { type: Array, value: function() { return []; }, }, + _hiddenActions: { + type: Array, + value: function() { return []; }, + }, }, ActionType: ActionType, @@ -110,11 +182,12 @@ ], observers: [ - '_actionsChanged(actions.*, _revisionActions.*, _additionalActions.*)', + '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', ], ready: function() { this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); + this._loading = false; }, reload: function() { @@ -126,7 +199,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 +239,19 @@ ], value); }, + setActionHidden: function(type, key, hidden) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error('Invalid action type given: ' + type); + } + + var idx = this._hiddenActions.indexOf(key); + if (hidden && idx === -1) { + this.push('_hiddenActions', key); + } else if (!hidden && idx !== -1) { + this.splice('_hiddenActions', idx, 1); + } + }, + _indexOfActionButtonWithKey: function(key) { for (var i = 0; i < this._additionalActions.length; i++) { if (this._additionalActions[i].__key === key) { @@ -180,10 +266,8 @@ this.patchNum); }, - _actionCount: function(actionsChangeRecord, additionalActionsChangeRecord) { - var additionalActions = (additionalActionsChangeRecord && - additionalActionsChangeRecord.base) || []; - return this._keyCount(actionsChangeRecord) + additionalActions.length; + _shouldHideActions: function(actions, loading) { + return loading || !actions || !actions.base || !actions.base.length; }, _keyCount: function(changeRecord) { @@ -197,6 +281,7 @@ this.hidden = this._keyCount(actionsChangeRecord) === 0 && this._keyCount(revisionActionsChangeRecord) === 0 && additionalActions.length === 0; + this._actionLoadingMessage = null; }, _getValuesFor: function(obj) { @@ -205,16 +290,80 @@ }); }, - _computeRevisionActionValues: function(actionsChangeRecord, - primariesChangeRecord, additionalActionsChangeRecord) { - return this._getActionValues(actionsChangeRecord, primariesChangeRecord, - additionalActionsChangeRecord, 'revision'); + _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; + } }, - _computeChangeActionValues: function(actionsChangeRecord, - primariesChangeRecord, additionalActionsChangeRecord) { - return this._getActionValues(actionsChangeRecord, primariesChangeRecord, - additionalActionsChangeRecord, 'change'); + /** + * 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 +380,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 +412,31 @@ }, _canSubmitChange: function() { - return this.$.jsAPI.canSubmitChange(); + return this.$.jsAPI.canSubmitChange(this.change, + this._getRevision(this.change, this.patchNum)); + }, + + _getRevision: function(change, patchNum) { + var num = window.parseInt(patchNum, 10); + for (var hash in change.revisions) { + var rev = change.revisions[hash]; + if (rev._number === num) { + return rev; + } + } + return null; }, _modifyRevertMsg: function() { return this.$.jsAPI.modifyRevertMsg(this.change, - this.$.confirmRevertDialog.message); + this.$.confirmRevertDialog.message, this.commitMessage); + }, + + showRevertDialog: function() { + this.$.confirmRevertDialog.populateRevertMessage( + this.commitMessage, this.change.current_revision); + this.$.confirmRevertDialog.message = this._modifyRevertMsg(); + this._showActionDialog(this.$.confirmRevertDialog); }, _handleActionTap: function(e) { @@ -274,11 +451,13 @@ if (type === ActionType.REVISION) { this._handleRevisionAction(key); } else if (key === ChangeActions.REVERT) { - this.$.confirmRevertDialog.populateRevertMessage(); - this.$.confirmRevertDialog.message = this._modifyRevertMsg(); - this._showActionDialog(this.$.confirmRevertDialog); - } else if (key === ChangeActions.ABANDON) { - this._showActionDialog(this.$.confirmAbandonDialog); + this.showRevertDialog(); + } else if (key === QUICK_APPROVE_ACTION.key) { + var action = this._allActionValues.find(function(o) { + return o.key === key; + }); + this._fireAction( + this._prependSlash(key), action, true, action.payload); } else { this._fireAction(this._prependSlash(key), this.actions[key], false); } @@ -289,17 +468,14 @@ case RevisionActions.REBASE: this._showActionDialog(this.$.confirmRebase); break; - case RevisionActions.CHERRYPICK: - 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 +484,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 +511,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 +527,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 +542,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,19 +550,29 @@ _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) { + this._actionLoadingMessage = this._computeLoadingLabel(key); + + // Return a NoOp for menu keys. @see Issue 5366 + if (MENU_ACTION_KEYS.indexOf(key) !== -1) { return function() {}; } + var buttonEl = this.$$('[data-action-key="' + key + '"]'); buttonEl.setAttribute('loading', true); buttonEl.disabled = true; return function() { + this._actionLoadingMessage = null; buttonEl.removeAttribute('loading'); buttonEl.disabled = false; - }; + }.bind(this); }, _fireAction: function(endpoint, action, revAction, opt_payload) { @@ -393,14 +583,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; @@ -413,7 +621,8 @@ } break; default: - this.fire('reload-change', null, {bubbles: false}); + this.dispatchEvent(new CustomEvent('reload-change', + {detail: {action: action.__key}, bubbles: false})); break; } }.bind(this)); @@ -437,5 +646,94 @@ return response; }.bind(this)).then(this._handleResponseError.bind(this)); }, + + _handleAbandonTap: function() { + this._showActionDialog(this.$.confirmAbandonDialog); + }, + + _handleCherrypickTap: function() { + this.$.confirmCherrypick.branch = ''; + this._showActionDialog(this.$.confirmCherrypick); + }, + + _handleDeleteTap: function() { + this._showActionDialog(this.$.confirmDeleteDialog); + }, + + /** + * Merge sources of change actions into a single ordered array of action + * values. + * @param {splices} changeActionsRecord + * @param {splices} revisionActionsRecord + * @param {splices} primariesRecord + * @param {splices} additionalActionsRecord + * @param {Object} change The change object. + * @return {Array} + */ + _computeAllActions: function(changeActionsRecord, revisionActionsRecord, + primariesRecord, additionalActionsRecord, change) { + var revisionActionValues = this._getActionValues(revisionActionsRecord, + primariesRecord, additionalActionsRecord, ActionType.REVISION); + var changeActionValues = this._getActionValues(changeActionsRecord, + primariesRecord, additionalActionsRecord, ActionType.CHANGE, change); + var quickApprove = this._getQuickApproveAction(); + if (quickApprove) { + changeActionValues.unshift(quickApprove); + } + return revisionActionValues + .concat(changeActionValues) + .sort(this._actionComparator); + }, + + /** + * Sort comparator to define the order of change actions. + */ + _actionComparator: function(actionA, actionB) { + // The code review action always appears first. + if (actionA.__key === 'review') { + return -1; + } else if (actionB.__key === 'review') { + return 1; + } + + // Primary actions always appear last. + if (actionA.__primary) { + return 1; + } else if (actionB.__primary) { + return -1; + } + + // Change actions appear before revision actions. + if (actionA.__type === 'change' && actionB.__type === 'revision') { + return 1; + } else if (actionA.__type === 'revision' && actionB.__type === 'change') { + return -1; + } + + // Otherwise, sort by the button label. + return actionA.label > actionB.label ? 1 : -1; + }, + + _computeTopLevelActions: function(actionRecord, hiddenActionsRecord) { + var hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base.filter(function(a) { + return MENU_ACTION_KEYS.indexOf(a.__key) === -1 && + hiddenActions.indexOf(a.__key) === -1; + }); + }, + + _computeMenuActions: function(actionRecord, hiddenActionsRecord) { + var hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base + .filter(function(a) { + return MENU_ACTION_KEYS.indexOf(a.__key) !== -1 && + hiddenActions.indexOf(a.__key) === -1; + }) + .map(function(action) { + var key = action.__key; + if (key === '/') { key = 'delete'; } + return {name: action.label, id: key, }; + }); + }, }); })();
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..c1b582e 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,146 @@ }); 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('_shouldHideActions', function() { + assert.isTrue(element._shouldHideActions(undefined, true)); + assert.isTrue(element._shouldHideActions({base: {}}, false)); + assert.isFalse(element._shouldHideActions({base: ['test']}, false)); + }); + + test('hide revision action', function(done) { flush(function() { - var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button'); - assert.equal(buttonEls.length, 3); + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.throws(element.setActionHidden.bind(element, 'invalid type')); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenActions, 1); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenActions, 1); + flush(function() { + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isNotOk(buttonEl); + + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, false); + flush(function() { + var buttonEl = element.$$('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + done(); + }); + }); + }); + }); + + test('hide menu action', function(done) { + flush(function() { + var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + assert.isOk(buttonEl); + assert.throws(element.setActionHidden.bind(element, 'invalid type')); + element.setActionHidden(element.ActionType.CHANGE, + element.ChangeActions.DELETE, true); + assert.lengthOf(element._hiddenActions, 1); + element.setActionHidden(element.ActionType.CHANGE, + element.ChangeActions.DELETE, true); + assert.lengthOf(element._hiddenActions, 1); + flush(function() { + var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + assert.isNotOk(buttonEl); + + element.setActionHidden(element.ActionType.CHANGE, + element.RevisionActions.DELETE, false); + flush(function() { + var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + assert.isOk(buttonEl); + done(); + }); + }); + }); + }); + + test('buttons exist', function(done) { + element._loading = false; + flush(function() { + var buttonEls = Polymer.dom(element.root) + .querySelectorAll('gr-button'); + var menuItems = element.$.moreActions.items; + assert.equal(buttonEls.length + menuItems.length, 6); assert.isFalse(element.hidden); done(); }); }); + test('delete buttons have explicit labels', function(done) { + flush(function() { + var deleteItems = element.$.moreActions.items.filter(function(item) { + return item.id === 'delete'; + }); + assert.equal(deleteItems.length, 2); + assert.notEqual(deleteItems[0].name, deleteItems[1].name); + assert.isTrue( + deleteItems[0].name === 'Delete Revision' || + deleteItems[0].name === 'Delete Change' + ); + assert.isTrue( + deleteItems[1].name === 'Delete Revision' || + deleteItems[1].name === '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('_actionComparator sort order', function() { + var actions = [ + {label: '123', __type: 'change', __key: 'review'}, + {label: 'abc', __type: 'revision'}, + {label: 'abc', __type: 'change'}, + {label: 'def', __type: 'change'}, + {label: 'def', __type: 'change', __primary: true}, + ]; + + var result = actions.slice(); + result.reverse(); + result.sort(element._actionComparator); + + assert.deepEqual(result, actions); + }); + 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 +261,7 @@ __key: 'rebase', __type: 'revision', __primary: false, + enabled: true, label: 'Rebase', method: 'POST', title: 'Rebase onto tip of branch or parent change', @@ -154,6 +287,22 @@ }); }); + 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); + + element._handleCherrypickTap(); + flushAsynchronousOperations(); + assert.isTrue(element.$.confirmRebase.hidden); + assert.isFalse(element.$.confirmCherrypick.hidden); + done(); + }); + }); + suite('cherry-pick', function() { var fireActionStub; var alertStub; @@ -169,8 +318,7 @@ }); test('works', function() { - var rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); - MockInteractions.tap(rebaseButton); + element._handleCherrypickTap(); var action = { __key: 'cherrypick', __type: 'revision', @@ -188,16 +336,31 @@ 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', message: 'foo message', - } + }, ]); }); + + test('branch name cleared when re-open cherrypick', function() { + var emptyBranchName = ''; + element.$.confirmCherrypick.branch = 'master'; + + element._handleCherrypickTap(); + assert.equal(element.$.confirmCherrypick.branch, emptyBranchName); + }); }); test('custom actions', function(done) { @@ -217,6 +380,32 @@ }); }); + test('_setLoadingOnButtonWithKey top-level', function() { + var key = 'rebase'; + var cleanup = element._setLoadingOnButtonWithKey(key); + assert.equal(element._actionLoadingMessage, 'Rebasing...'); + + var button = element.$$('[data-action-key="' + key + '"]'); + assert.isTrue(button.hasAttribute('loading')); + assert.isTrue(button.disabled); + + assert.isOk(cleanup); + assert.isFunction(cleanup); + cleanup(); + + assert.isFalse(button.hasAttribute('loading')); + assert.isFalse(button.disabled); + assert.isNotOk(element._actionLoadingMessage); + }); + + test('_setLoadingOnButtonWithKey overflow menu', function() { + // TODO(wyatta): Should not throw error when setting loading on an + // overflow action. @see Issue 5366 + var key = 'cherrypick'; + var cleanup = element._setLoadingOnButtonWithKey(key); + assert.isFunction(cleanup); + }); + suite('revert change', function() { var alertStub; var fireActionStub; @@ -229,8 +418,8 @@ method: 'POST', label: 'Revert', title: 'Revert the change', - enabled: true - } + enabled: true, + }, }; return element.reload(); }); @@ -241,6 +430,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 +452,9 @@ }); test('works', function() { + element.change = { + current_revision: 'abc1234', + }; var populateRevertMsgStub = sinon.stub( element.$.confirmRevertDialog, 'populateRevertMessage', function() { return 'original msg'; }); @@ -286,5 +481,215 @@ populateRevertMsgStub.restore(); }); }); + + suite('delete change', function() { + var fireActionStub; + var deleteAction; + + 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() { + element._handleDeleteTap(); + assert.isFalse(fireActionStub.called); + }); + + test('shows confirm dialog', function() { + element._handleDeleteTap(); + 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() { + element._handleDeleteTap(); + 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..4e362e5 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,12 @@ .notApproved { background-color: #ffd4d4; } + .labelStatus { + max-width: 9em; + } + .webLink { + display: block; + } @media screen and (max-width: 50em), screen and (min-width: 75em) { :host { display: table; @@ -91,6 +110,19 @@ </section> <template is="dom-if" if="[[_showReviewersByState]]"> <section> + <span class="title">Assignee</span> + <span class="value"> + <gr-account-list + max-count="1" + id="assigneeValue" + placeholder="Add assignee..." + accounts="{{_assignee}}" + change="[[change]]" + readonly="[[!mutable]]" + allow-any-user></gr-account-list> + </span> + </section> + <section> <span class="title">Reviewers</span> <span class="value"> <gr-reviewer-list @@ -111,6 +143,19 @@ </template> <template is="dom-if" if="[[!_showReviewersByState]]"> <section> + <span class="title">Assignee</span> + <span class="value"> + <gr-account-list + max-count="1" + id="assigneeValue" + placeholder="Add assignee..." + accounts="{{_assignee}}" + change="[[change]]" + readonly="[[!mutable]]" + allow-any-user></gr-account-list> + </span> + </section> + <section> <span class="title">Reviewers</span> <span class="value"> <gr-reviewer-list @@ -128,25 +173,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="[[!_topicReadOnly]]" + on-remove="_handleTopicRemoved"></gr-linked-chip> + </template> + <template is="dom-if" if="[[!change.topic]]"> + <gr-editable-label + value="{{change.topic}}" + placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" + read-only="[[_topicReadOnly]]" + on-changed="_handleTopicChanged"></gr-editable-label> + </template> </span> </section> <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden> @@ -159,7 +201,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 +211,38 @@ 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> + <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo)]]"> + <span class="title">Links</span> + <span class="value"> + <template is="dom-repeat" + items="[[_computeWebLinks(commitInfo)]]" as="link"> + <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank"> + [[link.name]] + </a> + </template> + </span> + </section> <gr-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..68b007d 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
@@ -30,14 +30,6 @@ 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,48 +38,71 @@ type: Boolean, computed: '_computeShowReviewersByState(serverConfig)', }, + _showLabelStatus: { + type: Boolean, + computed: '_computeShowLabelStatus(change)', + }, + + _assignee: Array, }, 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; + observers: [ + '_changeChanged(change)', + '_assigneeChanged(_assignee.*)', + ], + + _changeChanged: function(change) { + this._assignee = change.assignee ? [change.assignee] : []; }, - _computeWebLink: function(change, commitInfo, serverConfig) { - if (!this._computeShowWebLink(change, commitInfo, serverConfig)) { - return; + _assigneeChanged: function(assigneeRecord) { + if (!this.change) { return; } + var assignee = assigneeRecord.base; + if (assignee.length) { + var acct = assignee[0]; + if (this.change.assignee && + acct._account_id === this.change.assignee._account_id) { return; } + this.set(['change', 'assignee'], acct); + this.$.restAPI.setAssignee(this.change._number, acct._account_id); + } else { + if (!this.change.assignee) { return; } + this.set(['change', 'assignee'], undefined); + this.$.restAPI.deleteAssignee(this.change._number); } - - 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); }, + /** + * This is a whitelist of web link types that provide direct links to + * the commit in the url property. + */ + _isCommitWebLink: function(link) { + return link.name === 'gitiles' || link.name === 'gitweb'; + }, + + /** + * @param {Object} commitInfo + * @return {?Array} If array is empty, returns null instead so + * an existential check can be used to hide or show the webLinks + * section. + */ + _computeWebLinks: function(commitInfo) { + if (!commitInfo || !commitInfo.web_links) { return null } + // We are already displaying these types of links elsewhere, + // don't include in the metadata links section. + var webLinks = commitInfo.web_links.filter( + function(l) {return !this._isCommitWebLink(l); }.bind(this)); + + return webLinks.length ? webLinks : null; + }, + _computeStrategy: function(change) { return SubmitTypeLabel[change.submit_type]; }, @@ -96,8 +111,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 +144,7 @@ _handleTopicChanged: function(e, topic) { if (!topic.length) { topic = null; } - this.$.restAPI.setChangeTopic(this.change.id, topic); + this.$.restAPI.setChangeTopic(this.change._number, topic); }, _computeTopicReadOnly: function(mutable, change) { @@ -142,5 +158,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._number, 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..9c5804f 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> @@ -35,15 +33,22 @@ <script> suite('gr-change-metadata tests', function() { var element; + var sandbox; setup(function() { + sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { + getConfig: function() { return Promise.resolve({}); }, getLoggedIn: function() { return Promise.resolve(false); }, }); element = fixture('basic'); }); + teardown(function() { + sandbox.restore(); + }); + test('computed fields', function() { assert.isFalse(element._computeHideStrategy({status: 'NEW'})); assert.isFalse(element._computeHideStrategy({status: 'DRAFT'})); @@ -68,79 +73,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 +84,228 @@ 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'); + }); + + test('weblinks hidden when no weblinks', function() { + element.commitInfo = {}; + flushAsynchronousOperations(); + var webLinks = element.$.webLinks; + assert.isTrue(webLinks.hasAttribute('hidden')); + }); + + test('weblinks hidden when only gitiles weblink', function() { + element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]}; + flushAsynchronousOperations(); + var webLinks = element.$.webLinks; + assert.isTrue(webLinks.hasAttribute('hidden')); + assert.equal(element._computeWebLinks(element.commitInfo), null); + }); + + test('weblinks are visible when other weblinks', function() { + element.commitInfo = {web_links: [{name: 'test', url: '#'}]}; + flushAsynchronousOperations(); + var webLinks = element.$.webLinks; + assert.isFalse(webLinks.hasAttribute('hidden')); + assert.equal(element._computeWebLinks(element.commitInfo).length, 1); + // With two non-gitiles weblinks, there are two returned. + element.commitInfo = { + web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]}; + assert.equal(element._computeWebLinks(element.commitInfo).length, 2); + }); + + test('weblinks are visible when gitiles and other weblinks', function() { + element.commitInfo = { + web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]}; + flushAsynchronousOperations(); + var webLinks = element.$.webLinks; + assert.isFalse(webLinks.hasAttribute('hidden')); + // Only the non-gitiles weblink is returned. + assert.equal(element._computeWebLinks(element.commitInfo).length, 1); + }); + + suite('Topic removal', function() { + var change; + setup(function() { + change = { + _number: 'the number', + actions: { + topic: {enabled: false}, + }, + 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: [], + }; + }); + + test('_computeTopicReadOnly', function() { + var mutable = false; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + mutable = true; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + change.actions.topic.enabled = true; + assert.isFalse(element._computeTopicReadOnly(mutable, change)); + mutable = false; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + }); + + test('topic read only hides delete button', function() { + element.mutable = false; + element.change = change; + flushAsynchronousOperations(); + var button = element.$$('gr-linked-chip').$$('gr-button'); + assert.isTrue(button.hasAttribute('hidden')); + }); + + test('topic not read only does not hide delete button', function() { + element.mutable = true; + change.actions.topic.enabled = true; + element.change = change; + flushAsynchronousOperations(); + var button = element.$$('gr-linked-chip').$$('gr-button'); + assert.isFalse(button.hasAttribute('hidden')); + }); + }); + + suite('remove reviewer votes', function() { + setup(function() { + sandbox.stub(element, '_computeValueTooltip').returns(''); + sandbox.stub(element, '_computeTopicReadOnly').returns(true); + element.change = { + _number: 'the number', + 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: [], + }; + }); + + 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 number', '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); + }); + + suite('assignee field', function() { + var dummyAccount = { + _account_id: 1, + name: 'bojack', + }; + var deleteStub; + var setStub; + setup(function() { + deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee'); + setStub = sandbox.stub(element.$.restAPI, 'setAssignee'); + }); + + test('changing change recomputes _assignee', function() { + assert.isFalse(!!element._assignee.length); + var change = element.change; + change.assignee = dummyAccount; + element._changeChanged(change); + assert.deepEqual(element._assignee[0], dummyAccount); + }); + + test('modifying _assignee calls API', function() { + assert.isFalse(!!element._assignee.length); + element.set('_assignee', [dummyAccount]); + assert.isTrue(setStub.calledOnce); + assert.deepEqual(element.change.assignee, dummyAccount); + element.set('_assignee', [dummyAccount]); + assert.isTrue(setStub.calledOnce); + element.set('_assignee', []); + assert.isTrue(deleteStub.calledOnce); + assert.equal(element.change.assignee, undefined); + element.set('_assignee', []); + assert.isTrue(deleteStub.calledOnce); + }); + }); + }); }); </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..53a0c9e 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,69 +71,67 @@ 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); + } + .changeId { + color: #666; + font-family: var(--font-family); + margin-top: 1em; } .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; + word-break: break-all; + } + .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; @@ -144,13 +144,53 @@ 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 { + 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: 100%; + } + .mobile { + display: none; + } + .warning { + color: #d14836; + } + hr { + border: 0; + border-top: 1px solid #ddd; + height: 0; margin-bottom: 1em; - padding: 0 var(--default-horizontal-margin); + } + .patchInfo-header-wrapper { + width: 100%; } @media screen and (max-width: 50em) { - .headerContainer { - height: 5.15em; + .mobile { + display: block; } .header { align-items: flex-start; @@ -163,30 +203,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 { + .desktop { 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,43 +234,46 @@ 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 + id="changeStar" + change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> + <a + aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]" + href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><!-- + --><template is="dom-if" if="[[_changeStatus]]"><!-- + --> (<!-- + --><span + aria-label$="Change status: [[_changeStatus]]" + tabindex="0">[[_changeStatus]]</span><!-- + --><template + is="dom-if" + if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]"> + as + <gr-commit-info + change="[[_change]]" + commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" + server-config="[[serverConfig]]"></gr-commit-info><!-- + --></template><!-- + -->)<!-- + --></template><!-- + -->: [[_change.subject]] + </span> </div> <section class="changeInfo"> <div class="changeInfo-column changeMetadata"> @@ -254,46 +284,125 @@ 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 + disabled="[[_replyDisabled]]" + on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button> + <gr-change-actions id="actions" + change="[[_change]]" + actions="[[_change.actions]]" + rebase-on-current="[[_rebaseOnCurrent]]" + 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> + <hr class="mobile"> + <div class="commitAndRelated"> + <div class="commitMessage"> + <gr-editable-content id="commitMessageEditor" + editing="[[_editingCommitMessage]]" + content="{{_latestCommitMessage}}"> + <gr-linked-text pre + content="[[_latestCommitMessage]]" + config="[[_projectConfig.commentlinks]]" + remove-zero-width-space></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 class="changeId" hidden$="[[!_changeIdCommitMessageError]]"> + <hr> + Change-Id: + <span + class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]" + title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]"> + [[_change.change_id]] + </span> + </div> + </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 class="patchInfo-header-wrapper"> + <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]]"> + [[patchNum.num]] + / + [[_computeLatestPatchNum(_allPatchSets)]] + [[_computePatchSetDescription(_change, patchNum.num)]] + </option> + </template> + </select> + / + <gr-commit-info + change="[[_change]]" + server-config="[[serverConfig]]" + commit-info="[[_commitInfo]]"></gr-commit-info> + <span class="latestPatchContainer"> + / + <a href$="/c/[[_change._number]]">Go to latest patch set</a> + </span> + <span class="downloadContainer desktop"> + / + <gr-button link + class="download" + on-tap="_handleDownloadTap">Download</gr-button> + </span> + <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> + </div> + </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 +414,7 @@ </div> <gr-overlay id="downloadOverlay" with-backdrop> <gr-download-dialog + id="downloadDialog" change="[[_change]]" logged-in="[[_loggedIn]]" patch-num="[[_patchRange.patchNum]]" @@ -312,16 +422,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]]" + change="{{_change}}" + 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..6bd0a8b 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -14,6 +14,15 @@ (function() { 'use strict'; + var CHANGE_ID_ERROR = { + MISMATCH: 'mismatch', + MISSING: 'missing', + }; + var CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; + // Maximum length for patch set descriptions. + var PATCH_DESC_MAX_LENGTH = 500; + var REVIEWERS_REGEX = /^R=/gm; + Polymer({ is: 'gr-change-view', @@ -42,18 +51,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 +81,61 @@ _hideEditCommitMessage: { type: Boolean, computed: '_computeHideEditCommitMessage(_loggedIn, ' + - '_editingCommitMessage, _change.*, _patchRange.patchNum)', + '_editingCommitMessage, _change)', }, - _patchRange: Object, + _latestCommitMessage: { + type: String, + value: '', + }, + _changeIdCommitMessageError: { + type: String, + computed: + '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', + }, + _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, + _rebaseOnCurrent: Boolean, _replyButtonLabel: { type: String, value: 'Reply', computed: '_computeReplyButtonLabel(_diffDrafts.*)', }, + _selectedPatchSet: String, + _initialLoadComplete: { + type: Boolean, + value: false, + }, + _descriptionReadOnly: { + type: Boolean, + computed: '_computeDescriptionReadOnly(_loggedIn, _change, _account)', + }, + _replyDisabled: { + type: Boolean, + value: true, + computed: '_computeReplyDisabled(serverConfig)', + }, + _changeStatus: { + type: String, + computed: '_computeChangeStatus(_change, _patchRange.patchNum)', + }, }, behaviors: [ Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, Gerrit.RESTClientBehavior, ], @@ -98,13 +144,24 @@ '_paramsAndChangeChanged(params, _change)', ], - ready: function() { - this._headerEl = this.$$('.header'); + keyBindings: { + 'shift+r': '_handleCapitalRKey', + 'a': '_handleAKey', + 'd': '_handleDKey', + 's': '_handleSKey', + 'u': '_handleUKey', + 'x': '_handleXKey', + 'z': '_handleZKey', }, 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 +171,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 +186,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 = this._prepareCommitMsgForLinkify(message); this._editingCommitMessage = false; this._reloadWindow(); }.bind(this)).catch(function(err) { @@ -182,16 +218,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 +295,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), true); }, _handleReplyTap: function(e) { @@ -289,7 +305,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 +320,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 +353,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 +454,38 @@ } }, + _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() { + Gerrit.awaitPluginsLoaded() + .then(this._getLoggedIn.bind(this)) + .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 +500,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 +518,39 @@ 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 {number} patchNum the patchn number to be viewed. + * @param {boolean} opt_forceParams When set to true, the resulting URL will + * always include the patch range, even if the requested patchNum is + * known to be the latest. + */ + _changePatchNum: function(patchNum, opt_forceParams) { + if (!opt_forceParams) { + 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 && + this._patchRange.basePatchNum === 'PARENT') { + page.show(this.changePath(this._changeNum)); + return; + } + } + var patchExpr = this._patchRange.basePatchNum === 'PARENT' ? patchNum : + this._patchRange.basePatchNum + '..' + patchNum; + page.show(this.changePath(this._changeNum) + '/' + patchExpr); + }, + _computeChangePermalink: function(changeNum) { return '/' + changeNum; }, @@ -412,40 +558,89 @@ _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 || ''; + }, + + _computeShowCommitInfo: function(changeStatus, current_revision) { + return changeStatus === 'Merged' && current_revision; + }, + + _computeMergedCommitInfo: function(current_revision, revisions) { + var rev = revisions[current_revision]; + if (!rev || !rev.commit) { return {}; } + // CommitInfo.commit is optional. Set commit in all cases to avoid error + // in <gr-commit-info>. @see Issue 5337 + if (!rev.commit.commit) { rev.commit.commit = current_revision; } + return rev.commit; + }, + + _computeChangeIdClass: function(displayChangeId) { + return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; + }, + + _computeTitleAttributeWarning: function(displayChangeId) { + if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { + return 'Change-Id mismatch'; + } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { + return 'No Change-Id in commit message'; + } + }, + + _computeChangeIdCommitMessageError: function(commitMessage, change) { + if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; } + + // Find the last match in the commit message: + var changeId; + var changeIdArr; + + while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) { + changeId = changeIdArr[1]; + } + + if (changeId) { + // A change-id is detected in the commit message. + + if (changeId === change.change_id) { + // The change-id found matches the real change-id. + return null; + } + // The change-id found does not match the change-id. + return CHANGE_ID_ERROR.MISMATCH; + } + // There is no change-id in the commit message. + return CHANGE_ID_ERROR.MISSING; }, _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 +672,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 +685,99 @@ 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(); + }, + + _handleSKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.changeStar.toggleStar(); + }, + + _handleUKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._determinePageBack(); + }, + + _handleXKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.messageList.handleExpandCollapse(true); + }, + + _handleZKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.messageList.handleExpandCollapse(false); + }, + + _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, }); @@ -526,8 +790,16 @@ }.bind(this)); }, - _handleReloadChange: function() { - page.show(this.changePath(this._changeNum)); + _handleReloadChange: function(e) { + return this._reload().then(function() { + // If the change was rebased, we need to reload the related changes. + if (e.detail.action === 'rebase') { + this.$.relatedChanges.reload(); + this.set('_patchRange.patchNum', + this._computeLatestPatchNum(this._allPatchSets)); + this._updateSelected(); + } + }.bind(this)); }, _handleGetChangeDetailError: function(response) { @@ -552,6 +824,20 @@ }.bind(this)); }, + _updateRebaseAction: function(revisionActions) { + if (revisionActions && revisionActions.rebase) { + this._rebaseOnCurrent = !!revisionActions.rebase.enabled; + revisionActions.rebase.enabled = true; + } + return revisionActions; + }, + + _prepareCommitMsgForLinkify: function(msg) { + // This is a zero-with space. It is added to prevent the linkify library + // from including R= as part of the email address. + return msg.replace(REVIEWERS_REGEX, 'R=\u200B'); + }, + _getChangeDetail: function() { return this.$.restAPI.getChangeDetail(this._changeNum, this._handleGetChangeDetailError.bind(this)).then( @@ -561,7 +847,27 @@ if (!change.reviewer_updates) { change.reviewer_updates = null; } + var latestRevisionSha = this._getLatestRevisionSHA(change); + var currentRevision = change.revisions[latestRevisionSha]; + if (currentRevision.commit && currentRevision.commit.message) { + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + currentRevision.commit.message); + } else { + this._latestCommitMessage = null; + } + 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 = + this._updateRebaseAction(currentRevision.actions); + // TODO: Fetch and process files. + } }.bind(this)); }, @@ -572,6 +878,34 @@ }.bind(this)); }, + _getLatestCommitMessage: function() { + return this.$.restAPI.getChangeCommitInfo(this._changeNum, + this._computeLatestPatchNum(this._allPatchSets)).then( + function(commitInfo) { + this._latestCommitMessage = + this._prepareCommitMsgForLinkify(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 +934,90 @@ 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.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + }, + + _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)); + }, + + _computeReplyDisabled: function() { return false; }, + + _computeChangePermalinkAriaLabel: function(changeNum) { + return 'Change ' + changeNum; + }, }); })();
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..8162102 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,281 @@ <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('S should toggle the CL star', function() { + var starStub = sandbox.stub(element.$.changeStar, 'toggleStar'); + MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's'); + assert(starStub.called); + }); + + test('U should navigate to / if no backPage set', function() { + 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('X should expand all messages', function() { + var handleExpand = + sandbox.stub(element.$.messageList, 'handleExpandCollapse'); + MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x'); + assert(handleExpand.calledWith(true)); + }); + + test('Z should collapse all messages', function() { + var handleExpand = + sandbox.stub(element.$.messageList, 'handleExpandCollapse'); + MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z'); + assert(handleExpand.calledWith(false)); + }); + + test('shift + R should fetch and navigate to the latest patch set', + function(done) { + // Prevent all network requests to prevent random exceptions. + sandbox.stub(window, 'fetch', function() { + return Promise.resolve({ + ok: true, + text: function() { + return Promise.resolve(')]}\'\n'); + }, + }); + }); + + 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('_prepareCommitMsgForLinkify', function() { + var commitMessage = 'R=test@google.com'; + var result = element._prepareCommitMsgForLinkify(commitMessage); + assert.equal(result, 'R=\u200Btest@google.com'); + + commitMessage = 'R=test@google.com\nR=test@google.com'; + var result = element._prepareCommitMsgForLinkify(commitMessage); + assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com'); + }), + + 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('_updateRebaseAction', function() { + var currentRevisionActions = { + cherrypick: { + enabled: true, + label: 'Cherry Pick', + method: 'POST', + title: 'cherrypick' + }, + rebase: { + enabled: true, + label: 'Rebase', + method: 'POST', + title: 'Rebase onto tip of branch or parent change' + }, + }; + + // Rebase enabled should always end up true. + // When rebase is enabled initially, rebaseOnCurrent should be set to + // true. + assert.equal(element._updateRebaseAction(currentRevisionActions), + currentRevisionActions); + + assert.isTrue(element._rebaseOnCurrent); + + var newRevisionActions = currentRevisionActions + delete newRevisionActions.rebase.enabled; + + // When rebase is not enabled initially, rebaseOnCurrent should be set to + // false. + assert.equal(element._updateRebaseAction(newRevisionActions), + currentRevisionActions); + + assert.isFalse(element._rebaseOnCurrent); + }); + + 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 +348,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 +396,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'), @@ -163,9 +422,8 @@ selectEl.value = '3'; element.fire('change', {}, {node: selectEl}); } else if (numEvents == 2) { - assert(showStub.lastCall.calledWithExactly('/c/42'), - 'Should navigate to /c/42'); - showStub.restore(); + assert(showStub.lastCall.calledWithExactly('/c/42/3'), + 'Should navigate to /c/42/3'); done(); } }); @@ -191,20 +449,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 +475,6 @@ } else if (numEvents == 2) { assert(showStub.lastCall.calledWithExactly('/c/42/3'), 'Should navigate to /c/42/3'); - showStub.restore(); done(); } }); @@ -225,6 +482,102 @@ 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('include base patch when not parent', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '2', + patchNum: '3', + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + status: 'NEW', + labels: {}, + }; + + var showStub = sandbox.stub(page, 'show'); + + element._changePatchNum(13); + assert(showStub.lastCall.calledWithExactly('/c/42/2..13')); + + element._patchRange.basePatchNum = 'PARENT'; + + element._changePatchNum(3); + assert(showStub.lastCall.calledWithExactly('/c/42/3')); + }); + + test('related changes are updated and new patch selected after rebase', + function(done) { + sandbox.stub(element, '_computeLatestPatchNum', function() { + return 1; + }); + sandbox.stub(element, '_reload', + function() { return Promise.resolve(); }); + sandbox.stub(element, '_updateSelected'); + sandbox.stub(element.$.relatedChanges, 'reload'); + var e = {detail: {action: 'rebase'}}; + element._handleReloadChange(e).then(function() { + assert.isTrue(element.$.relatedChanges.reload.called); + assert.isTrue(element._updateSelected.called); + done(); + }); + }); + + test('related changes are not updated after other action', function(done) { + sandbox.stub(element, '_reload', + function() { return Promise.resolve(); }); + sandbox.stub(element, '_updateSelected'); + sandbox.stub(element.$.relatedChanges, 'reload'); + var e = {detail: {action: 'abandon'}}; + element._handleReloadChange(e).then(function() { + assert.isFalse(element.$.relatedChanges.reload.called); + assert.isFalse(element._updateSelected.called); + done(); + }); + }); + test('change status new', function() { element._changeNum = '1'; element._patchRange = { @@ -260,7 +613,26 @@ labels: {}, }; var status = element._computeChangeStatus(element._change, '1'); - assert.equal(status, '(Draft)'); + assert.equal(status, 'Draft'); + }); + + test('change status merged', function() { + element._changeNum = '1'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: element.ChangeStatus.MERGED, + labels: {}, + }; + var status = element._computeChangeStatus(element._change, '1'); + assert.equal(status, 'Merged'); }); test('revision status draft', function() { @@ -283,43 +655,161 @@ labels: {}, }; var status = element._computeChangeStatus(element._change, '2'); - assert.equal(status, '(Draft)'); + assert.equal(status, 'Draft'); + }); + + test('_computeMergedCommitInfo', function() { + var dummyRevs = { + 1: {commit: {commit: 1}}, + 2: {commit: {}}, + }; + assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {}); + assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs), + dummyRevs[1].commit); + + // Regression test for issue 5337. + var commit = element._computeMergedCommitInfo(2, dummyRevs); + assert.notDeepEqual(commit, dummyRevs[2]); + assert.deepEqual(commit, {commit: 2}); + }); + + 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('_computeChangeIdCommitMessageError', function() { + var commitMessage = + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483'; + var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + + commitMessage = 'This is the greatest change.'; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'missing'); + }); + + test('multiple change Ids in commit message picks last', function() { + var commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + ].join('\n'); + var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + }); + + test('does not count change Id that starts mid line', function() { + var commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + ].join(' and '); + var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + }); + + test('_computeTitleAttributeWarning', function() { + var changeIdCommitMessageError = 'missing'; + assert.equal( + element._computeTitleAttributeWarning(changeIdCommitMessageError), + 'No Change-Id in commit message'); + + var changeIdCommitMessageError = 'mismatch'; + assert.equal( + element._computeTitleAttributeWarning(changeIdCommitMessageError), + 'Change-Id mismatch'); + }); + + test('_computeChangeIdClass', function() { + var changeIdCommitMessageError = 'missing'; + assert.equal( + element._computeChangeIdClass(changeIdCommitMessageError), ''); + + var changeIdCommitMessageError = 'mismatch'; + assert.equal( + element._computeChangeIdClass(changeIdCommitMessageError), 'warning'); + }); + + 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 +821,155 @@ 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); + }); + sandbox.stub(Gerrit, 'awaitPluginsLoaded', function() { + return Promise.resolve(); + }); + + 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(); + assert.isTrue(Gerrit.awaitPluginsLoaded.called); + }); + + suite('scroll related tests', function() { + test('document scrolling calls function to set scroll height', + function(done) { + var originalHeight = document.body.scrollHeight; + var scrollStub = sandbox.stub(element, '_handleScroll', + function() { + assert.isTrue(scrollStub.called); + document.body.style.height = + originalHeight + 'px'; + scrollStub.restore(); + done(); + }); + document.body.style.height = '10000px'; + document.body.scrollTop = TEST_SCROLL_TOP_PX; + element._handleScroll(); + }); + + test('history is loaded correctly', function() { + history.replaceState( + { + scrollTop: 100, + path: location.pathname, + }, + location.pathname); + + var reloadStub = sandbox.stub(element, '_reload', + function() { + // When element is reloaded, ensure that the history + // state has the scrollTop set earlier. This will then + // be reset. + assert.isTrue(history.state.scrollTop == 100); + return Promise.resolve({}); + }); + + // 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'); + }); + }); + + test('reply button is disabled until server config is loaded', function() { + assert.isTrue(element._replyDisabled); + element.serverConfig = {}; + assert.isFalse(element._replyDisabled); + }); }); </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..08e2e21 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; @@ -34,13 +36,13 @@ margin: 5px 0; } .lineNum { - margin-right: .35em; - min-width: 7em; + margin-right: .5em; + min-width: 10em; + 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,11 @@ File comment: </span> </a> - <div class="message">[[comment.message]]</div> + <gr-formatted-text + class="message" + no-trailing-margin + 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..f1d02f2 --- /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" rel="noopener" + 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-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html index 3896ffa..129e325 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -31,6 +31,9 @@ label { cursor: pointer; } + .info { + font-style: italic; + } .parentRevisionContainer label, .parentRevisionContainer input[type="text"] { display: block; @@ -47,12 +50,16 @@ <gr-confirm-dialog confirm-label="Rebase" on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + on-cancel="_handleCancelTap" + disabled="[[!valueSelected]]"> <div class="header">Confirm rebase</div> <div class="main"> <div class="parentRevisionContainer"> <label for="parentInput"> - Parent revision (optional) + Parent revision + <span id="optionalText" hidden$="[[!rebaseOnCurrent]]"> (optional) + </span> + <span hidden$="[[rebaseOnCurrent]]"> (not current branch)</span> </label> <input is="iron-input" type="text" @@ -62,11 +69,18 @@ </div> <div class="clearParentContainer"> <input id="clearParent" + hidden$="[[!rebaseOnCurrent]]" type="checkbox" - on-tap="_handleClearParentTap"> - <label for="clearParent"> + on-tap="_handleClearParentTap" + disabled="[[!rebaseOnCurrent]]"> + <label id="clearParentLabel" for="clearParent" + hidden$="[[!rebaseOnCurrent]]"> Rebase on top of current branch (clear parent revision). </label> + <p id="rebaseUpToDateInfo" class="info" for="clearParent" + hidden$="[[rebaseOnCurrent]]"> + Already up to date with current branch. + </p> </div> </div> </gr-confirm-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js index 42f2167..6cf6797 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -31,7 +31,15 @@ properties: { base: String, - clearParent: Boolean, + clearParent: { + type: Boolean, + value: false, + }, + rebaseOnCurrent: Boolean, + valueSelected: { + type: Boolean, + computed: '_updateValueSelected(base, clearParent)', + }, }, _handleConfirmTap: function(e) { @@ -52,5 +60,9 @@ this.$.parentInput.disabled = clear; this.clearParent = clear; }, + + _updateValueSelected: function(base, clearParent) { + return base.length || clearParent; + }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html index c02e11e..d6e63d3 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -38,14 +38,47 @@ element = fixture('basic'); }); - test('controls', function() { + test('controls with rebase on current available', function() { + element.rebaseOnCurrent = true; + flushAsynchronousOperations(); + // The correct content is hidden/displayed regarding the ability to rebase + // on top of the current branch. + assert.isFalse(element.$.clearParentLabel.hasAttribute('hidden')); + assert.isTrue(element.$.rebaseUpToDateInfo.hasAttribute('hidden')); + assert.isFalse(element.$.optionalText.hasAttribute('hidden')); + + assert.isFalse(!!element.valueSelected); assert.isFalse(element.$.parentInput.hasAttribute('disabled')); + assert.isFalse(element.$.clearParent.hasAttribute('disabled')); assert.isFalse(element.$.clearParent.checked); element.base = 'something great'; + assert.isTrue(!!element.valueSelected); MockInteractions.tap(element.$.clearParent); + assert.isTrue(!!element.valueSelected); assert.isTrue(element.$.parentInput.hasAttribute('disabled')); assert.isTrue(element.$.clearParent.checked); assert.equal(element.base, ''); + MockInteractions.tap(element.$.clearParent); + assert.isFalse(!!element.valueSelected); + }); + + test('controls without rebase on current available', function() { + element.rebaseOnCurrent = false; + flushAsynchronousOperations(); + // The correct content is hidden/displayed regarding the ability to rebase + // on top of the current branch. + assert.isTrue(element.$.clearParentLabel.hasAttribute('hidden')); + assert.isFalse(element.$.rebaseUpToDateInfo.hasAttribute('hidden')); + assert.isTrue(element.$.optionalText.hasAttribute('hidden')); + + assert.isFalse(!!element.valueSelected); + assert.isFalse(element.$.parentInput.hasAttribute('disabled')); + assert.isTrue(element.$.clearParent.hasAttribute('disabled')); + assert.isTrue(element.$.clearParentLabel.hasAttribute('hidden')); + assert.isFalse(element.$.rebaseUpToDateInfo.hasAttribute('hidden')); + + element.base = 'something great'; + assert.isTrue(!!element.valueSelected); }); }); </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..72425a4 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,11 +50,24 @@ 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; + // Note (issue 5180): normalize the download scheme with lower-case. + this._selectedScheme = prefs.download_scheme.toLowerCase(); } }.bind(this)); }, @@ -61,7 +75,8 @@ _computeDownloadCommands: function(change, patchNum, _selectedScheme) { var commandObj; for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { + if (change.revisions[rev]._number == patchNum && + change.revisions[rev].fetch.hasOwnProperty(_selectedScheme)) { commandObj = change.revisions[rev].fetch[_selectedScheme].commands; break; }
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..7d80c09 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() { @@ -203,4 +226,23 @@ firstSchemeButton.getAttribute('data-scheme')); }); }); + + test('normalize scheme from preferences', function(done) { + stub('gr-rest-api-interface', { + getPreferences: function() { + return Promise.resolve({download_scheme: 'REPO'}); + }, + }); + element = fixture('loggedIn'); + element.change = getChangeObject(); + element.patchNum = 1; + element.config = { + schemes: {'anonymous http': {}, http: {}, repo: {}, ssh: {}}, + archives: ['tgz', 'tar', 'tbz2', 'txz'], + }; + element.$.restAPI.getPreferences.lastCall.returnValue.then(function() { + assert.equal(element._selectedScheme, 'repo'); + done(); + }); + }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html index ef5ceed..1ab8a77 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 { @@ -51,10 +62,10 @@ text-align: center; width: 1.5em; } - .row:not(.header):hover { + .file-row: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,57 @@ 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; + word-wrap: break-word; + } + .mobile { + display: none; + } @media screen and (max-width: 50em) { - .row[selected] { + .desktop { + display: none; + } + .mobile { + display: block; + } + .row.selected { background-color: transparent; } .stats { @@ -119,57 +175,137 @@ .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(_shownFiles.*, _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(_shownFiles.*, _maxFilesForBulkActions)]]"> + <div class="warning"> + Bulk actions disabled because there are too many 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" aria-label="Reviewed checkbox"> </div> - <div class$="[[_computeClass('status', file.__path)]]"> + <div class$="[[_computeClass('status', file.__path)]]" + tabindex="0" + aria-label$="[[_computeFileStatusLabel(file.status)]]"> [[_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)]]" + on-click="_handleFileClick"> + <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> + <div class="comments desktop"> + <span class="drafts"> + [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]] + </span> [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]] </div> + <div class="comments mobile"> + <span class="drafts"> + [[_computeDraftsStringMobile(drafts, patchRange.patchNum, + file.__path)]] + </span> + [[_computeCommentsStringMobile(comments, patchRange.patchNum, + file.__path)]] + </div> <div class$="[[_computeClass('stats', file.__path)]]"> - <span class="added">+[[file.lines_inserted]]</span> - <span class="removed">-[[file.lines_deleted]]</span> + <span + class="added" + tabindex="0" + aria-label$="[[file.lines_inserted]] lines added" + hidden$=[[file.binary]]> + +[[file.lines_inserted]] + </span> + <span + class="removed" + tabindex="0" + aria-label$="[[file.lines_deleted]] lines removed" + hidden$=[[file.binary]]> + -[[file.lines_deleted]] + </span> + <span class$="[[_computeBinaryClass(file.size_delta)]]" + hidden$=[[!file.binary]]> + [[_formatBytes(file.size_delta)]] + [[_formatPercentage(file.size, file.size_delta)]] + </span> + </div> + <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]"> + <label class="show-hide"> + <input type="checkbox" class="show-hide" + checked$="[[!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 +313,59 @@ path="[[file.__path]]" prefs="[[_diffPrefs]]" project-config="[[projectConfig]]" - view-mode="[[_userPrefs.diff_view]]"></gr-diff> + view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff> </template> + <div class="row totalChanges"> + <div class="total-stats" hidden$="[[_hideChangeTotals]]"> + <span + class="added" + tabindex="0" + aria-label$="[[_patchChange.inserted]] lines added"> + +[[_patchChange.inserted]] + </span> + <span + class="removed" + tabindex="0" + aria-label$="[[_patchChange.deleted]] lines removed"> + -[[_patchChange.deleted]] + </span> + </div> + </div> + <div class="row totalChanges"> + <div class="total-stats" hidden$="[[_hideBinaryChangeTotals]]"> + <span class="added" aria-label="Total lines added"> + [[_formatBytes(_patchChange.size_delta_inserted)]] + [[_formatPercentage(_patchChange.total_size, + _patchChange.size_delta_inserted)]] + </span> + <span class="removed" aria-label="Total lines 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..ca1ffe9 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
@@ -14,20 +14,33 @@ (function() { 'use strict'; + // Maximum length for patch set descriptions. + var PATCH_DESC_MAX_LENGTH = 500; + var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + var FileStatus = { + A: 'Added', + C: 'Copied', + D: 'Deleted', + R: 'Renamed', + W: 'Rewritten', + }; + Polymer({ 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 +50,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 +67,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: 50, + }, }, 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 +156,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 +166,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 +199,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 +236,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) { @@ -164,7 +266,17 @@ return this._computeCountString(drafts, patchNum, path, 'draft'); }, - _computeCountString: function(comments, patchNum, path, noun) { + _computeDraftsStringMobile: function(drafts, patchNum, path) { + var draftCount = this._computeCountString(drafts, patchNum, path); + return draftCount ? draftCount + 'd' : ''; + }, + + _computeCommentsStringMobile: function(comments, patchNum, path) { + var commentCount = this._computeCountString(comments, patchNum, path); + return commentCount ? commentCount + 'c' : ''; + }, + + _computeCountString: function(comments, patchNum, path, opt_noun) { if (!comments) { return ''; } var patchComments = (comments[path] || []).filter(function(c) { @@ -172,7 +284,8 @@ }); var num = patchComments.length; if (num === 0) { return ''; } - return num + ' ' + noun + (num > 1 ? 's' : ''); + if (!opt_noun) { return num; } + return num + ' ' + opt_noun + (num > 1 ? 's' : ''); }, _computeReviewed: function(file, _reviewed) { @@ -212,108 +325,160 @@ _getFiles: function() { return this.$.restAPI.getChangeFilesAsSpeciallySortedArray( - this.changeNum, this.patchRange); + this.changeNum, this.patchRange).then(function(files) { + // Append UI-specific properties. + return files.map(function(file) { + file.__expanded = false; + return file; + }); + }); }, - _handleKey: function(e) { - if (this.shouldSupressKeyboardShortcut(e)) { return; } - - switch (e.keyCode) { - case 37: // left - if (e.shiftKey && this._showInlineDiffs) { - 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; + _handleFileClick: function(e) { + // If the user prefers to expand inline diffs rather than opening the diff + // view, intercept the click event. + if (this._userPrefs && this._userPrefs.expand_inline_diffs) { + e.preventDefault(); + this._handleHiddenChange(e); } }, + _handleShiftLeftKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (!this._showInlineDiffs) { return; } + + 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 +488,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 +523,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 +537,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 +575,95 @@ 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, true); }.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; + }, + + /** + * _getDiffViewMode: Get the diff view (side-by-side or unified) based on + * the current state. + * + * The expected behavior is to use the mode specified in the user's + * preferences unless they have manually chosen the alternative view. + * + * Use side-by-side if there is no view mode or preferences. + * + * @return {String} + */ + _getDiffViewMode: function(diffViewMode, userPrefs) { + if (diffViewMode) { + return diffViewMode; + } else if (userPrefs) { + return this.diffViewMode = userPrefs.default_diff_view; + } + return 'SIDE_BY_SIDE'; + }, + + _fileListActionsVisible: function(shownFilesRecord, + maxFilesForBulkActions) { + return shownFilesRecord.base.length <= maxFilesForBulkActions; + }, + + _computePatchSetDescription: function(revisions, patchNum) { + var rev = this.getRevisionByPatchNum(revisions, patchNum); + return (rev && rev.description) ? + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + }, + + _computeFileStatusLabel: function(status) { + var statusCode = this._computeFileStatus(status); + return FileStatus.hasOwnProperty(statusCode) ? + FileStatus[statusCode] : 'Status Unknown'; + }, }); })();
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..4c8b4f1 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() { @@ -152,22 +377,50 @@ element._computeCountString(comments, '1', '/COMMIT_MSG', 'comment'), '2 comments'); assert.equal( + element._computeCommentsStringMobile(comments, '1', '/COMMIT_MSG'), + '2c'); + assert.equal( + element._computeDraftsStringMobile(comments, '1', '/COMMIT_MSG'), + '2d'); + assert.equal( element._computeCountString(comments, '1', 'myfile.txt', 'comment'), '1 comment'); assert.equal( + element._computeCommentsStringMobile(comments, '1', 'myfile.txt'), + '1c'); + assert.equal( + element._computeDraftsStringMobile(comments, '1', 'myfile.txt'), + '1d'); + assert.equal( element._computeCountString(comments, '1', - 'file_added_in_rev2.txt', 'comment'), - ''); + 'file_added_in_rev2.txt', 'comment'), ''); + assert.equal( + element._computeCommentsStringMobile(comments, '1', + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(comments, '1', + 'file_added_in_rev2.txt'), ''); assert.equal( element._computeCountString(comments, '2', '/COMMIT_MSG', 'comment'), '1 comment'); assert.equal( + element._computeCommentsStringMobile(comments, '2', '/COMMIT_MSG'), + '1c'); + assert.equal( + element._computeDraftsStringMobile(comments, '2', '/COMMIT_MSG'), + '1d'); + assert.equal( element._computeCountString(comments, '2', 'myfile.txt', 'comment'), '2 comments'); assert.equal( + element._computeCommentsStringMobile(comments, '2', 'myfile.txt'), + '2c'); + assert.equal( + element._computeDraftsStringMobile(comments, '2', 'myfile.txt'), + '2d'); + assert.equal( element._computeCountString(comments, '2', - 'file_added_in_rev2.txt', 'comment'), - ''); + 'file_added_in_rev2.txt', 'comment'), ''); }); test('computed properties', function() { @@ -187,9 +440,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 +450,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 +478,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 +508,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 +520,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 +534,157 @@ 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) { + var computeSpy = sandbox.spy(element, '_fileListActionsVisible'); + element._files = []; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.$.fileCursor.setCursorAtIndex(0); + 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('_getDiffViewMode', function() { + // No user prefs or diff view mode set. + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + // User prefs but no diff view mode set. + element.diffViewMode = null; + element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'}; + assert.equal( + element._getDiffViewMode(null, element._userPrefs), 'UNIFIED_DIFF'); + // User prefs and diff view mode set. + element.diffViewMode = 'SIDE_BY_SIDE'; + assert.equal(element._getDiffViewMode( + element.diffViewMode, element._userPrefs), 'SIDE_BY_SIDE'); + }); + test('expand_inline_diffs user preference', function() { + element._files = [ + {__path: '/COMMIT_MSG', __expanded: false}, + ]; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + flushAsynchronousOperations(); + var commitMsgFile = Polymer.dom(element.root) + .querySelectorAll('.row:not(.header) a')[0]; + + // Remove href attribute so the app doesn't route to a diff view + commitMsgFile.removeAttribute('href'); + var hiddenChangeSpy = sandbox.spy(element, '_handleHiddenChange'); + + MockInteractions.tap(commitMsgFile); + flushAsynchronousOperations(); + assert(hiddenChangeSpy.notCalled, 'file is opened as diff view'); + assert.isNotOk(element.$$('.expanded')); + + element._userPrefs = {expand_inline_diffs: true}; + flushAsynchronousOperations(); + MockInteractions.tap(commitMsgFile); + flushAsynchronousOperations(); + assert(hiddenChangeSpy.calledOnce, 'file is expanded'); + assert.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..3f5493c 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,21 +73,43 @@ .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, .collapsed .message, + .collapsed .updateCategory, gr-account-chip { 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,29 +130,42 @@ <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> + </template> + <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]"> + <div class="content"> + <template is="dom-repeat" items="[[message.updates]]" as="update"> + <div class="updateCategory"> + [[update.message]] + <template + is="dom-repeat" items="[[update.reviewers]]" as="reviewer"> + <gr-account-chip account="[[reviewer]]"> + </gr-account-chip> + </template> + </div> + </template> + </div> + </template> + <template is="dom-if" if="[[!message.id]]"> + <span class="date"> + <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter> + </span> + </template> + <template is="dom-if" if="[[message.id]]"> <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]]. - <gr-date-formatter class="date" date-str="[[message.updated]]"> - </gr-date-formatter> - </template> </div> <div class="replyContainer" hidden$="[[!showReplyButton]]" hidden> <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
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..02caa0d 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,19 @@ this.expanded = false; }, + _computeIsAutomated: function(message) { + return !!(message.reviewer || + (message.tag && message.tag.indexOf('autogenerated') === 0)); + }, + + _computeIsHidden: function(hideAutomated, isAutomated) { + return hideAutomated && isAutomated; + }, + + _computeIsReviewerUpdate: function(event) { + return event.type === 'REVIEWER_UPDATE'; + }, + _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..0f6c85d 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'); }); @@ -61,7 +63,7 @@ }); test('reviewer update', function() { - var updatedBy = { + var author = { _account_id: 1115495, name: 'Andrew Bonventre', email: 'andybons@chromium.org', @@ -72,18 +74,74 @@ email: 'barbar@chromium.org', }; element.message = { - updated_by: updatedBy, + id: 0xDEADBEEF, + author: author, reviewer: reviewer, - state: 'CC', - updated: '2016-01-12 20:24:49.448000000', + date: '2016-01-12 20:24:49.448000000', + type: 'REVIEWER_UPDATE', + updates: [ + { + message: 'Added to CC:', + reviewers: [reviewer], + } + ], }; flushAsynchronousOperations(); var content = element.$$('.contentContainer'); assert.isOk(content); - assert.strictEqual( - content.querySelector('gr-account-chip').account, reviewer); - assert.equal(0, content.textContent.trim().indexOf(updatedBy.name)); + assert.strictEqual(element.$$('gr-account-chip').account, reviewer); + assert.equal(author.name, element.$$('.name').textContent); }); + 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..e33458f 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); }, @@ -77,7 +80,7 @@ break; } mDate = mDate || util.parseDate(messages[mi].date); - rDate = rDate || util.parseDate(reviewerUpdates[ri].updated); + rDate = rDate || util.parseDate(reviewerUpdates[ri].date); if (rDate < mDate) { result.push(reviewerUpdates[ri++]); rDate = null; @@ -103,41 +106,85 @@ el.classList.add('highlighted'); }, - _handleExpandCollapseTap: function(e) { - e.preventDefault(); - this._expanded = !this._expanded; + /** + * @param {boolean} expand + */ + handleExpandCollapse: function(expand) { + this._expanded = expand; var messageEls = Polymer.dom(this.root).querySelectorAll('gr-message'); for (var i = 0; i < messageEls.length; i++) { - messageEls[i].expanded = this._expanded; + messageEls[i].expanded = expand; } }, + _handleExpandCollapseTap: function(e) { + e.preventDefault(); + this.handleExpandCollapse(!this._expanded); + }, + + _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 all messages' : 'Show comments only'; + }, + + /** + * 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..770da61 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,34 @@ } }); + test('expand/collapse from external keypress', function() { + element.handleExpandCollapse(true); + var allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + assert.isTrue(allMessageEls[i].expanded); + } + + // Expand/collapse all text also changes. + assert.equal(element.$$('#collapse-messages').textContent.trim(), + 'Collapse all'); + + element.handleExpandCollapse(false); + var allMessageEls = + Polymer.dom(element.root).querySelectorAll('gr-message'); + for (var i = 0; i < allMessageEls.length; i++) { + assert.isFalse(allMessageEls[i].expanded); + } + // Expand/collapse all text also changes. + assert.equal(element.$$('#collapse-messages').textContent.trim(), + 'Expand all'); + }); + + test('hide messages does not appear when no automated messages', + function() { + assert.isOk(element.$$('#automatedMessageToggleContainer[hidden]')); + }); + test('scroll to message', function() { var allMessageEls = Polymer.dom(element.root).querySelectorAll('gr-message'); @@ -116,7 +131,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 +142,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..69c8cc0 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
@@ -69,64 +69,79 @@ .submittable { color: #1b5e20; } - .hidden { + .hidden, + .mobile { display: none; } + @media screen and (max-width: 50em) { + .mobile { + display: block; + } + hr { + border: 0; + border-top: 1px solid #ddd; + height: 0; + margin-bottom: 1em; + } + } </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]]"> + <hr class="mobile"> + <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.branch]]: [[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..9cee168 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
@@ -20,6 +20,7 @@ <link rel="import" href="../../../behaviors/rest-client-behavior.html"> <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.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"> @@ -69,6 +70,7 @@ gr-account-list { display: flex; flex-wrap: wrap; + flex: 1; } #reviewerConfirmationOverlay { padding: 1em; @@ -93,13 +95,16 @@ padding: 0; font-family: var(--monospace-font-family); } + .previewContainer gr-formatted-text { + background: #f6f6f6; + max-height: 20vh; + overflow-y: scroll; + padding: 1em; + } .message { border: none; width: 100%; } - .labelsNotShown { - color: #666; - } .labelContainer:not(:first-of-type) { margin-top: .5em; } @@ -141,6 +146,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"> @@ -155,7 +168,8 @@ <div class="peopleListLabel">Reviewers</div> <gr-account-list id="reviewers" - accounts="[[_reviewers]]" + accounts="{{_reviewers}}" + removable-values="[[change.removable_reviewers]]" change="[[change]]" filter="[[filterReviewerSuggestion]]" pending-confirmation="{{_reviewerPendingConfirmation}}" @@ -167,7 +181,7 @@ <div class="peopleListLabel">CC</div> <gr-account-list id="ccs" - accounts="[[_ccs]]" + accounts="{{_ccs}}" change="[[change]]" filter="[[filterReviewerSuggestion]]" pending-confirmation="{{_ccPendingConfirmation}}" @@ -182,11 +196,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,37 +216,40 @@ <iron-autogrow-textarea id="textarea" class="message" + autocomplete="on" placeholder="Say something..." disabled="{{disabled}}" rows="4" max-rows="15" bind-value="{{draft}}" - on-bind-value-changed="_handleTextareaChanged"> + on-bind-value-changed="_handleHeightChanged"> </iron-autogrow-textarea> </section> + <section class="previewContainer"> + <label> + <input type="checkbox" checked="{{_previewFormatting::change}}"> + Preview formatting + </label> + <gr-formatted-text + content="[[draft]]" + hidden$="[[!_previewFormatting]]" + config="[[projectConfig.commentlinks]]"></gr-formatted-text> + </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 +257,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..8ee0dab 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -23,6 +23,11 @@ REVIEWERS: 'reviewers', }; + var ReviewerTypes = { + REVIEWER: 'REVIEWER', + CC: 'CC', + }; + Polymer({ is: 'gr-reply-dialog', @@ -48,7 +53,6 @@ properties: { change: Object, patchNum: String, - revisions: Object, disabled: { type: Boolean, value: false, @@ -59,6 +63,10 @@ value: '', observer: '_draftChanged', }, + quote: { + type: String, + value: '', + }, diffDrafts: Object, filterReviewerSuggestion: { type: Function, @@ -66,9 +74,9 @@ return this._filterReviewerSuggestion.bind(this); }, }, - labels: Object, permittedLabels: Object, serverConfig: Object, + projectConfig: Object, _account: Object, _ccs: Array, @@ -76,12 +84,29 @@ type: Object, observer: '_reviewerPendingConfirmationUpdated', }, + _labels: { + type: Array, + computed: '_computeLabels(change.labels.*, _account)', + }, _owner: Object, + _pendingConfirmationDetails: Object, _reviewers: Array, _reviewerPendingConfirmation: { type: Object, observer: '_reviewerPendingConfirmationUpdated', }, + _previewFormatting: { + type: Boolean, + value: false, + observer: '_handleHeightChanged', + }, + _reviewersPendingRemove: { + type: Object, + value: { + CC: [], + REVIEWER: [], + }, + }, }, FocusTarget: FocusTarget, @@ -92,11 +117,13 @@ observers: [ '_changeUpdated(change.reviewers.*, change.owner, serverConfig)', + '_ccsChanged(_ccs.splices)', + '_reviewersChanged(_reviewers.splices)', ], attached: function() { this._getAccount().then(function(account) { - this._account = account; + this._account = account || {}; }.bind(this)); }, @@ -131,6 +158,76 @@ selectorEl.selectIndex(selectorEl.indexOf(item)); }, + _ccsChanged: function(splices) { + if (splices && splices.indexSplices) { + this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC); + } + }, + + _reviewersChanged: function(splices) { + if (splices && splices.indexSplices) { + this._processReviewerChange(splices.indexSplices, + ReviewerTypes.REVIEWER); + } + }, + + _processReviewerChange: function(indexSplices, type) { + indexSplices.forEach(function(splice) { + splice.removed.forEach(function(account) { + if (!this._reviewersPendingRemove[type]) { + console.err('Invalid type ' + type + ' for reviewer.'); + return; + } + this._reviewersPendingRemove[type].push(account); + }.bind(this)); + }.bind(this)); + }, + + /** + * Resets the state of the _reviewersPendingRemove object, and removes + * accounts if necessary. + * + * @param {Boolean} isCancel true if the action is a cancel. + */ + _purgeReviewersPendingRemove: function(isCancel) { + var reviewerArr; + for (var type in this._reviewersPendingRemove) { + if (this._reviewersPendingRemove.hasOwnProperty(type)) { + if (!isCancel) { + reviewerArr = this._reviewersPendingRemove[type]; + for (var i = 0; i < reviewerArr.length; i++) { + this._removeAccount(reviewerArr[i], type); + } + } + this._reviewersPendingRemove[type] = []; + } + } + }, + + /** + * Removes an account from the change, both on the backend and the client. + * Does nothing if the account is a pending addition. + * + * @param {Object} account + * @param {ReviewerTypes} type + */ + _removeAccount: function(account, type) { + if (account._pendingAdd) { return; } + + return this.$.restAPI.removeChangeReviewer(this.change._number, + account._account_id).then(function(response) { + if (!response.ok) { return response; } + + var reviewers = this.change.reviewers[type] || []; + for (var i = 0; i < reviewers.length; i++) { + if (reviewers[i]._account_id == account._account_id) { + this.splice(['change', 'reviewers', type], i, 1); + break; + } + } + }.bind(this)); + }, + _mapReviewer: function(reviewer) { var reviewerId; var confirmed; @@ -148,17 +245,27 @@ drafts: 'PUBLISH_ALL_REVISIONS', labels: {}, }; + for (var label in this.permittedLabels) { if (!this.permittedLabels.hasOwnProperty(label)) { continue; } 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; @@ -207,7 +314,7 @@ _chooseFocusTarget: function() { // If we are the owner and the reviewers field is empty, focus on that. - if (this._account && this.change.owner && + if (this._account && this.change && this.change.owner && this._account._account_id === this.change.owner._account_id && (!this._reviewers || this._reviewers.length === 0)) { return FocusTarget.REVIEWERS; @@ -259,16 +366,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 +384,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; } @@ -324,17 +426,21 @@ }, _changeUpdated: function(changeRecord, owner, serverConfig) { + this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig); + }, + + _rebuildReviewerArrays: function(change, owner, serverConfig) { this._owner = owner; var reviewers = []; var ccs = []; - for (var key in changeRecord.base) { + for (var key in change) { if (key !== 'REVIEWER' && key !== 'CC') { console.warn('unexpected reviewer state:', key); continue; } - changeRecord.base[key].forEach(function(entry) { + change[key].forEach(function(entry) { if (entry._account_id === owner._account_id) { return; } @@ -392,11 +498,16 @@ _cancelTapHandler: function(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); + this._purgeReviewersPendingRemove(true); + this._rebuildReviewerArrays(this.change.reviewers, this._owner, + this.serverConfig); }, _sendTapHandler: function(e) { e.preventDefault(); - this.send(); + this.send().then(function() { + this._purgeReviewersPendingRemove(); + }.bind(this)); }, _saveReview: function(review, opt_errFn) { @@ -408,6 +519,8 @@ if (reviewer === null) { this.$.reviewerConfirmationOverlay.close(); } else { + this._pendingConfirmationDetails = + this._ccPendingConfirmation || this._reviewerPendingConfirmation; this.$.reviewerConfirmationOverlay.open(); } }, @@ -457,7 +570,7 @@ }, STORAGE_DEBOUNCE_INTERVAL_MS); }, - _handleTextareaChanged: function(e) { + _handleHeightChanged: function(e) { // If the textarea resizes, we need to re-fit the overlay. this.debounce('autogrow', function() { this.fire('autogrow');
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..f7058bd 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"> @@ -42,6 +41,10 @@ var setDraftCommentStub; var eraseDraftCommentStub; + var lastId = 0; + var makeAccount = function() { return {_account_id: lastId++}; }; + var makeGroup = function() { return {id: lastId++}; }; + setup(function() { sandbox = sinon.sandbox.create(); @@ -49,43 +52,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 +108,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 +198,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 +215,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 +254,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 +299,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 +365,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() { @@ -329,14 +385,6 @@ }); test('filterReviewerSuggestion', function() { - var counter = 0; - function makeAccount() { - return {_account_id: counter++}; - } - function makeGroup() { - return {id: counter++}; - } - var owner = makeAccount(); var reviewer1 = makeAccount(); var reviewer2 = makeGroup(); @@ -389,5 +437,126 @@ 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); + }); + + test('_processReviewerChange', function() { + var mockIndexSplices = function(toRemove) { + return [{ + removed: [toRemove], + }]; + }; + + element._processReviewerChange( + mockIndexSplices(makeAccount()), 'REVIEWER'); + assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1); + }); + + test('_purgeReviewersPendingRemove', function() { + var removeStub = sandbox.stub(element, '_removeAccount'); + var mock = function() { + element._reviewersPendingRemove = { + test: [makeAccount()], + test2: [makeAccount(), makeAccount()], + }; + }; + var checkObjEmpty = function(obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; } + } + return true; + }; + mock(); + element._purgeReviewersPendingRemove(true); // Cancel + assert.isFalse(removeStub.called); + assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); + + mock(); + element._purgeReviewersPendingRemove(false); // Submit + assert.isTrue(removeStub.called); + assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); + }); + + test('_removeAccount', function(done) { + sandbox.stub(element.$.restAPI, 'removeChangeReviewer') + .returns(Promise.resolve({ok: true})); + var arr = [makeAccount(), makeAccount()]; + element.change.reviewers = { + REVIEWER: arr.slice(), + }; + + element._removeAccount(arr[1], 'REVIEWER').then(function() { + assert.equal(element.change.reviewers.REVIEWER.length, 1); + assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1)); + done(); + }); + }); }); </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..015cfc5 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,16 @@ 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 + link + 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> + <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span> + <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden + image-size="56" aria-label="Account avatar"></gr-avatar> + </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..bee95ea 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', @@ -30,6 +31,7 @@ attached: function() { this.listen(document, 'server-error', '_handleServerError'); this.listen(document, 'network-error', '_handleNetworkError'); + this.listen(document, 'show-alert', '_handleShowAlert'); }, detached: function() { @@ -38,6 +40,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,11 +55,17 @@ }.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)); } }, + _handleShowAlert: function(e) { + this._showAlert(e.detail.message); + }, + _handleNetworkError: function(e) { this._showAlert('Server unavailable'); console.error(e.detail.error.message); @@ -140,6 +152,7 @@ 'height=' + SIGN_IN_HEIGHT_PX, 'left=' + left, 'top=' + top, + 'noopener=yes', ]; window.open('/login/%3FcloseAfterLogin', '_blank', options.join(',')); },
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..91780d2 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'); @@ -102,6 +116,8 @@ assert.isFalse(windowOpen.called); toast.fire('action'); assert.isTrue(windowOpen.called); + assert.notEqual(windowOpen.lastCall.args[2].indexOf('noopener=yes'), + -1); var hideToastSpy = sandbox.spy(toast, 'hide'); @@ -120,5 +136,12 @@ }); }); }); + + test('show alert', function() { + sandbox.stub(element, '_showAlert'); + element.fire('show-alert', {message: 'foo'}); + assert.isTrue(element._showAlert.calledOnce); + assert.isTrue(element._showAlert.lastCall.calledWithExactly('foo')); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html index 7291199..94e15b6 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> @@ -120,6 +113,20 @@ <td>Show previous file</td> </tr> <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">j</span> + </td> + <td>Show next file that has comments</td> + </tr> + <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">k</span> + </td> + <td>Show previous file that has comments</td> + </tr> + <tr> <td><span class="key">u</span></td> <td>Up to change</td> </tr> @@ -127,8 +134,8 @@ </table> <table> - <!-- Change List and Dashboard --> - <tbody hidden$="[[!_computeInChangeListView(view)]]" hidden> + <!-- Change List --> + <tbody hidden$="[[!_computeInView(view, 'gr-change-list-view')]]" hidden> <tr> <td></td><td class="header">Change list</td> </tr> @@ -141,6 +148,35 @@ <td>Show previous change</td> </tr> <tr> + <td><span class="key">n</span> or <span class="key">]</span></td> + <td>Go to next page</td> + </tr> + <tr> + <td><span class="key">p</span> or <span class="key">[</span></td> + <td>Go to previous page</td> + </tr> + <tr> + <td> + <span class="key">Enter</span> or + <span class="key">o</span> + </td> + <td>Show selected change</td> + </tr> + </tbody> + <!-- Dashboard --> + <tbody hidden$="[[!_computeInView(view, 'gr-dashboard-view')]]" hidden> + <tr> + <td></td><td class="header">Dashboard</td> + </tr> + <tr> + <td><span class="key">j</span></td> + <td>Select next change</td> + </tr> + <tr> + <td><span class="key">k</span></td> + <td>Show previous change</td> + </tr> + <tr> <td> <span class="key">Enter</span> or <span class="key">o</span> @@ -155,7 +191,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 +213,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 +257,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 +331,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 +370,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-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index 7ed5012..1a286c7 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -32,12 +32,7 @@ }, _computeInView: function(currentView, view) { - return view == currentView; - }, - - _computeInChangeListView: function(currentView) { - return currentView == 'gr-change-list-view' || - currentView == 'gr-dashboard-view'; + return view === currentView; }, _handleCloseTap: function(e) {
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..74dc9e7 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,58 +42,20 @@ 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; } - .downArrow { - border-left: .36em solid transparent; - border-right: .36em solid transparent; - border-top: .36em solid #ccc; - height: 0; - position: absolute; - right: 0; - top: calc(50% - .1em); - width: 0; - } - .links li:hover .downArrow { - border-top-color: #666; - } .rightItems { + align-items: center; display: flex; flex: 1; justify-content: flex-end; @@ -116,6 +78,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 +103,15 @@ <ul class="links"> <template is="dom-repeat" items="[[_links]]" as="linkGroup"> <li> - <span class="linksTitle"> - [[linkGroup.title]] <i class="downArrow"></i> + <gr-dropdown + link + down-arrow + items = [[linkGroup.links]] + horizontal-align="left"> + <span class="linksTitle" id="[[linkGroup.title]]"> + [[linkGroup.title]] </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..cd18f29 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,195 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 ERROR = { + TYPE: 'error', + CATEGORY: 'exception', + }; + + var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/; + var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/; + + var pending = []; + + var onError = function(oldOnError, msg, url, line, column, error) { + if (oldOnError) { + oldOnError(msg, url, line, column, error); + } + if (error) { + line = line || error.lineNumber; + column = column || error.columnNumber; + msg = msg || error.toString(); + } + var payload = { + url: url, + line: line, + column: column, + error: error, + }; + GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); + return true; + }; + + var catchErrors = function(opt_context) { + var context = opt_context || window; + context.onerror = onError.bind(null, context.onerror); + }; + catchErrors(); + + var GrReporting = 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})); + if (type === ERROR.TYPE) { + console.error(eventValue.error); + } else { + console.log(eventName + ': ' + eventValue); + } + }, + + cachingReporter: function(type, category, eventName, eventValue) { + if (type === ERROR.TYPE) { + console.error(eventValue.error); + } + 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]; + }, + }); + + window.GrReporting = GrReporting; + // Expose onerror installation so it would be accessible from tests. + window.GrReporting._catchErrors = catchErrors; + +})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html new file mode 100644 index 0000000..fd8a73b --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,208 @@ +<!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')); + }); + }); + + suite('exception logging', function() { + var fakeWindow; + var reporter; + + var emulateThrow = function(msg, url, line, column, error) { + return fakeWindow.onerror(msg, url, line, column, error); + }; + + setup(function() { + reporter = sandbox.stub(GrReporting.prototype, 'reporter'); + fakeWindow = {}; + sandbox.stub(console, 'error'); + window.GrReporting._catchErrors(fakeWindow); + }); + + test('is reported', function() { + var error = new Error('bar'); + emulateThrow('bar', 'http://url', 4, 2, error); + assert.isTrue( + reporter.calledWith('error', 'exception', 'bar')); + var payload = reporter.lastCall.args[3]; + assert.deepEqual(payload, { + url: 'http://url', + line: 4, + column: 2, + error: error, + }); + }); + + test('prevent default event handler', function() { + assert.isTrue(emulateThrow()); + }); + }); + }); +</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..b439fc8 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,7 +59,16 @@ } // For backward compatibility with GWT links. if (data.hash) { - page.redirect(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; + } + var newUrl = data.hash; + if (newUrl.indexOf('/VE/') === 0) { + newUrl = '/settings' + data.hash; + } + page.redirect(newUrl); return; } restAPI.getLoggedIn().then(function(loggedIn) { @@ -69,6 +91,17 @@ }); }); + page('/admin/(.*)', loadUser, function(data) { + restAPI.getLoggedIn().then(function(loggedIn) { + if (loggedIn) { + data.params.view = 'gr-admin-view'; + app.params = data.params; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + function queryHandler(data) { data.params.view = 'gr-change-list-view'; app.params = data.params; @@ -124,18 +157,48 @@ }; // 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; }); + page(/^\/settings\/VE\/(\S+)/, function(data) { + restAPI.getLoggedIn().then(function(loggedIn) { + if (loggedIn) { + app.params = { + view: 'gr-settings-view', + emailToken: data.params[0], + }; + } else { + page.show('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + page(/^\/settings\/?/, function(data) { restAPI.getLoggedIn().then(function(loggedIn) { if (loggedIn) { @@ -146,6 +209,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..cbc00b8 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
@@ -26,6 +26,9 @@ GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group) { var sectionEl = this._createElement('tbody', 'section'); sectionEl.classList.add(group.type); + if (this._isTotal(group)) { + sectionEl.classList.add('total'); + } var pairs = group.getSideBySidePairs(); for (var i = 0; i < pairs.length; i++) { sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left, @@ -34,6 +37,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'); @@ -58,9 +84,9 @@ row.appendChild(action); } else { var textEl = this._createTextEl(line, side); - var threadEl = this._commentThreadForLine(line, side); - if (threadEl) { - textEl.appendChild(threadEl); + var threadGroupEl = this._commentThreadGroupForLine(line, side); + if (threadGroupEl) { + textEl.appendChild(threadGroupEl); } row.appendChild(textEl); } @@ -69,7 +95,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..55a6bea 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
@@ -26,6 +26,9 @@ GrDiffBuilderUnified.prototype.buildSectionElement = function(group) { var sectionEl = this._createElement('tbody', 'section'); sectionEl.classList.add(group.type); + if (this._isTotal(group)) { + sectionEl.classList.add('total'); + } for (var i = 0; i < group.lines.length; ++i) { sectionEl.appendChild(this._createRow(sectionEl, group.lines[i])); @@ -33,6 +36,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, @@ -50,9 +73,9 @@ row.appendChild(action); } else { var textEl = this._createTextEl(line); - var threadEl = this._commentThreadForLine(line); - if (threadEl) { - textEl.appendChild(threadEl); + var threadGroupEl = this._commentThreadGroupForLine(line); + if (threadGroupEl) { + textEl.appendChild(threadGroupEl); } row.appendChild(textEl); } @@ -62,7 +85,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..9c03d23 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,13 @@ 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-comment-thread-group/gr-diff-comment-thread-group.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 +35,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 +59,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 +90,7 @@ _builder: Object, _groups: Array, _layers: Array, + _showTabs: Boolean, }, get diffElement() { @@ -87,8 +104,10 @@ attached: function() { // Setup annotation layers. this._layers = [ + this._createTrailingWhitespaceLayer(), this.$.syntaxLayer, this._createIntralineLayer(), + this._createTabIndicatorLayer(), this.$.rangeLayer, ]; @@ -99,6 +118,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 +131,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)); @@ -148,38 +178,6 @@ parseInt(lineEl.getAttribute('data-value'), 10) : null; }, - renderLineRange: function(startLine, endLine, opt_side) { - var groups = - this._builder.getGroupsByLineRange(startLine, endLine, opt_side); - groups.forEach(function(group) { - var newElement = this._builder.buildSectionElement(group); - var oldElement = group.element; - - // Transfer comment threads from existing section to new one. - var threads = Polymer.dom(newElement).querySelectorAll( - 'gr-diff-comment-thread'); - threads.forEach(function(threadEl) { - var lineEl = this.getLineElByChild(threadEl, oldElement); - if (!lineEl) { // New comment thread. - return; - } - var side = this.getSideByLineEl(lineEl); - var line = lineEl.getAttribute('data-value'); - var oldThreadEl = - this.getCommentThreadByLine(line, side, oldElement); - threadEl.parentNode.replaceChild(oldThreadEl, threadEl); - }, this); - - // Replace old group elements with new ones. - group.element.parentNode.replaceChild(newElement, group.element); - group.element = newElement; - }, this); - - this.async(function() { - this.fire('render'); - }, 1); - }, - getContentByLine: function(lineNumber, opt_side, opt_root) { return this._builder.getContentByLine(lineNumber, opt_side, opt_root); }, @@ -204,27 +202,15 @@ return result; }, - getCommentThreadByLine: function(lineNumber, opt_side, opt_root) { - var content = this.getContentByLine(lineNumber, opt_side, opt_root); - return this.getCommentThreadByContentEl(content); - }, - - getCommentThreadByContentEl: function(contentEl) { - if (contentEl.classList.contains('contentText')) { - contentEl = contentEl.parentElement; - } - return contentEl.querySelector('gr-diff-comment-thread'); - }, - getSideByLineEl: function(lineEl) { return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; }, - createCommentThread: function(changeNum, patchNum, path, side, + createCommentThreadGroup: function(changeNum, patchNum, path, side, projectConfig) { - return this._builder.createCommentThread(changeNum, patchNum, path, - side, projectConfig); + return this._builder.createCommentThreadGroup(changeNum, patchNum, + path, side, projectConfig); }, emitGroup: function(group, sectionEl) { @@ -298,8 +284,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 +309,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 +374,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..761fd32 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -14,6 +14,17 @@ (function(window, GrDiffGroup, GrDiffLine) { 'use strict'; + var HTML_ENTITY_PATTERN = /[&<>"'`\/]/g; + var HTML_ENTITY_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/', + '`': '`', + }; + // Prevent redefinition. if (window.GrDiffBuilder) { return; } @@ -29,7 +40,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 +78,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 +189,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 = []; @@ -306,18 +332,21 @@ return result; }; - GrDiffBuilder.prototype.createCommentThread = function(changeNum, patchNum, - path, side, projectConfig) { - var threadEl = document.createElement('gr-diff-comment-thread'); - threadEl.changeNum = changeNum; - threadEl.patchNum = patchNum; - threadEl.path = path; - threadEl.side = side; - threadEl.projectConfig = projectConfig; - return threadEl; + GrDiffBuilder.prototype.createCommentThreadGroup = + function(changeNum, patchNum, path, side, projectConfig, range) { + var threadGroupEl = + document.createElement('gr-diff-comment-thread-group'); + threadGroupEl.changeNum = changeNum; + threadGroupEl.patchNum = patchNum; + threadGroupEl.path = path; + threadGroupEl.side = side; + threadGroupEl.projectConfig = projectConfig; + threadGroupEl.range = range; + return threadGroupEl; }; - GrDiffBuilder.prototype._commentThreadForLine = function(line, opt_side) { + GrDiffBuilder.prototype._commentThreadGroupForLine = + function(line, opt_side) { var comments = this._getCommentsForLine(this._comments, line, opt_side); if (!comments || comments.length === 0) { return null; @@ -333,14 +362,17 @@ patchNum = this._comments.meta.patchRange.basePatchNum; } } - var threadEl = this.createCommentThread( + var threadGroupEl = this.createCommentThreadGroup( this._comments.meta.changeNum, patchNum, this._comments.meta.path, side, this._comments.meta.projectConfig); - threadEl.comments = comments; - return threadEl; + threadGroupEl.comments = comments; + if (opt_side) { + threadGroupEl.setAttribute('data-side', opt_side); + } + return threadGroupEl; }; GrDiffBuilder.prototype._createLineEl = function(line, number, type, @@ -363,15 +395,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); + var html = this._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); } @@ -389,9 +422,6 @@ contentText.innerHTML = html; } - td.classList.add(line.highlights.length > 0 ? - 'lightHighlight' : 'darkHighlight'); - this.layers.forEach(function(layer) { layer.annotate(contentText, line); }); @@ -493,7 +523,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 +533,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 +541,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 + ';'; @@ -552,5 +579,23 @@ throw Error('Subclasses must implement _getNextContentOnSide'); }; + /** + * Determines whether the given group is either totally an addition or totally + * a removal. + * @param {GrDiffGroup} group + * @return {Boolean} + */ + GrDiffBuilder.prototype._isTotal = function(group) { + return group.type === GrDiffGroup.Type.DELTA && + (!group.adds.length || !group.removes.length) && + !(!group.adds.length && !group.removes.length); + }; + + GrDiffBuilder.prototype._escapeHTML = function(str) { + return str.replace(HTML_ENTITY_PATTERN, function(s) { + return HTML_ENTITY_MAP[s]; + }); + }; + window.GrDiffBuilder = GrDiffBuilder; })(window, GrDiffGroup, GrDiffLine);
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..ad316bb 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); @@ -181,7 +214,7 @@ GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]); }); - test('comment thread creation', function() { + test('comment thread group creation', function() { var l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000'}; var l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000'}; var r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000'}; @@ -200,51 +233,86 @@ right: [r5], }; - function checkThreadProps(threadEl, patchNum, side, comments) { - assert.equal(threadEl.changeNum, '42'); - assert.equal(threadEl.patchNum, patchNum); - assert.equal(threadEl.path, '/path/to/foo'); - assert.equal(threadEl.side, side); - assert.deepEqual(threadEl.projectConfig, {foo: 'bar'}); - assert.deepEqual(threadEl.comments, comments); + function checkThreadGroupProps(threadGroupEl, patchNum, side, comments) { + assert.equal(threadGroupEl.changeNum, '42'); + assert.equal(threadGroupEl.patchNum, patchNum); + assert.equal(threadGroupEl.path, '/path/to/foo'); + assert.equal(threadGroupEl.side, side); + assert.deepEqual(threadGroupEl.projectConfig, {foo: 'bar'}); + assert.deepEqual(threadGroupEl.comments, comments); } var line = new GrDiffLine(GrDiffLine.Type.BOTH); line.beforeNumber = 5; line.afterNumber = 5; - var threadEl = builder._commentThreadForLine(line); - checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]); + var threadGroupEl = builder._commentThreadGroupForLine(line); + checkThreadGroupProps(threadGroupEl, '3', 'REVISION', [l5, r5]); - threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT); - checkThreadProps(threadEl, '3', 'REVISION', [r5]); + threadGroupEl = + builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT); + checkThreadGroupProps(threadGroupEl, '3', 'REVISION', [r5]); - threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT); - checkThreadProps(threadEl, '3', 'PARENT', [l5]); + threadGroupEl = + builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT); + checkThreadGroupProps(threadGroupEl, '3', 'PARENT', [l5]); builder._comments.meta.patchRange.basePatchNum = '1'; - threadEl = builder._commentThreadForLine(line); - checkThreadProps(threadEl, '3', 'REVISION', [l5, r5]); + threadGroupEl = builder._commentThreadGroupForLine(line); + checkThreadGroupProps(threadGroupEl, '3', 'REVISION', [l5, r5]); - threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT); - checkThreadProps(threadEl, '1', 'REVISION', [l5]); + threadEl = + builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT); + checkThreadGroupProps(threadEl, '1', 'REVISION', [l5]); - threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT); - checkThreadProps(threadEl, '3', 'REVISION', [r5]); + threadGroupEl = + builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT); + checkThreadGroupProps(threadGroupEl, '3', 'REVISION', [r5]); builder._comments.meta.patchRange.basePatchNum = 'PARENT'; line = new GrDiffLine(GrDiffLine.Type.REMOVE); line.beforeNumber = 5; line.afterNumber = 5; - threadEl = builder._commentThreadForLine(line); - checkThreadProps(threadEl, '3', 'PARENT', [l5, r5]); + threadGroupEl = builder._commentThreadGroupForLine(line); + checkThreadGroupProps(threadGroupEl, '3', 'PARENT', [l5, r5]); line = new GrDiffLine(GrDiffLine.Type.ADD); line.beforeNumber = 3; line.afterNumber = 5; - threadEl = builder._commentThreadForLine(line); - checkThreadProps(threadEl, '3', 'REVISION', [l3, r5]); + threadGroupEl = builder._commentThreadGroupForLine(line); + checkThreadGroupProps(threadGroupEl, '3', 'REVISION', [l3, r5]); + }); + + suite('_isTotal', function() { + test('is total for add', function() { + var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (var idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.ADD)); + } + assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); + }); + + test('is total for remove', function() { + var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (var idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE)); + } + assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); + }); + + test('not total for empty', function() { + var group = new GrDiffGroup(GrDiffGroup.Type.BOTH); + assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); + }); + + test('not total for non-delta', function() { + var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (var idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH)); + } + assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); + }); }); suite('intraline differences', function() { @@ -414,16 +482,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 +713,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 +733,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 +761,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 +783,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() { @@ -624,6 +953,23 @@ done(); }); }); + + test('_escapeHTML', function() { + var input = '<script>alert("XSS");<' + '/script>'; + var expected = '<script>alert("XSS");' + + '</script>'; + var result = GrDiffBuilder.prototype._escapeHTML(input); + assert.equal(result, expected); + + input = '& < > " \' / `'; + + // \u0026 is an ampersand. This is being used here instead of & + // because of the gjslinter. + expected = '\u0026amp; \u0026lt; \u0026gt; \u0026quot;' + + ' \u0026#39; \u0026#x2F; \u0026#96;'; + result = GrDiffBuilder.prototype._escapeHTML(input); + assert.equal(result, expected); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html new file mode 100644 index 0000000..05f7a1d --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
@@ -0,0 +1,41 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> + +<dom-module id="gr-diff-comment-thread-group"> + <template> + <style> + :host { + display: block; + white-space: normal; + } + </style> + <template is="dom-repeat" items="[[_threadGroups]]" + as="thread"> + <gr-diff-comment-thread + comments="[[thread.comments]]" + change-num="[[changeNum]]" + location-range="[[thread.locationRange]]" + patch-num="[[patchNum]]" + path="[[path]]" + side="[[side]]" + project-config="[[projectConfig]]"></gr-diff-comment-thread> + </template> + </template> + <script src="gr-diff-comment-thread-group.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js new file mode 100644 index 0000000..9c5626d --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
@@ -0,0 +1,119 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-diff-comment-thread-group', + + properties: { + changeNum: String, + comments: { + type: Array, + value: function() { return []; }, + }, + patchNum: String, + projectConfig: Object, + range: Object, + side: { + type: String, + value: 'REVISION', + }, + _threadGroups: { + type: Array, + value: function() { return []; }, + }, + }, + + observers: [ + '_commentsChanged(comments.*)', + ], + + addNewThread: function(locationRange) { + this.push('_threadGroups', { + comments: [], + locationRange: locationRange, + }); + }, + + removeThread: function(locationRange) { + for (var i = 0; i < this._threadGroups.length; i++) { + if (this._threadGroups[i].locationRange === locationRange) { + this.splice('_threadGroups', i, 1); + return; + } + } + }, + + getThreadForRange: function(rangeToCheck) { + var threads = [].filter.call( + Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'), + function(thread) { + return thread.locationRange === rangeToCheck; + }); + if (threads.length === 1) { + return threads[0]; + } + }, + + _commentsChanged: function() { + this._threadGroups = this._getThreadGroups(this.comments); + }, + + _sortByDate: function(threadGroups) { + if (!threadGroups.length) { return; } + return threadGroups.sort(function(a, b) { + return a.start_datetime > b.start_datetime; + }); + }, + + _calculateLocationRange: function(range) { + return 'range-' + range.start_line + '-' + + range.start_character + '-' + + range.end_line + '-' + + range.end_character; + }, + + _getThreadGroups: function(comments) { + var threadGroups = {}; + + comments.forEach(function(comment) { + var locationRange; + if (!comment.range) { + locationRange = 'line'; + } else { + locationRange = this._calculateLocationRange(comment.range); + } + + if (threadGroups[locationRange]) { + threadGroups[locationRange].comments.push(comment); + } else { + threadGroups[locationRange] = { + start_datetime: comment.updated, + comments: [comment], + locationRange: locationRange, + }; + } + }.bind(this)); + + var threadGroupArr = []; + var threadGroupKeys = Object.keys(threadGroups); + threadGroupKeys.forEach(function(threadGroupKey) { + threadGroupArr.push(threadGroups[threadGroupKey]); + }); + + return this._sortByDate(threadGroupArr); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html new file mode 100644 index 0000000..6c7fd60 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
@@ -0,0 +1,199 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-diff-comment-thread-group</title> + +<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-diff-comment-thread-group.html"> + +<test-fixture id="basic"> + <template> + <gr-diff-comment-thread-group></gr-diff-comment-thread-group> + </template> +</test-fixture> + +<script> + suite('gr-diff-comment-thread-group tests', function() { + var element; + var sandbox; + + setup(function() { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn: function() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('_getThreadGroups', function() { + var comments = [ + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:00:20.396000000', + }, + ]; + + var expectedThreadGroups = [ + { + start_datetime: '2015-12-23 15:00:20.396000000', + comments: [{ + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:00:20.396000000', + }], + locationRange: 'line', + }, + ]; + + assert.deepEqual(element._getThreadGroups(comments), + expectedThreadGroups); + + comments.push({ + id: 'betsys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:10.396000000', + range: { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 2, + } + }); + + expectedThreadGroups = [ + { + start_datetime: '2015-12-23 15:00:20.396000000', + comments: [{ + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:00:20.396000000', + }], + locationRange: 'line', + }, + { + start_datetime: '2015-12-24 15:00:10.396000000', + comments: [{ + id: 'betsys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:10.396000000', + range: { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 2, + }, + }], + locationRange: 'range-1-1-1-2', + }, + ]; + + assert.deepEqual(element._getThreadGroups(comments), + expectedThreadGroups); + }); + + test('_sortByDate', function() { + var threadGroups = expectedThreadGroups = [ + { + start_datetime: '2015-12-23 15:00:20.396000000', + comments: [], + locationRange: 'line', + }, + { + start_datetime: '2015-12-22 15:00:10.396000000', + comments: [], + locationRange: 'range-1-1-1-2', + }, + ]; + + var expectedResult = expectedThreadGroups = [ + { + start_datetime: '2015-12-22 15:00:10.396000000', + comments: [], + locationRange: 'range-1-1-1-2', + },{ + start_datetime: '2015-12-23 15:00:20.396000000', + comments: [], + locationRange: 'line', + }, + ]; + + assert.deepEqual(element._sortByDate(threadGroups), expectedResult); + }); + + test('_calculateLocationRange', function() { + var range = { + start_line: 1, + start_character: 2, + end_line: 3, + end_character: 4, + }; + assert.equal(element._calculateLocationRange(range), 'range-1-2-3-4'); + }); + + test('thread groups are updated when comments change', function() { + var commentsChangedStub = sandbox.stub(element, '_commentsChanged'); + element.comments = []; + element.comments.push({ + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + }); + assert(commentsChangedStub.called); + }); + + test('addNewThread', function() { + var locationRange = 'range-1-2-3-4'; + element._threadGroups = [{locationRange: 'line'}]; + element.addNewThread(locationRange); + assert(element._threadGroups.length, 2); + }); + + test('removeThread', function() { + var locationRange = 'range-1-2-3-4'; + element._threadGroups = [ + {locationRange: 'range-1-2-3-4', comments: []}, + {locationRange: 'line', comments: []} + ]; + flushAsynchronousOperations(); + element.removeThread(locationRange); + flushAsynchronousOperations(); + assert(element._threadGroups.length, 1); + }); + }); +</script>
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..3a0c3ad 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
@@ -16,19 +16,26 @@ <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="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../gr-diff-comment/gr-diff-comment.html"> <dom-module id="gr-diff-comment-thread"> <template> <style> :host { - border: 1px solid #ddd; - border-right: none; + border: 1px solid #bbb; display: block; + margin-bottom: 1px; white-space: normal; } + #container { + background-color: #fcfad6; + } + #container.unresolved { + background-color: #fcfaa6; + } </style> - <div id="container"> + <div id="container" class$="[[_computeHostClass(_unresolved)]]"> <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment"> <gr-diff-comment comment="{{comment}}" @@ -37,12 +44,15 @@ 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> + <gr-storage id="storage"></gr-storage> </template> <script src="gr-diff-comment-thread.js"></script> </dom-module>
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..8904ff6 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,9 @@ (function() { 'use strict'; + var UNRESOLVED_EXPAND_COUNT = 5; + var NEWLINE_PATTERN = /\n/g; + Polymer({ is: 'gr-diff-comment-thread', @@ -29,6 +32,11 @@ type: Array, value: function() { return []; }, }, + locationRange: String, + keyEventTarget: { + type: Object, + value: function() { return document.body; }, + }, patchNum: String, path: String, projectConfig: Object, @@ -39,20 +47,33 @@ _showActions: Boolean, _orderedComments: Array, + _unresolved: { + type: Boolean, + notify: true, + }, }, + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + listeners: { 'comment-update': '_handleCommentUpdate', }, observers: [ - '_commentsChanged(comments.splices)', + '_commentsChanged(comments.*)', ], + keyBindings: { + 'e shift+e': '_handleEKey', + }, + attached: function() { this._getLoggedIn().then(function(loggedIn) { this._showActions = loggedIn; }.bind(this)); + this._setInitialExpandedState(); }, addOrEditDraft: function(opt_lineNum) { @@ -69,6 +90,7 @@ addDraft: function(opt_lineNum, opt_range) { var draft = this._newDraft(opt_lineNum, opt_range); draft.__editing = true; + draft.unresolved = true; this.push('comments', draft); }, @@ -78,46 +100,88 @@ _commentsChanged: function(changeRecord) { this._orderedComments = this._sortedComments(this.comments); + this._unresolved = this._getLastComment().unresolved; + }, + + _getLastComment: function() { + return this._orderedComments[this._orderedComments.length - 1] || {}; + }, + + _handleEKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + // 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); + } + }, + + _expandCollapseComments: function(actionIsCollapse) { + var comments = + Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); + comments.forEach(function(comment) { + comment.collapsed = actionIsCollapse; + }); + }, + + /** + * Sets the initial state of the comment thread to have the last + * {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the + * thread is unresolved. + */ + _setInitialExpandedState: function() { + var comment; + for (var i = 0; i < this._orderedComments.length; i++) { + comment = this._orderedComments[i]; + comment.collapsed = + this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT || + !this._unresolved; + } }, _sortedComments: function(comments) { - comments.sort(function(c1, c2) { + return comments.slice().sort(function(c1, c2) { var c1Date = c1.__date || util.parseDate(c1.updated); var c2Date = c2.__date || util.parseDate(c2.updated); - return c1Date - c2Date; + var dateCompare = c1Date - c2Date; + // If same date, fall back to sorting by id. + return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); }); - - 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); - } - } - 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); + _createReplyComment: function(parent, content, opt_isEditing, + opt_unresolved) { + var reply = this._newReply( + this._orderedComments[this._orderedComments.length - 1].id, + parent.line, + content, + opt_unresolved); - 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); + // If there is currently a comment in an editing state, add an attribute + // so that the gr-diff-comment knows not to populate the draft text. + for (var i = 0; i < this.comments.length; i++) { + if (this.comments[i].__editing) { + reply.__otherEditing = true; + break; + } + } + + 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 +190,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, comment.unresolved); + }, + + _handleCommentAck: function(e) { + var comment = e.detail.comment; + this._createReplyComment(comment, 'Ack', false, comment.unresolved); }, _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', false, false); + }, - // 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, false, true); }, _commentElWithDraftID: function(id) { @@ -156,12 +223,15 @@ return null; }, - _newReply: function(inReplyTo, opt_lineNum, opt_message) { + _newReply: function(inReplyTo, opt_lineNum, opt_message, opt_unresolved) { var d = this._newDraft(opt_lineNum); d.in_reply_to = inReplyTo; if (opt_message != null) { d.message = opt_message; } + if (opt_unresolved !== undefined) { + d.unresolved = opt_unresolved; + } return d; }, @@ -199,6 +269,21 @@ if (this.comments.length == 0) { this.fire('thread-discard', {lastComment: comment}); } + + // Check to see if there are any other open comments getting edited and + // set the local storage value to its message value. + for (var i = 0; i < this.comments.length; i++) { + if (this.comments[i].__editing) { + var commentLocation = { + changeNum: this.changeNum, + patchNum: this.patchNum, + path: this.comments[i].path, + line: this.comments[i].line, + }; + return this.$.storage.setDraftComment(commentLocation, + this.comments[i].message); + } + } }, _handleCommentUpdate: function(e) { @@ -209,7 +294,7 @@ console.error('Comment update for another comment thread.'); return; } - this.comments[index] = comment; + this.set(['comments', index], comment); }, _indexOf: function(comment, arr) { @@ -222,5 +307,9 @@ } return -1; }, + + _computeHostClass: function(unresolved) { + return unresolved ? 'unresolved' : ''; + }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html index 641dc0f..53f22d4 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
@@ -40,6 +40,7 @@ <script> suite('gr-diff-comment-thread tests', function() { var element; + setup(function() { stub('gr-rest-api-interface', { getLoggedIn: function() { return Promise.resolve(false); }, @@ -54,33 +55,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 +85,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 +129,7 @@ line: 5, in_reply_to: 'baf0414d_60047215', updated: '2015-12-21 02:01:10.850000000', - message: 'Done' + message: 'Done', })); }, }); @@ -162,7 +153,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 +162,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,16 +234,38 @@ 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; }); assert.equal(drafts.length, 1); assert.equal(drafts[0].message, 'Done'); assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isFalse(drafts[0].unresolved); 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'); + assert.isTrue(drafts[0].unresolved); + done(); + }); + commentEl.fire('create-fix-comment', {comment: commentEl.comment}, + {bubbles: false}); }); test('discard', function(done) { @@ -230,6 +291,170 @@ draftEl.fire('comment-discard', null, {bubbles: false}); }); + test('first editing comment does not add __otherEditing attribute', + function(done) { + var commentEl = element.$$('gr-diff-comment'); + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + }]; + flushAsynchronousOperations(); + + commentEl.addEventListener('create-reply-comment', function() { + var editing = element._orderedComments.filter(function(c) { + return c.__editing == true; + }); + assert.equal(editing.length, 1); + assert.equal(!!editing[0].__otherEditing, false); + done(); + }); + commentEl.fire('create-reply-comment', {comment: commentEl.comment}, + {bubbles: false}); + }); + + test('two editing comments adds __otherEditing attribute', function(done) { + var commentEl = element.$$('gr-diff-comment'); + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + __editing: true, + __draft: true, + }]; + flushAsynchronousOperations(); + + commentEl.addEventListener('create-reply-comment', function() { + var editing = element._orderedComments.filter(function(c) { + return c.__editing == true; + }); + assert.equal(editing.length, 2); + assert.equal(editing[1].__otherEditing, true); + done(); + }); + commentEl.fire('create-reply-comment', {comment: commentEl.comment}, + {bubbles: false}); + }); + + test('When editing other comments, local storage set after discard', + function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:31.843000000', + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '1', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'yes', + updated: '2015-12-08 19:48:32.843000000', + __draft: true, + __editing: true, + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '2', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'no', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + __editing: true, + }]; + var storageStub = sinon.stub(element.$.storage, 'setDraftComment'); + flushAsynchronousOperations(); + + var draftEl = + Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; + assert.ok(draftEl); + draftEl.addEventListener('comment-discard', function() { + assert.isTrue(storageStub.called); + storageStub.restore(); + done(); + }); + draftEl.fire('comment-discard', null, {bubbles: false}); + }); + + test('When not editing other comments, local storage not set after discard', + function(done) { + element.changeNum = '42'; + element.patchNum = '1'; + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:31.843000000', + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '1', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'yes', + updated: '2015-12-08 19:48:32.843000000', + __draft: true, + __editing: true, + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '2', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'no', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + }]; + var storageStub = sinon.stub(element.$.storage, 'setDraftComment'); + flushAsynchronousOperations(); + + var draftEl = + Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; + assert.ok(draftEl); + draftEl.addEventListener('comment-discard', function() { + assert.isFalse(storageStub.called); + storageStub.restore(); + done(); + }); + draftEl.fire('comment-discard', null, {bubbles: false}); + }); + test('comment-update', function() { var commentEl = element.$$('gr-diff-comment'); var updatedComment = { @@ -240,33 +465,84 @@ assert.strictEqual(element.comments[0], updatedComment); }); - test('orphan replies', function() { - var comments = [ + suite('jack and sally comment data test consolidation', function() { + var getComments = function() { + return Polymer.dom(element.root).querySelectorAll('gr-diff-comment'); + }; + + setup(function() { + element.comments = [ { id: 'jacks_reply', message: 'i like you, too', in_reply_to: 'sallys_confession', updated: '2015-12-25 15:00:20.396000000', - }, - { + unresolved: false, + }, { id: 'sallys_confession', in_reply_to: 'nonexistent_comment', message: 'i like you, jack', updated: '2015-12-24 15:00:20.396000000', - }, - { + }, { id: 'sally_to_dr_finklestein', in_reply_to: 'nonexistent_comment', message: 'i’m running away', updated: '2015-10-31 09:00:20.396000000', - }, - { + }, { id: 'sallys_defiance', message: 'i will poison you so i can get away', updated: '2015-10-31 15:00:20.396000000', }]; - element.comments = comments; - assert.equal(4, element._orderedComments.length); + }); + + test('orphan replies', function() { + assert.equal(4, element._orderedComments.length); + }); + + test('keyboard shortcuts', function() { + 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(); + }); + + test('comment in_reply_to is either null or most recent comment id', + function() { + element._createReplyComment(element.comments[3], 'dummy', true); + flushAsynchronousOperations(); + assert.equal(element._orderedComments.length, 5); + assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply'); + }); + + test('resolvable comments', function() { + assert.isFalse(element._unresolved); + element._createReplyComment(element.comments[3], 'dummy', true, true); + flushAsynchronousOperations(); + assert.isTrue(element._unresolved); + }); + + test('_setInitialExpandedState', function() { + element._unresolved = true; + element._setInitialExpandedState(); + var comments = getComments(); + for (var i = 0; i < element.comments.length; i++) { + assert.isFalse(element.comments[i].collapsed); + } + element._unresolved = false; + element._setInitialExpandedState(); + var comments = getComments(); + for (var i = 0; i < element.comments.length; i++) { + assert.isTrue(element.comments[i].collapsed); + } + }); + }); + + test('_computeHostClass', function() { + assert.equal(element._computeHostClass(true), 'unresolved'); + assert.equal(element._computeHostClass(false), ''); }); }); </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..ceedf73 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-bottom: 0.7em; + 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 { @@ -81,17 +90,18 @@ } .editMessage { display: none; - margin: .5em .7em; - width: calc(100% - 1.4em - 2px); + margin: .5em 0; + width: 100%; } .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; } @@ -99,12 +109,14 @@ display: inline; } .draft:not(.editing) .save, - .draft:not(.editing) .cancel { + .draft:not(.editing) .cancel, + .draft:not(.editing) .resolve { display: none; } .editing .message, .editing .reply, .editing .quote, + .editing .ack, .editing .done, .editing .edit { display: none; @@ -113,42 +125,138 @@ 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; + } + .resolve { + margin: auto; + } + .resolve label { + color: #333; + font-size: 12px; + } </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> - <div class="danger"> - <gr-button class="action discard" on-tap="_handleDiscard">Discard</gr-button> + <gr-button class="action cancel hideOnPublished" + on-tap="_handleCancel" hidden>Cancel</gr-button> + <div class="action resolve hideOnPublished"> + <label> + <input type="checkbox" + checked$="[[resolved]]" + on-change="_handleToggleResolved"> + Resolved + </label> </div> + <div class="danger"> + <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>
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..69854a1 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. @@ -89,27 +114,50 @@ value: '', observer: '_messageTextChanged', }, + + resolved: { + type: Boolean, + observer: '_toggleResolved', + }, }, observers: [ '_commentMessageChanged(comment.message)', '_loadLocalDraft(changeNum, patchNum, comment)', + '_isRobotComment(comment)', + '_calculateActionstoShow(showActions, isRobotComment)', ], + attached: function() { + if (this.editing) { + this.collapsed = false; + } else if (this.comment) { + this.collapsed = this.comment.collapsed; + } + }, + 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,8 +182,19 @@ }.bind(this)); }, + _eraseDraftComment: function() { + this.$.storage.eraseDraftComment({ + changeNum: this.changeNum, + patchNum: this.patchNum, + path: this.comment.path, + line: this.comment.line, + range: this.comment.range, + }); + }, + _commentChanged: function(comment) { this.editing = !!comment.__editing; + this.resolved = !comment.unresolved; if (this.editing) { // It's a new draft/reply, notify. this._fireUpdate(); } @@ -198,8 +257,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'); } }, @@ -210,6 +290,9 @@ _messageTextChanged: function(newValue, oldValue) { if (!this.comment || (this.comment && this.comment.id)) { return; } + // Keep comment.message in sync so that gr-diff-comment-thread is aware + // of the current message in the case that another comment is deleted. + this.comment.message = this._messageText || ''; this.debounce('store', function() { var message = this._messageText; @@ -218,6 +301,7 @@ patchNum: this.patchNum, path: this.comment.path, line: this.comment.line, + range: this.comment.range, }; if ((!this._messageText || !this._messageText.length) && oldValue) { @@ -243,35 +327,51 @@ }, _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.set('comment.__editing', false); this.save(); }, _handleCancel: function(e) { - this._preventDefaultAndBlur(e); - if (this.comment.message == null || this.comment.message.length == 0) { + e.preventDefault(); + if (this.comment.message === null || + this.comment.message.trim().length === 0) { this._fireDiscard(); return; } @@ -285,12 +385,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 +411,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); }, @@ -326,7 +423,11 @@ _loadLocalDraft: function(changeNum, patchNum, comment) { // Only apply local drafts to comments that haven't been saved // remotely, and haven't been given a default message already. - if (!comment || comment.id || comment.message) { + // + // Don't get local draft if there is another comment that is currently + // in an editing state. + if (!comment || comment.id || comment.message || comment.__otherEditing) { + delete comment.__otherEditing; return; } @@ -335,6 +436,7 @@ patchNum: patchNum, path: comment.path, line: comment.line, + range: comment.range, }); if (draft) { @@ -349,5 +451,14 @@ _handleMouseLeave: function(e) { this.fire('comment-mouse-out', this._getEventPayload()); }, + + _handleToggleResolved: function() { + this.resolved = !this.resolved; + }, + + _toggleResolved: function(resolved) { + this.comment.unresolved = !resolved; + this.fire('comment-update', this._getEventPayload()); + }, }); })();
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..3575ccc 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,8 +115,15 @@ 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')); @@ -92,10 +139,93 @@ 'Should navigate to ' + dest + ' without triggering nav'); showStub.restore(); }); + + test('message is not retrieved from storage when other editing is true', + function(done) { + var storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + var loadSpy = sandbox.spy(element, '_loadLocalDraft'); + + element.changeNum = 1; + element.patchNum = 1; + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + line: 5, + __otherEditing: true, + }; + flush(function() { + assert.isTrue(loadSpy.called); + assert.isFalse(storageStub.called); + done(); + }); + }); + + test('message is retrieved from storage when there is no other editing', + function(done) { + var storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + var loadSpy = sandbox.spy(element, '_loadLocalDraft'); + + element.changeNum = 1; + element.patchNum = 1; + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + line: 5, + }; + flush(function() { + assert.isTrue(loadSpy.called); + assert.isTrue(storageStub.called); + done(); + }); + }); + + 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 +263,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 +286,12 @@ 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(isVisible(element.$$('.resolve')), + 'resolve 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 +300,11 @@ 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.isTrue(isVisible(element.$$('.resolve')), 'resolve is visible'); + assert.isFalse(element.$$('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); element.draft = false; element.editing = false; @@ -173,12 +315,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 +406,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 +421,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'); @@ -229,6 +475,8 @@ __editing: true, line: 5, path: '/path/to/file', + message: 'good news, everyone!', + unresolved: false, }, patchNum: 1, }, @@ -287,5 +535,20 @@ 'Should navigate to ' + dest + ' without triggering nav'); showStub.restore(); }); + + test('proper event fires on resolve', function(done) { + element.addEventListener('comment-update', function(e) { + assert.isTrue(e.detail.comment.unresolved); + done(); + }); + MockInteractions.tap(element.$$('.resolve input')); + }); + + test('resolved comment state indicated by checkbox', function() { + element.comment = {unresolved: false}; + assert.isTrue(element.$$('.resolve input').checked); + element.comment = {unresolved: true}; + assert.isFalse(element.$$('.resolve input').checked); + }); }); </script>
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..736cfd2 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
@@ -32,9 +32,9 @@ <tbody class="section both"> <tr class="diff-row side-by-side" left-type="both" right-type="both"> <td class="left lineNum" data-value="138">138</td> - <td class="content both darkHighlight"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td> + <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td> <td class="right lineNum" data-value="119">119</td> - <td class="content both darkHighlight"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td> + <td class="content both"><div class="contentText">[14] Nam cum ad me in Cumanum salutandi causa uterque venisset,</div></td> </tr> </tbody> @@ -42,21 +42,21 @@ <tr class="diff-row side-by-side" left-type="remove" right-type="add"> <td class="left lineNum" data-value="140">140</td> <!-- Next tag is formatted to eliminate zero-length text nodes. --> - <td class="content remove lightHighlight"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl></div><gr-diff-comment-thread> + <td class="content remove"><div class="contentText">na💢ti <hl class="foo">te, inquit</hl>, sumus <hl class="bar">aliquando</hl> otiosum, <hl>certe</hl> a <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl>udiam, <hl>quid</hl> sit, <span class="tab withIndicator" style="tab-size:8;"></span>quod <hl>Epicurum</hl></div><gr-diff-comment-thread> [Yet another random diff thread content here] </gr-diff-comment-thread></td> <td class="right lineNum" data-value="120">120</td> <!-- Next tag is formatted to eliminate zero-length text nodes. --> - <td class="content add lightHighlight"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum, <span class="tab withIndicator" style="tab-size:8;"></span> audiam, sit, quod</div></td> + <td class="content add"><div class="contentText">nacti , <hl>,</hl> sumus <hl><span class="tab withIndicator" style="tab-size:8;"></span></hl> otiosum, <span class="tab withIndicator" style="tab-size:8;"></span> audiam, sit, quod</div></td> </tr> </tbody> <tbody class="section both"> <tr class="diff-row side-by-side" left-type="both" right-type="both"> <td class="left lineNum" data-value="141"></td> - <td class="content both darkHighlight"><div class="contentText">nam et<hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;"> </span>verbis, quod vult, et dicit plane, quod intellegam;</div></td> + <td class="content both"><div class="contentText">nam et<hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl>complectitur<span class="tab withIndicator" style="tab-size:8;"> </span>verbis, quod vult, et dicit plane, quod intellegam;</div></td> <td class="right lineNum" data-value="130"></td> - <td class="content both darkHighlight"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td> + <td class="content both"><div class="contentText">nam et complectitur verbis, quod vult, et dicit plane, quod intellegam;</div></td> </tr> </tbody> @@ -81,21 +81,21 @@ </tr> </tbody> - <tbody class="section delta"> + <tbody class="section delta total"> <tr class="diff-row side-by-side" left-type="blank" right-type="add"> <td class="left"></td> - <td class="blank darkHighlight"></td> + <td class="blank"></td> <td class="right lineNum" data-value="146"></td> - <td class="content add darkHighlight"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td> + <td class="content add"><div class="contentText">[17] Quid igitur est? inquit; audire enim cupio, quid non probes. Principio, inquam,</div></td> </tr> </tbody> <tbody class="section both"> <tr class="diff-row side-by-side" left-type="both" right-type="both"> <td class="left lineNum" data-value="165"></td> - <td class="content both darkHighlight"><div class="contentText">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td> + <td class="content both"><div class="contentText">in physicis, quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td> <td class="right lineNum" data-value="147"></td> - <td class="content both darkHighlight"><div class="contentText">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td> + <td class="content both"><div class="contentText">in physicis, <hl><span class="tab withIndicator" style="tab-size:8;"> </span></hl> quibus maxime gloriatur, primum totus est alienus. Democritea dicit</div></td> </tr> </tbody> @@ -163,27 +163,10 @@ getContentsByLineRange: sandbox.stub().returns([]), getLineElByChild: sandbox.stub().returns({}), getSideByLineEl: sandbox.stub().returns('other-side'), - renderLineRange: sandbox.stub(), }; element._cachedDiffBuilder = builder; }); - test('ignores thread discard for line comment', function(done) { - element.fire('thread-discard', {lastComment: {}}); - flush(function() { - assert.isFalse(builder.renderLineRange.called); - done(); - }); - }); - - test('ignores comment discard for line comment', function(done) { - element.fire('comment-discard', {comment: {}}); - flush(function() { - assert.isFalse(builder.renderLineRange.called); - done(); - }); - }); - test('comment-mouse-over from line comments is ignored', function() { sandbox.stub(element, 'set'); element.fire('comment-mouse-over', {comment: {}}); @@ -487,6 +470,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..bfddf89 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 :not(.collapsedContent), + :host-context(.selected-comment) .contentWrapper ::content .unified .message :not(.collapsedContent){ -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..10f04c6 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,51 +58,217 @@ 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, 'diff-row')) { 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 = GrRangeNormalizer.normalize(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.id === 'output' && + !this._elementDescendedFromClass(el, 'collapsed')) { + content.push(this._getTextContentForRange(el, sel, range)); + } + } } + return content.join('\n'); + }, + + /** + * Given a DOM node, a selection, and a selection range, recursively get all + * of the text content within that selection. + * Using a domNode that isn't in the selection returns an empty string. + * + * @param {Element} domNode The root DOM node. + * @param {Selection} sel The selection. + * @param {Range} range The normalized selection range. + * @return {string} The text within the selection. + */ + _getTextContentForRange: function(domNode, sel, range) { + if (!sel.containsNode(domNode, true)) { return ''; } + var text = ''; - for (var i = 0; i < contentEls.length; i++) { - text += contentEls[i].textContent + '\n'; + if (domNode instanceof Text) { + text = domNode.textContent; + if (domNode === range.endContainer) { + text = text.substring(0, range.endOffset); + } + if (domNode === range.startContainer) { + text = text.substring(range.startOffset); + } + } else { + for (var i = 0; i < domNode.childNodes.length; i++) { + text += this._getTextContentForRange(domNode.childNodes[i], + sel, range); + } } return text; },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html index f99e373..d517a80 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,71 @@ <test-fixture id="basic"> <template> <gr-diff-selection> - <table> - <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> + <table id="diffTable" class="side-by-side"> + <tr class="diff-row"> + <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 id="output" 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> + <tr class="diff-row"> + <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 id="output" 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> + <tr class="diff-row"> + <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 id="output" class="gr-linked-text">This is <a>a</a> different comment 💩 unicode is fun</span> + </div> + </div> + </div> + </td> + <td class="lineNum right" data-value="3">3</td> + </tr> + <tr class="diff-row"> + <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> + <tr class="not-diff-row"> + <td class="other"> + <div class="contentText" data-side="right">some other text</div> + </td> </tr> </table> </gr-diff-selection> @@ -54,25 +101,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 +169,161 @@ }); test('ignores copy for non-content Element', function() { - sinon.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('.other')); + sandbox.stub(element, '_getSelectedText'); + emulateCopyOn(element.querySelector('.not-diff-row')); 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].childNodes[2], 7); + selection.addRange(range); + assert.equal('s is a comment\nThis is a differ', + element._getSelectedText('left', true)); + }); + + test('respects astral chars in 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(); + var nodes = element.querySelectorAll('.gr-formatted-text *'); + range.setStart(nodes[2].childNodes[2], 13); + range.setEnd(nodes[2].childNodes[2], 23); + selection.addRange(range); + assert.equal('mment 💩 u', + 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'); + }); + + suite('_getTextContentForRange', function() { + var selection; + var range; + var nodes; + + setup(function() { + element.classList.add('selected-left'); + element.classList.add('selected-comment'); + element.classList.remove('selected-right'); + selection = window.getSelection(); + selection.removeAllRanges(); + range = document.createRange(); + nodes = element.querySelectorAll('.gr-formatted-text *'); + }); + + test('multi level element contained in range', function() { + range.setStart(nodes[2].childNodes[0], 1); + range.setEnd(nodes[2].childNodes[2], 7); + selection.addRange(range); + assert.equal(element._getTextContentForRange(element, selection, range), + 'his is a differ'); + }); + + + test('multi level element as startContainer of range', function() { + range.setStart(nodes[2].childNodes[1], 0); + range.setEnd(nodes[2].childNodes[2], 7); + selection.addRange(range); + assert.equal(element._getTextContentForRange(element, selection, range), + 'a differ'); + }); + + test('startContainer === endContainer', function() { + range.setStart(nodes[0].firstChild, 2); + range.setEnd(nodes[0].firstChild, 12); + selection.addRange(range); + assert.equal(element._getTextContentForRange(element, selection, range), + 'is is a co'); + }); }); }); </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..3e1b415 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,25 @@ 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; + } + .navLinks { + white-space: nowrap; + } .reviewed { display: inline-block; margin: 0 .25em; @@ -44,7 +62,7 @@ .jumpToFileContainer { display: inline-block; } - .mobileJumpToFileContainer { + .mobile { display: none; } .downArrow { @@ -97,84 +115,139 @@ padding: 0 var(--default-horizontal-margin) 1em; color: #666; } - .header { - align-items: center; - display: flex; - justify-content: space-between; + .subHeader { + flex-wrap: wrap; margin: 0 var(--default-horizontal-margin) .75em; } + .subHeader > div { + margin-top: .25em; + } .prefsButton { text-align: right; } - #modeSelect { - margin-left: .5em; + .separator { + margin: 0 .25em; } @media screen and (max-width: 50em) { + header { + padding: .5em var(--default-horizontal-margin); + } .dash { display: none; } + .desktop { + display: none; + } + .fileNav { + align-items: flex-start; + display: flex; + margin: 0 .25em; + } + .fullFileName { + display: block; + font-size: .9em; + font-style: italic; + min-width: 50%; + padding: 0 .1em; + text-align: center; + width: 100%; + word-wrap: break-word; + } .reviewed { vertical-align: -.1em; } - .jumpToFileContainer { - display: none; - } .mobileJumpToFileContainer { display: block; width: 100%; } + .mobileJumpToFileContainer select { + width: 100%; + } + .mobileNavLink { + color: #000; + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + } + .mobileNavLink:not([href]) { + color: #bbb; + } } </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 desktop"> + <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 mobile"> + <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 class="navLinks desktop"> + <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="download desktop"> + <span class="separator">/</span> + <a class="downloadLink" + href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]"> + Download + </a> + </span> + </div> <div> <select id="modeSelect" @@ -185,21 +258,32 @@ <option value="UNIFIED_DIFF">Unified</option> </select> <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"> - <span - hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span> - <gr-button link - class="prefsButton" - on-tap="_handlePrefsTap">Preferences</gr-button> + <span class="preferences desktop"> + <span + hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span> + <gr-button link + class="prefsButton" + on-tap="_handlePrefsTap">Preferences</gr-button> + </span> </span> </div> </div> <gr-overlay id="prefsOverlay" with-backdrop> <gr-diff-preferences + id="diffPreferences" prefs="{{_prefs}}" local-prefs="{{_localPrefs}}" on-save="_handlePrefsSave" on-cancel="_handlePrefsCancel"></gr-diff-preferences> </gr-overlay> + <div class="fileNav mobile"> + <a class="mobileNavLink" + href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]"><</a> + <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]] + </div> + <a class="mobileNavLink" + href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">></a> + </div> <gr-diff id="diff" project="[[_change.project]]"
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..63e3574 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', @@ -85,18 +80,50 @@ }, _isImageDiff: Boolean, _filesWeblinks: Object, + + /** + * Map of paths in the current chnage and patch range that have comments + * or drafts or robot comments. + */ + _commentMap: Object, + + /** + * Object to contain the path of the next and previous file in the current + * change and patch range that has comments. + */ + _commentSkips: { + type: Object, + computed: '_computeCommentSkips(_commentMap, _fileList, _path)', + }, }, 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 +131,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 +146,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 +180,10 @@ return this.$.restAPI.getPreferences(); }, + _getWindowWidth: function() { + return window.innerWidth; + }, + _handleReviewedChange: function(e) { this._setReviewed(Polymer.dom(e).rootTarget.checked); }, @@ -170,106 +202,212 @@ 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)) { return; } + if (e.detail.keyboardEvent.shiftKey && + e.detail.keyboardEvent.keyCode === 75) { // 'K' + this._moveToPreviousFileWithComment(); + return; + } + if (this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.diff.displayLine = true; + this.$.cursor.moveUp(); + }, + + _handleDownKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (e.detail.keyboardEvent.shiftKey && + e.detail.keyboardEvent.keyCode === 74) { // 'J' + this._moveToNextFileWithComment(); + return; + } + if (this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.diff.displayLine = true; + this.$.cursor.moveDown(); + }, + + _moveToPreviousFileWithComment: function() { + if (this._commentSkips && this._commentSkips.previous) { + page.show(this._getDiffURL(this._changeNum, this._patchRange, + this._commentSkips.previous)); } }, - _navToFile: function(fileList, direction) { - if (fileList.length == 0) { return; } + _moveToNextFileWithComment: function() { + if (this._commentSkips && this._commentSkips.next) { + page.show(this._getDiffURL(this._changeNum, this._patchRange, + this._commentSkips.next)); + } + }, - 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)); + _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); + } + }, + + _handleLeftBracketKey: function(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + 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) { + var file = direction > 0 ? fileList[0] : fileList[fileList.length - 1]; + return this._getDiffURL(this._changeNum, this._patchRange, file); + } + + 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) { @@ -310,6 +448,10 @@ Promise.all(promises) .then(function() { return this.$.diff.reload(); }.bind(this)) .then(function() { this._loading = false; }.bind(this)); + + this._loadCommentMap().then(function(commentMap) { + this._commentMap = commentMap; + }.bind(this)); }, /** @@ -318,7 +460,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 +482,7 @@ _getDiffURL: function(changeNum, patchRange, path) { return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' + - path; + this.encodeURL(path, true); }, _computeDiffURL: function(changeNum, patchRangeRecord, path) { @@ -389,7 +531,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 +573,7 @@ _handlePrefsTap: function(e) { e.preventDefault(); - this.$.prefsOverlay.open(); + this._openPrefs(); }, _handlePrefsSave: function(e) { @@ -458,9 +605,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 +617,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 +631,75 @@ _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; + }, + + /** + * Request all comments (and drafts and robot comments) for the current + * change and construct the map of file paths that have comments for the + * current patch range. + * @return {Promise} A promise that yields a comment map object. + */ + _loadCommentMap: function() { + function filterByRange(comment) { + var patchNum = comment.patch_set + ''; + return patchNum === this._patchRange.patchNum || + patchNum === this._patchRange.basePatchNum; + }; + + return Promise.all([ + this.$.restAPI.getDiffComments(this._changeNum), + this._getDiffDrafts(), + this.$.restAPI.getDiffRobotComments(this._changeNum), + ]).then(function(results) { + var commentMap = {}; + results.forEach(function(response) { + for (var path in response) { + if (response.hasOwnProperty(path) && + response[path].filter(filterByRange.bind(this)).length) { + commentMap[path] = true; + } + } + }.bind(this)); + return commentMap; + }.bind(this)); + }, + + _getDiffDrafts: function() { + return this._getLoggedIn().then(function(loggedIn) { + if (!loggedIn) { return Promise.resolve({}); } + return this.$.restAPI.getDiffDrafts(this._changeNum); + }.bind(this)); + }, + + _computeCommentSkips: function(commentMap, fileList, path) { + var skips = {previous: null, next: null}; + if (!fileList.length) { return skips; } + var pathIndex = fileList.indexOf(path); + + // Scan backward for the previous file. + for (var i = pathIndex - 1; i >= 0; i--) { + if (commentMap[fileList[i]]) { + skips.previous = fileList[i]; + break; + } + } + + // Scan forward for the next file. + for (i = pathIndex + 1; i < fileList.length; i++) { + if (commentMap[fileList[i]]) { + skips.next = fileList[i]; + break; + } + } + + return skips; }, }); })();
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..99dcd41 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,69 @@ 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'); + element._path = 'not_a_real_file'; + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/wheatley.md'); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/chell.go'); + }); + + 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 +438,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 +454,6 @@ assert.isTrue(commitMsg.checked); assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true)); - saveReviewedStub.restore(); done(); }); }); @@ -371,8 +461,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 +472,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 +482,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 +521,169 @@ 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'); + }); + + suite('_loadCommentMap', function() { + test('empty', function(done) { + stub('gr-rest-api-interface', { + getDiffRobotComments: function() { return Promise.resolve({}); }, + getDiffComments: function() { return Promise.resolve({}); }, + }); + element._loadCommentMap().then(function(map) { + assert.equal(Object.keys(map).length, 0); + done(); + }); + }); + + test('paths in patch range', function(done) { + stub('gr-rest-api-interface', { + getDiffRobotComments: function() { return Promise.resolve({}); }, + getDiffComments: function() { + return Promise.resolve({ + 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}], + 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}], + }); + }, + }); + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '3', + patchNum: '5', + }; + element._loadCommentMap().then(function(map) { + assert.deepEqual(Object.keys(map), + ['path/to/file/one.cpp', 'path-to/file/two.py']); + done(); + }); + }); + + test('empty for paths outside patch range', function(done) { + stub('gr-rest-api-interface', { + getDiffRobotComments: function() { return Promise.resolve({}); }, + getDiffComments: function() { + return Promise.resolve({ + 'path/to/file/one.cpp': [{patch_set: 'PARENT', message: 'lorem'}], + 'path-to/file/two.py': [{patch_set: 2, message: 'ipsum'}], + }); + }, + }); + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '3', + patchNum: '5', + }; + element._loadCommentMap().then(function(map) { + assert.equal(Object.keys(map).length, 0); + done(); + }); + }); + }); + + suite('_computeCommentSkips', function() { + test('empty file list', function() { + var commentMap = { + 'path/one.jpg': true, + 'path/three.wav': true, + }; + var path = 'path/two.m4v'; + var fileList = []; + var result = element._computeCommentSkips(commentMap, fileList, path); + assert.isNull(result.previous); + assert.isNull(result.next); + }); + + test('finds skips', function() { + var fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; + var path = fileList[1]; + var commentMap = {}; + commentMap[fileList[0]] = true; + commentMap[fileList[1]] = false; + commentMap[fileList[2]] = true; + + var result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[0]); + assert.equal(result.next, fileList[2]); + + commentMap[fileList[1]] = true; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[0]); + assert.equal(result.next, fileList[2]); + + path = fileList[0]; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.isNull(result.previous); + assert.equal(result.next, fileList[1]); + + path = fileList[2]; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[1]); + assert.isNull(result.next); + }); }); }); </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..54e9c6e 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,21 +122,26 @@ 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 { + .delta.total .content.add { background-color: var(--dark-add-highlight-color); } - .content.add.lightHighlight { + .content.add { background-color: var(--light-add-highlight-color); } .content.remove .intraline, - .content.remove.darkHighlight { + .delta.total .content.remove { background-color: var(--dark-remove-highlight-color); } - .content.remove.lightHighlight { + .content.remove { 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..69eb000 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(' '); }, @@ -194,7 +232,8 @@ var contentEl = contentText.parentElement; var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); var side = this._getSideByLineAndContent(lineEl, contentEl); - var threadEl = this._getOrCreateThreadAtLine(contentEl, patchNum, side); + var threadEl = + this._getOrCreateThreadAtLineRange(contentEl, patchNum, side, range); threadEl.addDraft(line, range); }, @@ -204,20 +243,43 @@ var contentEl = contentText.parentElement; var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); var side = this._getSideByLineAndContent(lineEl, contentEl); - var threadEl = this._getOrCreateThreadAtLine(contentEl, patchNum, side); + var threadEl = + this._getOrCreateThreadAtLineRange(contentEl, patchNum, side); threadEl.addOrEditDraft(opt_lineNum); }, - _getOrCreateThreadAtLine: function(contentEl, patchNum, side) { - var threadEl = contentEl.querySelector('gr-diff-comment-thread'); + _getThreadForRange: function(threadGroupEl, rangeToCheck) { + return threadGroupEl.getThreadForRange(rangeToCheck); + }, - if (!threadEl) { - threadEl = this.$.diffBuilder.createCommentThread( - this.changeNum, patchNum, this.path, side, this.projectConfig); - contentEl.appendChild(threadEl); + _getThreadGroupForLine: function(contentEl) { + return contentEl.querySelector('gr-diff-comment-thread-group'); + }, + + _getOrCreateThreadAtLineRange: function(contentEl, patchNum, side, range) { + var rangeToCheck = range ? + 'range-' + + range.startLine + '-' + + range.startChar + '-' + + range.endLine + '-' + + range.endChar : 'line'; + + // Check if thread group exists. + var threadGroupEl = this._getThreadGroupForLine(contentEl); + if (!threadGroupEl) { + threadGroupEl = this.$.diffBuilder.createCommentThreadGroup( + this.changeNum, patchNum, this.path, side, this.projectConfig); + contentEl.appendChild(threadGroupEl); } + var threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck); + + if (!threadEl) { + threadGroupEl.addNewThread(rangeToCheck); + Polymer.dom.flush(); + threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck); + } return threadEl; }, @@ -243,7 +305,7 @@ _handleThreadDiscard: function(e) { var el = Polymer.dom(e).rootTarget; - el.parentNode.removeChild(el); + el.parentNode.removeThread(el.locationRange); }, _handleCommentDiscard: function(e) { @@ -334,9 +396,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 +432,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 +448,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 +477,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 +506,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 +516,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..158a9a2 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
@@ -34,9 +34,17 @@ <script> suite('gr-diff tests', function() { var element; + var sandbox; + + setup(function() { + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); suite('not logged in', function() { - setup(function() { stub('gr-rest-api-interface', { getLoggedIn: function() { return Promise.resolve(false); }, @@ -51,20 +59,45 @@ 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 = sandbox.spy(element, '_computeContainerClass'); + element.displayLine = true; + assert.isTrue(spy.called); + assert.isTrue( + element.$$('.diffContainer').classList.contains('displayLine')); + }); + test('get drafts', function(done) { element.patchRange = {basePatchNum: 0, patchNum: 0}; - var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts'); + var getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts'); element._getDiffDrafts().then(function(result) { assert.deepEqual(result, {baseComments: [], comments: []}); sinon.assert.notCalled(getDraftsStub); - getDraftsStub.restore(); + done(); + }); + }); + + test('get robot comments', function(done) { + element.patchRange = {basePatchNum: 0, patchNum: 0}; + + var getDraftsStub = sandbox.stub(element.$.restAPI, + 'getDiffRobotComments'); + element._getDiffDrafts().then(function(result) { + assert.deepEqual(result, {baseComments: [], comments: []}); + sinon.assert.notCalled(getDraftsStub); done(); }); }); test('loads files weblinks', function(done) { - var diffStub = sinon.stub(element.$.restAPI, 'getDiff').returns( + var diffStub = sandbox.stub(element.$.restAPI, 'getDiff').returns( Promise.resolve({ meta_a: { web_links: 'foo', @@ -81,7 +114,6 @@ }); done(); }); - diffStub.restore(); }); test('remove comment', function() { @@ -184,6 +216,53 @@ })); }); + test('thread groups', function() { + var contentEl = document.createElement('div'); + var rangeToCheck = 'line'; + var patchNum = 1; + var side = 'PARENT'; + var range = { + startLine: 1, + startChar: 1, + endLine: 1, + endChar: 2, + }; + + sandbox.stub(element.$.diffBuilder, 'createCommentThreadGroup', + function() { + return document.createElement('gr-diff-comment-thread-group'); + }); + + // No thread groups. + assert.isNotOk(element._getThreadGroupForLine(contentEl)); + + // A thread group gets created. + assert.isOk( + element._getOrCreateThreadAtLineRange(contentEl, patchNum, side)); + + // Try to fetch a thread with a different range. + range = { + startLine: 1, + startChar: 1, + endLine: 1, + endChar: 3, + }; + + assert.isOk(element._getOrCreateThreadAtLineRange( + contentEl, patchNum, side, range)); + // The new thread group can be fetched. + assert.isOk(element._getThreadGroupForLine(contentEl)); + + assert.equal(contentEl.querySelectorAll( + 'gr-diff-comment-thread-group').length, 1); + + var threadGroup = contentEl.querySelector( + 'gr-diff-comment-thread-group'); + var threadLength = Polymer.dom(threadGroup.root). + querySelectorAll('gr-diff-comment-thread').length; + assert.equal(threadLength, 2); + }); + test('renders image diffs', function(done) { var mockDiff = { meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, @@ -234,19 +313,19 @@ var mockComments = {baseComments: [], comments: []}; var stubs = []; - stubs.push(sinon.stub(element, '_getDiff', + stubs.push(sandbox.stub(element, '_getDiff', function() { return Promise.resolve(mockDiff); })); - stubs.push(sinon.stub(element.$.restAPI, 'getCommitInfo', + stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo', function() { return Promise.resolve(mockCommit); })); - stubs.push(sinon.stub(element.$.restAPI, + stubs.push(sandbox.stub(element.$.restAPI, 'getCommitFileContents', function() { return Promise.resolve(mockFile1); })); - stubs.push(sinon.stub(element.$.restAPI, + stubs.push(sandbox.stub(element.$.restAPI, 'getChangeFileContents', function() { return Promise.resolve(mockFile2); })); - stubs.push(sinon.stub(element.$.restAPI, '_getDiffComments', + stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments', function() { return Promise.resolve(mockComments); })); - stubs.push(sinon.stub(element.$.restAPI, 'getDiffDrafts', + stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts', function() { return Promise.resolve(mockComments); })); element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; @@ -270,7 +349,6 @@ // Cleanup. element.removeEventListener('render', rendered); - stubs.forEach(function(stub) { stub.restore(); }); done(); }; @@ -312,28 +390,38 @@ var content = document.createElement('div'); var lineEl = document.createElement('div'); - var selectStub = sinon.stub(element, '_selectLine'); - var getLineStub = sinon.stub(element.$.diffBuilder, 'getLineElByChild', - function() { return lineEl; }); + var selectStub = sandbox.stub(element, '_selectLine'); + var getLineStub = sandbox.stub(element.$.diffBuilder, + 'getLineElByChild', function() { return lineEl; }); content.className = 'content'; content.addEventListener('click', function(e) { element._handleTap(e); assert.isTrue(selectStub.called); assert.equal(selectStub.lastCall.args[0], lineEl); - selectStub.restore(); - getLineStub.restore(); done(); }); 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'); }); @@ -344,11 +432,10 @@ baseComments: [{id: 'foo'}], comments: [{id: 'bar'}], }; - var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts', + var getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts', function() { return Promise.resolve(draftsResponse); }); element._getDiffDrafts().then(function(result) { assert.deepEqual(result, draftsResponse); - getDraftsStub.restore(); done(); }); }); @@ -364,7 +451,7 @@ {id: 'c2'}, ], }; - var diffCommentsStub = sinon.stub(element, '_getDiffComments', + var diffCommentsStub = sandbox.stub(element, '_getDiffComments', function() { return Promise.resolve(comments); }); var drafts = { @@ -377,9 +464,25 @@ {id: 'd2'}, ], }; - var diffDraftsStub = sinon.stub(element, '_getDiffDrafts', + + var diffDraftsStub = sandbox.stub(element, '_getDiffDrafts', function() { return Promise.resolve(drafts); }); + var robotComments = { + baseComments: [ + {id: 'br1'}, + {id: 'br2'}, + ], + comments: [ + {id: 'r1'}, + {id: 'r2'}, + ], + }; + + var diffRobotCommentStub = sandbox.stub(element, + '_getDiffRobotComments', function() { + return Promise.resolve(robotComments); }); + element.changeNum = '42'; element.patchRange = { basePatchNum: 'PARENT', @@ -404,17 +507,19 @@ {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(); done(); }); }); @@ -467,6 +572,22 @@ assert.equal(drafts.length, 1); assert.equal(drafts[0].id, id); }); + + test('_handleShowDiff reloads when expanded is made true', + function(done) { + element.expanded = false; + element.changeNum = element._comments.meta.changeNum; + element.patchRange = element._comments.meta.patchRange; + element.path = element._comments.meta.path; + + var stub = sandbox.stub(element, 'reload', function() { + assert.isTrue(stub.called); + 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..66e6ee7 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,35 +27,51 @@ .patchRange { display: inline-block; } + select { + max-width: 15em; + } + @media screen and (max-width: 50em) { + .filesWeblinks { + display: none; + } + select { + max-width: 5.25em; + } + } </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> - <span is="dom-if" if="[[filesWeblinks.meta_a]]"> + <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink"> - <a target="_blank" + <a target="_blank" rel="noopener" href$="[[weblink.url]]">[[weblink.name]]</a> </template> </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]]"> + <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink"> <a target="_blank" href$="[[weblink.url]]">[[weblink.name]]</a>
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..58d29bd 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
@@ -14,6 +14,9 @@ (function() { 'use strict'; + // Maximum length for patch set descriptions. + var PATCH_DESC_MAX_LENGTH = 500; + Polymer({ is: 'gr-patch-range-select', @@ -21,26 +24,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 +60,24 @@ 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.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html index 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..61c3a44 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,12 @@ 'gr-diff gr-syntax gr-syntax-selector-class': true, }; + var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; + var CPP_WCHAR_PATTERN = /L\'.\'/g; + var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; + var GO_BACKSLASH_LITERAL = '\'\\\\\''; + var GLOBAL_LT_PATTERN = /</g; + Polymer({ is: 'gr-syntax-layer', @@ -106,6 +111,7 @@ value: function() { return []; }, }, _processHandle: Number, + _hljs: Object, }, addListener: function(fn) { @@ -254,13 +260,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 +283,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 +307,90 @@ 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) { + if (language === 'cpp') { + /** + * 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 (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) { + line = line.replace(GLOBAL_LT_PATTERN, '|'); + } + + /** + * Rewrite CPP wchar_t characters literals to wchar_t string literals + * because HLJS only understands the string form. + * {@see Issue 5242} + * {#see https://github.com/isagalaev/highlight.js/issues/1412} + */ + if (CPP_WCHAR_PATTERN.test(line)) { + line = line.replace(CPP_WCHAR_PATTERN, 'L"."'); + } + + return line; + } + + /** + * 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 '); + } + + /** + * HLJS misunderstands backslash character literals in Go. + * {@see Issue 5007} + * {#see https://github.com/isagalaev/highlight.js/issues/1411} + */ + if (language === 'go' && line.indexOf(GO_BACKSLASH_LITERAL) !== -1) { + return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"'); + } + + return line; + }, + + /** * Tells whether the state has exhausted its current section. * @param {!Object} state * @return {boolean} @@ -358,45 +433,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..096206f 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,71 @@ 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. + 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); + }); + + test('workaround CPP whcar_t character literals', 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 wchar_t string. + line = 'wchar_t* sz = L"abc 123";'; + assert.equal(element._workaround('cpp', line), line); + + // Converts wchar_t character literal to string. + line = 'wchar_t myChar = L\'#\''; + var expected = 'wchar_t myChar = L"."'; + assert.equal(element._workaround('cpp', line), expected); + }); + + test('workaround go backslash character literals', function() { + // Does nothing to regular line. + var line = 'func foo(in []byte) (lit []byte, n int, err error) {'; + assert.equal(element._workaround('go', line), line); + + // Does nothing to string with backslash literal + line = 'c := "\\\\"'; + assert.equal(element._workaround('go', line), line); + + // Converts backslash literal character to a string. + line = 'c := \'\\\\\''; + var expected = 'c := "\\\\"'; + assert.equal(element._workaround('go', 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..633e24a 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
@@ -19,66 +19,63 @@ /** * @overview Highlight.js emits the following classes that do not have * styles here: - * subst, symbol, class, function, doctag, meta-string, section, - * builtin-name, bulletm, code, formula, quote, addition, deletion + * subst, symbol, class, function, doctag, meta-string, section, name, + * builtin-name, bulletm, code, formula, quote, addition, deletion, + * attribute * @see {@link http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html} */ - .gr-syntax-literal, - .gr-syntax-keyword, - .gr-syntax-selector-tag { + .gr-syntax-meta { + color: #FF1717; + } + .gr-syntax-keyword { + color: #7F0055; font-weight: bold; - color: #00f; + line-height: 1em; } - .gr-syntax-built_in { - color: #555; + .gr-syntax-number, + .gr-syntax-selector-class { + color: #164; } - .gr-syntax-type, - .gr-syntax-selector-pseudo, + .gr-syntax-variable { + color: black; + } .gr-syntax-template-variable { - color: #ff00e7; + color: #0000C0; } - .gr-syntax-number { - color: violet; - } - .gr-syntax-regexp, - .gr-syntax-variable, - .gr-syntax-selector-attr, - .gr-syntax-template-tag { - color: #FA8602; + .gr-syntax-comment { + color: #3F7F5F; } .gr-syntax-string, .gr-syntax-selector-id { - color: #018846; + color: #2A00FF; } - .gr-syntax-title { - color: teal; - } - .gr-syntax-params { - color: red; - } - .gr-syntax-comment { - color: #af72a9; - font-style: italic; - } - .gr-syntax-meta { - color: #0091AD; - } - .gr-syntax-meta-keyword { - color: #00426b; - font-weight: bold; + .gr-syntax-built_in { + color: #30a; } .gr-syntax-tag { - color: #DB7C00; + color: #170; } - .gr-syntax-name { /* XML/HTML Tag Name */ - color: brown; + .gr-syntax-link, + .gr-syntax-meta-keyword { + color: #219; } - .gr-syntax-attr { /* XML/HTML Attribute */ - color: #8C7250; + .gr-syntax-params, + .gr-syntax-type { + color: #00f; } - .gr-syntax-attribute { /* CSS Property */ - color: #299596; + .gr-syntax-title { + color: #0000C0; + } + .gr-syntax-attr, + .gr-syntax-literal { /* XML/HTML Attribute */ + color: #219; + } + .gr-syntax-selector-pseudo, + .gr-syntax-regexp, + .gr-syntax-selector-attr, + .gr-syntax-template-tag { + color: #FA8602; } .gr-syntax-emphasis { font-style: italic; @@ -86,12 +83,6 @@ .gr-syntax-strong { font-weight: bold; } - .gr-syntax-link { - color: blue; - } - .gr-syntax-selector-class { - color: #1F71FF; - } </style> </template> </dom-module> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index c20795b..6499f08 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -15,18 +15,22 @@ --> <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="./admin/gr-admin-view/gr-admin-view.html"> + <link rel="import" href="./core/gr-error-manager/gr-error-manager.html"> <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> <link rel="import" href="./core/gr-main-header/gr-main-header.html"> <link rel="import" href="./core/gr-router/gr-router.html"> +<link rel="import" href="./core/gr-reporting/gr-reporting.html"> <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> <link rel="import" href="./change/gr-change-view/gr-change-view.html"> <link rel="import" href="./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 +49,16 @@ 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); + display: flex; + justify-content: space-between; + padding: .5rem var(--default-horizontal-margin); } main { flex: 1; @@ -85,7 +92,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 +111,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 +120,13 @@ 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 + params="[[params]]" + on-account-detail-update="_handleAccountDetailUpdate"> + </gr-settings-view> + </template> + <template is="dom-if" if="[[_showAdminView]]" restamp="true"> + <gr-admin-view path="[[_path]]"></gr-admin-view> </template> <div id="errorView" class="errorView" hidden> <div class="errorEmoji">[[_lastError.emoji]]</div> @@ -120,22 +135,35 @@ </div> </main> <footer role="contentinfo"> - Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> - ([[_version]]) - | - <a class="feedback" - href="https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue" - target="_blank"> - Report PolyGerrit Bug - </a> + <div> + Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" + target="_blank">Gerrit Code Review</a> + ([[_version]]) + </div> + <div> + <a class="feedback" + href="https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue" + rel="noopener" target="_blank">Report bug</a> + <template is="dom-if" if="[[_computeShowGwtUiLink(_serverConfig)]]"> + | + <a id="gwtLink" href$="/?polygerrit=0#[[_path]]" rel="external">Old UI</a> + </template> + </div> </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..f38bb91 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,19 @@ this.set('_showChangeView', view === 'gr-change-view'); this.set('_showDiffView', view === 'gr-diff-view'); this.set('_showSettingsView', view === 'gr-settings-view'); + this.set('_showAdminView', view === 'gr-admin-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 +140,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 +171,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 +199,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-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html new file mode 100644 index 0000000..2deb291 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -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. +--> +<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<link rel="import" href="../../../styles/gr-settings-styles.html"> + +<dom-module id="gr-change-table-editor"> + <template> + <style> + th.nameHeader { + width: 11em; + } + tbody tr:first-of-type td .move-up-button, + tbody tr:last-of-type td .move-down-button { + display: none; + } + .newTitleInput { + width: 10em; + } + .newUrlInput { + width: 23em; + } + .addOptions { + margin-top: 1em; + } + </style> + <style include="gr-settings-styles"></style> + <div class="gr-settings-styles"> + <table> + <thead> + <tr> + <th class="nameHeader">Column</th> + </tr> + </thead> + <tbody> + <template is="dom-repeat" items="[[changeTableItems]]"> + <tr> + <td>[[item]]</td> + <td> + <gr-button + data-index="[[index]]" + on-tap="_handleDeleteButton" + class="remove-button">Delete</gr-button> + </td> + </tr> + </template> + </tbody> + </table> + + <template is="dom-if" if="[[changeTableNotDisplayed.length]]"> + <table class="addOptions"> + <thead> + <tr> + <th class="nameHeader">Hidden</th> + </tr> + </thead> + <tbody> + <template is="dom-repeat" items="[[changeTableNotDisplayed]]"> + <tr> + <td>[[item]]</td> + <td> + <gr-button + on-tap="_handleAddButton" + class="add-button" + data-index="[[index]]"> + Add + </gr-button> + </td> + </tr> + </template> + </tbody> + </table> + </template> + </div> + </template> + <script src="gr-change-table-editor.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js new file mode 100644 index 0000000..6486397 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-change-table-editor', + + properties: { + changeTableItems: Array, + changeTableNotDisplayed: Array, + }, + + behaviors: [ + Gerrit.ChangeTableBehavior, + ], + + _handleDeleteButton: function(e) { + var index = e.target.dataIndex; + this.splice('changeTableItems', index, 1); + + // Use the change table behavior to make sure ordering of unused + // columns ends up in the correct order. If the removed item is appended + // to the end, when it is saved, the unused column order may shift around. + this.set('changeTableNotDisplayed', + this.getComplementColumns(this.changeTableItems)); + + }, + + _handleAddButton: function(e) { + var index = e.target.dataIndex; + var newColumn = this.changeTableNotDisplayed[index]; + this.splice('changeTableNotDisplayed', index, 1); + + this.splice('changeTableItems', this.getComplementColumns( + this.changeTableNotDisplayed).indexOf(newColumn), 0, newColumn); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html new file mode 100644 index 0000000..0030e2e --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -0,0 +1,193 @@ +<!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-settings-view</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-change-table-editor.html"> + +<test-fixture id="basic"> + <template> + <gr-change-table-editor></gr-change-table-editor> + </template> +</test-fixture> + +<script> + suite('gr-settings-view tests', function() { + var element; + var columns; + + // Click the up/down button (according to direction) for the index'th row. + // The index of the first row is 0, corresponding to the array. + function move(element, index, direction) { + var selector = + 'tr:nth-child(' + (index + 1) + ') .move-' + direction + '-button'; + var button = element.$$('tbody').querySelector(selector); + MockInteractions.tap(button); + flushAsynchronousOperations(); + } + + setup(function() { + element = fixture('basic'); + columns = [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + ]; + + columnsNotDisplayed = ['Size']; + + element.set('changeTableItems', columns); + element.set('changeTableNotDisplayed', columnsNotDisplayed); + flushAsynchronousOperations(); + }); + + test('renders', function() { + var rows = element.$$('tbody').querySelectorAll('tr'); + var tds; + + assert.equal(rows.length, columns.length); + for (var i = 0; i < columns.length; i++) { + tds = rows[i].querySelectorAll('td'); + assert.equal(tds[0].textContent, columns[i]); + } + }); + + test('add hidden item', function() { + var originalNumberColumns = element.changeTableItems.length; + var originalNumberHiddenColumns = element.changeTableNotDisplayed.length; + + var addBtn = element.$$('.addOptions gr-button'); + var columnName = element.$$('.addOptions tr td').innerHTML; + + assert.equal(element.$$('.addOptions').style.display, ''); + + MockInteractions.tap(addBtn); + flushAsynchronousOperations(); + + assert.equal(element.changeTableItems.length, originalNumberColumns + 1); + assert.equal(element.changeTableNotDisplayed.length, + originalNumberHiddenColumns - 1); + + assert.equal( + element.changeTableItems[element.changeTableItems.length - 1], + columnName); + + assert.equal(element.$$('.addOptions').style.display, 'none'); + }); + + test('remove item', function() { + var columns = element.changeTableItems.length; + assert.deepEqual(element.changeTableItems, [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + ]); + + // Tap the delete button for the second item. + MockInteractions.tap( + element.$$('tbody').querySelector('tr:nth-child(2) .remove-button')); + + assert.deepEqual(element.changeTableItems, [ + 'Subject', + 'Owner', + 'Project', + 'Branch', + 'Updated', + ]); + + assert.deepEqual(element.changeTableNotDisplayed, [ + 'Status', + 'Size', + ]); + + // Delete remaining items. + for (var i = 0; i < columns - 1; i++) { + MockInteractions.tap( + element.$$('tbody').querySelector('tr:first-child .remove-button')); + } + assert.deepEqual(element.changeTableItems, []); + assert.deepEqual(element.changeTableNotDisplayed, [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]); + }); + + test('add item', function() { + + element.set('changeTableItems', [ + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + ]); + + element.set('changeTableNotDisplayed', ['Subject', 'Size']); + + var columns = element.changeTableItems.length; + flushAsynchronousOperations(); + + // Tap the add button for the second item. + MockInteractions.tap( + element.$$('.addOptions').querySelector( + 'tr:nth-child(2) .add-button')); + + flushAsynchronousOperations(); + assert.deepEqual(element.changeTableItems, [ + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ]); + assert.deepEqual(element.changeTableNotDisplayed, ['Subject']); + + // Add remaining item. + MockInteractions.tap( + element.$$('.addOptions').querySelector('.add-button')); + flushAsynchronousOperations(); + + assert.deepEqual(element.changeTableNotDisplayed, []); + assert.deepEqual(element.changeTableItems, [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size' + ]); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html index e58f1f2..d96237e 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -15,7 +15,9 @@ --> <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-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <dom-module id="gr-http-password"> @@ -24,9 +26,24 @@ .password { font-family: var(--monospace-font-family); } - .noPassword { - color: #777; + #generatedPasswordOverlay { + padding: 2em; + width: 50em; + } + #generatedPasswordDisplay { + margin: 1em 0; + } + #generatedPasswordDisplay .value { + font-family: var(--monospace-font-family); + } + #passwordWarning { font-style: italic; + text-align: center; + } + .closeButton { + bottom: 2em; + position: absolute; + right: 2em; } </style> <style include="gr-settings-styles"></style> @@ -35,28 +52,28 @@ <span class="title">Username</span> <span class="value">[[_username]]</span> </section> - <section> - <span class="title">Password</span> - <span hidden$="[[!_hasPassword]]"> - <span class="value" hidden$="[[_passwordVisible]]"> - <gr-button - link - on-tap="_handleViewPasswordTap">Click to view</gr-button> - </span> - <span - class="value password" - hidden$="[[!_passwordVisible]]">[[_password]]</span> - </span> - <span class="value noPassword" hidden$="[[_hasPassword]]">(None)</span> - </section> <gr-button id="generateButton" on-tap="_handleGenerateTap">Generate New Password</gr-button> - <gr-button - id="clearButton" - on-tap="_handleClearTap" - disabled="[[!_hasPassword]]">Clear Password</gr-button> </div> + <gr-overlay + id="generatedPasswordOverlay" + on-iron-overlay-closed="_generatedPasswordOverlayClosed" + with-backdrop> + <div class="gr-settings-styles"> + <section id="generatedPasswordDisplay"> + <span class="title">New Password:</span> + <span class="value">[[_generatedPassword]]</span> + </section> + <section id="passwordWarning"> + This password will not be displayed again.<br> + If you lose it, you will need to generate a new one. + </section> + <gr-button + class="closeButton" + on-tap="_closeOverlay">Close</gr-button> + </div> + </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-http-password.js"></script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js index 9248632..bde36aa 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -17,65 +17,31 @@ Polymer({ is: 'gr-http-password', - /** - * Fired when getting the password fails with non-404. - * - * @event network-error - */ - properties: { - _serverConfig: Object, _username: String, - _password: String, - _passwordVisible: { - type: Boolean, - value: false, - }, - _hasPassword: Boolean, + _generatedPassword: String, }, loadData: function() { - var promises = []; - - promises.push(this.$.restAPI.getAccount().then(function(account) { + return this.$.restAPI.getAccount().then(function(account) { this._username = account.username; - }.bind(this))); - - promises.push(this.$.restAPI - .getAccountHttpPassword(this._handleGetPasswordError.bind(this)) - .then(function(pass) { - this._password = pass; - this._hasPassword = !!pass; - }.bind(this))); - - return Promise.all(promises); - }, - - _handleGetPasswordError: function(response) { - if (response.status === 404) { - this._hasPassword = false; - } else { - this.fire('network-error', {response: response}); - } - }, - - _handleViewPasswordTap: function() { - this._passwordVisible = true; + }.bind(this)); }, _handleGenerateTap: function() { + this._generatedPassword = 'Generating...'; + this.$.generatedPasswordOverlay.open(); this.$.restAPI.generateAccountHttpPassword().then(function(newPassword) { - this._hasPassword = true; - this._passwordVisible = true; - this._password = newPassword; + this._generatedPassword = newPassword; }.bind(this)); }, - _handleClearTap: function() { - this.$.restAPI.deleteAccountHttpPassword().then(function() { - this._password = ''; - this._hasPassword = false; - }.bind(this)); + _closeOverlay: function() { + this.$.generatedPasswordOverlay.close(); + }, + + _generatedPasswordOverlayClosed: function() { + this._generatedPassword = null; }, }); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html index 36c9abf..e675de2 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -31,7 +31,7 @@ </test-fixture> <script> - suite('gr-http-password tests (already has password)', function() { + suite('gr-http-password tests', function() { var element; var account; var password; @@ -51,106 +51,30 @@ element.loadData().then(function() { flush(done); }); }); - test('loads data', function() { - assert.equal(element._username, 'user name'); - assert.equal(element._password, 'the password'); - assert.isFalse(element._passwordVisible); - assert.isTrue(element._hasPassword); - }); - - test('view password', function() { - var button = element.$$('.value gr-button'); - assert.isFalse(element._passwordVisible); - MockInteractions.tap(button); - assert.isTrue(element._passwordVisible); - }); - test('generate password', function() { var button = element.$.generateButton; var nextPassword = 'the new password'; + var generateResolve; var generateStub = sinon.stub(element.$.restAPI, 'generateAccountHttpPassword', function() { - return Promise.resolve(nextPassword); + return new Promise(function(resolve) { + generateResolve = resolve; + }); }); - assert.isTrue(element._hasPassword); - assert.isFalse(element._passwordVisible); - assert.equal(element._password, 'the password'); + assert.isNotOk(element._generatedPassword); MockInteractions.tap(button); assert.isTrue(generateStub.called); + assert.equal(element._generatedPassword, 'Generating...'); + + generateResolve(nextPassword); + generateStub.lastCall.returnValue.then(function() { - assert.isTrue(element._passwordVisible); - assert.isTrue(element._hasPassword); - assert.equal(element._password, 'the new password'); - }); - }); - - test('clear password', function() { - var button = element.$.clearButton; - var clearStub = sinon.stub(element.$.restAPI, 'deleteAccountHttpPassword', - function() { return Promise.resolve(); }); - - assert.isTrue(element._hasPassword); - assert.equal(element._password, 'the password'); - - MockInteractions.tap(button); - - assert.isTrue(clearStub.called); - clearStub.lastCall.returnValue.then(function() { - assert.isFalse(element._hasPassword); - assert.equal(element._password, ''); + assert.equal(element._generatedPassword, nextPassword); }); }); }); - suite('gr-http-password tests (has no password)', function() { - var element; - var account; - - setup(function(done) { - account = {username: 'user name'}; - - stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve(account); }, - getAccountHttpPassword: function(errFn) { - errFn({status: 404}); - return Promise.resolve(''); - }, - }); - - element = fixture('basic'); - element.loadData().then(function() { flush(done); }); - }); - - test('loads data', function() { - assert.equal(element._username, 'user name'); - assert.isNotOk(element._password); - assert.isFalse(element._passwordVisible); - assert.isFalse(element._hasPassword); - }); - - test('generate password', function() { - var button = element.$.generateButton; - var nextPassword = 'the new password'; - var generateStub = sinon.stub(element.$.restAPI, - 'generateAccountHttpPassword', function() { - return Promise.resolve(nextPassword); - }); - - assert.isFalse(element._hasPassword); - assert.isFalse(element._passwordVisible); - assert.isNotOk(element._password); - - MockInteractions.tap(button); - - assert.isTrue(generateStub.called); - generateStub.lastCall.returnValue.then(function() { - assert.isTrue(element._passwordVisible); - assert.isOk(element._hasPassword); - assert.equal(element._password, 'the new password'); - }); - }); - }); </script>
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..afa5163 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
@@ -20,6 +20,7 @@ <link rel="import" href="../gr-email-editor/gr-email-editor.html"> <link rel="import" href="../gr-group-list/gr-group-list.html"> <link rel="import" href="../gr-http-password/gr-http-password.html"> +<link rel="import" href="../gr-change-table-editor/gr-change-table-editor.html"> <link rel="import" href="../gr-menu-editor/gr-menu-editor.html"> <link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html"> <link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html"> @@ -98,6 +99,8 @@ <li><a href="#Profile">Profile</a></li> <li><a href="#Preferences">Preferences</a></li> <li><a href="#DiffPreferences">Diff Preferences</a></li> + <li><a href="#Menu">Menu</a></li> + <li><a href="#ChangeTableColumns">Change Table Columns</a></li> <li><a href="#Notifications">Notifications</a></li> <li><a href="#EmailAddresses">Email Addresses</a></li> <li><a href="#HTTPCredentials">HTTP Credentials</a></li> @@ -154,8 +157,8 @@ <select is="gr-select" bind-value="{{_localPrefs.time_format}}"> - <option value="HHMM_12">4:10 PM</option> - <option value="HHMM_24">16:10</option> + <option value="HHMM_12">4:10 PM (PST)</option> + <option value="HHMM_24">16:10 (PST)</option> </select> </span> </section> @@ -165,10 +168,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> @@ -183,6 +185,16 @@ </select> </span> </section> + <section> + <span class="title">Expand Inline Diffs</span> + <span class="value"> + <input + id="expandInlineDiffs" + type="checkbox" + checked$="[[_localPrefs.expand_inline_diffs]]" + on-change="_handleExpandInlineDiffsChanged"> + </span> + </section> <gr-button id="savePrefs" on-tap="_handleSavePreferences" @@ -211,7 +223,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 +254,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 +276,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 @@ -257,7 +300,7 @@ on-tap="_handleSaveDiffPreferences" disabled$="[[!_diffPrefsChanged]]">Save Changes</gr-button> </fieldset> - <h2 class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2> + <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2> <fieldset id="menu"> <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor> <gr-button @@ -265,6 +308,20 @@ on-tap="_handleSaveMenu" disabled="[[!_menuChanged]]">Save Changes</gr-button> </fieldset> + <h2 id="ChangeTableColumns" + class$="[[_computeHeaderClass(_changeTableChanged)]]"> + Change Table Columns + </h2> + <fieldset id="changeTableColumns"> + <gr-change-table-editor + change-table-items="{{_localChangeTableColumns}}" + change-table-not-displayed="{{_changeTableColumnsNotDisplayed}}"> + </gr-change-table-editor> + <gr-button + id="saveChangeTable" + on-tap="_handleSaveChangeTable" + disabled="[[!_changeTableChanged]]">Save Changes</gr-button> + </fieldset> <h2 id="Notifications" class$="[[_computeHeaderClass(_watchedProjectsChanged)]]">
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..e12d434 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
@@ -20,6 +20,7 @@ 'time_format', 'email_strategy', 'diff_view', + 'expand_inline_diffs', ]; Polymer({ @@ -31,18 +32,33 @@ * @event title-change */ + /** + * Fired with email confirmation text. + * + * @event show-alert + */ + properties: { prefs: { type: Object, value: function() { return {}; }, }, + params: { + type: Object, + value: function() { return {}; }, + }, _accountInfoMutable: Boolean, _accountInfoChanged: Boolean, _diffPrefs: Object, + _changeTableColumnsNotDisplayed: Array, _localPrefs: { type: Object, value: function() { return {}; }, }, + _localChangeTableColumns: { + type: Array, + value: function() { return []; }, + }, _localMenu: { type: Array, value: function() { return []; }, @@ -51,6 +67,10 @@ type: Boolean, value: true, }, + _changeTableChanged: { + type: Boolean, + value: false, + }, _prefsChanged: { type: Boolean, value: false, @@ -89,10 +109,15 @@ _loadingPromise: Object, }, + behaviors: [ + Gerrit.ChangeTableBehavior, + ], + observers: [ '_handlePrefsChanged(_localPrefs.*)', '_handleDiffPrefsChanged(_diffPrefs.*)', '_handleMenuChanged(_localMenu.splices)', + '_handleChangeTableChanged(_localChangeTableColumns.splices)', ], attached: function() { @@ -101,7 +126,6 @@ var promises = [ this.$.accountInfo.loadData(), this.$.watchedProjectsEditor.loadData(), - this.$.emailEditor.loadData(), this.$.groupList.loadData(), this.$.httpPass.loadData(), ]; @@ -110,6 +134,7 @@ this.prefs = prefs; this._copyPrefs('_localPrefs', 'prefs'); this._cloneMenu(); + this._cloneChangeTableColumns(); }.bind(this))); promises.push(this.$.restAPI.getDiffPreferences().then(function(prefs) { @@ -123,6 +148,18 @@ } }.bind(this))); + if (this.params.emailToken) { + promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then( + function(message) { + if (message) { + this.fire('show-alert', {message: message}); + } + this.$.emailEditor.loadData(); + }.bind(this))); + } else { + promises.push(this.$.emailEditor.loadData()); + } + this._loadingPromise = Promise.all(promises).then(function() { this._loading = false; }.bind(this)); @@ -134,6 +171,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; @@ -172,6 +216,30 @@ this._localMenu = menu; }, + _cloneChangeTableColumns: function() { + var columns = this.prefs.change_table; + + if (columns.length === 0) { + columns = this.CHANGE_TABLE_COLUMNS; + this._changeTableColumnsNotDisplayed = []; + } else { + this._changeTableColumnsNotDisplayed = this.getComplementColumns( + this.prefs.change_table); + } + this._localChangeTableColumns = columns; + }, + + _formatChangeTableColumns: function(changeTableArray) { + return changeTableArray.map(function(item) { + return {column: item}; + }); + }, + + _handleChangeTableChanged: function() { + if (this._isLoading()) { return; } + this._changeTableChanged = true; + }, + _handlePrefsChanged: function(prefs) { if (this._isLoading()) { return; } this._prefsChanged = true; @@ -182,6 +250,11 @@ this._diffPrefsChanged = true; }, + _handleExpandInlineDiffsChanged: function() { + this.set('_localPrefs.expand_inline_diffs', + this.$.expandInlineDiffs.checked); + }, + _handleMenuChanged: function() { if (this._isLoading()) { return; } this._menuChanged = true; @@ -199,15 +272,32 @@ }.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); }, + _handleSaveChangeTable: function() { + this.set('prefs.change_table', this._localChangeTableColumns); + this._cloneChangeTableColumns(); + return this.$.restAPI.savePreferences(this.prefs).then(function() { + this._changeTableChanged = false; + }.bind(this)); + }, + _handleSaveDiffPreferences: function() { return this.$.restAPI.saveDiffPreferences(this._diffPrefs) .then(function() {
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..fbfdcc3 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
@@ -43,6 +43,7 @@ var preferences; var diffPreferences; var config; + var sandbox; function valueOf(title, fieldsetid) { var sections = element.$[fieldsetid].querySelectorAll('section'); @@ -65,11 +66,12 @@ } function stubAddAccountEmail(statusCode) { - return sinon.stub(element.$.restAPI, 'addAccountEmail', + return sandbox.stub(element.$.restAPI, 'addAccountEmail', function() { return Promise.resolve({status: statusCode}); }); } setup(function(done) { + sandbox = sinon.sandbox.create(); account = { _account_id: 123, name: 'user name', @@ -88,12 +90,15 @@ {url: '/first/url', name: 'first name', target: '_blank'}, {url: '/second/url', name: 'second name', target: '_blank'}, ], + change_table: [], }; 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, @@ -126,8 +131,12 @@ element._loadingPromise.then(done); }); + teardown(function() { + sandbox.restore(); + }); + test('calls the title-change event', function() { - var titleChangedStub = sinon.stub(); + var titleChangedStub = sandbox.stub(); // Create a new view. var newElement = document.createElement('gr-settings-view'); @@ -156,6 +165,8 @@ .firstElementChild.bindValue, preferences.email_strategy); assert.equal(valueOf('Diff View', 'preferences') .firstElementChild.bindValue, preferences.diff_view); + assert.equal(valueOf('Expand Inline Diffs', 'preferences') + .firstElementChild.checked, false); assert.isFalse(element._prefsChanged); assert.isFalse(element._menuChanged); @@ -163,8 +174,13 @@ // Change the diff view element. var diffSelect = valueOf('Diff View', 'preferences').firstElementChild; diffSelect.bindValue = 'SIDE_BY_SIDE'; + + var expandInlineDiffs = + valueOf('Expand Inline Diffs', 'preferences').firstElementChild; diffSelect.fire('change'); + MockInteractions.tap(expandInlineDiffs); + assert.isTrue(element._prefsChanged); assert.isFalse(element._menuChanged); @@ -172,6 +188,7 @@ savePreferences: function(prefs) { assert.equal(prefs.diff_view, 'SIDE_BY_SIDE'); assertMenusEqual(prefs.my, preferences.my); + assert.equal(prefs.expand_inline_diffs, true); return Promise.resolve(); } }); @@ -188,12 +205,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 +241,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); @@ -314,5 +347,53 @@ done(); }); }); + + test('emails are loaded without emailToken', function() { + sandbox.stub(element.$.emailEditor, 'loadData'); + element.params = {}; + element.attached(); + assert.isTrue(element.$.emailEditor.loadData.calledOnce); + }); + + suite('when email verification token is provided', function() { + var resolveConfirm; + + setup(function() { + sandbox.stub(element.$.emailEditor, 'loadData'); + sandbox.stub(element.$.restAPI, 'confirmEmail', function() { + return new Promise(function(resolve) { resolveConfirm = resolve; }); + }); + element.params = {emailToken: 'foo'}; + element.attached(); + }); + + test('it is used to confirm email via rest API', function() { + assert.isTrue(element.$.restAPI.confirmEmail.calledOnce); + assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo')); + }); + + test('emails are not loaded initially', function() { + assert.isFalse(element.$.emailEditor.loadData.called); + }); + + test('user emails are loaded after email confirmed', function(done) { + element._loadingPromise.then(function() { + assert.isTrue(element.$.emailEditor.loadData.calledOnce); + done(); + }); + resolveConfirm(); + }); + + test('show-alert is fired when email is confirmed', function(done) { + sandbox.spy(element, 'fire'); + element._loadingPromise.then(function() { + assert.isTrue( + element.fire.calledWith('show-alert', {message: 'bar'})); + done(); + }); + resolveConfirm('bar'); + }); + + }); }); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html index 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..ddfdae7 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
@@ -44,21 +44,40 @@ } gr-button.remove { background: #eee; + border: 0; color: #666; font-size: 1.7em; font-weight: normal; height: .6em; line-height: .6em; margin-left: .15em; + margin-top: -.05em; 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 - hidden$="[[!removable]]" hidden - class="remove" on-tap="_handleRemoveTap">×</gr-button> + id="remove" + hidden$="[[!removable]]" + hidden + aria-label="Remove" + 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..43721fe 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,14 @@ <span class="text"> <span>[[account.name]]</span> <span hidden$="[[!_computeShowEmail(showEmail, account)]]"> - ([[account.email]]) + [[_computeEmailStr(account)]] </span> + <template is="dom-if" if="[[account.status]]"> + <span>([[account.status]])</span> + </template> </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..e9f18df 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,10 +30,13 @@ }, _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 += account.name; + } if (account.email) { - result += ' <' + util.escapeHTML(account.email) + '>'; + result += ' <' + account.email + '>'; } return result; }, @@ -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..3ff4ace 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
@@ -28,7 +28,11 @@ _computeOwnerLink: function(account) { if (!account) { return; } var accountID = account.email || account._account_id; - return '/q/owner:' + encodeURIComponent(accountID) + '+status:open'; + return '/q/owner:' + encodeURIComponent(accountID); + }, + + _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..8c1af21 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'); }); @@ -44,10 +46,14 @@ _account_id: 123, email: 'andybons+gerrit@gmail.com' }), - '/q/owner:andybons%2Bgerrit%40gmail.com+status:open'); + '/q/owner:andybons%2Bgerrit%40gmail.com'); assert.equal(element._computeOwnerLink({_account_id: 42}), - '/q/owner:42+status:open'); + '/q/owner:42'); + + 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..aeb7e5f 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"> @@ -43,6 +44,9 @@ cursor: pointer; padding: .5em .75em; } + li:focus { + outline: none; + } li.selected { background-color: #eee; } @@ -54,16 +58,21 @@ disabled$="[[disabled]]" bind-value="{{text}}" placeholder="[[placeholder]]" - on-keydown="_handleInputKeydown" - on-focus="_updateSuggestions" + on-keydown="_handleKeydown" + on-focus="_onInputFocus" autocomplete="off" /> <div id="suggestions" - hidden$="[[_computeSuggestionsHidden(_suggestions)]]"> + role="listbox" + hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]"> <ul> <template is="dom-repeat" items="[[_suggestions]]"> <li data-index$="[[index]]" + tabindex="-1" + aria-label$="[[item.name]]" + on-keydown="_handleKeydown" + role="option" on-tap="_handleSuggestionTap">[[item.name]]</li> </template> </ul> @@ -72,6 +81,8 @@ id="cursor" index="{{_index}}" cursor-target-class="selected" + scroll-behavior="keep-visible" + focus-on-move stops="[[_getSuggestionElems(_suggestions)]]"></gr-cursor-manager> </template> <script src="gr-autocomplete.js"></script>
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..9ebb794 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) { @@ -166,7 +198,12 @@ return this.$.suggestions.querySelectorAll('li'); }, - _handleInputKeydown: function(e) { + /** + * _handleKeydown used for key handling in the this.$.input AND all child + * autocomplete options. + */ + _handleKeydown: function(e) { + this._focused = true; switch (e.keyCode) { case 38: // Up e.preventDefault(); @@ -181,12 +218,21 @@ 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; + default: + // For any normal keypress, return focus to the input to allow for + // unbroken user input. + this.$.input.focus(); } + this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); }, _cancel: function() { @@ -199,34 +245,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 +299,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..2ec32d0 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,62 @@ } :host([disabled]) { cursor: default; - pointer-events: none; } :host([loading]), :host([loading][disabled]) { cursor: wait; } - :host(:focus), - :host(:hover) { - border-color: #666; + :host:focus:not([link]), + :host:hover:not([link]) { + background-color: #f8f8f8; + border-color: #aaa; } :host(:active) { border-color: #d1d2d3; color: #aaa; } - :host([primary]:focus) { - border-color: #fff; - box-shadow: 0 0 1px #00f; + :host([primary]:focus), + :host([secondary]:focus), + :host([primary]:active), + :host([secondary]:active) { + color: #fff; } - :host([primary]:hover) { + :host([primary]:focus) { + box-shadow: 0 0 1px #00f; + background-color: #4d90fe; + } + :host([primary]:not([disabled]):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; + background-color: #d14836; + } + :host([secondary]:not([disabled]):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: #4d90fe; + color: #fff; + opacity: .5; + } </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..800b1df 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -18,6 +18,10 @@ is: 'gr-button', properties: { + link: { + type: Boolean, + reflectToAttribute: true, + }, disabled: { type: Boolean, observer: '_disabledChanged', @@ -29,6 +33,11 @@ }, }, + listeners: { + 'tap': '_handleAction', + 'click': '_handleAction', + }, + behaviors: [ Gerrit.KeyboardShortcutBehavior, Gerrit.TooltipBehavior, @@ -39,6 +48,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 +66,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-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html index 05fe10b..03be4bb 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -42,7 +42,10 @@ fill: #ffac33; } </style> - <button class$="[[_computeStarClass(change.starred)]]" on-tap="_handleStarTap"> + <button + class$="[[_computeStarClass(change.starred)]]" + aria-label="Change star" + on-tap="toggleStar"> <!-- Public Domain image from the Noun Project: https://thenounproject.com/search/?q=star&i=25969 --> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M26.439,95.601c-5.608,2.949-9.286,0.276-8.216-5.968l4.5-26.237L3.662,44.816c-4.537-4.423-3.132-8.746,3.137-9.657 l26.343-3.829L44.923,7.46c2.804-5.682,7.35-5.682,10.154,0l11.78,23.87l26.343,3.829c6.27,0.911,7.674,5.234,3.138,9.657 L77.277,63.397l4.501,26.237c1.07,6.244-2.608,8.916-8.216,5.968L50,83.215L26.439,95.601z"></path></svg> </button>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js index 23c56b4..beb0ff1 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -34,7 +34,7 @@ return classes.join(' '); }, - _handleStarTap: function() { + toggleStar: function() { var newVal = !this.change.starred; this.set('change.starred', newVal); this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number,
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..bec75ee 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" disabled="[[disabled]]"> + [[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-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js index 0f20e0a..dbddb04 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -33,7 +33,11 @@ confirmLabel: { type: String, value: 'Confirm', - } + }, + disabled: { + type: Boolean, + value: false, + }, }, hostAttributes: {
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..63a1a7d 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
@@ -15,7 +15,6 @@ 'use strict'; var ScrollBehavior = { - ALWAYS: 'always', NEVER: 'never', KEEP_VISIBLE: 'keep-visible', }; @@ -55,22 +54,21 @@ }, /** - * The scroll behavior for the cursor. Values are 'never', 'always' and + * The scroll behavior for the cursor. Values are 'never' and * '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. + * When true, will call element.focus() during scrolling. */ - foldOffsetTop: { - type: Number, - value: 0, + focusOnMove: { + type: Boolean, + value: false, }, }, @@ -89,12 +87,22 @@ /** * Set the cursor to an arbitrary element. * @param {DOMElement} element + * @param {boolean} opt_noScroll prevent any potential scrolling in response + * setting the cursor. */ - setCursor: function(element) { + setCursor: function(element, opt_noScroll) { + var behavior; + if (opt_noScroll) { + behavior = this.scrollBehavior; + this.scrollBehavior = ScrollBehavior.NEVER; + } + this.unsetCursor(); this.target = element; this._updateIndex(); this._decorateTarget(); + + if (opt_noScroll) { this.scrollBehavior = behavior; } }, unsetCursor: function() { @@ -117,6 +125,10 @@ } }, + setCursorAtIndex: function(index, opt_noScroll) { + this.setCursor(this.stops[index], opt_noScroll); + }, + /** * Move the cursor forward or backward by delta. Noop if moving past either * end of the stop list. @@ -144,6 +156,8 @@ this.index = newIndex; this.target = newTarget; + if (this.focusOnMove) { this.target.focus(); } + this._decorateTarget(); }, @@ -182,6 +196,11 @@ // If we failed to satisfy the condition: if (opt_condition && !opt_condition(this.stops[newIndex])) { + if (delta > 0) { + return this.stops.length - 1; + } else if (delta < 0) { + return 0; + } return this.index; } @@ -202,20 +221,39 @@ } }, - _scrollToTarget: function() { - if (!this.target || this.scroll === ScrollBehavior.NEVER) { return; } - - // Calculate where the element is relative to the window. - var top = this.target.offsetTop; - for (var offsetParent = this.target.offsetParent; + /** + * Calculate where the element is relative to the window. + * @param {object} target Target to scroll to. + * @return {number} Distance to top of the target. + */ + _getTop: function(target) { + var top = target.offsetTop; + for (var offsetParent = target.offsetParent; offsetParent; offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } + return top; + }, - if (this.scroll === ScrollBehavior.KEEP_VISIBLE && - top > window.pageYOffset + this.foldOffsetTop && - top < window.pageYOffset + window.innerHeight) { return; } + /** + * @return {boolean} + */ + _targetIsVisible: function(top) { + return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && + top > window.pageYOffset && + top < window.pageYOffset + window.innerHeight; + }, + + _scrollToTarget: function() { + if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { + return; + } + + var top = this._getTop(this.target); + if (this._targetIsVisible(top)) { + return; + } // Scroll the element to the middle of the window. Dividing by a third // instead of half the inner height feels a bit better otherwise the
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html index 1ad014d..0f48501 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -26,9 +26,7 @@ <test-fixture id="basic"> <template> - <gr-cursor-manager - cursor-stop-selector="li" - cursor-target-class="targeted"></gr-cursor-manager> + <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager> <ul> <li>A</li> <li>B</li> @@ -40,15 +38,21 @@ <script> suite('gr-cursor-manager tests', function() { + var sandbox; var element; var list; setup(function() { + sandbox = sinon.sandbox.create(); var fixtureElements = fixture('basic'); element = fixtureElements[0]; list = fixtureElements[1]; }); + teardown(function() { + sandbox.restore(); + }); + test('core cursor functionality', function() { // The element is initialized into the proper state. assert.isArray(element.stops); @@ -120,5 +124,63 @@ assert.isNotOk(element.target); assert.equal(element.index, -1); }); + + test('opt_noScroll', function() { + sandbox.stub(element, '_targetIsVisible', function() { return false; }); + var scrollStub = sandbox.stub(window, 'scrollTo'); + element.stops = list.querySelectorAll('li'); + element.scrollBehavior = 'keep-visible'; + + element.setCursorAtIndex(1, true); + assert.isFalse(scrollStub.called); + + element.setCursorAtIndex(2); + assert.isTrue(scrollStub.called); + }); + + test('_getNextindex', function() { + var isLetterB = function(row) { + return row.textContent === 'B'; + }; + element.stops = list.querySelectorAll('li'); + // Start cursor at the first stop. + element.setCursor(list.children[0]); + + // Move forward to meet the next condition. + assert.equal(element._getNextindex(1, isLetterB), 1); + element.index = 1; + + // Nothing else meets the condition, should be at last stop. + assert.equal(element._getNextindex(1, isLetterB), 3); + element.index = 3; + + // Should stay at last stop if try to proceed. + assert.equal(element._getNextindex(1, isLetterB), 3); + + // Go back to the previous condition met. Should be back at. + // stop 1. + assert.equal(element._getNextindex(-1, isLetterB), 1); + element.index = 1; + + // Go back. No more meet the condition. Should be at stop 0. + assert.equal(element._getNextindex(-1, isLetterB), 0); + }); + + test('focusOnMove prop', function() { + var listEls = list.querySelectorAll('li'); + for (var i = 0; i < listEls.length; i++) { + sandbox.spy(listEls[i], 'focus'); + } + element.stops = listEls; + element.setCursor(list.children[0]); + + element.focusOnMove = false; + element.next(); + assert.isFalse(element.target.focus.called); + + element.focusOnMove = true; + element.next(); + assert.isTrue(element.target.focus.called); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html index 3d0cf5a..ae6bb75 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -26,8 +26,9 @@ display: inline; } </style> - <span title$="[[_computeFullDateStr(dateStr, _timeFormat)]]" - >[[_computeDateStr(dateStr, _timeFormat, _relative)]]</span> + <span title$="[[_computeFullDateStr(dateStr, _timeFormat)]]"> + [[_computeDateStr(dateStr, _timeFormat, _relative)]] + </span> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-date-formatter.js"></script>
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..a8c9010 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,9 @@ var TimeFormats = { TIME_12: 'h:mm A', // 2:14 PM - TIME_24: 'H:mm', // 14:14 + TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM + TIME_24: 'HH:mm', // 14:14 + TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00 MONTH_DAY: 'MMM DD', // Aug 29 MONTH_DAY_YEAR: 'MMM DD, YYYY', // Aug 29, 1997 }; @@ -44,6 +46,14 @@ this._loadPreferences(); }, + _getTzString: function() { + return ' ' + new Date().toString().split(' ').pop(); + }, + + _getUtcOffsetString: function() { + return ' UTC' + moment().format('Z'); + }, + _loadPreferences: function() { return this._getLoggedIn().then(function(loggedIn) { if (!loggedIn) { @@ -111,23 +121,36 @@ 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; if (this._isWithinDay(now, date)) { - format = timeFormat; + return date.format(timeFormat) + this._getTzString(); } else if (this._isWithinHalfYear(now, date)) { format = TimeFormats.MONTH_DAY; } return date.format(format); }, + _timeToSecondsFormat: function(timeFormat) { + return timeFormat === TimeFormats.TIME_12 ? + TimeFormats.TIME_12_WITH_SEC : + TimeFormats.TIME_24_WITH_SEC; + }, + _computeFullDateStr: function(dateStr, timeFormat) { if (!dateStr) { return ''; } var date = moment(util.parseDate(dateStr)); if (!date.isValid()) { return ''; } - return date.format(TimeFormats.MONTH_DAY_YEAR + ', ' + timeFormat); + var format = TimeFormats.MONTH_DAY_YEAR + ', '; + format += this._timeToSecondsFormat(timeFormat); + return date.format(format) + this._getUtcOffsetString(); }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html index d1886e7..0faf102 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
@@ -33,6 +33,15 @@ <script> suite('gr-date-formatter tests', function() { var element; + var sandbox; + + setup(function() { + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); /** * Parse server-formatter date and normalize into current timezone. @@ -47,13 +56,12 @@ // Normalize and convert the date to mimic server response. dateStr = normalizedDate(dateStr) .toJSON().replace('T', ' ').slice(0, -1); - var clock = sinon.useFakeTimers(normalizedDate(nowStr).getTime()); + sandbox.useFakeTimers(normalizedDate(nowStr).getTime()); element.dateStr = dateStr; flush(function() { var span = element.$$('span'); - assert.equal(span.textContent, expected); + assert.equal(span.textContent.trim(), expected); assert.equal(span.title, expectedTooltip); - clock.restore(); done(); }); } @@ -74,6 +82,8 @@ {time_format: 'HHMM_24', relative_date_in_change_table: false} ).then(function() { element = fixture('basic'); + sandbox.stub(element, '_getTzString').returns(''); + sandbox.stub(element, '_getUtcOffsetString').returns(''); element._loadPreferences().then(function() { done(); }); }); }); @@ -85,26 +95,26 @@ test('Within 24 hours on same day', function(done) { testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', 'Jul 29, 2015, 15:34', done); + '2015-07-29 15:34:14.985000000', + '15:34', 'Jul 29, 2015, 15:34:14', done); }); test('Within 24 hours on different days', function(done) { testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - 'Jul 28', 'Jul 28, 2015, 20:25', done); + '2015-07-28 20:25:14.985000000', + 'Jul 28', 'Jul 28, 2015, 20:25:14', done); }); test('More than 24 hours but less than six months', function(done) { testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - 'Jun 15', 'Jun 15, 2015, 3:25', done); + '2015-06-15 03:25:14.985000000', + 'Jun 15', 'Jun 15, 2015, 03:25:14', 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); + '2015-01-15 03:25:00.000000000', + 'Jan 15, 2015', 'Jan 15, 2015, 03:25:00', done); }); }); @@ -115,14 +125,16 @@ {time_format: 'HHMM_12'} ).then(function() { element = fixture('basic'); + sandbox.stub(element, '_getTzString').returns(''); + sandbox.stub(element, '_getUtcOffsetString').returns(''); element._loadPreferences().then(function() { done(); }); }); }); test('Within 24 hours on same day', function(done) { testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', 'Jul 29, 2015, 3:34 PM', done); + '2015-07-29 15:34:14.985000000', + '3:34 PM', 'Jul 29, 2015, 3:34:14 PM', done); }); }); @@ -132,20 +144,22 @@ {time_format: 'HHMM_12', relative_date_in_change_table: true} ).then(function() { element = fixture('basic'); + sandbox.stub(element, '_getTzString').returns(''); + sandbox.stub(element, '_getUtcOffsetString').returns(''); element._loadPreferences().then(function() { done(); }); }); }); test('Within 24 hours on same day', function(done) { testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '5 hours ago', 'Jul 29, 2015, 3:34 PM', done); + '2015-07-29 15:34:14.985000000', + '5 hours ago', 'Jul 29, 2015, 3:34:14 PM', done); }); test('More than six months', function(done) { testDates('2015-09-15 20:34:00.000000000', - '2015-01-15 03:25:00.000000000', - '8 months ago', 'Jan 15, 2015, 3:25 AM', done); + '2015-01-15 03:25:00.000000000', + '8 months ago', 'Jan 15, 2015, 3:25:00 AM', done); }); }); @@ -155,6 +169,7 @@ {time_format: 'HHMM_12', relative_date_in_change_table: true} ).then(function() { element = fixture('basic'); + sandbox.stub(element, '_getTzString').returns(''); element._loadPreferences().then(function() { done(); }); }); }); @@ -169,12 +184,13 @@ setup(function(done) { return stubRestAPI(null).then(function() { element = fixture('basic'); + sandbox.stub(element, '_getTzString').returns(''); element._loadPreferences().then(function() { done(); }); }); }); 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..c72e3da --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -0,0 +1,146 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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; + width: 100%; + } + .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; + } + :host[down-arrow] .dropdown-trigger { + padding-right: 1.4em; + } + 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 .itemAction { + cursor: pointer; + display: block; + padding: .85em 1em; + } + li .itemAction:link, + li .itemAction:visited { + color: #00e; + text-decoration: none; + } + li .itemAction:hover { + background-color: #6B82D6; + color: #fff; + } + .topContent { + display: block; + padding: .85em 1em; + } + .bold-text { + font-weight: bold; + } + :host:not([down-arrow]) .downArrow { display: none; } + :host[down-arrow] .downArrow { + border-left: .36em solid transparent; + border-right: .36em solid transparent; + border-top: .36em solid #ccc; + height: 0; + position: absolute; + right: .3em; + top: calc(50% - .05em); + transition: border-top-color 200ms; + width: 0; + } + .dropdown-trigger:hover .downArrow { + border-top-color: #666; + } + </style> + <gr-button link="[[link]]" class="dropdown-trigger" id="trigger" + on-tap="_showDropdownTapHandler"> + <content></content> + <i class="downArrow"></i> + </gr-button> + <iron-dropdown id="dropdown" + vertical-align="top" + vertical-offset="[[verticalOffset]]" + 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> + <span + class="itemAction" + data-id$="[[link.id]]" + on-tap="_handleItemTap" + hidden$="[[link.url]]">[[link.name]]</span> + <a + class="itemAction" + href$="[[_computeRelativeURL(link.url)]]" + hidden$="[[!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..d1fae7f --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-dropdown', + + /** + * Fired when a non-link dropdown item with the given ID is tapped. + * + * @event tap-item-<id> + */ + + properties: { + items: Array, + topContent: Object, + horizontalAlign: { + type: String, + value: 'left', + }, + + /** + * Style the dropdown trigger as a link (rather than a button). + */ + link: { + type: Boolean, + value: false, + }, + + verticalOffset: { + type: Number, + value: 40, + }, + + _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); + }, + + _handleItemTap: function(e) { + var id = e.target.getAttribute('data-id'); + if (id) { this.dispatchEvent(new CustomEvent('tap-item-' + id)); } + }, + }); +})();
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..2794caf --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -0,0 +1,84 @@ +<!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')); + }); + + test('non link items', function() { + element.items = [ + {name: 'item one', id: 'foo'}, {name: 'item two', id: 'bar'}]; + var stub = sinon.stub(); + element.addEventListener('tap-item-foo', stub); + flushAsynchronousOperations(); + MockInteractions.tap(element.$$('.itemAction')); + assert.isTrue(stub.called); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html index 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..32cff2a 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,20 @@ <dom-module id="gr-editable-label"> <template> <style> + :host { + align-items: center; + display: inline-flex; + } + input, + label { + width: 100%; + } input { font: inherit; - max-width: 8em; } label { color: #777; display: inline-block; - max-width: 8em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -47,6 +53,7 @@ <label hidden$="[[editing]]" class$="[[_computeLabelClass(readOnly, value, placeholder)]]" + title$="[[_computeLabel(value, placeholder)]]" on-tap="_open">[[_computeLabel(value, placeholder)]]</label> </template> <script src="gr-editable-label.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js index eb604f7..f3e83f9 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -45,6 +45,10 @@ _inputText: String, }, + hostAttributes: { + tabindex: '0', + }, + _usePlaceholder: function(value, placeholder) { return (!value || !value.length) && placeholder; },
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..8855b0a --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -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. +--> +<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; + } + :host.noTrailingMargin p:last-child, + :host.noTrailingMargin ul:last-child, + :host.noTrailingMargin blockquote:last-child, + :host.noTrailingMargin gr-linked-text.pre:last-child { + margin: 0; + } + blockquote { + 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..c17e9e4 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -0,0 +1,276 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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, + noTrailingMargin: { + type: Boolean, + value: false, + }, + }, + + observers: [ + '_contentOrConfigChanged(content, config)', + ], + + ready: function() { + if (this.noTrailingMargin) { + this.classList.add('noTrailingMargin'); + } + }, + + /** + * 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..4ca7f28 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
@@ -49,6 +49,7 @@ setup(function() { element = fixture('basic'); + element.change = {}; var plugin; Gerrit.install(function(p) { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); @@ -119,5 +120,21 @@ }); }); }); + + 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.isNotOk(button); + 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..13ee10e 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
@@ -33,26 +33,30 @@ var element; var plugin; var errorStub; + var sandbox; + var throwErrFn = function() { throw Error('Unfortunately, this handler has stopped'); }; setup(function() { + sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { getAccount: function() { return Promise.resolve({name: 'Judy Hopps'}); }, }); element = fixture('basic'); - errorStub = sinon.stub(console, 'error'); + errorStub = sandbox.stub(console, 'error'); + Gerrit._setPluginsCount(1); Gerrit.install(function(p) { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); }); teardown(function() { + sandbox.restore(); element._removeEventCallbacks(); plugin = null; - errorStub.restore(); }); test('url', function() { @@ -75,10 +79,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 +92,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 = sandbox.spy(); + Gerrit._setPluginsCount(1); + plugin.on(element.EventType.SHOW_CHANGE, spy); + element.handleEvent(element.EventType.SHOW_CHANGE, + {change: testChange, patchNum: 1}); + assert.isFalse(spy.called); + Gerrit._setPluginsCount(0); + flush(function() { + assert.isTrue(spy.called); + done(); + }); + }); + test('comment event', function(done) { var testCommentNode = {foo: 'bar'}; plugin.on(element.EventType.COMMENT, throwErrFn); @@ -102,25 +121,52 @@ element.handleEvent(element.EventType.COMMENT, {node: testCommentNode}); }); - test('revert event', function(done) { - function appendToRevertMsg(c, msg) { - return msg + '\ninfo'; + test('revert event', function() { + 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() { + function getLabels(c) { + return {'Code-Review': 1}; + } + + 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); @@ -144,7 +190,7 @@ }); test('versioning', function() { - var callback = sinon.spy(); + var callback = sandbox.spy(); Gerrit.install(callback, '0.0pre-alpha'); assert(callback.notCalled); }); @@ -156,5 +202,65 @@ }); }); + 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() { + assert.equal(Gerrit._pluginsPending, 0); + done(); + } + }); + Gerrit._setPluginsCount(2); + Gerrit._pluginInstalled(); + assert.equal(Gerrit._pluginsPending, 1); + Gerrit._pluginInstalled(); + }); + + test('install calls _pluginInstalled', function() { + sandbox.stub(Gerrit, '_pluginInstalled'); + Gerrit.install(function(p) { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + assert.isTrue(Gerrit._pluginInstalled.calledOnce); + }); + + test('install calls _pluginInstalled on error', function() { + sandbox.stub(Gerrit, '_pluginInstalled'); + Gerrit.install(function() {}, '0.0pre-alpha'); + assert.isTrue(Gerrit._pluginInstalled.calledOnce); + }); + + test('installGwt calls _pluginInstalled', function() { + sandbox.stub(Gerrit, '_pluginInstalled'); + Gerrit.installGwt(); + assert.isTrue(Gerrit._pluginInstalled.calledOnce); + }); + + test('installGwt returns a stub object', function() { + var plugin = Gerrit.installGwt(); + sandbox.stub(console, 'warn'); + assert.isAbove(Object.keys(plugin).length, 0); + Object.keys(plugin).forEach(function(name) { + console.warn.reset(); + plugin[name](); + assert.isTrue(console.warn.calledOnce); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index 21d76f1..b3ae649 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -14,6 +14,16 @@ (function(window) { 'use strict'; + var warnNotSupported = function(opt_name) { + console.warn('Plugin API method ' + (opt_name || '') + ' is not supported'); + }; + + var stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel']; + var GWT_PLUGIN_STUB = {}; + stubbedMethods.forEach(function(name) { + GWT_PLUGIN_STUB[name] = warnNotSupported.bind(null, name); + }); + var API_VERSION = '0.1'; // GWT JSNI uses $wnd to refer to window. @@ -44,6 +54,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 +78,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,20 +102,68 @@ 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)); + var plugin = new Plugin(src); + try { + callback(plugin); + } catch (e) { + console.warn(plugin.getPluginName() + ' install failed: ' + + e.name + ': ' + e.message); + } + Gerrit._pluginInstalled(); }; Gerrit.getLoggedIn = function() { return document.createElement('gr-rest-api-interface').getLoggedIn(); }; + /** + * Polyfill GWT API dependencies to avoid runtime exceptions when loading + * GWT-compiled plugins. + * @deprecated Not supported in PolyGerrit. + */ Gerrit.installGwt = function() { - // NOOP since PolyGerrit doesn’t support GWT plugins. + Gerrit._pluginInstalled(); + return GWT_PLUGIN_STUB; + }; + + 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..5828e7b --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -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. +--> + +<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; + border: 0; + color: #666; + font-size: 1.7em; + font-weight: normal; + height: .6em; + line-height: .6em; + margin-left: .15em; + margin-top: -.05em; + 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..ef436f6 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
@@ -18,6 +18,7 @@ is: 'gr-linked-text', properties: { + removeZeroWidthSpace: Boolean, content: { type: String, observer: '_contentChanged', @@ -50,27 +51,20 @@ _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'; + a.rel = 'noopener'; 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)); } - }); + }, this.removeZeroWidthSpace); 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..311275d 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. @@ -70,6 +74,7 @@ element.content = url; var linkEl = element.$.output.childNodes[0]; assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.rel, 'noopener'); assert.equal(linkEl.href, url); assert.equal(linkEl.textContent, url); }); @@ -87,6 +92,7 @@ element.content = 'Bug 3650'; linkEl = element.$.output.childNodes[0]; assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.rel, 'noopener'); assert.equal(linkEl.href, url); assert.equal(linkEl.textContent, 'Bug 3650'); }); @@ -123,6 +129,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 +177,35 @@ assert.equal(element.$.output.innerHTML, 'foo:baz'); }); + test('R=email labels link correctly', function() { + element.removeZeroWidthSpace = true; + element.content = 'R=\u200Btest@google.com'; + assert.equal(element.$.output.textContent, 'R=test@google.com'); + assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1); + }); + + 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..f4d99d6 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
@@ -14,9 +14,10 @@ 'use strict'; -function GrLinkTextParser(linkConfig, callback) { +function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) { this.linkConfig = linkConfig; this.callback = callback; + this.removeZeroWidthSpace = opt_removeZeroWidthSpace; Object.preventExtensions(this); } @@ -27,17 +28,112 @@ 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'; + a.rel = 'noopener'; + 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), }); }; GrLinkTextParser.prototype.parseChunk = function(text, href) { + if (this.removeZeroWidthSpace) { + // Remove the zero-width space added in gr-change-view. + text = text.replace(/^R=\u200B/gm, 'R='); + } + if (href) { this.addText(text, href); } else { @@ -46,6 +142,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 +164,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..07ff632 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,11 +14,12 @@ 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> <dom-module id="gr-rest-api-interface"> <script src="gr-rest-api-interface.js"></script> + <script src="gr-reviewer-updates-parser.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..07f59ae 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, @@ -199,11 +212,19 @@ }, savePreferences: function(prefs, opt_errFn, opt_ctx) { + // Note (Issue 5142): normalize the download scheme with lower case before + // saving. + if (prefs.download_scheme) { + prefs.download_scheme = prefs.download_scheme.toLowerCase(); + } + return this.send('PUT', '/accounts/self/preferences', prefs, opt_errFn, opt_ctx); }, 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 +253,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 +306,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 +365,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 +399,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 +415,15 @@ 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) + .then(GrReviewerUpdatesParser.parse); }, getDiffChangeDetail: function(changeNum, opt_errFn, opt_cancelCondition) { @@ -392,13 +464,13 @@ 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)); + if (!response) { return []; } + var paths = Object.keys(response).sort(this.specialFilePathCompare); var files = []; for (var i = 0; i < paths.length; i++) { var info = response[paths[i]]; @@ -410,42 +482,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 +503,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 +581,7 @@ ].join(' '); var params = { O: options, - q: query + q: query, }; return this.fetchJSON('/changes/', null, null, params); }, @@ -648,6 +698,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 +933,37 @@ 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}); + }, + + confirmEmail: function(token) { + return this.send('PUT', '/config/server/email.confirm', {token: token}) + .then(function(response) { + if (response.status === 204) { + return 'Email confirmed successfully.'; + } + return null; + }); + }, + + setAssignee: function(changeNum, assignee) { + return this.send('PUT', + this.getChangeActionURL(changeNum, null, '/assignee'), + {assignee: assignee}); + }, + + deleteAssignee: function(changeNum) { + return this.send('DELETE', + this.getChangeActionURL(changeNum, null, '/assignee')); + }, }); })();
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..3d65062 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,117 @@ }); }); }); + + 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'; + sandbox.stub(element, 'send'); + element._cache[cacheKey] = {tab_size: 4}; + element.saveDiffPreferences({tab_size: 8}); + assert.isTrue(element.send.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(); + }); + }); + + test('savPreferences normalizes download scheme', function() { + sandbox.stub(element, 'send'); + element.savePreferences({download_scheme: 'HTTP'}); + assert.isTrue(element.send.called); + assert.equal(element.send.lastCall.args[2].download_scheme, 'http'); + }); + + test('confirmEmail', function() { + sandbox.spy(element, 'send'); + element.confirmEmail('foo'); + assert.isTrue(element.send.calledWith( + 'PUT', '/config/server/email.confirm', {token: 'foo'})); + }); + + test('GrReviewerUpdatesParser.parse is used', function(done) { + sandbox.stub(GrReviewerUpdatesParser, 'parse').returns( + Promise.resolve('foo')); + element.getChangeDetail(42).then(function(result) { + assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce); + assert.equal(result, 'foo'); + done(); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js new file mode 100644 index 0000000..21a6bc6 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -0,0 +1,193 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function(window) { + 'use strict'; + + // Prevent redefinition. + if (window.GrReviewerUpdatesParser) { return; } + + function GrReviewerUpdatesParser(change) { + // TODO (viktard): Polyfill Object.assign for IE. + this.result = Object.assign({}, change); + this._lastState = {}; + }; + + GrReviewerUpdatesParser.parse = function(change) { + if (!change || + !change.messages || + !change.reviewer_updates || + !change.reviewer_updates.length) { + return change; + } + var parser = new GrReviewerUpdatesParser(change); + parser._filterRemovedMessages(); + parser._groupUpdates(); + parser._formatUpdates(); + return parser.result; + }; + + GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000; + + GrReviewerUpdatesParser.prototype.result = null; + GrReviewerUpdatesParser.prototype._batch = null; + GrReviewerUpdatesParser.prototype._updateItems = null; + GrReviewerUpdatesParser.prototype._lastState = null; + + /** + * Removes messages that describe removed reviewers, since reviewer_updates + * are used. + */ + GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() { + this.result.messages = this.result.messages.filter(function(message) { + return message.tag !== 'autogenerated:gerrit:deleteReviewer'; + }); + }; + + /** + * Is a part of _groupUpdates(). Creates a new batch of updates. + * @param {Object} update instance of ReviewerUpdateInfo + */ + GrReviewerUpdatesParser.prototype._startBatch = function(update) { + this._updateItems = []; + return { + author: update.updated_by, + date: update.updated, + type: 'REVIEWER_UPDATE', + }; + }; + + /** + * Is a part of _groupUpdates(). Validates current batch: + * - filters out updates that don't change reviewer state. + * - updates current reviewer state. + * @param {Object} update instance of ReviewerUpdateInfo + */ + GrReviewerUpdatesParser.prototype._completeBatch = function(update) { + var items = []; + for (var accountId in this._updateItems) { + if (!this._updateItems.hasOwnProperty(accountId)) continue; + var updateItem = this._updateItems[accountId]; + if (this._lastState[accountId] !== updateItem.state) { + this._lastState[accountId] = updateItem.state; + items.push(updateItem); + } + } + if (items.length) { + this._batch.updates = items; + } + }; + + /** + * Groups reviewer updates. Sequential updates are grouped if: + * - They were performed within short timeframe (6 seconds) + * - Made by the same person + * - Non-change updates are discarded within a group + * - Groups with no-change updates are discarded (eg CC -> CC) + */ + GrReviewerUpdatesParser.prototype._groupUpdates = function() { + var updates = this.result.reviewer_updates; + var newUpdates = updates.reduce(function(newUpdates, update) { + if (!this._batch) { + this._batch = this._startBatch(update); + } + var updateDate = util.parseDate(update.updated).getTime(); + var batchUpdateDate = util.parseDate(this._batch.date).getTime(); + var reviewerId = update.reviewer._account_id.toString(); + if (updateDate - batchUpdateDate > + GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS || + update.updated_by._account_id !== this._batch.author._account_id) { + // Next sequential update should form new group. + this._completeBatch(); + if (this._batch.updates && this._batch.updates.length) { + newUpdates.push(this._batch); + } + this._batch = this._startBatch(update); + } + this._updateItems[reviewerId] = { + reviewer: update.reviewer, + state: update.state, + }; + if (this._lastState[reviewerId]) { + this._updateItems[reviewerId].prev_state = this._lastState[reviewerId]; + } + return newUpdates; + }.bind(this), []); + this._completeBatch(); + if (this._batch.updates && this._batch.updates.length) { + newUpdates.push(this._batch); + } + this.result.reviewer_updates = newUpdates; + }; + + /** + * Generates update message for reviewer state change. + * @param {string} prev previous reviewer state. + * @param {string} state current reviewer state. + * @return {string} + */ + GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) { + if (prev === 'REMOVED' || !prev) { + return 'added to ' + state + ': '; + } else if (state === 'REMOVED') { + if (prev) { + return 'removed from ' + prev + ': '; + } else { + return 'removed : '; + } + } else { + return 'moved from ' + prev + ' to ' + state + ': '; + } + }; + + /** + * Groups updates for same category (eg CC->CC) into a hash arrays of + * reviewers. + * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo. + * @return {!Object} Hash of arrays of AccountInfo, message as key. + */ + GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) { + return updates.reduce(function(result, item) { + var message = this._getUpdateMessage(item.prev_state, item.state); + if (!result[message]) { + result[message] = []; + } + result[message].push(item.reviewer); + return result; + }.bind(this), {}); + }; + + /** + * Generates text messages for grouped reviewer updates. + * Formats reviewer updates to a (not yet implemented) EventInfo instance. + * @see https://gerrit-review.googlesource.com/c/94490/ + */ + GrReviewerUpdatesParser.prototype._formatUpdates = function() { + this.result.reviewer_updates.forEach(function(update) { + var grouppedReviewers = this._groupUpdatesByMessage(update.updates); + var newUpdates = []; + for (var message in grouppedReviewers) { + if (grouppedReviewers.hasOwnProperty(message)) { + newUpdates.push({ + message: message, + reviewers: grouppedReviewers[message], + }); + } + } + update.updates = newUpdates; + }.bind(this)); + }; + + window.GrReviewerUpdatesParser = GrReviewerUpdatesParser; + +})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html new file mode 100644 index 0000000..7da5f74 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -0,0 +1,253 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reviewer-updates-parser</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"> + +<script src="../../../scripts/util.js"></script> +<script src="gr-reviewer-updates-parser.js"></script> + +<script> + suite('gr-reviewer-updates-parser tests', function() { + var sandbox; + var instance; + + setup(function() { + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('ignores changes without messages', function() { + var change = {}; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); + + test('ignores changes without reviewer updates', function() { + var change = { + messages: [], + }; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); + + test('ignores changes with empty reviewer updates', function() { + var change = { + messages: [], + reviewer_updates: [], + }; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); + + test('filter removed messages', function() { + var change = { + messages: [ + { + message: 'msg1', + tag: 'autogenerated:gerrit:deleteReviewer', + }, + { + message: 'msg2', + tag: 'foo', + } + ], + }; + instance = new GrReviewerUpdatesParser(change); + instance._filterRemovedMessages(); + assert.deepEqual(instance.result, { + messages: [{ + message: 'msg2', + tag: 'foo', + }], + }); + }); + + test('group reviewer updates', function() { + var reviewer1 = {_account_id: 1}; + var reviewer2 = {_account_id: 2}; + var date1 = '2017-01-26 12:11:50.000000000'; + var date2 = '2017-01-26 12:11:55.000000000'; // Within threshold. + var date3 = '2017-01-26 12:33:50.000000000'; + var date4 = '2017-01-26 12:44:50.000000000'; + var makeItem = function(state, reviewer, opt_date, opt_author) { + return { + reviewer: reviewer, + updated: opt_date || date1, + updated_by: opt_author || reviewer1, + state: state, + }; + }; + var change = { + reviewer_updates: [ + makeItem('REVIEWER', reviewer1), // New group. + makeItem('CC', reviewer2), // Appended. + makeItem('REVIEWER', reviewer2, date2), // Overrides previous one. + + makeItem('CC', reviewer1, date2, reviewer2), // New group. + + makeItem('REMOVED', reviewer2, date3), // Group has no state change. + makeItem('REVIEWER', reviewer2, date3), + + makeItem('CC', reviewer1, date4), // No change, removed. + makeItem('REVIEWER', reviewer1, date4), // Forms new group + makeItem('REMOVED', reviewer2, date4), // Should be grouped. + ], + }; + + instance = new GrReviewerUpdatesParser(change); + instance._groupUpdates(); + change = instance.result; + + assert.equal(change.reviewer_updates.length, 3); + assert.equal(change.reviewer_updates[0].updates.length, 2); + assert.equal(change.reviewer_updates[1].updates.length, 1); + assert.equal(change.reviewer_updates[2].updates.length, 2); + + assert.equal(change.reviewer_updates[0].date, date1); + assert.deepEqual(change.reviewer_updates[0].author, reviewer1); + assert.deepEqual(change.reviewer_updates[0].updates, [ + { + reviewer: reviewer1, + state: 'REVIEWER', + }, + { + reviewer: reviewer2, + state: 'REVIEWER', + }, + ]); + + assert.equal(change.reviewer_updates[1].date, date2); + assert.deepEqual(change.reviewer_updates[1].author, reviewer2); + assert.deepEqual(change.reviewer_updates[1].updates, [ + { + reviewer: reviewer1, + state: 'CC', + prev_state: 'REVIEWER', + }, + ]); + + assert.equal(change.reviewer_updates[2].date, date4); + assert.deepEqual(change.reviewer_updates[2].author, reviewer1); + assert.deepEqual(change.reviewer_updates[2].updates, [ + { + reviewer: reviewer1, + prev_state: 'CC', + state: 'REVIEWER', + }, + { + reviewer: reviewer2, + prev_state: 'REVIEWER', + state: 'REMOVED', + }, + ]); + }); + + test('format reviewer updates', function() { + var reviewer1 = {_account_id: 1}; + var reviewer2 = {_account_id: 2}; + var makeItem = function(prev, state, opt_reviewer) { + return { + reviewer: opt_reviewer || reviewer1, + prev_state: prev, + state: state, + }; + }; + var makeUpdate = function(items) { + return { + author: reviewer1, + updated: '', + updates: items, + }; + }; + var change = { + reviewer_updates: [ + makeUpdate([ + makeItem(undefined, 'CC'), + makeItem(undefined, 'CC', reviewer2) + ]), + makeUpdate([ + makeItem('CC', 'REVIEWER'), + makeItem('REVIEWER', 'REMOVED'), + makeItem('REMOVED', 'REVIEWER'), + makeItem(undefined, 'REVIEWER', reviewer2), + ]), + ], + }; + + instance = new GrReviewerUpdatesParser(change); + instance._formatUpdates(); + + assert.equal(change.reviewer_updates.length, 2); + assert.equal(change.reviewer_updates[0].updates.length, 1); + assert.equal(change.reviewer_updates[1].updates.length, 3); + + var items = change.reviewer_updates[0].updates; + assert.equal(items[0].message, 'added to CC: '); + assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]); + + items = change.reviewer_updates[1].updates; + assert.equal(items[0].message, 'moved from CC to REVIEWER: '); + assert.deepEqual(items[0].reviewers, [reviewer1]); + assert.equal(items[1].message, 'removed from REVIEWER: '); + assert.deepEqual(items[1].reviewers, [reviewer1]); + assert.equal(items[2].message, 'added to REVIEWER: '); + assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]); + }); + }); +</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/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js index ff41a74..1f7f85c 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -57,8 +57,15 @@ }, _getDraftKey: function(location) { - return ['draft', location.changeNum, location.patchNum, location.path, + var range = location.range ? location.range.start_line + '-' + + location.range.start_character + '-' + location.range.end_character + + '-' + location.range.end_line : null; + var key = ['draft', location.changeNum, location.patchNum, location.path, location.line || ''].join(':'); + if (range) { + key = key + ':' + range; + } + return key; }, _cleanupDrafts: function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html index 54e5577..f6c24cb 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -79,6 +79,7 @@ cleanupStorage(); }); + test('automatically removes old drafts', function() { var changeNum = 1234; var patchNum = 5; @@ -90,6 +91,7 @@ path: path, line: line, }; + var key = element._getDraftKey(location); // Make sure that the call to cleanup doesn't get throttled. @@ -113,5 +115,28 @@ cleanupSpy.restore(); cleanupStorage(); }); + + test('_getDraftKey', function() { + var changeNum = 1234; + var patchNum = 5; + var path = 'my_source_file.js'; + var line = 123; + var location = { + changeNum: changeNum, + patchNum: patchNum, + path: path, + line: line, + }; + var expectedResult = 'draft:1234:5:my_source_file.js:123'; + assert.equal(element._getDraftKey(location), expectedResult); + location.range = { + start_character: 1, + start_line: 1, + end_character: 1, + end_line: 2, + }; + expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2'; + assert.equal(element._getDraftKey(location), expectedResult); + }); }); </script>
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/polygerrit_wct_tests.py b/polygerrit-ui/app/polygerrit_wct_tests.py deleted file mode 100644 index eb34fef..0000000 --- a/polygerrit-ui/app/polygerrit_wct_tests.py +++ /dev/null
@@ -1,118 +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. - -from __future__ import print_function - -import atexit -from distutils import spawn -import json -import os -import pkg_resources -import shlex -import shutil -import subprocess -import sys -import tempfile -import unittest -import zipfile - - -def _write_wct_conf(root, exports): - with open(os.path.join(root, 'wct.conf.js'), 'w') as f: - f.write('module.exports = %s;\n' % json.dumps(exports)) - - -def _wct_cmd(): - return ['wct'] + shlex.split(os.environ.get('WCT_ARGS', '')) - - -class PolyGerritWctTests(unittest.TestCase): - - # Should really be setUpClass/tearDownClass, but Buck's test runner doesn't - # produce sane stack traces from those methods. There's only one test method - # anyway, so just use setUp. - - def _check_wct(self): - self.assertTrue( - spawn.find_executable('wct'), - msg='wct not found; try `npm install -g web-component-tester`') - - def _extract_resources(self): - tmpdir = tempfile.mkdtemp() - atexit.register(lambda: shutil.rmtree(tmpdir)) - root = os.path.join(tmpdir, 'polygerrit') - os.mkdir(root) - - tr = 'test_resources.zip' - zip_path = os.path.join(tmpdir, tr) - s = pkg_resources.resource_stream(__name__, tr) - with open(zip_path, 'w') as f: - shutil.copyfileobj(s, f) - - with zipfile.ZipFile(zip_path, 'r') as z: - z.extractall(root) - - return tmpdir, root - - def test_wct(self): - self._check_wct() - tmpdir, root = self._extract_resources() - - cmd = _wct_cmd() - print('Running %s in %s' % (cmd, root), file=sys.stderr) - - _write_wct_conf(root, { - 'suites': ['test'], - 'webserver': { - 'pathMappings': [ - {'/components/bower_components': 'bower_components'}, - ], - }, - 'plugins': { - 'local': { - # 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 - # seems to still work, so there's that. - 'skipSeleniumInstall': True, - }, - 'sauce': { - # 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 - 'disabled': True, - 'browsers': [ - 'OS X 10.11/chrome', - 'Windows 10/chrome', - 'Linux/firefox', - 'OS X 10.11/safari', - 'Windows 10/microsoftedge', - ], - }, - }, - }) - - p = subprocess.Popen(cmd, cwd=root, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - sys.stdout.write(out) - sys.stderr.write(err) - self.assertEquals(0, p.returncode) - - # Only remove tmpdir if successful, to allow debugging. - shutil.rmtree(tmpdir) - - -if __name__ == '__main__': - unittest.main()
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh new file mode 100644 index 0000000..95d30e5 --- /dev/null +++ b/polygerrit-ui/app/run_test.sh
@@ -0,0 +1,24 @@ +#!/bin/bash + +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..6c83905 100644 --- a/polygerrit-ui/app/scripts/util.js +++ b/polygerrit-ui/app/scripts/util.js
@@ -24,22 +24,6 @@ return new Date(dateStr.replace(' ', 'T') + 'Z'); }; - util.htmlEntityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/', - '`': '`', - }; - - util.escapeHTML = function(str) { - return str.replace(/[&<>"'`\/]/g, function(s) { - return util.htmlEntityMap[s]; - }); - }; - util.getCookie = function(name) { var key = name + '='; var cookies = document.cookie.split(';'); @@ -55,5 +39,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..773b341 100644 --- a/polygerrit-ui/app/styles/app-theme.html +++ b/polygerrit-ui/app/styles/app-theme.html
@@ -20,12 +20,17 @@ --selection-background-color: #ebf5fb; --default-text-color: #000; --view-background-color: #fff; - --default-horizontal-margin: 1.25rem; - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --default-horizontal-margin: 1rem; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace; --iron-overlay-backdrop: { transition: none; }; } +@media screen and (max-width: 50em) { + :root { + --default-horizontal-margin: .7rem; + } +} </style>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html index d367a75..544aed8 100644 --- a/polygerrit-ui/app/styles/gr-change-list-styles.html +++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -16,8 +16,8 @@ <dom-module id="gr-change-list-styles"> <template> <style> - .headerRow { - display: flex; + :host { + font-size: 13px; } .topHeader, .groupHeader { @@ -27,7 +27,6 @@ } .topHeader { background-color: #ddd; - flex-shrink: 0; } .noChanges { border-bottom: 1px solid #eee; @@ -35,28 +34,22 @@ } .keyboard, .star { - align-items: center; - display: flex; - justify-content: center; padding: 0; - width: 2em; } - .star { - padding-top: .05em; + gr-change-star { + vertical-align: middle; } .number { - width: 4em; + max-width: 4em; } .subject { - flex-grow: 1; - flex-shrink: 1; word-break: break-word; } .status { - width: 9em; + max-width: 9em; } .owner { - width: 15em; + max-width: 15em; } .project, .branch { @@ -65,26 +58,23 @@ text-overflow: ellipsis; } .project { - width: 10em; + max-width: 10em; } .branch { - width: 7em; + max-width: 7em; } .updated { - width: 9em; + max-width: 9em; text-align: right; } .size { - width: 9em; + max-width: 9em; text-align: right; } .label { - width: 2.6em; + max-width: 2.6em; text-align: center; } - :host { - font-size: 11px; - } @media only screen and (max-width: 50em) { :host { font-size: 14px; @@ -107,9 +97,8 @@ display: none; } .star { - align-items: flex-start; padding-left: .35em; - padding-top: .4em; + padding-top: .25em; } .subject { margin-bottom: .25em; @@ -121,20 +110,7 @@ width: auto; } } - @media only screen and (min-width: 1240px) { - :host { - font-size: 12px; - } - } - @media only screen and (min-width: 1340px) { - :host { - font-size: 13px; - } - } @media only screen and (min-width: 1450px) { - :host { - font-size: 14px; - } .project { width: 20em; }
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css index 39be0ff..6e48ae5 100644 --- a/polygerrit-ui/app/styles/main.css +++ b/polygerrit-ui/app/styles/main.css
@@ -35,6 +35,6 @@ * Work around this using font-size and font-family. */ font-size: 13px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 1.4; }
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index d3cb316..3eddcec 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,15 @@ '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-thread-group/gr-diff-comment-thread-group_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 +68,54 @@ '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-rest-api-interface/gr-reviewer-updates-parser_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..e6f3e0e --- /dev/null +++ b/polygerrit-ui/app/wct_test.sh
@@ -0,0 +1,55 @@ +#!/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' ./polygerrit-ui/app/run_test.sh + +cat <<EOF > $t/wct.conf.js +module.exports = { + 'suites': ['test'], + 'webserver': { + 'pathMappings': [ + {'/components/bower_components': 'bower_components'} + ] + }, + 'plugins': { + 'local': { + 'skipSeleniumInstall': true + }, + 'sauce': { + 'disabled': true, + 'browsers': [ + 'OS X 10.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/run-server.sh b/polygerrit-ui/run-server.sh index e6d782f..603d34a 100755 --- a/polygerrit-ui/run-server.sh +++ b/polygerrit-ui/run-server.sh
@@ -15,22 +15,22 @@ set -eu -while [[ ! -f .buckconfig && "$PWD" != / ]]; do +while [[ ! -f WORKSPACE && "$PWD" != / ]]; do cd .. done -if [[ ! -f .buckconfig ]]; then +if [[ ! -f WORKSPACE ]]; then echo "$(basename "$0"): must be run from a gerrit checkout" 1>&2 exit 1 fi -buck build \ +bazel build \ //polygerrit-ui/app:test_components \ - //polygerrit-ui:fonts + //polygerrit-ui:fonts.zip cd polygerrit-ui/app rm -rf bower_components -unzip -q ../../buck-out/gen/polygerrit-ui/app/test_components/test_components.bower_components.zip +unzip -q ../../bazel-bin/polygerrit-ui/app/test_components.zip rm -rf fonts -unzip -q ../../buck-out/gen/polygerrit-ui/fonts/fonts.zip -d fonts +unzip -q ../../bazel-bin/polygerrit-ui/fonts.zip -d fonts cd .. exec go run server.go "$@"
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/BUCK b/tools/BUCK deleted file mode 100644 index 489dffc..0000000 --- a/tools/BUCK +++ /dev/null
@@ -1,51 +0,0 @@ -python_binary( - name = 'download_file', - main = 'download_file.py', - deps = [':util'], - visibility = ['PUBLIC'], -) - -python_binary( - name = 'merge_jars', - main = 'merge_jars.py', - visibility = ['PUBLIC'], -) - -python_binary( - name = 'pack_war', - main = 'pack_war.py', - deps = [':util'], - visibility = ['PUBLIC'], -) - -python_library( - name = 'util', - srcs = [ - 'util.py', - '__init__.py' - ], - visibility = ['PUBLIC'], -) - -python_test( - name = 'util_test', - srcs = ['util_test.py'], - deps = [':util'], - visibility = ['PUBLIC'], -) - -def shquote(s): - return s.replace("'", "'\\''") - -def os_path(): - from os import environ - return environ.get('PATH') - -genrule( - name = 'buck', - cmd = 'echo buck=`which buck`>$OUT;' + - ("echo PATH=\''%s'\' >>$OUT;" % shquote(os_path())), - out = 'buck.properties', - visibility = ['PUBLIC'], -) -
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/build.defs b/tools/build.defs deleted file mode 100644 index 3ea506c..0000000 --- a/tools/build.defs +++ /dev/null
@@ -1,80 +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. - -# These definitions support building a runnable version of Gerrit. - -DOCS_HTML = '//Documentation:html' -DOCS_LIB = '//Documentation:index_lib' -LIBS = [ - '//gerrit-war:log4j-config', - '//gerrit-war:init', - '//lib:postgresql', - '//lib/log:impl_log4j', -] -PGMLIBS = ['//gerrit-pgm:pgm'] - -def scan_plugins(): - import os - deps = [] - for n in os.listdir('plugins'): - if os.path.exists(os.path.join('plugins', n, 'BUCK')): - deps.append('//plugins/%s:%s__plugin' % (n, n)) - return deps - -def war( - name, - libs = [], - pgmlibs = [], - context = [], - visibility = [], - docs = False - ): - cmd = ['$(exe //tools:pack_war)', '-o', '$OUT', '--tmp', '$TMP'] - for l in libs: - cmd.extend(['--lib', '$(classpath %s)' % l]) - for l in pgmlibs: - cmd.extend(['--pgmlib', '$(classpath %s)' % l]) - - if docs: - cmd.append('$(location %s)' % DOCS_HTML) - cmd.extend(['--lib', '$(classpath %s)' % DOCS_LIB]) - if context: - for t in context: - cmd.append('$(location %s)' % t) - - genrule( - name = name, - cmd = ' '.join(cmd), - out = name + '.war', - visibility = visibility, - ) - -def gerrit_war(name, ui = 'ui_optdbg', context = [], docs = False, visibility = []): - ui_deps = [] - if ui: - if ui == 'polygerrit' or ui == 'ui_optdbg' or ui == 'ui_optdbg_r': - ui_deps.append('//polygerrit-ui/app:polygerrit_ui') - if ui != 'polygerrit': - ui_deps.append('//gerrit-gwtui:%s' % ui) - war( - name = name, - libs = LIBS + ['//gerrit-war:version'], - pgmlibs = PGMLIBS, - context = ui_deps + context + [ - '//gerrit-main:main_bin', - '//gerrit-war:webapp_assets', - ], - docs = docs, - visibility = visibility, - )
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..4dcb9ff 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 -qrD $$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..7acf382 --- /dev/null +++ b/tools/bzl/js.bzl
@@ -0,0 +1,383 @@ +NPMJS = "NPMJS" + +GERRIT = "GERRIT:" + +NPM_VERSIONS = { + "bower": "1.8.0", + "crisper": "2.0.2", + "vulcanize": "1.14.8", +} + +NPM_SHA1S = { + "bower": "55dbebef0ad9155382d9e9d3e497c1372345b44a", + "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", + }, +) +"""Groups a set of bower components together in a zip file. + +Outputs: + NAME-versions.json: + a JSON file containing a PKG-NAME => PKG-NAME#VERSION mapping for the + transitive dependencies. + NAME.zip: + a zip file containing the transitive dependencies for this bundle. +""" + +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..bf47370 --- /dev/null +++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,142 @@ +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] + if ctx.attr.sha1: + args.extend(["-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(), + "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..6e5faf0 --- /dev/null +++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,93 @@ +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 = [], + target_suffix = "", + **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 + target_suffix, + stamp = 1, + srcs = ['%s__non_stamped_deploy.jar' % name], + cmd = " && ".join([ + "GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | 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%s.jar' % (name, target_suffix)], + 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 deleted file mode 100644 index 191dfe5..0000000 --- a/tools/default.defs +++ /dev/null
@@ -1,225 +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. - -# Rule definitions loaded by default into every BUCK file. - -include_defs('//lib/auto/auto_value.defs') -include_defs('//tools/gwt-constants.defs') -include_defs('//tools/java_doc.defs') -include_defs('//tools/java_sources.defs') -include_defs('//tools/git.defs') -import copy -import traceback -import os -from multiprocessing import cpu_count - -# Set defaults on java rules: -# - Add AutoValue annotation processing support. -# - Treat source files as UTF-8. - -_buck_java_library = java_library -def java_library(*args, **kwargs): - _munge_args(kwargs) - _buck_java_library(*args, **kwargs) - -_buck_java_test = java_test -def java_test(*args, **kwargs): - _munge_args(kwargs) - _buck_java_test(*args, **kwargs) - - -# Munge kwargs to set Gerrit-specific defaults. -def _munge_args(kwargs): - _set_auto_value(kwargs) - _set_extra_arguments(kwargs) - -def _set_extra_arguments(kwargs): - ext = 'extra_arguments' - if ext not in kwargs: - kwargs[ext] = [] - extra_args = kwargs[ext] - - for arg in extra_args: - if arg.startswith('-encoding'): - return - - extra_args.extend(['-encoding', 'UTF-8']) - -def _set_auto_value(kwargs): - apk = 'annotation_processors' - if apk not in kwargs: - kwargs[apk] = [] - aps = kwargs.get(apk, []) - - apdk = 'annotation_processor_deps' - if apdk not in kwargs: - kwargs[apdk] = [] - apds = kwargs.get(apdk, []) - - all_deps = kwargs.get('deps', []) + kwargs.get('exported_deps', []) - if AUTO_VALUE_DEP in all_deps: - aps.extend(AUTO_VALUE_PROCESSORS) - apds.extend(AUTO_VALUE_PROCESSOR_DEPS) - - -# Add 'license' argument to genrule. -_buck_genrule = genrule -def genrule(*args, **kwargs): - license = kwargs.pop('license', None) - if license: - license = '//lib:LICENSE-%s' % license - # genrule has no deps attribute, but locations listed in the command show - # up as deps of the target with buck audit. - kwargs['cmd'] = 'true $(location %s); %s' % (license, kwargs['cmd']) - _buck_genrule(*args, **kwargs) - - -def genantlr( - name, - srcs, - out): - genrule( - name = name, - srcs = srcs, - cmd = '$(exe //lib/antlr:antlr-tool) -o $TMP $SRCS;' + - 'cd $TMP;' + - 'zip -qr $OUT .', - out = out, - ) - -def gwt_module(gwt_xml=None, **kwargs): - kw = copy.deepcopy(kwargs) - if 'resources' not in kw: - kw['resources'] = [] - if gwt_xml: - kw['resources'] += [gwt_xml] - if 'srcs' in kw: - kw['resources'] += kw['srcs'] - - # Buck does not accept duplicate resources. Callers may have - # included gwt_xml or srcs as part of resources, so de-dupe. - kw['resources'] = list(set(kw['resources'])) - - java_library(**kw) - -def gerrit_extension( - name, - deps = [], - provided_deps = [], - srcs = [], - resources = [], - manifest_file = None, - manifest_entries = [], - visibility = ['PUBLIC']): - gerrit_plugin( - name = name, - deps = deps, - provided_deps = provided_deps, - srcs = srcs, - resources = resources, - manifest_file = manifest_file, - manifest_entries = manifest_entries, - type = 'extension', - visibility = visibility, - ) - -def gerrit_plugin( - name, - deps = [], - provided_deps = [], - srcs = [], - resources = [], - gwt_module = None, - manifest_file = None, - manifest_entries = [], - type = 'plugin', - visibility = ['PUBLIC'], - target_suffix = ''): - tb = traceback.extract_stack() - calling_BUCK_file = tb[-2][0] - calling_BUCK_dir = os.path.abspath(os.path.dirname(calling_BUCK_file)) - mf_cmd = 'v=%s;' % git_describe(calling_BUCK_dir) - if manifest_file: - mf_src = [manifest_file] - mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT' - else: - mf_src = [] - mf_cmd += 'echo "Manifest-Version: 1.0" >$OUT;' - mf_cmd += 'echo "Gerrit-ApiType: %s" >>$OUT;' % type - mf_cmd += 'echo "Implementation-Version: $v" >>$OUT;' - mf_cmd += 'echo "Implementation-Vendor: Gerrit Code Review" >>$OUT' - for line in manifest_entries: - line = line.replace('$', '\$') - mf_cmd += ';echo "%s" >> $OUT' % line - genrule( - name = name + '__manifest', - cmd = mf_cmd, - srcs = mf_src, - out = 'MANIFEST.MF', - ) - static_jars = [] - if gwt_module: - static_jars = [':%s-static-jar' % name] - java_library( - name = name + '__plugin', - srcs = srcs, - resources = resources, - deps = deps, - provided_deps = ['//gerrit-%s-api:lib' % type] + - provided_deps + - GWT_PLUGIN_DEPS, - visibility = ['PUBLIC'], - ) - if gwt_module: - java_library( - name = name + '__gwt_module', - srcs = [], - resources = list(set(srcs + resources)), - deps = deps, - provided_deps = GWT_PLUGIN_DEPS, - visibility = ['PUBLIC'], - ) - prebuilt_jar( - name = '%s-static-jar' % name, - binary_jar = ':%s-static' % name, - ) - genrule( - name = '%s-static' % name, - cmd = 'mkdir -p $TMP/static' + - ';unzip -qd $TMP/static $(location %s)' % - ':%s__gwt_application' % name + - ';cd $TMP' + - ';zip -qr $OUT .', - out = '%s-static.zip' % name, - ) - gwt_binary( - name = name + '__gwt_application', - modules = [gwt_module], - deps = GWT_PLUGIN_DEPS + GWT_TRANSITIVE_DEPS + ['//lib/gwt:dev'], - module_deps = [':%s__gwt_module' % name], - local_workers = cpu_count(), - strict = True, - experimental_args = GWT_COMPILER_ARGS, - vm_args = GWT_JVM_ARGS, - ) - - java_binary( - name = name + target_suffix, - manifest_file = ':%s__manifest' % name, - merge_manifests = False, - deps = [ - ':%s__plugin' % name, - ] + static_jars, - visibility = visibility, - )
diff --git a/tools/download_all.py b/tools/download_all.py deleted file mode 100755 index 58316ca..0000000 --- a/tools/download_all.py +++ /dev/null
@@ -1,44 +0,0 @@ -#!/usr/bin/env python -# 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. - -from optparse import OptionParser -import re -from subprocess import check_call, CalledProcessError, Popen, PIPE - -MAIN = ['//tools/eclipse:classpath'] -PAT = re.compile(r'"(//.*?)" -> "//tools:download_file"') - -opts = OptionParser() -opts.add_option('--src', action='store_true') -args, _ = opts.parse_args() - -targets = set() - -p = Popen(['buck', 'audit', 'classpath', '--dot'] + MAIN, stdout = PIPE) -for line in p.stdout: - m = PAT.search(line) - if m: - n = m.group(1) - if args.src and n.endswith('__download_bin'): - n = n[:-13] + 'src' - targets.add(n) -r = p.wait() -if r != 0: - exit(r) - -try: - check_call(['buck', 'build'] + sorted(targets)) -except CalledProcessError as err: - exit(1)
diff --git a/tools/download_file.py b/tools/download_file.py index bd67b50..8f5aa7c 100755 --- a/tools/download_file.py +++ b/tools/download_file.py
@@ -25,11 +25,8 @@ from zipfile import ZipFile, BadZipfile, LargeZipFile GERRIT_HOME = path.expanduser('~/.gerritcodereview') +# TODO(davido): Rename in bazel-cache 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 +75,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 +85,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': + if n == 'WORKSPACE': 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 +104,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 deleted file mode 100644 index 0bcde9d..0000000 --- a/tools/eclipse/BUCK +++ /dev/null
@@ -1,31 +0,0 @@ -include_defs('//tools/build.defs') - -java_library( - name = 'classpath', - deps = LIBS + PGMLIBS + [ - '//gerrit-acceptance-tests:lib', - '//gerrit-gpg:gpg_tests', - '//gerrit-gwtdebug:gwtdebug', - '//gerrit-gwtui:ui_module', - '//gerrit-gwtui:ui_tests', - '//gerrit-httpd:httpd_tests', - '//gerrit-main:main_lib', - '//gerrit-patch-jgit:jgit_patch_tests', - '//gerrit-plugin-gwtui:gwtui-api-lib', - '//gerrit-reviewdb:client_tests', - '//gerrit-server:server', - '//gerrit-server:server_tests', - '//lib/asciidoctor:asciidoc_lib', - '//lib/asciidoctor:doc_indexer_lib', - '//lib/auto:auto-value', - '//lib/bouncycastle:bcprov', - '//lib/bouncycastle:bcpg', - '//lib/bouncycastle:bcpkix', - '//lib/gwt:javax-validation', - '//lib/gwt:javax-validation_src', - '//lib/jetty:servlets', - '//lib/prolog:compiler_lib', - '//polygerrit-ui:polygerrit_components', - '//Documentation:index_lib', - ] + scan_plugins(), -)
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..a7ddf6f 100755 --- a/tools/eclipse/project.py +++ b/tools/eclipse/project.py
@@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2013 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. @@ -16,46 +16,62 @@ # 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 path -from subprocess import Popen, PIPE, CalledProcessError, check_call +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'] +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.7', + '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, '.buckconfig')): +while not path.exists(path.join(ROOT, 'WORKSPACE')): ROOT = path.dirname(ROOT) opts = OptionParser() -opts.add_option('--src', action='store_true', - help='(deprecated) attach sources') -opts.add_option('--no-src', dest='no_src', action='store_true', - help='do not attach sources') 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 _query_classpath(targets): - deps = [] - p = Popen(['buck', 'audit', 'classpath'] + targets, stdout=PIPE) - for line in p.stdout: - deps.append(line.strip()) - s = p.wait() - if s != 0: - exit(s) - return deps +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') @@ -63,7 +79,7 @@ print("""\ <?xml version="1.0" encoding="UTF-8"?> <projectDescription> - <name>""" + name + """</name> + <name>%(name)s</name> <buildSpec> <buildCommand> <name>org.eclipse.jdt.core.javabuilder</name> @@ -73,27 +89,27 @@ <nature>org.eclipse.jdt.core.javanature</nature> </natures> </projectDescription>\ -""", file=fd) + """ % {"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 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"/> </classpath>""" % {"testpath": testpath}, file=fd) -def gen_classpath(): +def gen_classpath(ext): def make_classpath(): impl = minidom.getDOMImplementation() return impl.createDocument(None, 'classpath', None) @@ -101,6 +117,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) @@ -118,24 +139,31 @@ plugins = set() # Classpath entries are absolute for cross-cell support - java_library = re.compile('.*/buck-out/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$') + 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 - if 'buck-out/gen/lib/gwt/' in p: - # gwt_module() depends on huge shaded GWT JARs that import - # incorrect versions of classes for Gerrit. Collect into - # a private grouping for later use. - 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): @@ -174,14 +202,27 @@ for libs in [lib, gwt_lib]: for j in sorted(libs): s = None - if j.endswith('.jar'): - s = j[:-4] + '_src.jar' - if not path.exists(s): - 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): @@ -204,12 +245,12 @@ print('error generating project for %s: %s' % (plugin, err), file=sys.stderr) -def gen_factorypath(): +def gen_factorypath(ext): doc = minidom.getDOMImplementation().createDocument(None, 'factorypath', None) - for jar in _query_classpath(['//lib/auto:auto-value']): + for jar in _query_classpath(AUTO): e = doc.createElement('factorypathentry') e.setAttribute('kind', 'EXTJAR') - e.setAttribute('id', path.join(ROOT, jar)) + e.setAttribute('id', path.join(ext, jar)) e.setAttribute('enabled', 'true') e.setAttribute('runInBatchMode', 'false') doc.documentElement.appendChild(e) @@ -219,20 +260,20 @@ doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8') try: - if not args.no_src: - try: - check_call([path.join(ROOT, 'tools', 'download_all.py'), '--src']) - except CalledProcessError as err: - exit(1) - + ext_location = retrieve_ext_location() gen_project(args.project_name) - gen_classpath() - gen_factorypath() + 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: - targets = ['//tools:buck'] + MAIN + GWT - check_call(['buck', 'build', '--deep'] + targets) - except CalledProcessError as err: + 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)
diff --git a/tools/git.defs b/tools/git.defs deleted file mode 100644 index 859f173..0000000 --- a/tools/git.defs +++ /dev/null
@@ -1,26 +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. - -def git_describe(directory = None): - import subprocess - cmd = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty'] - if not directory: - p = subprocess.Popen(cmd, stdout = subprocess.PIPE) - else: - p = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = directory) - v = p.communicate()[0].strip() - r = p.returncode - if r != 0: - raise subprocess.CalledProcessError(r, ' '.join(cmd)) - return v
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs deleted file mode 100644 index 8bafddb..0000000 --- a/tools/gwt-constants.defs +++ /dev/null
@@ -1,24 +0,0 @@ -GWT_JVM_ARGS = ['-Xmx512m'] - -GWT_COMPILER_ARGS = [ - '-XdisableClassMetadata', -] - -GWT_COMPILER_ARGS_RELEASE_MODE = GWT_COMPILER_ARGS + [ - '-XdisableCastChecking', -] - -GWT_PLUGIN_DEPS = [ - '//gerrit-plugin-gwtui:gwtui-api-lib', - '//lib/gwt:user', -] - -GWT_TRANSITIVE_DEPS = [ - '//lib/gwt:javax-validation', - '//lib/gwt:javax-validation_src', - '//lib/ow2:ow2-asm', - '//lib/ow2:ow2-asm-analysis', - '//lib/ow2:ow2-asm-commons', - '//lib/ow2:ow2-asm-tree', - '//lib/ow2:ow2-asm-util', -]
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 deleted file mode 100644 index 41a8730..0000000 --- a/tools/java_doc.defs +++ /dev/null
@@ -1,40 +0,0 @@ -def java_doc( - name, - title, - pkgs, - paths, - 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') - genrule( - name = name, - cmd = ' '.join([ - 'while ! test -f .buckconfig; do cd ..; done;', - 'javadoc', - '-quiet', - '-protected', - '-encoding UTF-8', - '-charset UTF-8', - '-notimestamp', - '-windowtitle "' + title + '"', - ' '.join(['-link %s' % url for url in external_docs]), - '-subpackages ', - ':'.join(pkgs), - '-sourcepath ', - ':'.join(sourcepath), - ' -classpath ', - ':'.join(['$(classpath %s)' % n for n in deps]), - '-d $TMP', - ]) + ';jar cf $OUT -C $TMP .', - srcs = srcs, - out = name + '.jar', - visibility = visibility, -)
diff --git a/tools/java_sources.defs b/tools/java_sources.defs deleted file mode 100644 index 0b3974e..0000000 --- a/tools/java_sources.defs +++ /dev/null
@@ -1,10 +0,0 @@ -def java_sources( - name, - srcs, - visibility = [] - ): - java_library( - name = name, - resources = srcs, - 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 deleted file mode 100644 index ba4f19c..0000000 --- a/tools/js/BUCK +++ /dev/null
@@ -1,20 +0,0 @@ -python_binary( - name = 'bower2buck', - main = 'bower2buck.py', - deps = ['//tools:util'], - visibility = ['PUBLIC'], -) - -python_binary( - name = 'download_bower', - main = 'download_bower.py', - deps = ['//tools:util'], - visibility = ['PUBLIC'], -) - -python_binary( - name = 'run_npm_binary', - main = 'run_npm_binary.py', - deps = ['//tools:util'], - 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..d12129e --- /dev/null +++ b/tools/js/bower2bazel.py
@@ -0,0 +1,231 @@ +#!/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 + +# list of licenses for packages that don't specify one in their bower.json file. +package_licenses = { + "es6-promise": "es6-promise", + "fetch": "fetch", + "iron-a11y-announcer": "polymer", + "iron-a11y-keys-behavior": "polymer", + "iron-autogrow-textarea": "polymer", + "iron-behaviors": "polymer", + "iron-dropdown": "polymer", + "iron-fit-behavior": "polymer", + "iron-flex-layout": "polymer", + "iron-form-element-behavior": "polymer", + "iron-input": "polymer", + "iron-meta": "polymer", + "iron-overlay-behavior": "polymer", + "iron-resizable-behavior": "polymer", + "iron-selector": "polymer", + "iron-validatable-behavior": "polymer", + "moment": "moment", + "neon-animation": "polymer", + "page": "page.js", + "polymer": "polymer", + "promise-polyfill": "promise-polyfill", + "web-animations-js": "Apache2.0", + "webcomponentsjs": "polymer", +} + + +def build_bower_json(version_targets, seeds): + """Generate bower JSON file, return its path. + + Args: + version_targets: bazel target names of the versions.json file. + seeds: an iterable of bower package names of the seed packages, ie. + the packages whose versions we control manually. + """ + bower_json = collections.OrderedDict() + bower_json['name'] = 'bower2bazel-output' + bower_json['version'] = '0.0.0' + bower_json['description'] = 'Auto-generated bower.json for dependency management' + bower_json['private'] = True + bower_json['dependencies'] = {} + + seeds = set(seeds) + 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[""] + + trimmed = {} + for k, v in j.items(): + if k in seeds: + trimmed[k] = v + + bower_json['dependencies'].update(trimmed) + + 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, seeds) + 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\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\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 = package_licenses.get(pkg_name, "DO_NOT_DISTRIBUTE") + + 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 deleted file mode 100755 index 81072da..0000000 --- a/tools/js/bower2buck.py +++ /dev/null
@@ -1,214 +0,0 @@ -#!/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. - -from __future__ import print_function - -import atexit -import collections -import json -import hashlib -import optparse -import os -import shutil -import subprocess -import sys -import tempfile - -from tools import util - - -# This script is run with `buck run`, but needs to shell out to buck; this is -# only possible if we avoid buckd. -BUCK_ENV = dict(os.environ) -BUCK_ENV['NO_BUCKD'] = '1' - -HEADER = """\ -include_defs('//lib/js.defs') - -# AUTOGENERATED BY BOWER2BUCK -# -# This file should be merged with an existing BUCK file containing these rules. -# -# This comment SHOULD NOT be copied to the existing BUCK file, and you should -# leave alone any non-bower_component contents of the file. -# -# Generally, the following attributes SHOULD be copied from this file to the -# existing BUCK file: -# - package: the normalized package name -# - version: the exact version number -# - deps: direct dependencies of the package -# - sha1: a hash of the package contents -# -# The following fields SHOULD NOT be copied to the existing BUCK file: -# - semver: manually-specified semantic version, not included in autogenerated -# output. -# -# The following fields require SPECIAL HANDLING: -# - license: all licenses in this file are specified as TODO. You must replace -# this text with one of the existing licenses defined in lib/BUCK, or -# define a new one if necessary. Leave existing licenses alone. - -""" - - -def usage(): - print(('Usage: %s -o <outfile> [//path/to:bower_components_rule...]' - % sys.argv[0]), - file=sys.stderr) - return 1 - - -class Rule(object): - def __init__(self, bower_json_path): - with open(bower_json_path) as f: - bower_json = json.load(f) - self.name = bower_json['name'] - 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( - hashlib.sha1(), os.path.dirname(bower_json_path)).hexdigest() - - def to_rule(self, packages): - if self.name not in packages: - raise ValueError('No package name found for %s' % self.name) - - lines = [ - 'bower_component(', - " name = '%s'," % self.name, - " package = '%s'," % packages[self.name], - " version = '%s'," % self.version, - ] - if self.deps: - if len(self.deps) == 1: - lines.append(" deps = [':%s']," % next(self.deps.iterkeys())) - else: - lines.append(' deps = [') - lines.extend(" ':%s'," % d for d in sorted(self.deps.iterkeys())) - lines.append(' ],') - lines.extend([ - " license = 'TODO: %s'," % self.license, - " sha1 = '%s'," % self.sha1, - ')']) - return '\n'.join(lines) - - -def build_bower_json(targets, buck_out): - 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'] = {} - - deps = subprocess.check_output( - ['buck', 'query', '-v', '0', - "filter('__download_bower', deps(%s))" % '+'.join(targets)], - env=BUCK_ENV) - deps = deps.replace('__download_bower', '__bower_version').split() - subprocess.check_call(['buck', 'build'] + deps, env=BUCK_ENV) - - for dep in deps: - dep = dep.replace(':', '/').lstrip('/') - depout = os.path.basename(dep) - version_json = os.path.join(buck_out, 'gen', dep, depout) - with open(version_json) as f: - bower_json['dependencies'].update(json.load(f)) - - tmpdir = tempfile.mkdtemp() - atexit.register(lambda: shutil.rmtree(tmpdir)) - ret = os.path.join(tmpdir, 'bower.json') - with open(ret, 'w') as f: - json.dump(bower_json, f, indent=2) - return ret - - -def get_package_name(name, package_version): - v = package_version.lower() - if '#' in v: - return v[:v.find('#')] - return name - - -def get_packages(path): - with open(path) as f: - bower_json = json.load(f) - return dict((n, get_package_name(n, v)) - for n, v in bower_json.get('dependencies', {}).iteritems()) - - -def collect_rules(packages): - # TODO(dborowitz): Use run_npm_binary instead of system bower. - rules = {} - subprocess.check_call(['bower', 'install']) - for dirpath, dirnames, filenames in os.walk('.', topdown=True): - if '.bower.json' not in filenames: - continue - del dirnames[:] - rule = Rule(os.path.join(dirpath, '.bower.json')) - rules[rule.name] = rule - - # Oddly, the package name referred to in the deps section of dependents, - # e.g. 'PolymerElements/iron-ajax', is not found anywhere in this - # bower.json, which only contains 'iron-ajax'. Build up a map of short name - # to package name so we can resolve them later. - # TODO(dborowitz): We can do better: - # - Infer 'user/package' from GitHub URLs (i.e. a simple subset of Bower's package - # resolution logic). - # - Resolve aliases using https://bower.herokuapp.com/packages/shortname - # (not currently biting us but it might in the future.) - for n, v in rule.deps.iteritems(): - p = get_package_name(n, v) - old = packages.get(n) - if old is not None and old != p: - raise ValueError('multiple packages named %s: %s != %s' % (n, p, old)) - packages[n] = p - - return rules - - -def find_buck_out(): - dir = os.getcwd() - while not os.path.isfile(os.path.join(dir, '.buckconfig')): - dir = os.path.dirname(dir) - return os.path.join(dir, 'buck-out') - - -def main(args): - opts = optparse.OptionParser() - opts.add_option('-o', help='output file location') - opts, args = opts.parse_args() - - if not opts.o or not all(a.startswith('//') for a in args): - return usage() - outfile = os.path.abspath(opts.o) - buck_out = find_buck_out() - - targets = args if args else ['//polygerrit-ui/...'] - bower_json_path = build_bower_json(targets, buck_out) - os.chdir(os.path.dirname(bower_json_path)) - packages = get_packages(bower_json_path) - rules = collect_rules(packages) - - with open(outfile, 'w') as f: - f.write(HEADER) - for _, r in sorted(rules.iteritems()): - f.write('\n\n%s' % r.to_rule(packages)) - - print('Wrote bower_components rules to:\n %s' % outfile) - - -if __name__ == '__main__': - main(sys.argv[1:])
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 deleted file mode 100644 index 322b5a2..0000000 --- a/tools/maven/BUCK +++ /dev/null
@@ -1,33 +0,0 @@ -include_defs('//VERSION') -include_defs('//tools/maven/package.defs') -include_defs('//tools/maven/repository.defs') - -if GERRIT_VERSION.endswith('-SNAPSHOT'): - URL = MAVEN_SNAPSHOT_URL -else: - URL = MAVEN_RELEASE_URL - -maven_package( - repository = MAVEN_REPOSITORY, - url = URL, - version = GERRIT_VERSION, - jar = { - 'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework', - 'gerrit-extension-api': '//gerrit-extension-api:extension-api', - 'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api', - 'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api', - }, - src = { - 'gerrit-acceptance-framework': '//gerrit-acceptance-framework:acceptance-framework-src', - 'gerrit-extension-api': '//gerrit-extension-api:extension-api-src', - 'gerrit-plugin-api': '//gerrit-plugin-api:plugin-api-src', - 'gerrit-plugin-gwtui': '//gerrit-plugin-gwtui:gwtui-api-src', - }, - 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', - }, - war = {'gerrit-war': '//:release'}, -)
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..92fc0dd 100755 --- a/tools/maven/api.sh +++ b/tools/maven/api.sh
@@ -14,10 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -if [[ "$#" == "0" ]] ; then +if [[ "$#" != "1" ]] ; then cat <<EOF -Usage: run "$0 COMMAND" from the top of your workspace, where -COMMAND is one of +Usage: run "$0 COMMAND" from the top of your workspace, +where COMMAND is one of install deploy @@ -34,7 +34,6 @@ set -o errexit set -o nounset - case "$1" in install) command="api_install" @@ -58,12 +57,7 @@ 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 ; } +bazel build //tools/maven:gen_${command} || \ + { echo "bazel 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} +./bazel-genfiles/tools/maven/${command}.sh
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py index 4011d71..f7b5aa8 100755 --- a/tools/maven/mvn.py +++ b/tools/maven/mvn.py
@@ -33,7 +33,7 @@ exit(1) root = path.abspath(__file__) -while not path.exists(path.join(root, '.buckconfig')): +while not path.exists(path.join(root, 'WORKSPACE')): root = path.dirname(root) if 'install' == args.a:
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 deleted file mode 100644 index c412ebd..0000000 --- a/tools/maven/package.defs +++ /dev/null
@@ -1,95 +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. - -sh_bang_template = (' && '.join([ - "echo '#!/bin/bash -eu' > $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 "" >> $OUT', - 'echo %s >> $OUT', - 'echo "" >> $OUT', - 'echo %s >> $OUT', - # This is supposed to be handled by executable=True, but it doesn't - # work. Bug? - 'chmod +x $OUT' ])) - -def maven_package( - version, - repository = None, - url = None, - jar = {}, - src = {}, - doc = {}, - war = {}): - - build_cmd = ['buck', 'build'] - - # This is not using python_binary() to avoid the baggage and bugs - # that PEX brings along. - 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.iteritems()): - api_cmd.append('-s %s:%s:$(location %s)' % (a,type,t)) - api_targets.append(t) - - genrule( - name = 'gen_api_install', - cmd = sh_bang_template % ( - ' '.join(build_cmd + api_targets), - ' '.join(api_cmd + ['-a', 'install'])), - out = 'api_install.sh', - executable = True, - ) - - if repository and url: - genrule( - name = 'gen_api_deploy', - cmd = sh_bang_template % ( - ' '.join(build_cmd + api_targets), - ' '.join(api_cmd + ['-a', 'deploy', - '--repository', repository, - '--url', url])), - out = 'api_deploy.sh', - executable = True, - ) - - war_cmd = mvn_cmd[:] - war_targets = [] - for a,t in sorted(war.iteritems()): - war_cmd.append('-s %s:war:$(location %s)' % (a,t)) - war_targets.append(t) - - genrule( - name = 'gen_war_install', - cmd = sh_bang_template % (' '.join(build_cmd + war_targets), - ' '.join(war_cmd + ['-a', 'install'])), - out = 'war_install.sh', - executable = True, - ) - - if repository and url: - genrule( - name = 'gen_war_deploy', - cmd = sh_bang_template % ( - ' '.join(build_cmd + war_targets), - ' '.join(war_cmd + [ - '-a', 'deploy', - '--repository', repository, - '--url', url])), - out = 'war_deploy.sh', - executable = True, - )
diff --git a/tools/maven/repository.defs b/tools/maven/repository.defs deleted file mode 100644 index c4e8fbf..0000000 --- a/tools/maven/repository.defs +++ /dev/null
@@ -1,3 +0,0 @@ -MAVEN_REPOSITORY = 'sonatype-nexus-staging' -MAVEN_SNAPSHOT_URL = 'https://oss.sonatype.org/content/repositories/snapshots' -MAVEN_RELEASE_URL = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
diff --git a/tools/pack_war.py b/tools/pack_war.py deleted file mode 100755 index ca21790..0000000 --- a/tools/pack_war.py +++ /dev/null
@@ -1,55 +0,0 @@ -#!/usr/bin/env python -# 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. - -from __future__ import print_function -from optparse import OptionParser -from os import makedirs, path, symlink -from subprocess import check_call -import sys - -opts = OptionParser() -opts.add_option('-o', help='path to write WAR to') -opts.add_option('--lib', action='append', help='target for WEB-INF/lib') -opts.add_option('--pgmlib', action='append', help='target for WEB-INF/pgm-lib') -opts.add_option('--tmp', help='temporary directory') -args, ctx = opts.parse_args() - -war = args.tmp -jars = set() - -def prune(l): - return [j for e in l for j in e.split(':')] - -def link_jars(libs, directory): - makedirs(directory) - for j in libs: - if j not in jars: - jars.add(j) - n = path.basename(j) - if j.find('buck-out/gen/gerrit-') > 0: - n = j[j.find('buck-out'):].split('/')[2] + '-' + n - symlink(j, path.join(directory, n)) - -if args.lib: - link_jars(prune(args.lib), path.join(war, 'WEB-INF', 'lib')) -if args.pgmlib: - link_jars(prune(args.pgmlib), path.join(war, 'WEB-INF', 'pgm-lib')) -try: - for s in ctx: - check_call(['unzip', '-q', '-d', war, s]) - check_call(['zip', '-9qr', args.o, '.'], cwd=war) -except KeyboardInterrupt: - print('Interrupted by user', file=sys.stderr) - exit(1)
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..1ef91b8 --- /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.bzl b/version.bzl new file mode 100644 index 0000000..24a3c27 --- /dev/null +++ b/version.bzl
@@ -0,0 +1,5 @@ +# Maven style API version (e.g. '2.x-SNAPSHOT'). +# Used by :api_install and :api_deploy targets +# when talking to the destination repository. +# +GERRIT_VERSION = "2.14-SNAPSHOT"