Merge branch 'stable-2.12'
* stable-2.12:
Document that ldap.groupBase and ldap.accountBase are repeatable
Put Change-Id after Test: footers in commit messages.
Remove bucklets/local_jar.bucklet soft-link to removed lib/local.defs
Normalize case of {Author,Committer}Predicate
OAuth-Linking: Don't create new account when claimed identity unknown
Update 2.11.5 release notes to mention forked buck
Revert "Update buck to ba9f239f69287a553ca93af76a27484d83693563"
Change-Id: I46c53b5c43ecbdc4d63cb03da25c35737b2c5afd
diff --git a/.buckconfig b/.buckconfig
index 51318f3..c766d30 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -8,7 +8,9 @@
docs = //Documentation:searchfree
firefox = //:firefox
gerrit = //:gerrit
+ gwtgerrit = //:gwtgerrit
headless = //:headless
+ polygerrit = //:polygerrit
release = //:release
safari = //:safari
soyc = //gerrit-gwtui:ui_soyc
@@ -22,8 +24,11 @@
src_roots = java, resources
[project]
- ignore = .git
+ ignore = .git, eclipse-out
[cache]
mode = dir
dir = ~/.gerritcodereview/buck-cache/locally-built-artifacts
+
+[test]
+ excluded_labels = manual
diff --git a/.buckversion b/.buckversion
index 9daac2c..b2b427a 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-1b03b4313b91b634bd604fc3487a05f877e59dee
+75b74ccf90a590b284b1a1553dc48af8844a9ca7
diff --git a/.gitignore b/.gitignore
index 32a1826..ed6b163 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
/.buckd
/buck-cache
/buck-out
+/eclipse-out
/extras
/local.properties
*.pyc
diff --git a/BUCK b/BUCK
index c986874..9657ff3 100644
--- a/BUCK
+++ b/BUCK
@@ -1,10 +1,12 @@
include_defs('//tools/build.defs')
gerrit_war(name = 'gerrit')
-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 = '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:'])
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 126bf1f..62c0e07 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -1,12 +1,16 @@
include_defs('//Documentation/asciidoc.defs')
include_defs('//Documentation/config.defs')
+include_defs('//Documentation/license.defs')
include_defs('//tools/git.defs')
DOC_DIR = 'Documentation'
-JSUI = '//gerrit-gwtui:ui_module'
-MAIN = '//gerrit-pgm:pgm'
+
+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',
@@ -28,21 +32,20 @@
visibility = ['PUBLIC'],
)
-genrule(
+genlicenses(
name = 'licenses.txt',
- cmd = '$(exe :gen_licenses) --asciidoc '
- + '--classpath $(classpath %s) ' % MAIN
- + '--classpath $(classpath %s) ' % JSUI
- + MAIN + ' ' + JSUI + ' >$OUT',
+ 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.
-genrule(
+genlicenses(
name = 'js_licenses.txt',
- cmd = '$(exe :gen_licenses) --partial '
- + '--classpath $(classpath %s) ' % JSUI
- + JSUI + ' >$OUT',
+ opts = ['--partial'],
+ java_deps = JSUI_JAVA_DEPS,
+ non_java_deps = JSUI_NON_JAVA_DEPS,
out = 'js_licenses.txt',
)
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 79f7651..d5b4908 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -10,17 +10,16 @@
[--add <REVIEWER> ... | -a <REVIEWER> ...]
[--remove <REVIEWER> ... | -r <REVIEWER> ...]
[--]
- {COMMIT | CHANGE-ID}...
+ {CHANGE-ID}...
--
== DESCRIPTION
Adds or removes reviewers to the specified change, sending email
notifications when changes are made.
-Changes should be specified as complete or abbreviated Change-Ids
-such as 'Iac6b2ac2'. They may also be specified by numeric change
-identifiers, such as '8242' or by complete or abbreviated commit
-SHA-1s.
+Changes can be specified in the
+link:rest-api-changes.html#change-id[same format] supported by the REST
+API.
== OPTIONS
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 4306fdb..df9ec26 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -145,7 +145,7 @@
The configured <<ldap.username,ldap.username>> identity is not used to obtain
account information.
+
-* OAUTH
+* `OAUTH`
+
OAuth is a protocol that lets external apps request authorization to private
details in a user's account without getting their password. This is
@@ -437,9 +437,9 @@
[[auth.gitBasicAuth]]auth.gitBasicAuth::
+
If true then Git over HTTP and HTTP/S traffic is authenticated using
-standard BasicAuth and the credentials are validated against the randomly
-generated HTTP password or against LDAP when it is configured as Gerrit
-Web UI authentication method.
+standard BasicAuth. Depending on the configured `auth.type` credentials
+are validated against the randomly generated HTTP password, against LDAP
+(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
+
This parameter affects git over HTTP traffic and access to the REST
API. If set to false then Gerrit will authenticate through DIGEST
@@ -449,8 +449,30 @@
When `auth.type` is `LDAP`, service users that only exist in the Gerrit
database are still authenticated by their HTTP passwords.
+
+When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
+instead of passwords in the Basic authentication header. Note that provider
+specific plugins must be installed to facilitate this authentication scheme.
+If multiple OAuth 2 provider plugins are installed one of them must be
+selected as default with the `auth.gitOAuthProvider` option.
++
By default this is set to false.
+[[auth.gitOAuthProvider]]auth.gitOAuthProvider::
++
+Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
++
+In general there is no way to determine from an access token alone, which
+OAuth 2 provider to address to verify that token, and the BasicAuth
+scheme does not support amending such details. If multiple OAuth provider
+plugins in a system offer support for git over HTTP authentication site
+administrators must configure, which one to use as default provider.
+In case the provider cannot be determined from a request the access token
+will be sent to the default provider for verification.
++
+The value of this parameter must be the identifier of an OAuth 2 provider
+in the form `plugin-name:provider-name`. Consult the respective plugin
+documentation for details.
+
[[auth.userNameToLowerCase]]auth.userNameToLowerCase::
+
If set the username that is received to authenticate a git operation
@@ -1414,7 +1436,8 @@
httpd and sshd threads as some request processing code paths may
need multiple connections.
+
-Default is 8.
+Default is <<sshd.threads, sshd.threads>>
+ + <<httpd.maxThreads, httpd.maxThreads>> + 2.
+
This setting only applies if
<<database.connectionPool,database.connectionPool>> is true.
@@ -1432,7 +1455,7 @@
Maximum number of connections to keep idle in the pool. If there
are more idle connections, connections will be closed instead of
being returned back to the pool.
-Default is 4.
+Default is min(<<database.poolLimit, database.poolLimit>>, 16).
+
This setting only applies if
<<database.connectionPool,database.connectionPool>> is true.
@@ -3150,6 +3173,28 @@
+
Default is 10x reductionLimit (1,000,000).
+[[rules.maxSourceBytes]]rules.maxSourceBytes::
++
+Maximum input size (in bytes) of a Prolog rules.pl file. Larger
+source files may need a larger rules.compileReductionLimit. Consider
+using link:pgm-rulec.html[rulec] to precompile larger rule files.
++
+A size of 0 bytes disables rules, same as rules.enable = false.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
++
+Default is 128 KiB.
+
+[[rules.maxPrologDatabaseSize]]rules.maxPrologDatabaseSize::
++
+Number of predicate clauses allowed to be defined in the Prolog
+database by project rules. Very complex rules may need more than the
+default 256 limit, but cost more memory and may need more time to
+evaluate. Consider using link:pgm-rulec.html[rulec] to precompile
+larger rule files.
++
+Default is 256.
+
[[execution]]
=== Section execution
@@ -3415,7 +3460,7 @@
If additional requests are received while all threads are busy they
are queued and serviced in a first-come-first-served order.
+
-By default, 1.5x the number of CPUs available to the JVM.
+By default, 2x the number of CPUs available to the JVM.
[[sshd.batchThreads]]sshd.batchThreads::
+
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index a40c5f3..9cf4e50 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -217,6 +217,19 @@
The label is purely informational and values are not considered when
determining whether a change is submittable.
+* `PatchSetLock`
++
+The `PatchSetLock` function provides a locking mechanism for patch
+sets. This function's values are not considered when determining
+whether a change is submittable. When set, no new patchsets can be
+created and rebase and abandon are blocked.
++
+This function is designed to allow overlapping locks, so several lock
+accounts could lock the same change.
++
+Allowed range of values are 0 (Patch Set Unlocked) to 1 (Patch Set
+Locked).
+
[[label_copyMinScore]]
=== `label.Label-Name.copyMinScore`
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 6e248c8..37e3eba 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -386,6 +386,27 @@
link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/config.md[
Configuration]
+[[metrics-reporter-elasticsearch]]
+
+This plugin reports Gerrit metrics to Elasticsearch.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[
+Project].
+
+[[metrics-reporter-graphite]]
+
+This plugin reports Gerrit metrics to Graphite.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[
+Project].
+
+[[metrics-reporter-jmx]]
+
+This plugin reports Gerrit metrics to JMX.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[
+Project].
+
[[motd]]
=== motd
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index ec8515f..0cd5e84 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -6,8 +6,8 @@
Note that you need to use Java 7 for building gerrit.
There is currently no binary distribution of Buck, so it has to be manually
-built and installed. Apache Ant is required. Currently only Linux and Mac
-OS are supported.
+built and installed. Apache Ant and gcc are required. Currently only Linux
+and Mac OS are supported.
Clone the git and build it:
@@ -97,7 +97,7 @@
=== Gerrit Development WAR File
-To build the Gerrit web application:
+To build the Gerrit web application that includes GWT UI and PolyGerrit UI:
----
buck build gerrit
@@ -109,6 +109,18 @@
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
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index fdc7fab..13b6978 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@
----
mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
-DarchetypeArtifactId=gerrit-plugin-archetype \
- -DarchetypeVersion=2.12 \
+ -DarchetypeVersion=2.13-SNAPSHOT \
-DgroupId=com.googlesource.gerrit.plugins.testplugin \
-DartifactId=testplugin
----
@@ -587,6 +587,48 @@
$ ssh -p 29418 review.example.com sh ps
----
+[[search_operators]]
+=== Search Operators ===
+
+Plugins can define new search operators to extend change searching by
+implementing the `ChangeQueryBuilder.ChangeOperatorFactory` interface
+and registering it to an operator name in the plugin module's
+`configure()` method. The search operator name is defined during
+registration via the DynamicMap annotation mechanism. The plugin
+name will get appended to the annotated name, with an underscore
+in between, leading to the final operator name. An example
+registration looks like this:
+
+ bind(ChangeOperatorFactory.class)
+ .annotatedWith(Exports.named("sample"))
+ .to(SampleOperator.class);
+
+If this is registered in the `myplugin` plugin, then the resulting
+operator will be named `sample_myplugin`.
+
+The search operator itself is implemented by ensuring that the
+`create()` method of the class implementing the
+`ChangeQueryBuilder.ChangeOperatorFactory` interface returns a
+`Predicate<ChangeData>`. Here is a sample operator factory
+defintion which creates a `MyPredicate`:
+
+[source,java]
+----
+@Singleton
+public class SampleOperator
+ implements ChangeQueryBuilder.ChangeOperatorFactory {
+ public static class MyPredicate extends OperatorPredicate<ChangeData> {
+ ...
+ }
+
+ @Override
+ public Predicate<ChangeData> create(ChangeQueryBuilder builder, String value)
+ throws QueryParseException {
+ return new MyPredicate(value);
+ }
+}
+----
+
[[simple-configuration]]
== Simple Configuration in `gerrit.config`
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index db3480b..47a132c 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -19,8 +19,8 @@
import argparse
from collections import defaultdict, deque
+import json
from os import chdir, path
-import re
from shutil import copyfileobj
from subprocess import Popen, PIPE
from sys import stdout, stderr
@@ -28,7 +28,6 @@
parser = argparse.ArgumentParser()
parser.add_argument('--asciidoc', action='store_true')
parser.add_argument('--partial', action='store_true')
-parser.add_argument('--classpath', action='append')
parser.add_argument('targets', nargs='+')
args = parser.parse_args()
@@ -38,37 +37,35 @@
'//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('..')
- # TODO(davido): use passed in classpath from Buck instead
- p = Popen(
- ['buck', 'audit', 'classpath', '--dot'] + args.targets,
- stdout = PIPE)
- for line in p.stdout:
- m = re.search(r'"(//.*?)" -> "(//.*?)";', line)
- if not m:
- continue
- target, dep = m.group(1), m.group(2)
- if args.partial:
- if dep == '//lib/codemirror:js_minifier':
- if target == '//lib/codemirror:js':
- continue
- if target.startswith('//lib/codemirror:mode_'):
- continue
- if target == '//gerrit-gwtui:ui_module' and \
- dep == '//gerrit-gwtexpui:CSS':
+ 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
- # Dependencies included in provided_deps set are contained in audit
- # classpath and must be sorted out. That's safe thing to do because
- # they are not included in the final artifact.
- if "DO_NOT_DISTRIBUTE" in dep:
- if not target in KNOWN_PROVIDED_DEPS:
- print('DO_NOT_DISTRIBUTE license for target: %s' % target, file=stderr)
- exit(1)
- else:
+ if args.partial:
+ if dep == '//lib/codemirror:js_minifier':
+ if target == '//lib/codemirror:js':
+ continue
+ if target.startswith('//lib/codemirror:mode_'):
+ continue
+ if (target == '//gerrit-gwtui:ui_module'
+ and dep == '//gerrit-gwtexpui:CSS'):
+ continue
+
graph[target].append(dep)
r = p.wait()
if r != 0:
@@ -78,14 +75,27 @@
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:
@@ -156,6 +166,8 @@
p = d[len('//lib:'):]
else:
p = d[d.index(':')+1:].lower()
+ if '__' in p:
+ p = p[:p.index('__')]
print('* ' + p)
if args.asciidoc:
print()
diff --git a/Documentation/license.defs b/Documentation/license.defs
new file mode 100644
index 0000000..42dd3eb
--- /dev/null
+++ b/Documentation/license.defs
@@ -0,0 +1,29 @@
+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/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index ad19046..ee05aa3 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1431,7 +1431,11 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
+ "starred": true,
"mergeable": true,
+ "submittable": false,
+ "insertions": 145,
+ "deletions": 12,
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -1943,6 +1947,12 @@
|`url_aliases` |optional|
A map of URL path pairs, where the first URL path is an alias for the
second URL path.
+|`email_notifications` ||
+The type of email strategy to use. On `ENABLED`, the user will receive emails
+from Gerrit. On `CC_ON_OWN_COMMENTS` the user will also receive emails for
+their own comments. On `DISABLED` the user will not receive any email
+notifications from Gerrit.
+Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
|============================================
[[preferences-input]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 07f0f65..07d3cc3 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -209,8 +209,8 @@
--
* `DETAILED_LABELS`: detailed label information, including numeric
values of all existing approvals, recognized label values, values
- permitted to be set by the current user, and reviewers that may be
- removed by the current user.
+ permitted to be set by the current user, all reviewers by state, and
+ reviewers that may be removed by the current user.
--
[[current-revision]]
@@ -414,35 +414,42 @@
"files": {
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
"lines_deleted": 8,
- "size_delta": -412
+ "size_delta": -412,
+ "size": 7782
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
"lines_inserted": 1,
- "size_delta": 23
+ "size_delta": 23,
+ "size": 6762
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
"lines_inserted": 11,
"lines_deleted": 19,
- "size_delta": -298
+ "size_delta": -298,
+ "size": 47023
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
"lines_inserted": 23,
"lines_deleted": 20,
- "size_delta": 132
+ "size_delta": 132,
+ "size": 17727
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
"status": "D",
"lines_deleted": 139,
- "size_delta": -5512
+ "size_delta": -5512,
+ "size": 13098
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
"status": "A",
"lines_inserted": 204,
- "size_delta": 8345
+ "size_delta": 8345,
+ "size": 8345
},
"gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
"lines_deleted": 9,
- "size_delta": -343
+ "size_delta": -343,
+ "size": 5385
}
}
}
@@ -634,6 +641,22 @@
"username": "jroe"
}
],
+ "reviewers": {
+ "REVIEWER": [
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ },
+ {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com",
+ "username": "jroe"
+ }
+ ]
+ },
"messages": [
{
"id": "YH-egE",
@@ -1197,6 +1220,13 @@
"_account_id": 1000000
}
],
+ "reviewers": {
+ "REVIEWER": [
+ {
+ "_account_id": 1000000
+ }
+ ]
+ },
"current_revision": "9adb9f4c7b40eeee0646e235de818d09164d7379",
"revisions": {
"9adb9f4c7b40eeee0646e235de818d09164d7379": {
@@ -1308,6 +1338,13 @@
"_account_id": 1000000
}
],
+ "reviewers": {
+ "REVIEWER": [
+ {
+ "_account_id": 1000000
+ }
+ ]
+ },
"current_revision": "1bd7c12a38854a2c6de426feec28800623f492c4",
"revisions": {
"1bd7c12a38854a2c6de426feec28800623f492c4": {
@@ -2227,6 +2264,55 @@
HTTP/1.1 204 No Content
----
+[[list-votes]]
+=== List Votes
+--
+'GET /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/'
+--
+
+Lists the votes for a specific reviewer of the change.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ 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-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\}]'
+--
+
+Deletes a single vote from a change. Note, that even when the last vote of
+a reviewer is removed the reviewer itself is still listed on the change.
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/votes/Code-Review HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[revision-endpoints]]
== Revision Endpoints
@@ -2440,6 +2526,20 @@
"email": "jane.roe@example.com"
}
],
+ "reviewers": {
+ "REVIEWER": [
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ },
+ {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com"
+ }
+ ]
+ },
"current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
"revisions": {
"674ac754f91e64a0efb8087e59a176484bd534d1": {
@@ -3270,12 +3370,14 @@
"/COMMIT_MSG": {
"status": "A",
"lines_inserted": 7,
- "size_delta": 551
+ "size_delta": 551,
+ "size": 551
},
"gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
"lines_inserted": 5,
"lines_deleted": 3,
- "size_delta": 98
+ "size_delta": 98,
+ "size": 23348
}
}
----
@@ -3694,6 +3796,10 @@
=== \{draft-id\}
UUID of a draft comment.
+[[label-id]]
+=== \{label-id\}
+The name of the label.
+
[[file-id]]
\{file-id\}
~~~~~~~~~~~~
@@ -3875,6 +3981,15 @@
The reviewers that can be removed by the calling user as a list of
link:rest-api-accounts.html#account-info[AccountInfo] entities. +
Only set if link:#detailed-labels[detailed labels] are requested.
+|`reviewers` ||
+The reviewers as a map that maps a reviewer state to a list of
+link:rest-api-accounts.html#account-info[AccountInfo] entities.
+Possible reviewer states are `REVIEWER`, `CC` and `REMOVED`. +
+`REVIEWER`: Users with at least one non-zero vote on the change. +
+`CC`: Users that were added to the change, but have not voted. +
+`REMOVED`: Users that were previously reviewers on the change, but have
+been removed. +
+Only set if link:#detailed-labels[detailed labels] are requested.
|`messages`|optional|
Messages associated with the change as a list of
link:#change-message-info[ChangeMessageInfo] entities. +
@@ -4218,6 +4333,8 @@
Not set for binary files or if no lines were deleted.
|`size_delta` ||
Number of bytes by which the file size increased/decreased.
+|`size` ||
+File size in bytes.
|=============================
[[fix-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index b1b795c..3a64dc0 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -115,6 +115,7 @@
"gerrit": {
"all_projects": "All-Projects",
"all_users": "All-Users"
+ "doc_search": true
},
"sshd": {},
"suggest": {
@@ -1189,6 +1190,8 @@
|`all_users_name` ||
Name of the link:config-gerrit.html#gerrit.allUsers[project in which
meta data of all users is stored].
+|`doc_search` ||
+Whether documentation search is available.
|`doc_url` |optional|
Custom base URL where Gerrit server documentation is located.
(Documentation may still be available at /Documentation relative to the
diff --git a/VERSION b/VERSION
index e84df7e..573f909 100644
--- a/VERSION
+++ b/VERSION
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = '2.12'
+GERRIT_VERSION = '2.13-SNAPSHOT'
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index d8f0276..9ee1572 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -2,13 +2,18 @@
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:junit',
'//lib/log:impl_log4j',
'//lib/log:log4j',
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index 410c148..2d93315 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.12</version>
+ <version>2.13-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>API for Gerrit Plugins</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 7dbbfb5..3cea4a2 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -17,7 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.initSsh;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
+import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
import com.google.common.base.Function;
import com.google.common.base.Optional;
@@ -233,13 +233,6 @@
return cfg.getBoolean("change", null, "submitWholeTopic", false);
}
- private static boolean isNoteDbTestEnabled() {
- final String[] RUN_FLAGS = {"yes", "y", "true"};
- String value = System.getenv("GERRIT_ENABLE_NOTEDB");
- return value != null &&
- Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
- }
-
protected void beforeTest(Description description) throws Exception {
GerritServer.Description classDesc =
GerritServer.Description.forTestClass(description, configName);
@@ -475,7 +468,8 @@
}
protected Context setApiUserAnonymous() {
- return atrScope.newContext(reviewDbProvider, null, anonymousUser.get());
+ return atrScope.set(
+ atrScope.newContext(reviewDbProvider, null, anonymousUser.get()));
}
protected static Gson newGson() {
@@ -548,6 +542,14 @@
saveProjectConfig(project, cfg);
}
+ protected PermissionRule block(String permission, AccountGroup.UUID id, String ref)
+ throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ PermissionRule rule = Util.block(cfg, permission, id, ref);
+ saveProjectConfig(project, cfg);
+ return rule;
+ }
+
protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
throws Exception {
MetaDataUpdate md = metaDataUpdateFactory.create(p);
@@ -580,16 +582,14 @@
projectCache.evict(config.getProject());
}
- protected void blockRead(Project.NameKey project, String ref) throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- block(cfg, Permission.READ, REGISTERED_USERS, ref);
- saveProjectConfig(project, cfg);
+ protected void blockRead(String ref) throws Exception {
+ block(Permission.READ, REGISTERED_USERS, ref);
}
protected void blockForgeCommitter(Project.NameKey project, String ref)
throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
+ Util.block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
saveProjectConfig(project, cfg);
}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
index 58bb9f2..e0f9d4a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
@@ -23,5 +23,5 @@
@Target({METHOD})
@Retention(RUNTIME)
public @interface GerritConfigs {
- public GerritConfig[] value();
+ GerritConfig[] value();
}
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 634db7c..672a5a7 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
@@ -18,6 +18,8 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePath;
@@ -67,6 +69,7 @@
bind(GitRepositoryManager.class)
.toInstance(new InMemoryRepositoryManager());
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
bind(DataSourceType.class).to(InMemoryH2Type.class);
bind(InMemoryDatabase.class).in(SINGLETON);
bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
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 f0b9f46..4c77edc 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
@@ -43,7 +43,6 @@
private static final String BUCKOUT = "buck-out";
private Path gen;
- private Path testSite;
private Path pluginRoot;
private Path pluginsSitePath;
private Path pluginSubPath;
@@ -51,6 +50,8 @@
private String pluginName;
private boolean standalone;
+ protected Path testSite;
+
@Override
protected void beforeTest(Description description) throws Exception {
locatePaths();
@@ -58,9 +59,13 @@
buildPluginJar();
createTestSiteDirs();
copyJarToTestSite();
+ beforeTestServerStarts();
super.beforeTest(description);
}
+ protected void beforeTestServerStarts() throws Exception {
+ }
+
protected void setPluginConfigString(String name, String value)
throws IOException, ConfigInvalidException {
SitePaths sitePath = new SitePaths(testSite);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
index 261b894..d76cb81 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -14,9 +14,12 @@
package com.google.gerrit.acceptance;
+import static com.google.common.truth.Truth.assert_;
import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
import static java.nio.charset.StandardCharsets.UTF_8;
+import org.apache.http.HttpStatus;
+
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
@@ -36,4 +39,51 @@
}
return reader;
}
+
+ public void assertStatus(int status) throws Exception {
+ assert_()
+ .withFailureMessage(String.format("Expected status code %d", status))
+ .that(getStatusCode())
+ .isEqualTo(status);
+ }
+
+ public void assertOK() throws Exception {
+ assertStatus(HttpStatus.SC_OK);
+ }
+
+ public void assertNotFound() throws Exception {
+ assertStatus(HttpStatus.SC_NOT_FOUND);
+ }
+
+ public void assertConflict() throws Exception {
+ assertStatus(HttpStatus.SC_CONFLICT);
+ }
+
+ public void assertForbidden() throws Exception {
+ assertStatus(HttpStatus.SC_FORBIDDEN);
+ }
+
+ public void assertNoContent() throws Exception {
+ assertStatus(HttpStatus.SC_NO_CONTENT);
+ }
+
+ public void assertBadRequest() throws Exception {
+ assertStatus(HttpStatus.SC_BAD_REQUEST);
+ }
+
+ public void assertUnprocessableEntity() throws Exception {
+ assertStatus(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+ }
+
+ public void assertMethodNotAllowed() throws Exception {
+ assertStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
+ }
+
+ public void assertCreated() throws Exception {
+ assertStatus(HttpStatus.SC_CREATED);
+ }
+
+ public void assertPreconditionFailed() throws Exception {
+ assertStatus(HttpStatus.SC_PRECONDITION_FAILED);
+ }
}
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 39296d0..37d5484 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
@@ -141,6 +141,19 @@
}
@Test
+ public void getByIntId() throws Exception {
+ AccountInfo info = gApi
+ .accounts()
+ .id("admin")
+ .get();
+ AccountInfo infoByIntId = gApi
+ .accounts()
+ .id(info._accountId)
+ .get();
+ assertThat(info.name).isEqualTo(infoByIntId.name);
+ }
+
+ @Test
public void self() throws Exception {
AccountInfo info = gApi
.accounts()
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 92d7980..9be1865 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
@@ -17,15 +17,18 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.extensions.client.ReviewerState.CC;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.Util.blockLabel;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
+import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
+import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -36,15 +39,19 @@
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.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
+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.GitPerson;
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.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
@@ -58,9 +65,11 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.EnumSet;
+import java.util.Iterator;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
@NoHttpd
public class ChangeIT extends AbstractDaemonTest {
@@ -88,6 +97,30 @@
}
@Test
+ public void getAmbiguous() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ String changeId = r1.getChangeId();
+ gApi.changes().id(changeId).get();
+
+ BranchInput b = new BranchInput();
+ b.revision = repo().getRef("HEAD").getObjectId().name();
+ gApi.projects()
+ .name(project.get())
+ .branch("other")
+ .create(b);
+
+ PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo,
+ PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME,
+ PushOneCommit.FILE_CONTENT, changeId);
+ PushOneCommit.Result r2 = push2.to("refs/for/other");
+ assertThat(r2.getChangeId()).isEqualTo(changeId);
+
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Multiple changes found for " + changeId);
+ gApi.changes().id(changeId).get();
+ }
+
+ @Test
public void abandon() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW);
@@ -297,17 +330,6 @@
.rebase(ri);
}
- private Set<Account.Id> getReviewers(String changeId) throws Exception {
- ChangeInfo ci = gApi.changes().id(changeId).get();
- Set<Account.Id> result = Sets.newHashSet();
- for (LabelInfo li : ci.labels.values()) {
- for (ApprovalInfo ai : li.all) {
- result.add(new Account.Id(ai._accountId));
- }
- }
- return result;
- }
-
@Test
public void addReviewer() throws Exception {
PushOneCommit.Result r = createChange();
@@ -317,8 +339,20 @@
.id(r.getChangeId())
.addReviewer(in);
- assertThat(getReviewers(r.getChangeId()))
- .containsExactlyElementsIn(ImmutableSet.of(user.id));
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+
+ // When notedb is enabled adding a reviewer records that user as reviewer
+ // in notedb. When notedb is disabled adding a reviewer results in a dummy 0
+ // approval on the change which is treated as CC when the ChangeInfo is
+ // created.
+ Collection<AccountInfo> reviewers = isNoteDbTestEnabled()
+ ? c.reviewers.get(REVIEWER)
+ : c.reviewers.get(CC);
+ assertThat(reviewers).hasSize(1);
+ assertThat(reviewers.iterator().next()._accountId)
+ .isEqualTo(user.getId().get());
}
@Test
@@ -333,16 +367,155 @@
.revision(r.getCommit().name())
.submit();
- assertThat(getReviewers(r.getChangeId()))
- .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
+ assertThat(reviewers).hasSize(1);
+ assertThat(reviewers.iterator().next()._accountId)
+ .isEqualTo(admin.getId().get());
+ assertThat(c.reviewers).doesNotContainKey(CC);
AddReviewerInput in = new AddReviewerInput();
in.reviewer = user.email;
gApi.changes()
.id(r.getChangeId())
.addReviewer(in);
- assertThat(getReviewers(r.getChangeId()))
- .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
+
+ c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ reviewers = c.reviewers.get(REVIEWER);
+ if (isNoteDbTestEnabled()) {
+ // When notedb is enabled adding a reviewer records that user as reviewer
+ // in notedb.
+ assertThat(reviewers).hasSize(2);
+ Iterator<AccountInfo> reviewerIt = reviewers.iterator();
+ assertThat(reviewerIt.next()._accountId)
+ .isEqualTo(admin.getId().get());
+ assertThat(reviewerIt.next()._accountId)
+ .isEqualTo(user.getId().get());
+ assertThat(c.reviewers).doesNotContainKey(CC);
+ } else {
+ // When notedb is disabled adding a reviewer results in a dummy 0 approval
+ // on the change which is treated as CC when the ChangeInfo is created.
+ assertThat(reviewers).hasSize(1);
+ assertThat(reviewers.iterator().next()._accountId)
+ .isEqualTo(admin.getId().get());
+ Collection<AccountInfo> ccs = c.reviewers.get(CC);
+ assertThat(ccs).hasSize(1);
+ assertThat(ccs.iterator().next()._accountId)
+ .isEqualTo(user.getId().get());
+ }
+ }
+
+ @Test
+ public void listVotes() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.approve());
+
+ Map<String, Short> m = gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(admin.getId().toString())
+ .votes();
+
+ assertThat(m).hasSize(1);
+ assertThat(m).containsEntry("Code-Review", new Short((short)2));
+
+ setApiUser(user);
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.dislike());
+
+ m = gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.getId().toString())
+ .votes();
+
+ assertThat(m).hasSize(1);
+ assertThat(m).containsEntry("Code-Review", new Short((short)-1));
+ }
+
+ @Test
+ public void deleteVote() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.approve());
+
+ setApiUser(user);
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.recommend());
+
+ setApiUser(admin);
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(admin.getId().toString())
+ .deleteVote("Code-Review");
+
+ Map<String, Short> m = gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(admin.getId().toString())
+ .votes();
+
+ if (isNoteDbTestEnabled()) {
+ // 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", new Short((short)0));
+ }
+
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+
+ assertThat(Iterables.getLast(c.messages).message).isEqualTo(
+ "Removed Code-Review+2 by Administrator <admin@example.com>\n");
+ if (isNoteDbTestEnabled()) {
+ // 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.
+ assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+ .containsExactlyElementsIn(
+ ImmutableSet.of(admin.getId(), user.getId()));
+ } else {
+ // When notedb is disabled users that have only dummy 0 approvals on the
+ // change are returned as CC and not as REVIEWER.
+ assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+ .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
+ assertThat(getReviewers(c.reviewers.get(CC)))
+ .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+ }
+ }
+
+ @Test
+ public void deleteVoteNotPermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.approve());
+
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("delete not permitted");
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(admin.getId().toString())
+ .deleteVote("Code-Review");
}
@Test
@@ -644,4 +817,36 @@
assertThat(rev2.pushCertificate.certificate).isNull();
assertThat(rev2.pushCertificate.key).isNull();
}
+
+ @Test
+ public void anonymousRestApi() throws Exception {
+ setApiUserAnonymous();
+ PushOneCommit.Result r = createChange();
+
+ ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
+ assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+ String triplet = project.get() + "~master~" + r.getChangeId();
+ info = gApi.changes().id(triplet).get();
+ assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+ info = gApi.changes().id(info._number).get();
+ assertThat(info.changeId).isEqualTo(r.getChangeId());
+
+ exception.expect(AuthException.class);
+ gApi.changes()
+ .id(triplet)
+ .current()
+ .review(ReviewInput.approve());
+ }
+
+ 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);
+ }
+ });
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
index 475803a..37a5071 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
@@ -25,6 +25,10 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.Inject;
import org.junit.Test;
@@ -33,6 +37,15 @@
@NoHttpd
public class CheckIT extends AbstractDaemonTest {
+ @Inject
+ private ChangeControl.GenericFactory changeControlFactory;
+
+ @Inject
+ private IdentifiedUser.GenericFactory userFactory;
+
+ @Inject
+ private ChangeUpdate.Factory changeUpdateFactory;
+
// Most types of tests belong in ConsistencyCheckerTest; these mostly just
// test paths outside of ConsistencyChecker, like API wiring.
@Test
@@ -66,6 +79,12 @@
Change c = getChange(r);
c.setStatus(Change.Status.NEW);
db.changes().update(Collections.singleton(c));
+ ChangeUpdate changeUpdate =
+ changeUpdateFactory.create(
+ changeControlFactory.controlFor(
+ c, userFactory.create(admin.id)));
+ changeUpdate.setStatus(Change.Status.NEW);
+ changeUpdate.commit();
indexer.index(db, c);
ChangeInfo info = gApi.changes()
@@ -81,6 +100,11 @@
assertThat(info.problems).hasSize(1);
assertThat(info.problems.get(0).status).isEqualTo(ProblemInfo.Status.FIXED);
assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+
+ info = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
}
private Change getChange(PushOneCommit.Result r) throws Exception {
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 3e970e4..3cddcd9 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
@@ -56,6 +56,7 @@
import java.io.ByteArrayOutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -159,7 +160,11 @@
assertThat(orig.get().messages).hasSize(1);
ChangeApi cherry = orig.revision(r.getCommit().name())
.cherryPick(in);
- assertThat(orig.get().messages).hasSize(2);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .get().messages;
+ assertThat(messages).hasSize(2);
String cherryPickedRevision = cherry.get().currentRevision;
String expectedMessage = String.format(
@@ -167,7 +172,7 @@
"This patchset was cherry picked to branch %s as commit %s",
in.destination, cherryPickedRevision);
- Iterator<ChangeMessageInfo> origIt = orig.get().messages.iterator();
+ Iterator<ChangeMessageInfo> origIt = messages.iterator();
origIt.next();
assertThat(origIt.next().message).isEqualTo(expectedMessage);
@@ -279,7 +284,11 @@
assertThat(orig.get().messages).hasSize(1);
ChangeApi cherry = orig.revision(r.getCommit().name())
.cherryPick(in);
- assertThat(orig.get().messages).hasSize(2);
+
+ Collection<ChangeMessageInfo> messages = gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .get().messages;
+ assertThat(messages).hasSize(2);
assertThat(cherry.get().subject).contains(in.message);
cherry.current().review(ReviewInput.approve());
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 ed20e24..f4e7cb7 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
@@ -17,13 +17,7 @@
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.http.HttpStatus.SC_CONFLICT;
-import static org.apache.http.HttpStatus.SC_FORBIDDEN;
-import static org.apache.http.HttpStatus.SC_NOT_FOUND;
-import static org.apache.http.HttpStatus.SC_NO_CONTENT;
-import static org.apache.http.HttpStatus.SC_OK;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
@@ -58,6 +52,7 @@
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.stream.JsonReader;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -66,9 +61,6 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -81,7 +73,6 @@
import java.util.Date;
import java.util.Iterator;
import java.util.List;
-import java.util.concurrent.atomic.AtomicLong;
public class ChangeEditIT extends AbstractDaemonTest {
@@ -114,20 +105,12 @@
@BeforeClass
public static void setTimeForTesting() {
- final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
- final AtomicLong clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
- DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
- @Override
- public long getMillis() {
- return clockMs.getAndAdd(clockStepMs);
- }
- });
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
}
@AfterClass
public static void restoreTime() {
- DateTimeUtils.setCurrentMillisSystem();
+ TestTimeUtil.useSystemTime();
}
@Before
@@ -185,8 +168,7 @@
modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
Optional<ChangeEdit> edit = editUtil.byChange(change);
- RestResponse r = adminSession.post(urlPublish());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.post(urlPublish()).assertNoContent();
edit = editUtil.byChange(change);
assertThat(edit.isPresent()).isFalse();
PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
@@ -204,8 +186,7 @@
modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
Optional<ChangeEdit> edit = editUtil.byChange(change);
- RestResponse r = adminSession.delete(urlEdit());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.delete(urlEdit()).assertNoContent();
edit = editUtil.byChange(change);
assertThat(edit.isPresent()).isFalse();
}
@@ -219,11 +200,9 @@
assertThat(
modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
- RestResponse r = adminSession.post(urlPublish());
- assertThat(r.getStatusCode()).isEqualTo(SC_FORBIDDEN);
+ adminSession.post(urlPublish()).assertForbidden();
setUseContributorAgreements(InheritableBoolean.FALSE);
- r = adminSession.post(urlPublish());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.post(urlPublish()).assertNoContent();
}
@Test
@@ -260,8 +239,7 @@
assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
current.getPatchSetId() - 1);
Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
- RestResponse r = adminSession.post(urlRebase());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.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);
@@ -287,8 +265,7 @@
pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, FILE_NAME,
new String(CONTENT_NEW2), changeId2);
push.to("refs/for/master").assertOkStatus();
- RestResponse r = adminSession.post(urlRebase());
- assertThat(r.getStatusCode()).isEqualTo(SC_CONFLICT);
+ adminSession.post(urlRebase()).assertConflict();
}
@Test
@@ -370,24 +347,21 @@
@Test
public void updateMessageRest() throws Exception {
- assertThat(adminSession.get(urlEditMessage()).getStatusCode())
- .isEqualTo(SC_NOT_FOUND);
+ adminSession.get(urlEditMessage()).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());
- assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.put(urlEditMessage(), in).assertNoContent();
RestResponse r = adminSession.getJsonAccept(urlEditMessage());
- assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+ r.assertOK();
assertThat(readContentFromJson(r)).isEqualTo(in.message);
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertThat(edit.get().getEditCommit().getFullMessage())
.isEqualTo(in.message);
in.message = String.format("New commit message2\n\nChange-Id: %s\n",
change.getKey());
- assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.put(urlEditMessage(), in).assertNoContent();
edit = editUtil.byChange(change);
assertThat(edit.get().getEditCommit().getFullMessage())
.isEqualTo(in.message);
@@ -400,8 +374,7 @@
@Test
public void retrieveEdit() throws Exception {
- RestResponse r = adminSession.get(urlEdit());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.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, RestSession.newRawInput(CONTENT_NEW)))
@@ -414,8 +387,7 @@
edit = editUtil.byChange(change);
editUtil.delete(edit.get());
- r = adminSession.get(urlEdit());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.get(urlEdit()).assertNoContent();
}
@Test
@@ -460,8 +432,7 @@
@Test
public void createEditByDeletingExistingFileRest() throws Exception {
- RestResponse r = adminSession.delete(urlEditFile());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.delete(urlEditFile()).assertNoContent();
Optional<ChangeEdit> edit = editUtil.byChange(change);
exception.expect(ResourceNotFoundException.class);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -470,15 +441,13 @@
@Test
public void deletingNonExistingEditRest() throws Exception {
- RestResponse r = adminSession.delete(urlEdit());
- assertThat(r.getStatusCode()).isEqualTo(SC_NOT_FOUND);
+ adminSession.delete(urlEdit()).assertNotFound();
}
@Test
public void deleteExistingFileRest() throws Exception {
assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
- assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.delete(urlEditFile()).assertNoContent();
Optional<ChangeEdit> edit = editUtil.byChange(change);
exception.expect(ResourceNotFoundException.class);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
@@ -527,8 +496,7 @@
Post.Input in = new Post.Input();
in.oldPath = FILE_NAME;
in.newPath = FILE_NAME3;
- assertThat(adminSession.post(urlEdit(), in).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.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);
@@ -541,8 +509,7 @@
public void restoreDeletedFileInPatchSetRest() throws Exception {
Post.Input in = new Post.Input();
in.restorePath = FILE_NAME;
- assertThat(adminSession.post(urlEdit2(), in).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.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);
@@ -568,14 +535,12 @@
public void createAndChangeEditInOneRequestRest() throws Exception {
Put.Input in = new Put.Input();
in.content = RestSession.newRawInput(CONTENT_NEW);
- assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.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);
in.content = RestSession.newRawInput(CONTENT_NEW2);
- assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.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);
@@ -586,8 +551,7 @@
assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
Put.Input in = new Put.Input();
in.content = RestSession.newRawInput(CONTENT_NEW);
- assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.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);
@@ -596,8 +560,7 @@
@Test
public void emptyPutRequest() throws Exception {
assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
- assertThat(adminSession.put(urlEditFile()).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.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());
@@ -605,8 +568,7 @@
@Test
public void createEmptyEditRest() throws Exception {
- assertThat(adminSession.post(urlEdit()).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.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);
@@ -616,14 +578,13 @@
public void getFileContentRest() throws Exception {
Put.Input in = new Put.Input();
in.content = RestSession.newRawInput(CONTENT_NEW);
- assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
- .isEqualTo(SC_NO_CONTENT);
+ adminSession.putRaw(urlEditFile(), in.content).assertNoContent();
Optional<ChangeEdit> edit = editUtil.byChange(change);
assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
.isEqualTo(RefUpdate.Result.FORCED);
edit = editUtil.byChange(change);
RestResponse r = adminSession.getJsonAccept(urlEditFile());
- assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+ r.assertOK();
assertThat(readContentFromJson(r)).isEqualTo(
StringUtils.newStringUtf8(CONTENT_NEW2));
}
@@ -631,11 +592,9 @@
@Test
public void getFileNotFoundRest() throws Exception {
assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
- assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
- SC_NO_CONTENT);
+ adminSession.delete(urlEditFile()).assertNoContent();
Optional<ChangeEdit> edit = editUtil.byChange(change);
- RestResponse r = adminSession.get(urlEditFile());
- assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+ adminSession.get(urlEditFile()).assertNoContent();
exception.expect(ResourceNotFoundException.class);
fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
@@ -832,9 +791,9 @@
+ "/edit:rebase";
}
- private EditInfo toEditInfo(boolean files) throws IOException {
+ private EditInfo toEditInfo(boolean files) throws Exception {
RestResponse r = adminSession.get(files ? urlGetFiles() : urlEdit());
- assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+ r.assertOK();
return newGson().fromJson(r.getReader(), EditInfo.class);
}
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 900d85a..73a02a5 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
@@ -17,7 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableSet;
@@ -26,23 +26,28 @@
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
+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.project.Util;
+import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicLong;
public abstract class AbstractPushForReview extends AbstractDaemonTest {
protected enum Protocol {
@@ -51,28 +56,39 @@
}
private String sshUrl;
+ private LabelType patchSetLock;
@BeforeClass
public static void setTimeForTesting() {
- final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
- final AtomicLong clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
- DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
- @Override
- public long getMillis() {
- return clockMs.getAndAdd(clockStepMs);
- }
- });
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
}
@AfterClass
public static void restoreTime() {
- DateTimeUtils.setCurrentMillisSystem();
+ TestTimeUtil.useSystemTime();
}
@Before
public void setUp() throws Exception {
sshUrl = sshSession.getUrl();
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ patchSetLock = Util.patchSetLock();
+ cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
+ AccountGroup.UUID anonymousUsers =
+ SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+ Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers,
+ "refs/heads/*");
+ saveProjectConfig(cfg);
+ grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
+ }
+
+ private void saveProjectConfig(ProjectConfig cfg) throws Exception {
+ MetaDataUpdate md = metaDataUpdateFactory.create(project);
+ try {
+ cfg.commit(md);
+ } finally {
+ md.close();
+ }
}
protected void selectProtocol(Protocol p) throws Exception {
@@ -276,6 +292,18 @@
}
@Test
+ public void testPushNewPatchsetToPatchSetLockedChange() throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+ PushOneCommit.SUBJECT, "b.txt", "anotherContent", r.getChangeId());
+ revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
+ r = push.to("refs/for/master");
+ r.assertErrorStatus("cannot replace " + r.getChange().change().getChangeId()
+ + ". Change is patch set locked.");
+ }
+
+ @Test
public void testPushForMasterWithApprovals_MissingLabel() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
r.assertErrorStatus("label \"Verify\" is not a configured label");
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 27b8b0a..41f47a2 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
@@ -15,13 +15,11 @@
package com.google.gerrit.acceptance.git;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.Util.block;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.server.git.ProjectConfig;
import org.junit.Before;
import org.junit.Test;
@@ -31,9 +29,7 @@
@Before
public void setUp() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
- saveProjectConfig(project, cfg);
+ block(Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*");
}
@Test
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 3e7c2bf..768c923 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
@@ -34,7 +34,6 @@
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class CapabilitiesIT extends AbstractDaemonTest {
@@ -53,7 +52,7 @@
try {
RestResponse r =
userSession.get("/accounts/self/capabilities");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
new TypeToken<CapabilityInfo>() {}.getType());
for (String c : GlobalCapability.getAllNames()) {
@@ -81,7 +80,7 @@
public void testCapabilitiesAdmin() throws Exception {
RestResponse r =
adminSession.get("/accounts/self/capabilities");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
new TypeToken<CapabilityInfo>() {}.getType());
for (String c : GlobalCapability.getAllNames()) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
index 5b2e7ba..9a66958 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
@@ -23,23 +22,21 @@
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.client.Theme;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class DiffPreferencesIT extends AbstractDaemonTest {
@Test
public void getDiffPreferencesOfNonExistingAccount_NotFound()
throws Exception {
- assertEquals(HttpStatus.SC_NOT_FOUND,
- adminSession.get("/accounts/non-existing/preferences.diff")
- .getStatusCode());
+ adminSession.get("/accounts/non-existing/preferences.diff")
+ .assertNotFound();
}
@Test
public void getDiffPreferences() throws Exception {
RestResponse r = adminSession.get("/accounts/" + admin.email
+ "/preferences.diff");
- assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ r.assertOK();
DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
DiffPreferencesInfo o =
newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
@@ -98,7 +95,7 @@
RestResponse r = adminSession.put("/accounts/" + admin.email
+ "/preferences.diff", i);
- assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ r.assertOK();
DiffPreferencesInfo o = newGson().fromJson(r.getReader(),
DiffPreferencesInfo.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
index cf89c5a..627d9bd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
@@ -22,7 +22,6 @@
import com.google.gerrit.extensions.client.KeyMapType;
import com.google.gerrit.extensions.client.Theme;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.io.IOException;
@@ -32,7 +31,7 @@
public void getSetEditPreferences() throws Exception {
String endPoint = "/accounts/" + admin.email + "/preferences.edit";
RestResponse r = adminSession.get(endPoint);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
EditPreferencesInfo out = getEditPrefInfo(r);
assertThat(out.lineLength).isEqualTo(100);
@@ -62,22 +61,20 @@
out.theme = Theme.TWILIGHT;
out.keyMapType = KeyMapType.EMACS;
- r = adminSession.put(endPoint, out);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ adminSession.put(endPoint, out).assertNoContent();
r = adminSession.get(endPoint);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
EditPreferencesInfo info = getEditPrefInfo(r);
assertEditPreferences(info, out);
// Partially filled input record
EditPreferencesInfo in = new EditPreferencesInfo();
in.tabSize = 42;
- r = adminSession.put(endPoint, in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ adminSession.put(endPoint, in).assertNoContent();
r = adminSession.get(endPoint);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
info = getEditPrefInfo(r);
out.tabSize = in.tabSize;
assertEditPreferences(info, out);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index 2543095..9ba7810 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -26,7 +26,6 @@
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.util.Collections;
@@ -41,7 +40,7 @@
in.username = "myUsername";
RestResponse r =
adminSession.put("/accounts/" + createUser().get() + "/username", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(
in.username);
}
@@ -50,25 +49,25 @@
public void setExisting_Conflict() throws Exception {
PutUsername.Input in = new PutUsername.Input();
in.username = admin.username;
- RestResponse r =
- adminSession.put("/accounts/" + createUser().get() + "/username", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ adminSession
+ .put("/accounts/" + createUser().get() + "/username", in)
+ .assertConflict();
}
@Test
public void setNew_MethodNotAllowed() throws Exception {
PutUsername.Input in = new PutUsername.Input();
in.username = "newUsername";
- RestResponse r =
- adminSession.put("/accounts/" + admin.username + "/username", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+ adminSession
+ .put("/accounts/" + admin.username + "/username", in)
+ .assertMethodNotAllowed();
}
@Test
public void delete_MethodNotAllowed() throws Exception {
- RestResponse r =
- adminSession.put("/accounts/" + admin.username + "/username");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+ adminSession
+ .put("/accounts/" + admin.username + "/username")
+ .assertMethodNotAllowed();
}
private Account.Id createUser() throws OrmException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index d726d70..91f2962 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -23,41 +22,28 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Iterator;
-import java.util.concurrent.atomic.AtomicLong;
@RunWith(ConfigSuite.class)
public class ChangeMessagesIT extends AbstractDaemonTest {
private String systemTimeZone;
- private volatile long clockStepMs;
@Before
public void setTimeForTesting() {
systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
- clockStepMs = MILLISECONDS.convert(1, SECONDS);
- final AtomicLong clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
- DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
- @Override
- public long getMillis() {
- return clockMs.getAndAdd(clockStepMs);
- }
- });
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
}
@After
public void resetTime() {
- DateTimeUtils.setCurrentMillisSystem();
+ TestTimeUtil.useSystemTime();
System.setProperty("user.timezone", systemTimeZone);
}
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 2229577..b04dc6d 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
@@ -76,11 +76,8 @@
}
private void assertApproveFails(TestAccount a, String changeId) throws Exception {
- try {
- approve(a, changeId);
- } catch (AuthException expected) {
- // Expected.
- }
+ exception.expect(AuthException.class);
+ approve(a, changeId);
}
private void grantApproveToChangeOwner() throws IOException,
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 d0bcfd9..9edf9b2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,6 +16,7 @@
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;
@@ -26,8 +27,11 @@
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
import org.junit.Test;
@NoHttpd
@@ -37,6 +41,17 @@
return allowDraftsDisabledConfig();
}
+ @BeforeClass
+ public static void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
+ }
+
+ @AfterClass
+ public static void restoreTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
+
@Test
public void createEmptyChange_MissingBranch() throws Exception {
ChangeInfo ci = new ChangeInfo();
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 47d071f..3f2107c 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
@@ -28,7 +28,6 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.io.IOException;
@@ -45,7 +44,7 @@
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
RestResponse r = deletePatchSet(changeId, ps, adminSession);
assertThat(r.getEntityContent()).isEqualTo("Patch set is not a draft");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ r.assertConflict();
}
@Test
@@ -58,7 +57,7 @@
assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
RestResponse r = deletePatchSet(changeId, ps, userSession);
assertThat(r.getEntityContent()).isEqualTo("Not found: " + changeId);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ r.assertNotFound();
}
@Test
@@ -69,12 +68,10 @@
ChangeInfo c = get(triplet);
assertThat(c.id).isEqualTo(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
- RestResponse r = deletePatchSet(changeId, ps, adminSession);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ deletePatchSet(changeId, ps, adminSession).assertNoContent();
assertThat(getChange(changeId).patchSets()).hasSize(1);
ps = getCurrentPatchSet(changeId);
- r = deletePatchSet(changeId, ps, adminSession);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ deletePatchSet(changeId, ps, adminSession).assertNoContent();
assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
}
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 2707507..1fc91d2 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
@@ -29,7 +29,6 @@
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gwtorm.server.OrmException;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -51,8 +50,9 @@
assertThat(c.id).isEqualTo(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
RestResponse response = deleteChange(changeId, adminSession);
- assertThat(response.getEntityContent()).isEqualTo("Change is not a draft");
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ assertThat(response.getEntityContent())
+ .isEqualTo("Change is not a draft: " + c._number);
+ response.assertConflict();
}
@Test
@@ -65,8 +65,7 @@
ChangeInfo c = get(triplet);
assertThat(c.id).isEqualTo(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
- RestResponse response = deleteChange(changeId, adminSession);
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ deleteChange(changeId, adminSession).assertNoContent();
exception.expect(ResourceNotFoundException.class);
get(triplet);
@@ -83,8 +82,7 @@
assertThat(c.id).isEqualTo(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
assertThat(c.revisions.get(c.currentRevision).draft).isTrue();
- RestResponse response = publishChange(changeId);
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ publishChange(changeId).assertNoContent();
c = get(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
assertThat(c.revisions.get(c.currentRevision).draft).isNull();
@@ -100,8 +98,7 @@
ChangeInfo c = get(triplet);
assertThat(c.id).isEqualTo(triplet);
assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
- RestResponse response = publishPatchSet(changeId);
- assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ publishPatchSet(changeId).assertNoContent();
assertThat(get(triplet).status).isEqualTo(ChangeStatus.NEW);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 405f8e5..7035bf9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -14,27 +14,25 @@
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.RestResponse;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class IndexChangeIT extends AbstractDaemonTest {
@Test
public void indexChange() throws Exception {
String changeId = createChange().getChangeId();
- RestResponse r = adminSession.post("/changes/" + changeId + "/index/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ adminSession
+ .post("/changes/" + changeId + "/index/")
+ .assertNoContent();
}
@Test
public void indexChangeOnNonVisibleBranch() throws Exception {
String changeId = createChange().getChangeId();
- blockRead(project, "refs/heads/master");
- RestResponse r = userSession.post("/changes/" + changeId + "/index/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ blockRead("refs/heads/master");
+ userSession
+ .post("/changes/" + changeId + "/index/")
+ .assertNotFound();
}
}
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 7e68a03..7754a53 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
@@ -25,7 +25,6 @@
import com.google.gerrit.server.config.ListCaches.CacheInfo;
import com.google.gerrit.server.config.PostCaches;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.util.Arrays;
@@ -39,7 +38,7 @@
assertThat(cacheInfo.entries.mem).isGreaterThan((long) 0);
r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
r = adminSession.get("/config/server/caches/project_list");
@@ -49,16 +48,16 @@
@Test
public void flushAll_Forbidden() throws Exception {
- RestResponse r = userSession.post("/config/server/caches/",
- new PostCaches.Input(FLUSH_ALL));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession.post("/config/server/caches/",
+ new PostCaches.Input(FLUSH_ALL)).assertForbidden();
}
@Test
public void flushAll_BadRequest() throws Exception {
- RestResponse r = adminSession.post("/config/server/caches/",
- new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .post("/config/server/caches/",
+ new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")))
+ .assertBadRequest();
}
@Test
@@ -73,7 +72,7 @@
r = adminSession.post("/config/server/caches/",
new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
r = adminSession.get("/config/server/caches/project_list");
@@ -87,16 +86,18 @@
@Test
public void flush_Forbidden() throws Exception {
- RestResponse r = userSession.post("/config/server/caches/",
- new PostCaches.Input(FLUSH, Arrays.asList("projects")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .post("/config/server/caches/",
+ new PostCaches.Input(FLUSH, Arrays.asList("projects")))
+ .assertForbidden();
}
@Test
public void flush_BadRequest() throws Exception {
- RestResponse r = adminSession.post("/config/server/caches/",
- new PostCaches.Input(FLUSH));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .post("/config/server/caches/",
+ new PostCaches.Input(FLUSH))
+ .assertBadRequest();
}
@Test
@@ -107,7 +108,7 @@
r = adminSession.post("/config/server/caches/",
new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+ r.assertUnprocessableEntity();
r.consume();
r = adminSession.get("/config/server/caches/projects");
@@ -122,12 +123,13 @@
try {
RestResponse r = userSession.post("/config/server/caches/",
new PostCaches.Input(FLUSH, Arrays.asList("projects")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
- r = userSession.post("/config/server/caches/",
- new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .post("/config/server/caches/",
+ new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")))
+ .assertForbidden();
} finally {
removeGlobalCapabilities(REGISTERED_USERS,
GlobalCapability.FLUSH_CACHES, GlobalCapability.VIEW_CACHES);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
index 9d8320ad..0638637 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ConfirmEmailIT.java
@@ -14,17 +14,13 @@
package com.google.gerrit.acceptance.rest.config;
-import static com.google.common.truth.Truth.assertThat;
-
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.server.config.ConfirmEmail;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.inject.Inject;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -44,23 +40,26 @@
public void confirm() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = emailTokenVerifier.encode(admin.getId(), "new.mail@example.com");
- RestResponse r = adminSession.put("/config/server/email.confirm", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ adminSession
+ .put("/config/server/email.confirm", in)
+ .assertNoContent();
}
@Test
public void confirmForOtherUser_UnprocessableEntity() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = emailTokenVerifier.encode(user.getId(), "new.mail@example.com");
- RestResponse r = adminSession.put("/config/server/email.confirm", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+ adminSession
+ .put("/config/server/email.confirm", in)
+ .assertUnprocessableEntity();
}
@Test
public void confirmInvalidToken_UnprocessableEntity() throws Exception {
ConfirmEmail.Input in = new ConfirmEmail.Input();
in.token = "invalidToken";
- RestResponse r = adminSession.put("/config/server/email.confirm", in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+ adminSession
+ .put("/config/server/email.confirm", in)
+ .assertUnprocessableEntity();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index bb63928..0113672 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -22,7 +22,6 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class FlushCacheIT extends AbstractDaemonTest {
@@ -34,7 +33,7 @@
assertThat(result.entries.mem).isGreaterThan((long)0);
r = adminSession.post("/config/server/caches/groups/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
r = adminSession.get("/config/server/caches/groups");
@@ -44,26 +43,30 @@
@Test
public void flushCache_Forbidden() throws Exception {
- RestResponse r = userSession.post("/config/server/caches/accounts/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .post("/config/server/caches/accounts/flush")
+ .assertForbidden();
}
@Test
public void flushCache_NotFound() throws Exception {
- RestResponse r = adminSession.post("/config/server/caches/nonExisting/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ adminSession
+ .post("/config/server/caches/nonExisting/flush")
+ .assertNotFound();
}
@Test
public void flushCacheWithGerritPrefix() throws Exception {
- RestResponse r = adminSession.post("/config/server/caches/gerrit-accounts/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ adminSession
+ .post("/config/server/caches/gerrit-accounts/flush")
+ .assertOK();
}
@Test
public void flushWebSessionsCache() throws Exception {
- RestResponse r = adminSession.post("/config/server/caches/web_sessions/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ adminSession
+ .post("/config/server/caches/web_sessions/flush")
+ .assertOK();
}
@Test
@@ -72,11 +75,12 @@
GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
try {
RestResponse r = userSession.post("/config/server/caches/accounts/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
- r = userSession.post("/config/server/caches/web_sessions/flush");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .post("/config/server/caches/web_sessions/flush")
+ .assertForbidden();
} finally {
removeGlobalCapabilities(REGISTERED_USERS,
GlobalCapability.VIEW_CACHES, GlobalCapability.FLUSH_CACHES);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
index 02a1b73..ff8a3b9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.server.config.ListCaches.CacheInfo;
import com.google.gerrit.server.config.ListCaches.CacheType;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class GetCacheIT extends AbstractDaemonTest {
@@ -29,7 +28,7 @@
@Test
public void getCache() throws Exception {
RestResponse r = adminSession.get("/config/server/caches/accounts");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
assertThat(result.name).isEqualTo("accounts");
@@ -45,26 +44,29 @@
userSession.get("/config/server/version").consume();
r = adminSession.get("/config/server/caches/accounts");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
result = newGson().fromJson(r.getReader(), CacheInfo.class);
assertThat(result.entries.mem).isEqualTo(2);
}
@Test
public void getCache_Forbidden() throws Exception {
- RestResponse r = userSession.get("/config/server/caches/accounts");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .get("/config/server/caches/accounts")
+ .assertForbidden();
}
@Test
public void getCache_NotFound() throws Exception {
- RestResponse r = adminSession.get("/config/server/caches/nonExisting");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ adminSession
+ .get("/config/server/caches/nonExisting")
+ .assertNotFound();
}
@Test
public void getCacheWithGerritPrefix() throws Exception {
- RestResponse r = adminSession.get("/config/server/caches/gerrit-accounts");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ adminSession
+ .get("/config/server/caches/gerrit-accounts")
+ .assertOK();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index acd900c..8455062 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.server.config.ListTasks.TaskInfo;
import com.google.gson.reflect.TypeToken;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.util.List;
@@ -32,7 +31,7 @@
public void getTask() throws Exception {
RestResponse r =
adminSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
TaskInfo info =
newGson().fromJson(r.getReader(),
new TypeToken<TaskInfo>() {}.getType());
@@ -44,9 +43,9 @@
@Test
public void getTask_NotFound() throws Exception {
- RestResponse r =
- userSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ userSession
+ .get("/config/server/tasks/" + getLogFileCompressorTaskId())
+ .assertNotFound();
}
private String getLogFileCompressorTaskId() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 11c61c6..88b93c8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.server.config.ListTasks.TaskInfo;
import com.google.gson.reflect.TypeToken;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.util.List;
@@ -37,7 +36,7 @@
assertThat(taskCount).isGreaterThan(0);
r = adminSession.delete("/config/server/tasks/" + result.get(0).id);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+ r.assertNoContent();
r.consume();
r = adminSession.get("/config/server/tasks/");
@@ -54,8 +53,9 @@
r.consume();
assertThat(result.size()).isGreaterThan(0);
- r = userSession.delete("/config/server/tasks/" + result.get(0).id);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ userSession
+ .delete("/config/server/tasks/" + result.get(0).id)
+ .assertNotFound();
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index fb89e1b..964c759 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -24,7 +24,6 @@
import com.google.gerrit.server.config.ListCaches.CacheType;
import com.google.gson.reflect.TypeToken;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.util.Base64;
import org.junit.Test;
@@ -37,7 +36,7 @@
@Test
public void listCaches() throws Exception {
RestResponse r = adminSession.get("/config/server/caches/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
Map<String, CacheInfo> result =
newGson().fromJson(r.getReader(),
new TypeToken<Map<String, CacheInfo>>() {}.getType());
@@ -56,7 +55,7 @@
userSession.get("/config/server/version").consume();
r = adminSession.get("/config/server/caches/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
result = newGson().fromJson(r.getReader(),
new TypeToken<Map<String, CacheInfo>>() {}.getType());
assertThat(result.get("accounts").entries.mem).isEqualTo(2);
@@ -64,14 +63,15 @@
@Test
public void listCaches_Forbidden() throws Exception {
- RestResponse r = userSession.get("/config/server/caches/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .get("/config/server/caches/")
+ .assertForbidden();
}
@Test
public void listCacheNames() throws Exception {
RestResponse r = adminSession.get("/config/server/caches/?format=LIST");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
List<String> result =
newGson().fromJson(r.getReader(),
new TypeToken<List<String>>() {}.getType());
@@ -83,7 +83,7 @@
@Test
public void listCacheNamesTextList() throws Exception {
RestResponse r = adminSession.get("/config/server/caches/?format=TEXT_LIST");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
List<String> list = Arrays.asList(result.split("\n"));
assertThat(list).contains("accounts");
@@ -93,7 +93,8 @@
@Test
public void listCaches_BadRequest() throws Exception {
- RestResponse r = adminSession.get("/config/server/caches/?format=NONSENSE");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .get("/config/server/caches/?format=NONSENSE")
+ .assertBadRequest();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
index 58f3361..0b2c6cc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.server.config.ListTasks.TaskInfo;
import com.google.gson.reflect.TypeToken;
-import org.apache.http.HttpStatus;
import org.junit.Test;
import java.util.List;
@@ -31,7 +30,7 @@
@Test
public void listTasks() throws Exception {
RestResponse r = adminSession.get("/config/server/tasks/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
List<TaskInfo> result =
newGson().fromJson(r.getReader(),
new TypeToken<List<TaskInfo>>() {}.getType());
@@ -52,7 +51,7 @@
@Test
public void listTasksWithoutViewQueueCapability() throws Exception {
RestResponse r = userSession.get("/config/server/tasks/");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
List<TaskInfo> result =
newGson().fromJson(r.getReader(),
new TypeToken<List<TaskInfo>>() {}.getType());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
index 242e1ee..1fbebe8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddMemberIT.java
@@ -14,19 +14,15 @@
package com.google.gerrit.acceptance.rest.group;
-import static com.google.common.truth.Truth.assertThat;
-
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class AddMemberIT extends AbstractDaemonTest {
@Test
public void addNonExistingMember_NotFound() throws Exception {
- int status =
- adminSession.put("/groups/Administrators/members/non-existing")
- .getStatusCode();
- assertThat(status).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ adminSession
+ .put("/groups/Administrators/members/non-existing")
+ .assertNotFound();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
index 682059c..0362f59 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
@@ -23,7 +23,6 @@
import com.google.gerrit.server.project.BanCommit;
import com.google.gerrit.server.project.BanCommit.BanResultInfo;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.junit.Test;
@@ -39,7 +38,7 @@
RestResponse r =
adminSession.put("/projects/" + project.get() + "/ban/",
BanCommit.Input.fromCommits(c.name()));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
assertThat(Iterables.getOnlyElement(info.newlyBanned)).isEqualTo(c.name());
assertThat(info.alreadyBanned).isNull();
@@ -59,7 +58,7 @@
r = adminSession.put("/projects/" + project.get() + "/ban/",
BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
assertThat(Iterables.getOnlyElement(info.alreadyBanned))
.isEqualTo("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96");
@@ -69,9 +68,9 @@
@Test
public void banCommit_Forbidden() throws Exception {
- RestResponse r =
- userSession.put("/projects/" + project.get() + "/ban/",
- BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .put("/projects/" + project.get() + "/ban/", BanCommit.Input.fromCommits(
+ "a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"))
+ .assertForbidden();
}
}
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 62dd729..46f93b6 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
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -29,7 +28,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.git.ProjectConfig;
import org.eclipse.jgit.lib.Constants;
import org.junit.Before;
@@ -84,9 +82,7 @@
}
private void blockCreateReference() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- block(cfg, Permission.CREATE, ANONYMOUS_USERS, "refs/*");
- saveProjectConfig(project, cfg);
+ block(Permission.CREATE, ANONYMOUS_USERS, "refs/*");
}
private void grantOwner() throws Exception {
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 030897b..07a2110 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
@@ -39,7 +39,6 @@
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.ProjectState;
-import org.apache.http.HttpStatus;
import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
@@ -58,7 +57,7 @@
public void testCreateProjectHttp() throws Exception {
String newProjectName = name("newProject");
RestResponse r = adminSession.put("/projects/" + newProjectName);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+ r.assertCreated();
ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
assertThat(p.name).isEqualTo(newProjectName);
ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
@@ -70,32 +69,36 @@
@Test
public void testCreateProjectHttpWhenProjectAlreadyExists_Conflict()
throws Exception {
- RestResponse r = adminSession.put("/projects/" + allProjects.get());
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ adminSession
+ .put("/projects/" + allProjects.get())
+ .assertConflict();
}
@Test
public void testCreateProjectHttpWhenProjectAlreadyExists_PreconditionFailed()
throws Exception {
- RestResponse r = adminSession.putWithHeader("/projects/" + allProjects.get(),
- new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_PRECONDITION_FAILED);
+ adminSession
+ .putWithHeader("/projects/" + allProjects.get(),
+ new BasicHeader(HttpHeaders.IF_NONE_MATCH, "*"))
+ .assertPreconditionFailed();
}
@Test
@UseLocalDisk
public void testCreateProjectHttpWithUnreasonableName_BadRequest()
throws Exception {
- RestResponse r = adminSession.put("/projects/" + Url.encode(name("invalid/../name")));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .put("/projects/" + Url.encode(name("invalid/../name")))
+ .assertBadRequest();
}
@Test
public void testCreateProjectHttpWithNameMismatch_BadRequest() throws Exception {
ProjectInput in = new ProjectInput();
in.name = name("otherName");
- RestResponse r = adminSession.put("/projects/" + name("someName"), in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .put("/projects/" + name("someName"), in)
+ .assertBadRequest();
}
@Test
@@ -103,8 +106,9 @@
throws Exception {
ProjectInput in = new ProjectInput();
in.branches = Collections.singletonList(name("invalid ref name"));
- RestResponse r = adminSession.put("/projects/" + name("newProject"), in);
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+ adminSession
+ .put("/projects/" + name("newProject"), in)
+ .assertBadRequest();
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index c9347cd..b02357e 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
@@ -16,7 +16,6 @@
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -26,7 +25,6 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.git.ProjectConfig;
import org.junit.Before;
import org.junit.Test;
@@ -76,9 +74,7 @@
}
private void blockForcePush() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
- saveProjectConfig(project, cfg);
+ block(Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
}
private void grantOwner() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
new file mode 100644
index 0000000..66d04df
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
@@ -0,0 +1,59 @@
+// 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.acceptance.rest.project;
+
+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;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Branch;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class FileBranchIT extends AbstractDaemonTest {
+
+ private Branch.NameKey branch;
+
+ @Before
+ public void setUp() throws Exception {
+ branch = new Branch.NameKey(project, "master");
+ PushOneCommit.Result change = createChange();
+ approve(change.getChangeId());
+ revision(change).submit();
+ }
+
+ @Test
+ public void getFileContent() throws Exception {
+ BinaryResult content = branch().file(PushOneCommit.FILE_NAME);
+ assertThat(content.asString()).isEqualTo(PushOneCommit.FILE_CONTENT);
+ }
+
+ @Test(expected = ResourceNotFoundException.class)
+ public void getNonExistingFile() throws Exception {
+ branch().file("does-not-exist");
+ }
+
+ private BranchApi branch() throws Exception {
+ return gApi.projects()
+ .name(branch.getParentKey().get())
+ .branch(branch.get());
+ }
+}
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 d680541..f55f591 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
@@ -14,8 +14,6 @@
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.GcAssert;
import com.google.gerrit.acceptance.RestResponse;
@@ -23,7 +21,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.inject.Inject;
-import org.apache.http.HttpStatus;
import org.junit.Before;
import org.junit.Test;
@@ -43,29 +40,27 @@
@Test
public void testGcNonExistingProject_NotFound() throws Exception {
- assertThat(POST("/projects/non-existing/gc")).isEqualTo(
- HttpStatus.SC_NOT_FOUND);
+ POST("/projects/non-existing/gc").assertNotFound();
}
@Test
public void testGcNotAllowed_Forbidden() throws Exception {
- assertThat(
- userSession.post("/projects/" + allProjects.get() + "/gc")
- .getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ userSession
+ .post("/projects/" + allProjects.get() + "/gc")
+ .assertForbidden();
}
@Test
@UseLocalDisk
public void testGcOneProject() throws Exception {
- assertThat(POST("/projects/" + allProjects.get() + "/gc")).isEqualTo(
- HttpStatus.SC_OK);
+ POST("/projects/" + allProjects.get() + "/gc").assertOK();
gcAssert.assertHasPackFile(allProjects);
gcAssert.assertHasNoPackFile(project, project2);
}
- private int POST(String endPoint) throws IOException {
+ private RestResponse POST(String endPoint) throws IOException {
RestResponse r = adminSession.post(endPoint);
r.consume();
- return r.getStatusCode();
+ return r;
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index ea3a5db..f87b921 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -14,7 +14,6 @@
package com.google.gerrit.acceptance.rest.project;
-import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -71,10 +70,8 @@
private void assertChildNotFound(Project.NameKey parent, String child)
throws Exception {
- try {
- gApi.projects().name(parent.get()).child(child);
- } catch (ResourceNotFoundException e) {
- assertThat(e.getMessage()).contains(child);
- }
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage(child);
+ gApi.projects().name(parent.get()).child(child).get();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 7044ad0..d32be8b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -25,7 +25,6 @@
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.server.git.ProjectConfig;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -40,7 +39,7 @@
@Before
public void setUp() throws Exception {
repo = GitUtil.newTestRepository(repoManager.openRepository(project));
- blockRead(project, "refs/*");
+ blockRead("refs/*");
}
@After
@@ -130,15 +129,15 @@
}
private void assertNotFound(ObjectId id) throws Exception {
- RestResponse r = userSession.get(
- "/projects/" + project.get() + "/commits/" + id.name());
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+ userSession
+ .get("/projects/" + project.get() + "/commits/" + id.name())
+ .assertNotFound();
}
private CommitInfo getCommit(ObjectId id) throws Exception {
RestResponse r = userSession.get(
"/projects/" + project.get() + "/commits/" + id.name());
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
CommitInfo result = newGson().fromJson(r.getReader(), CommitInfo.class);
r.consume();
return result;
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 0799d48..a3a107d 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
@@ -37,7 +37,7 @@
@Test
public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
- blockRead(project, "refs/*");
+ blockRead("refs/*");
setApiUser(user);
exception.expect(ResourceNotFoundException.class);
gApi.projects().name(project.get()).branches().get();
@@ -66,7 +66,7 @@
@Test
public void listBranchesSomeHidden() throws Exception {
- blockRead(project, "refs/heads/dev");
+ blockRead("refs/heads/dev");
String master = pushTo("refs/heads/master").getCommit().name();
pushTo("refs/heads/dev");
setApiUser(user);
@@ -79,7 +79,7 @@
@Test
public void listBranchesHeadHidden() throws Exception {
- blockRead(project, "refs/heads/master");
+ blockRead("refs/heads/master");
pushTo("refs/heads/master");
String dev = pushTo("refs/heads/dev").getCommit().name();
setApiUser(user);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 80ad493..78e0ba2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -14,7 +14,6 @@
package com.google.gerrit.acceptance.rest.project;
-import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -29,11 +28,9 @@
@Test
public void listChildrenOfNonExistingProject_NotFound() throws Exception {
- try {
- gApi.projects().name(name("non-existing")).child("children");
- } catch (ResourceNotFoundException e) {
- assertThat(e.getMessage()).contains("non-existing");
- }
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("non-existing");
+ gApi.projects().name(name("non-existing")).child("children");
}
@Test
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 1e51571..e86bb29 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
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
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;
@@ -197,9 +198,10 @@
.containsExactly(allProjects, allUsers, project).inOrder();
}
- private static void assertBadRequest(ListRequest req) throws Exception {
+ private void assertBadRequest(ListRequest req) throws Exception {
try {
req.get();
+ fail("Expected BadRequestException");
} catch (BadRequestException expected) {
// Expected.
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index 67d4d1f..e09c63a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.SetParent;
-import org.apache.http.HttpStatus;
import org.junit.Test;
public class SetParentIT extends AbstractDaemonTest {
@@ -31,7 +30,7 @@
RestResponse r =
userSession.put("/projects/" + project.get() + "/parent",
newParentInput(parent));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
+ r.assertForbidden();
r.consume();
}
@@ -41,11 +40,11 @@
RestResponse r =
adminSession.put("/projects/" + project.get() + "/parent",
newParentInput(parent));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
r.consume();
r = adminSession.get("/projects/" + project.get() + "/parent");
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+ r.assertOK();
String newParent =
newGson().fromJson(r.getReader(), String.class);
assertThat(newParent).isEqualTo(parent);
@@ -57,7 +56,7 @@
RestResponse r =
adminSession.put("/projects/" + allProjects.get() + "/parent",
newParentInput(project.get()));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ r.assertConflict();
r.consume();
}
@@ -66,19 +65,19 @@
RestResponse r =
adminSession.put("/projects/" + project.get() + "/parent",
newParentInput(project.get()));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ r.assertConflict();
r.consume();
Project.NameKey child = createProject("child", project, true);
r = adminSession.put("/projects/" + project.get() + "/parent",
newParentInput(child.get()));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ r.assertConflict();
r.consume();
String grandchild = createProject("grandchild", child, true).get();
r = adminSession.put("/projects/" + project.get() + "/parent",
newParentInput(grandchild));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+ r.assertConflict();
r.consume();
}
@@ -87,7 +86,7 @@
RestResponse r =
adminSession.put("/projects/" + project.get() + "/parent",
newParentInput("non-existing"));
- assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
+ r.assertUnprocessableEntity();
r.consume();
}
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 b22d26b9..33cfe99 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
@@ -26,7 +26,6 @@
import com.google.gerrit.extensions.api.projects.TagInfo;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import org.apache.http.HttpStatus;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.transport.PushResult;
@@ -42,8 +41,9 @@
@Test
public void listTagsOfNonExistingProject() throws Exception {
- assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
- .isEqualTo(HttpStatus.SC_NOT_FOUND);
+ adminSession
+ .get("/projects/non-existing/tags")
+ .assertNotFound();
}
@Test
@@ -60,15 +60,15 @@
@Test
public void listTagsOfNonVisibleProject() throws Exception {
- blockRead(project, "refs/*");
- assertThat(
- userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
- .isEqualTo(HttpStatus.SC_NOT_FOUND);
+ blockRead("refs/*");
+ userSession
+ .get("/projects/" + project.get() + "/tags")
+ .assertNotFound();
}
@Test
public void listTagsOfNonVisibleProjectWithApi() throws Exception {
- blockRead(project, "refs/*");
+ blockRead("refs/*");
setApiUser(user);
exception.expect(ResourceNotFoundException.class);
gApi.projects().name(project.get()).tags().get();
@@ -76,7 +76,7 @@
@Test
public void getTagOfNonVisibleProjectWithApi() throws Exception {
- blockRead(project, "refs/*");
+ blockRead("refs/*");
exception.expect(ResourceNotFoundException.class);
gApi.projects().name(project.get()).tag("tag").get();
}
@@ -211,7 +211,7 @@
assertThat(result.get(1).ref).isEqualTo("refs/tags/" + tag2.name);
assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
- blockRead(project, "refs/heads/hidden");
+ blockRead("refs/heads/hidden");
result = getTags().get();
assertThat(result).hasSize(1);
assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
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 b784f05..ae4003c 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
@@ -289,6 +289,20 @@
}
@Test
+ public void listChangeWithDrafts() throws Exception {
+ for (Integer line : lines) {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ DraftInput comment = newDraft(
+ "file1", Side.REVISION, line, "comment 1");
+ addDraft(changeId, revId, comment);
+ assertThat(gApi.changes().query(
+ "change:" + changeId + " has:draft").get()).hasSize(1);
+ }
+ }
+
+ @Test
public void publishCommentsAllRevisions() throws Exception {
PushOneCommit.Result r1 = createChange();
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 357f268..387092d 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
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
+import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -127,7 +128,7 @@
revision(r).review(new ReviewInput().label(P.getName(), 0));
ChangeInfo c = get(r.getChangeId());
LabelInfo q = c.labels.get(P.getName());
- assertThat(q.all).hasSize(2);
+ assertThat(q.all).hasSize(isNoteDbTestEnabled() ? 1 : 2);
assertThat(q.disliked).isNull();
assertThat(q.rejected).isNull();
assertThat(q.blocking).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index 363a7e4..f7e11fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.Util;
@@ -46,6 +47,15 @@
}
@Test
+ public void failChangedLabelValueOnClosedChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ merge(r);
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("change is closed");
+ revision(r).review(ReviewInput.reject());
+ }
+
+ @Test
public void noCopyMinScoreOnRework() throws Exception {
codeReview.setCopyMinScore(false);
saveLabelConfig();
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 28e0d24..b6cd45f 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
@@ -49,6 +49,7 @@
public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
public static final String ADMIN_PLUGINS = "/admin/plugins/";
public static final String MY_GROUPS = "/groups/self";
+ public static final String DOCUMENTATION = "/Documentation/";
public static String toChange(final ChangeInfo c) {
return toChange(c.getId());
@@ -140,6 +141,10 @@
return SETTINGS_EXTENSION + pluginName + "/" + path;
}
+ public static String toDocumentationQuery(String query) {
+ return DOCUMENTATION + KeyUtil.encode(query);
+ }
+
private static String status(Status status) {
switch (status) {
case ABANDONED:
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index e879347..07f2965 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.12</version>
+ <version>2.13-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/GerritApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
index 8b9f520..4404f94 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/GerritApi.java
@@ -22,11 +22,11 @@
import com.google.gerrit.extensions.restapi.NotImplementedException;
public interface GerritApi {
- public Accounts accounts();
- public Changes changes();
- public Config config();
- public Groups groups();
- public Projects projects();
+ Accounts accounts();
+ Changes changes();
+ Config config();
+ Groups groups();
+ Projects projects();
/**
* A default implementation which allows source compatibility
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 32f8488..9cddda9 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
@@ -38,6 +38,11 @@
AccountApi id(String id) throws RestApiException;
/**
+ * @see #id(String)
+ */
+ AccountApi id(int id) throws RestApiException;
+
+ /**
* Look up the account of the current in-scope user.
*
* @see #id(String)
@@ -118,6 +123,11 @@
}
@Override
+ public AccountApi id(int id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public AccountApi self() throws RestApiException {
throw new NotImplementedException();
}
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 ce07098..d927231 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
@@ -58,6 +58,19 @@
*/
RevisionApi revision(String id) throws RestApiException;
+ /**
+ * Look up the reviewer of the change.
+ * <p>
+ * @param id ID of the account, can be a string of the format
+ * "Full Name <mail@example.com>", just the email address, a full name
+ * if it is unique, an account ID, a user name or 'self' for the
+ * calling user.
+ * @return API for accessing the reviewer.
+ * @throws RestApiException if id is not account ID or is a user that isn't
+ * known to be a reviewer for this change.
+ */
+ ReviewerApi reviewer(String id) throws RestApiException;
+
void abandon() throws RestApiException;
void abandon(AbandonInput in) throws RestApiException;
@@ -177,6 +190,11 @@
}
@Override
+ public ReviewerApi reviewer(String id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public RevisionApi revision(String id) throws RestApiException {
throw new NotImplementedException();
}
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
new file mode 100644
index 0000000..11b670d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -0,0 +1,25 @@
+// 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.extensions.api.changes;
+
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+import java.util.Map;
+
+public interface ReviewerApi {
+
+ Map<String, Short> votes() throws RestApiException;
+ void deleteVote(String label) throws RestApiException;
+}
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 91cb70e..e7c98e9 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
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -25,6 +26,11 @@
void delete() throws RestApiException;
/**
+ * Returns the content of a file from the HEAD revision.
+ */
+ BinaryResult file(String path) throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility
* when adding new methods to the interface.
**/
@@ -43,5 +49,10 @@
public void delete() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public BinaryResult file(String path) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
new file mode 100644
index 0000000..3fa7bb2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
@@ -0,0 +1,42 @@
+// 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.extensions.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.io.IOException;
+
+@ExtensionPoint
+public interface OAuthLoginProvider {
+
+ /**
+ * Performs a login with an OAuth2 provider for Git over HTTP
+ * communication.
+ *
+ * An implementation of this interface must transmit the given
+ * user name and secret, which can be either an OAuth2 access token
+ * or a password, to the OAuth2 backend for verification.
+ *
+ * @param username the user's identifier.
+ * @param secret the secret to verify, e.g. a previously received
+ * access token or a password.
+ *
+ * @return information about the logged in user, at least
+ * external id, user name and email address.
+ *
+ * @throws IOException if the login failed.
+ */
+ OAuthUserInfo login(String username, String secret) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ReviewerState.java
similarity index 63%
rename from gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerState.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ReviewerState.java
index b829a69..a58c959 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ReviewerState.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// 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.
@@ -12,28 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.notedb;
+package com.google.gerrit.extensions.client;
-import org.eclipse.jgit.revwalk.FooterKey;
-
-/** State of a reviewer on a change. */
public enum ReviewerState {
/** The user has contributed at least one nonzero vote on the change. */
- REVIEWER(new FooterKey("Reviewer")),
+ REVIEWER,
/** The reviewer was added to the change, but has not voted. */
- CC(new FooterKey("CC")),
+ CC,
/** The user was previously a reviewer on the change, but was removed. */
- REMOVED(new FooterKey("Removed"));
-
- private final FooterKey footerKey;
-
- private ReviewerState(FooterKey footerKey) {
- this.footerKey = footerKey;
- }
-
- FooterKey getFooterKey() {
- return footerKey;
- }
+ REMOVED;
}
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 cdfe0c6..455243a 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
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ReviewerState;
import java.sql.Timestamp;
import java.util.Collection;
@@ -48,6 +49,7 @@
public Map<String, LabelInfo> labels;
public Map<String, Collection<String>> permittedLabels;
public Collection<AccountInfo> removableReviewers;
+ public Map<ReviewerState, Collection<AccountInfo>> reviewers;
public Collection<ChangeMessageInfo> messages;
public String currentRevision;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
index 00d0c18..a812908 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/FileInfo.java
@@ -21,4 +21,5 @@
public Integer linesInserted;
public Integer linesDeleted;
public long sizeDelta;
+ public long size;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index 93da347..b3ed37b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -21,9 +21,9 @@
/** Listener interested in server startup and shutdown events. */
@ExtensionPoint
public interface LifecycleListener extends EventListener {
- /** Invoke when the server is starting. */
- public void start();
+ /** Invoked when the server is starting. */
+ void start();
/** Invoked when the server is stopping. */
- public void stop();
+ void stop();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
index 365d056..a6b8d38 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/UsageDataPublishedListener.java
@@ -35,10 +35,10 @@
}
public interface MetaData {
- public String getName();
- public String getUnitName();
- public String getUnitSymbol();
- public String getDescription();
+ String getName();
+ String getUnitName();
+ String getUnitSymbol();
+ String getDescription();
}
void onUsageDataPublished(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
index 2243786..4688f7c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
@@ -17,5 +17,5 @@
/** Handle for registered information. */
public interface RegistrationHandle {
/** Delete this registration. */
- public void remove();
+ void remove();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index 7284296..ef38303 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
@@ -18,7 +18,7 @@
import com.google.inject.Provider;
public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
- public Key<T> getKey();
+ Key<T> getKey();
- public RegistrationHandle replace(Key<T> key, Provider<T> item);
+ RegistrationHandle replace(Key<T> key, Provider<T> item);
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
index f95161d..3b32829 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
@@ -18,5 +18,5 @@
* A view which may change, although the underlying resource did not change
*/
public interface ETagView<R extends RestResource> extends RestReadView<R> {
- public String getETag(R rsrc);
+ String getETag(R rsrc);
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
index 8032531..29c824f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -30,11 +30,11 @@
* @return time for the Last-Modified header. HTTP truncates the header
* value to seconds.
*/
- public Timestamp getLastModified();
+ Timestamp getLastModified();
}
/** A resource with an ETag. */
public interface HasETag {
- public String getETag();
+ String getETag();
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
index 63172ce..fb81e0c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -29,7 +29,7 @@
* assumed unavailable and not presented. This is usually the same as
* {@code setVisible(false)}.
*/
- public Description getDescription(R resource);
+ Description getDescription(R resource);
/** Describes an action invokable through the web interface. */
public static class Description {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
index 05f41d4..4b67260 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -15,5 +15,5 @@
package com.google.gwtexpui.globalkey.client;
public interface KeyCommandFilter {
- public boolean include(KeyCommand key);
+ boolean include(KeyCommand key);
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
index f7bc907..487e613 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/FindReplace.java
@@ -22,7 +22,7 @@
* @return regular expression to match substrings with; should be treated as
* immutable.
*/
- public RegExp pattern();
+ RegExp pattern();
/**
* Find and replace a single instance of this pattern in an input.
@@ -36,5 +36,5 @@
* @return result of regular expression replacement.
* @throws IllegalArgumentException if the input could not be safely sanitized.
*/
- public String replace(String input);
+ String replace(String input);
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index 950400a..d94f243 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -129,9 +129,8 @@
}
public static boolean hasCacheHeader(HttpServletResponse res) {
- return res.getHeader("Cache-Control") != null
- || res.getHeader("Expires") != null
- || "no-cache".equals(res.getHeader("Pragma"));
+ return res.containsHeader("Cache-Control")
+ || res.containsHeader("Expires");
}
private static void cache(HttpServletResponse res,
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 a5a02cd..eca28a0 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
@@ -19,92 +19,92 @@
public interface Resources extends ClientBundle {
@Source("addFileComment.png")
- public ImageResource addFileComment();
+ ImageResource addFileComment();
@Source("arrowDown.png")
- public ImageResource arrowDown();
+ ImageResource arrowDown();
@Source("arrowRight.png")
- public ImageResource arrowRight();
+ ImageResource arrowRight();
@Source("arrowUp.png")
- public ImageResource arrowUp();
+ ImageResource arrowUp();
@Source("deleteHover.png")
- public ImageResource deleteHover();
+ ImageResource deleteHover();
@Source("deleteNormal.png")
- public ImageResource deleteNormal();
+ ImageResource deleteNormal();
@Source("diffy26.png")
- public ImageResource gerritAvatar26();
+ ImageResource gerritAvatar26();
@Source("downloadIcon.png")
- public ImageResource downloadIcon();
+ ImageResource downloadIcon();
@Source("draftComments.png")
- public ImageResource draftComments();
+ ImageResource draftComments();
@Source("editText.png")
- public ImageResource edit();
+ ImageResource edit();
@Source("editUndo.png")
- public ImageResource editUndo();
+ ImageResource editUndo();
@Source("gear.png")
- public ImageResource gear();
+ ImageResource gear();
@Source("goNext.png")
- public ImageResource goNext();
+ ImageResource goNext();
@Source("goPrev.png")
- public ImageResource goPrev();
+ ImageResource goPrev();
@Source("goUp.png")
- public ImageResource goUp();
+ ImageResource goUp();
@Source("greenCheck.png")
- public ImageResource greenCheck();
+ ImageResource greenCheck();
@Source("info.png")
- public ImageResource info();
+ ImageResource info();
@Source("listAdd.png")
- public ImageResource listAdd();
+ ImageResource listAdd();
@Source("mediaFloppy.png")
- public ImageResource save();
+ ImageResource save();
@Source("merge.png")
- public ImageResource merge();
+ ImageResource merge();
@Source("queryIcon.png")
- public ImageResource queryIcon();
+ ImageResource queryIcon();
@Source("readOnly.png")
- public ImageResource readOnly();
+ ImageResource readOnly();
@Source("redNot.png")
- public ImageResource redNot();
+ ImageResource redNot();
@Source("sideBySideDiff.png")
- public ImageResource sideBySideDiff();
+ ImageResource sideBySideDiff();
@Source("starFilled.png")
- public ImageResource starFilled();
+ ImageResource starFilled();
@Source("starOpen.png")
- public ImageResource starOpen();
+ ImageResource starOpen();
@Source("undoNormal.png")
- public ImageResource undoNormal();
+ ImageResource undoNormal();
@Source("unifiedDiff.png")
- public ImageResource unifiedDiff();
+ ImageResource unifiedDiff();
@Source("warning.png")
- public ImageResource warning();
+ ImageResource warning();
@Source("question.png")
- public ImageResource question();
+ ImageResource question();
}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
index 11a1b6a..afde038 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AccountPreferencesInfo.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.EmailStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
import com.google.gwt.core.client.JavaScriptObject;
@@ -44,7 +45,6 @@
p.useFlashClipboard(defaultPrefs.isUseFlashClipboard());
p.downloadScheme(defaultPrefs.getDownloadUrl());
p.downloadCommand(defaultPrefs.getDownloadCommand());
- p.copySelfOnEmail(defaultPrefs.isCopySelfOnEmails());
p.dateFormat(defaultPrefs.getDateFormat());
p.timeFormat(defaultPrefs.getTimeFormat());
p.relativeDateInChangeTable(defaultPrefs.isRelativeDateInChangeTable());
@@ -53,6 +53,7 @@
p.muteCommonPathPrefixes(defaultPrefs.isMuteCommonPathPrefixes());
p.reviewCategoryStrategy(defaultPrefs.getReviewCategoryStrategy());
p.diffView(defaultPrefs.getDiffView());
+ p.emailStrategy(defaultPrefs.getEmailStrategy());
return p;
}
@@ -82,9 +83,6 @@
private final native String downloadCommandRaw()
/*-{ return this.download_command }-*/;
- public final native boolean copySelfOnEmail()
- /*-{ return this.copy_self_on_email || false }-*/;
-
public final DateFormat dateFormat() {
String s = dateFormatRaw();
return s != null ? DateFormat.valueOf(s) : null;
@@ -125,6 +123,14 @@
private final native String diffViewRaw()
/*-{ return this.diff_view }-*/;
+ public final EmailStrategy emailStrategy() {
+ String s = emailStrategyRaw();
+ return s != null ? EmailStrategy.valueOf(s) : null;
+ }
+
+ private final native String emailStrategyRaw()
+ /*-{ return this.email_strategy }-*/;
+
public final native JsArray<TopMenuItem> my()
/*-{ return this.my; }-*/;
@@ -146,9 +152,6 @@
public final native void downloadCommandRaw(String d)
/*-{ this.download_command = d }-*/;
- public final native void copySelfOnEmail(boolean c)
- /*-{ this.copy_self_on_email = c }-*/;
-
public final void dateFormat(DateFormat f) {
dateFormatRaw(f != null ? f.toString() : null);
}
@@ -185,6 +188,12 @@
private final native void diffViewRaw(String d)
/*-{ this.diff_view = d }-*/;
+ public final void emailStrategy(EmailStrategy s) {
+ emailStrategyRaw(s != null ? s.toString() : null);
+ }
+ private final native void emailStrategyRaw(String s)
+ /*-{ this.email_strategy = s }-*/;
+
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/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index cace7ad..c5639c1 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
@@ -30,6 +30,7 @@
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
@@ -84,6 +85,16 @@
return allLabels().keySet();
}
+ public final Set<Integer> removableReviewerIds() {
+ Set<Integer> removable = new HashSet<>();
+ if (removableReviewers() != null) {
+ for (AccountInfo a : Natives.asList(removableReviewers())) {
+ removable.add(a._accountId());
+ }
+ }
+ return removable;
+ }
+
public final native String id() /*-{ return this.id; }-*/;
public final native String project() /*-{ return this.project; }-*/;
public final native String branch() /*-{ return this.branch; }-*/;
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 d95f9ef..555e06e 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
@@ -34,6 +34,11 @@
// 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
+ public final long size() {
+ return (long)_size();
+ }
+ private final native double _size() /*-{ return this.size || 0; }-*/;
+
public final long sizeDelta() {
return (long)_sizeDelta();
}
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 55ef892..750412d 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
@@ -36,6 +36,7 @@
public final native String allProjects() /*-{ return this.all_projects; }-*/;
public final native String allUsers() /*-{ return this.all_users; }-*/;
+ public final native boolean docSearch() /*-{ return this.doc_search; }-*/;
public final native String docUrl() /*-{ return this.doc_url; }-*/;
public final native boolean editGpgKeys() /*-{ return this.edit_gpg_keys || false; }-*/;
public final native String reportBugUrl() /*-{ return this.report_bug_url; }-*/;
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index 783c343..88d603a 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -70,7 +70,7 @@
strict = True,
experimental_args = args,
vm_args = GWT_JVM_ARGS,
- visibility = ['//:eclipse'],
+ visibility = ['PUBLIC'],
)
gwt_binary(
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 c77b71f..1c44ebc 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
@@ -14,6 +14,7 @@
package com.google.gerrit.client;
+import com.google.gerrit.client.change.Resources;
import com.google.gerrit.client.info.AccountInfo;
import com.google.gerrit.client.info.AccountPreferencesInfo;
import com.google.gerrit.reviewdb.client.Account;
@@ -134,4 +135,19 @@
+ NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
+ " " + "KMGTPE".charAt(exp - 1) + "iB";
}
+
+ public static String formatPercentage(long size, long delta) {
+ if (size == 0) {
+ return Resources.C.notAvailable();
+ }
+ return (delta > 0 ? "+" : "-") + formatAbsPercentage(size, delta);
+ }
+
+ public static String formatAbsPercentage(long size, long delta) {
+ if (size == 0) {
+ return Resources.C.notAvailable();
+ }
+ int p = Math.abs(Math.round(delta * 100 / size));
+ return p + "%";
+ }
}
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 560fa9e..139b9c0 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
@@ -25,6 +25,7 @@
import com.google.gerrit.client.admin.ProjectScreen;
import com.google.gerrit.client.api.ApiGlue;
import com.google.gerrit.client.api.PluginLoader;
+import com.google.gerrit.client.change.LocalComments;
import com.google.gerrit.client.changes.ChangeConstants;
import com.google.gerrit.client.changes.ChangeListScreen;
import com.google.gerrit.client.config.ConfigServerApi;
@@ -117,6 +118,7 @@
private static AccountPreferencesInfo myPrefs;
private static UrlAliasMatcher urlAliasMatcher;
private static boolean hasDocumentation;
+ private static boolean docSearch;
private static String docUrl;
private static HostPageData.Theme myTheme;
private static String defaultScreenToken;
@@ -205,6 +207,9 @@
doSignIn(token);
} else {
view.setToken(token);
+ if (isSignedIn()) {
+ LocalComments.saveInlineComments();
+ }
body.setView(view);
}
}
@@ -483,6 +488,7 @@
hasDocumentation = true;
docUrl = du;
}
+ docSearch = info.gerrit().docSearch();
}
}));
HostPageDataService hpd = GWT.create(HostPageDataService.class);
@@ -914,6 +920,10 @@
urlAliasMatcher.updateUserAliases(myPrefs.urlAliases());
}
+ public static boolean hasDocSearch() {
+ return docSearch;
+ }
+
private static void getDocIndex(final AsyncCallback<DocInfo> cb) {
RequestBuilder req =
new RequestBuilder(RequestBuilder.HEAD, GWT.getHostPageBaseURL()
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 269999c..6802a0d 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
@@ -126,4 +126,7 @@
String stringListPanelDelete();
String stringListPanelUp();
String stringListPanelDown();
+
+ String searchDropdownChanges();
+ String searchDropdownDoc();
}
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 fb74506..83736cd 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
@@ -109,3 +109,6 @@
stringListPanelDelete = Delete
stringListPanelUp = Up
stringListPanelDown = Down
+
+searchDropdownChanges = Changes
+searchDropdownDoc = Docs
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
index 21da8ce..58442b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
@@ -36,4 +36,6 @@
String cannotDownloadPlugin(String scriptPath);
String parentUpdateFailed(String message);
+
+ String fileCount(int fileNumber, int fileCount);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 2832d41..b2d67b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -17,3 +17,5 @@
cannotDownloadPlugin = Cannot load plugin from {0}
parentUpdateFailed = Setting parent project failed: {0}
+
+fileCount = File {0} of {1}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index 45b1d52..83a187b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -27,6 +27,7 @@
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwtexpui.globalkey.client.GlobalKey;
@@ -34,6 +35,7 @@
class SearchPanel extends Composite {
private final HintTextBox searchBox;
+ private final ListBox dropdown;
private HandlerRegistration regFocus;
SearchPanel() {
@@ -54,6 +56,18 @@
}
});
+ if (Gerrit.hasDocSearch()) {
+ dropdown = new ListBox();
+ dropdown.setStyleName("searchDropdown");
+ dropdown.addItem(Gerrit.C.searchDropdownChanges());
+ dropdown.addItem(Gerrit.C.searchDropdownDoc());
+ dropdown.setVisibleItemCount(1);
+ dropdown.setSelectedIndex(0);
+ } else {
+ // Doc search is NOT available.
+ dropdown = null;
+ }
+
final SuggestBox suggestBox =
new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
searchBox.setStyleName("searchTextBox");
@@ -70,6 +84,9 @@
});
body.add(suggestBox);
+ if (dropdown != null) {
+ body.add(dropdown);
+ }
body.add(searchButton);
}
@@ -110,14 +127,23 @@
searchBox.setFocus(false);
- if (query.matches("^[1-9][0-9]*$")) {
- Gerrit.display(PageLinks.toChange(Change.Id.parse(query)));
+ if (dropdown != null
+ && dropdown.getSelectedValue().equals(Gerrit.C.searchDropdownDoc())) {
+ // doc
+ Gerrit.display(PageLinks.toDocumentationQuery(query));
} else {
- Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
+ // changes
+ if (query.matches("^[1-9][0-9]*$")) {
+ Gerrit.display(PageLinks.toChange(Change.Id.parse(query)));
+ } else {
+ Gerrit.display(
+ PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
+ }
}
}
- private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
+ private static class MySuggestionDisplay
+ extends SuggestBox.DefaultSuggestionDisplay {
private boolean isSuggestionSelected;
private MySuggestionDisplay() {
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 94884fa..e49b95f 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
@@ -31,7 +31,6 @@
String contextWholeFile();
String showSiteHeader();
String useFlashClipboard();
- String copySelfOnEmails();
String reviewCategoryLabel();
String messageShowInReviewCategoryNone();
String messageShowInReviewCategoryName();
@@ -159,4 +158,9 @@
String welcomeAgreementText();
String welcomeAgreementLater();
String welcomeContinue();
+
+ String messageEnabled();
+ String messageCCMeOnMyComments();
+ String messageDisabled();
+ String emailFieldLabel();
}
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 0944448..9a00ded 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
@@ -7,7 +7,6 @@
accountId = Account ID
showSiteHeader = Show Site Header
useFlashClipboard = Use Flash Clipboard Widget
-copySelfOnEmails = CC Me On Comments I Write
reviewCategoryLabel = Display In Review Category
messageShowInReviewCategoryNone = None (default)
messageShowInReviewCategoryName = Show Name
@@ -15,6 +14,11 @@
messageShowInReviewCategoryUsername = Show Username
messageShowInReviewCategoryAbbrev = Show Abbreviated Name
+emailFieldLabel = Email Notifications:
+messageEnabled = Enabled
+messageCCMeOnMyComments = CC Me On Comments I Write
+messageDisabled = Disabled
+
maximumPageSizeFieldLabel = Maximum Page Size:
diffViewLabel = Diff View:
dateFormatLabel = Date/Time Format:
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 2b20ad6..c0b556c 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
@@ -29,6 +29,7 @@
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.EmailStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -50,7 +51,6 @@
public class MyPreferencesScreen extends SettingsScreen {
private CheckBox showSiteHeader;
private CheckBox useFlashClipboard;
- private CheckBox copySelfOnEmails;
private CheckBox relativeDateInChangeTable;
private CheckBox sizeBarInChangeTable;
private CheckBox legacycidInChangeTable;
@@ -60,6 +60,7 @@
private ListBox timeFormat;
private ListBox reviewCategoryStrategy;
private ListBox diffView;
+ private ListBox emailStrategy;
private StringListPanel myMenus;
private Button save;
@@ -69,7 +70,6 @@
showSiteHeader = new CheckBox(Util.C.showSiteHeader());
useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
- copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
maximumPageSize = new ListBox();
for (final short v : PAGESIZE_CHOICES) {
maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
@@ -92,6 +92,20 @@
Util.C.messageShowInReviewCategoryAbbrev(),
AccountGeneralPreferences.ReviewCategoryStrategy.ABBREV.name());
+ emailStrategy = new ListBox();
+ emailStrategy.addItem(Util.C.messageEnabled(),
+ AccountGeneralPreferences.EmailStrategy.ENABLED.name());
+ emailStrategy
+ .addItem(
+ Util.C.messageCCMeOnMyComments(),
+ AccountGeneralPreferences.EmailStrategy.CC_ON_OWN_COMMENTS
+ .name());
+ emailStrategy
+ .addItem(
+ Util.C.messageDisabled(),
+ AccountGeneralPreferences.EmailStrategy.DISABLED
+ .name());
+
diffView = new ListBox();
diffView.addItem(
com.google.gerrit.client.changes.Util.C.sideBySide(),
@@ -141,7 +155,7 @@
muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
- final Grid formGrid = new Grid(10 + (flashClippy ? 1 : 0), 2);
+ final Grid formGrid = new Grid(11 + (flashClippy ? 1 : 0), 2);
int row = 0;
formGrid.setText(row, labelIdx, "");
@@ -154,10 +168,6 @@
row++;
}
- formGrid.setText(row, labelIdx, "");
- formGrid.setWidget(row, fieldIdx, copySelfOnEmails);
- row++;
-
formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
row++;
@@ -186,6 +196,10 @@
formGrid.setWidget(row, fieldIdx, muteCommonPathPrefixes);
row++;
+ formGrid.setText(row, labelIdx, Util.C.emailFieldLabel());
+ formGrid.setWidget(row, fieldIdx, emailStrategy);
+ row++;
+
formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
formGrid.setWidget(row, fieldIdx, diffView);
@@ -208,7 +222,6 @@
final OnEditEnabler e = new OnEditEnabler(save);
e.listenTo(showSiteHeader);
e.listenTo(useFlashClipboard);
- e.listenTo(copySelfOnEmails);
e.listenTo(maximumPageSize);
e.listenTo(dateFormat);
e.listenTo(timeFormat);
@@ -218,6 +231,7 @@
e.listenTo(muteCommonPathPrefixes);
e.listenTo(diffView);
e.listenTo(reviewCategoryStrategy);
+ e.listenTo(emailStrategy);
}
@Override
@@ -240,7 +254,6 @@
private void enable(final boolean on) {
showSiteHeader.setEnabled(on);
useFlashClipboard.setEnabled(on);
- copySelfOnEmails.setEnabled(on);
maximumPageSize.setEnabled(on);
dateFormat.setEnabled(on);
timeFormat.setEnabled(on);
@@ -250,12 +263,12 @@
muteCommonPathPrefixes.setEnabled(on);
reviewCategoryStrategy.setEnabled(on);
diffView.setEnabled(on);
+ emailStrategy.setEnabled(on);
}
private void display(AccountPreferencesInfo p) {
showSiteHeader.setValue(p.showSiteHeader());
useFlashClipboard.setValue(p.useFlashClipboard());
- copySelfOnEmails.setValue(p.copySelfOnEmail());
setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.changesPerPage());
setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
p.dateFormat());
@@ -271,6 +284,9 @@
setListBox(diffView,
AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
p.diffView());
+ setListBox(emailStrategy,
+ AccountGeneralPreferences.EmailStrategy.ENABLED,
+ p.emailStrategy());
display(p.my());
}
@@ -337,7 +353,6 @@
AccountPreferencesInfo p = AccountPreferencesInfo.create();
p.showSiteHeader(showSiteHeader.getValue());
p.useFlashClipboard(useFlashClipboard.getValue());
- p.copySelfOnEmail(copySelfOnEmails.getValue());
p.changesPerPage(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
p.dateFormat(getListBox(dateFormat,
AccountGeneralPreferences.DateFormat.STD,
@@ -355,6 +370,8 @@
p.diffView(getListBox(diffView,
AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
AccountGeneralPreferences.DiffView.values()));
+ p.emailStrategy(getListBox(emailStrategy,
+ EmailStrategy.ENABLED, EmailStrategy.values()));
List<TopMenuItem> items = new ArrayList<>();
for (List<String> v : myMenus.getValues()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
index f1ac27f..63ae5f4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -25,11 +25,11 @@
AdminCss css();
@Source("deleteNormal.png")
- public ImageResource deleteNormal();
+ ImageResource deleteNormal();
@Source("deleteHover.png")
- public ImageResource deleteHover();
+ ImageResource deleteHover();
@Source("undoNormal.png")
- public ImageResource undoNormal();
+ ImageResource undoNormal();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index b86d0a8..0bf1b4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -231,19 +231,20 @@
private Set<String> getDiffs(ProjectAccess wantedAccess,
ProjectAccess newAccess) {
- final List<AccessSection> wantedSections =
+ List<AccessSection> wantedSections =
mergeSections(removeEmptyPermissionsAndSections(wantedAccess.getLocal()));
- final HashSet<AccessSection> same = new HashSet<>(wantedSections);
- final HashSet<AccessSection> different =
- new HashSet<>(wantedSections.size()
- + newAccess.getLocal().size());
+ List<AccessSection> newSections =
+ removeEmptyPermissionsAndSections(newAccess.getLocal());
+ HashSet<AccessSection> same = new HashSet<>(wantedSections);
+ HashSet<AccessSection> different =
+ new HashSet<>(wantedSections.size() + newSections.size());
different.addAll(wantedSections);
- different.addAll(newAccess.getLocal());
- same.retainAll(newAccess.getLocal());
+ different.addAll(newSections);
+ same.retainAll(newSections);
different.removeAll(same);
- final Set<String> differentNames = new HashSet<>();
- for (final AccessSection s : different) {
+ Set<String> differentNames = new HashSet<>();
+ for (AccessSection s : different) {
differentNames.add(s.getName());
}
return differentNames;
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 f5b26d1..63de389 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
@@ -16,7 +16,7 @@
import com.google.gwt.i18n.client.Constants;
-interface ChangeConstants extends Constants {
+public interface ChangeConstants extends Constants {
String previousChange();
String nextChange();
String openChange();
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 2673f49..60d21ba 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
@@ -146,6 +146,7 @@
private boolean hasDraftComments;
private CommentLinkProcessor commentLinkProcessor;
private EditInfo edit;
+ private LocalComments lc;
private List<HandlerRegistration> handlers = new ArrayList<>(4);
private UpdateCheckTimer updateCheck;
@@ -232,6 +233,7 @@
this.revision = normalize(revision);
this.openReplyBox = openReplyBox;
this.fileTableMode = mode;
+ this.lc = new LocalComments(changeId);
add(uiBinder.createAndBindUi(this));
}
@@ -363,7 +365,7 @@
.openDiv()
.append(Gerrit.info().change().replyLabel())
.closeDiv());
- if (hasDraftComments) {
+ if (hasDraftComments || lc.hasReplyComment()) {
reply.setStyleName(style.highlight());
}
reply.setVisible(true);
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 d1ca517..01337ea 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
@@ -15,7 +15,9 @@
package com.google.gerrit.client.change;
import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
+import static com.google.gerrit.client.FormatUtil.formatAbsPercentage;
import static com.google.gerrit.client.FormatUtil.formatBytes;
+import static com.google.gerrit.client.FormatUtil.formatPercentage;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
@@ -464,6 +466,7 @@
private boolean hasNonBinaryFile;
private int inserted;
private int deleted;
+ private long binOldSize;
private long bytesInserted;
private long bytesDeleted;
@@ -520,6 +523,7 @@
private void computeInsertedDeleted() {
inserted = 0;
deleted = 0;
+ binOldSize = 0;
bytesInserted = 0;
bytesDeleted = 0;
for (int i = 0; i < list.length(); i++) {
@@ -531,6 +535,7 @@
deleted += info.linesDeleted();
} else {
hasBinaryFile = true;
+ binOldSize += info.size() - info.sizeDelta();
if (info.sizeDelta() >= 0) {
bytesInserted += info.sizeDelta();
} else {
@@ -771,6 +776,12 @@
}
} else if (info.binary()) {
sb.append(formatBytes(info.sizeDelta()));
+ long oldSize = info.size() - info.sizeDelta();
+ if (oldSize != 0) {
+ sb.append(" (")
+ .append(formatPercentage(oldSize, info.sizeDelta()))
+ .append(")");
+ }
}
sb.closeTd();
}
@@ -827,8 +838,17 @@
if (hasNonBinaryFile) {
sb.br();
}
- sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
- formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
+ if (binOldSize != 0) {
+ sb.append(Util.M.patchTableSize_ModifyBinaryFilesWithPercentages(
+ formatAbsBytes(bytesInserted),
+ formatAbsPercentage(binOldSize, bytesInserted),
+ formatAbsBytes(bytesDeleted),
+ formatAbsPercentage(binOldSize, bytesDeleted)));
+ } else {
+ sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
+ formatAbsBytes(bytesInserted),
+ formatAbsBytes(bytesDeleted)));
+ }
}
sb.closeTh();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 4139348..bba63c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -48,20 +48,26 @@
/** Displays a table of label and reviewer scores. */
class Labels extends Grid {
private static final String DATA_ID = "data-id";
- private static final String REMOVE;
+ private static final String DATA_VOTE = "data-vote";
+ private static final String REMOVE_REVIEWER;
+ private static final String REMOVE_VOTE;
static {
- REMOVE = DOM.createUniqueId().replace('-', '_');
- init(REMOVE);
+ REMOVE_REVIEWER = DOM.createUniqueId().replace('-', '_');
+ REMOVE_VOTE = DOM.createUniqueId().replace('-', '_');
+ init(REMOVE_REVIEWER, REMOVE_VOTE);
}
- private static final native void init(String r) /*-{
+ private static final native void init(String r, String v) /*-{
$wnd[r] = $entry(function(e) {
- @com.google.gerrit.client.change.Labels::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+ @com.google.gerrit.client.change.Labels::onRemoveReviewer(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+ });
+ $wnd[v] = $entry(function(e) {
+ @com.google.gerrit.client.change.Labels::onRemoveVote(Lcom/google/gwt/dom/client/NativeEvent;)(e)
});
}-*/;
- private static void onRemove(NativeEvent event) {
+ private static void onRemoveReviewer(NativeEvent event) {
Integer user = getDataId(event);
if (user != null) {
final ChangeScreen screen = ChangeScreen.get(event);
@@ -77,6 +83,23 @@
}
}
+ private static void onRemoveVote(NativeEvent event) {
+ Integer user = getDataId(event);
+ String vote = getVoteId(event);
+ if (user != null && vote != null) {
+ final ChangeScreen screen = ChangeScreen.get(event);
+ ChangeApi.vote(screen.getChangeId().get(), user, vote).delete(
+ new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject result) {
+ if (screen.isCurrentView()) {
+ Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+ }
+ }
+ });
+ }
+ }
+
private static Integer getDataId(NativeEvent event) {
Element e = event.getEventTarget().cast();
while (e != null) {
@@ -89,6 +112,18 @@
return null;
}
+ private static String getVoteId(NativeEvent event) {
+ Element e = event.getEventTarget().cast();
+ while (e != null) {
+ String v = e.getAttribute(DATA_VOTE);
+ if (!v.isEmpty()) {
+ return v;
+ }
+ e = e.getParentElement();
+ }
+ return null;
+ }
+
private ChangeScreen.Style style;
void init(ChangeScreen.Style style) {
@@ -97,6 +132,7 @@
void set(ChangeInfo info) {
List<String> names = new ArrayList<>(info.labels());
+ Set<Integer> removable = info.removableReviewerIds();
Collections.sort(names);
resize(names.size(), 2);
@@ -106,14 +142,14 @@
LabelInfo label = info.label(name);
setText(row, 0, name);
if (label.all() != null) {
- setWidget(row, 1, renderUsers(label));
+ setWidget(row, 1, renderUsers(label, removable));
}
getCellFormatter().setStyleName(row, 0, style.labelName());
getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
}
}
- private Widget renderUsers(LabelInfo label) {
+ private Widget renderUsers(LabelInfo label, Set<Integer> removable) {
Map<Integer, List<ApprovalInfo>> m = new HashMap<>(4);
int approved = 0;
int rejected = 0;
@@ -150,8 +186,8 @@
html.setStyleName(style.label_reject());
}
html.append(val).append(" ");
- html.append(formatUserList(style, m.get(v),
- Collections.<Integer> emptySet(), null));
+ html.append(formatUserList(style, m.get(v), removable,
+ label.name(), null));
html.closeSpan();
}
return html.toBlockWidget();
@@ -198,6 +234,7 @@
static SafeHtml formatUserList(ChangeScreen.Style style,
Collection<? extends AccountInfo> in,
Set<Integer> removable,
+ String label,
Map<Integer, VotableInfo> votable) {
List<AccountInfo> users = new ArrayList<>(in);
Collections.sort(users, new Comparator<AccountInfo>() {
@@ -257,6 +294,9 @@
.setAttribute(DATA_ID, ai._accountId())
.setAttribute("title", getTitle(ai, votableCategories))
.setStyleName(style.label_user());
+ if (label != null) {
+ html.setAttribute(DATA_VOTE, label);
+ }
if (img != null) {
html.openElement("img")
.setStyleName(style.avatar())
@@ -271,10 +311,15 @@
}
html.append(name);
if (removable.contains(ai._accountId())) {
- html.openElement("button")
- .setAttribute("title", Util.M.removeReviewer(name))
- .setAttribute("onclick", REMOVE + "(event)")
- .append("×")
+ html.openElement("button");
+ if (label != null) {
+ html.setAttribute("title", Util.M.removeVote(label))
+ .setAttribute("onclick", REMOVE_VOTE + "(event)");
+ } else {
+ html.setAttribute("title", Util.M.removeReviewer(name))
+ .setAttribute("onclick", REMOVE_REVIEWER + "(event)");
+ }
+ html.append("×")
.closeElement("button");
}
html.closeSpan();
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
new file mode 100644
index 0000000..a0480d0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -0,0 +1,238 @@
+// 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.client.change;
+
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.storage.client.Storage;
+import com.google.gwt.user.client.Cookies;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LocalComments {
+ private final Change.Id changeId;
+ private final PatchSet.Id psId;
+ private final StorageBackend storage;
+
+ private static class InlineComment {
+ final PatchSet.Id psId;
+ final CommentInfo commentInfo;
+
+ InlineComment(PatchSet.Id psId, CommentInfo commentInfo) {
+ this.psId = psId;
+ this.commentInfo = commentInfo;
+ }
+ }
+
+ private static class StorageBackend {
+ private final Storage storageBackend;
+
+ StorageBackend() {
+ storageBackend = (Storage.isLocalStorageSupported())
+ ? Storage.getLocalStorageIfSupported()
+ : Storage.getSessionStorageIfSupported();
+ }
+
+ String getItem(String key) {
+ if (storageBackend == null) {
+ return Cookies.getCookie(key);
+ }
+ return storageBackend.getItem(key);
+ }
+
+ void setItem(String key, String value) {
+ if (storageBackend == null) {
+ Cookies.setCookie(key, value);
+ return;
+ }
+ storageBackend.setItem(key, value);
+ }
+
+ void removeItem(String key) {
+ if (storageBackend == null) {
+ Cookies.removeCookie(key);
+ return;
+ }
+ storageBackend.removeItem(key);
+ }
+
+ Collection<String> getKeys() {
+ if (storageBackend == null) {
+ return Cookies.getCookieNames();
+ }
+ ArrayList<String> result = new ArrayList<>(storageBackend.getLength());
+ for (int i = 0; i < storageBackend.getLength(); i++) {
+ result.add(storageBackend.key(i));
+ }
+ return result;
+ }
+ }
+
+ public LocalComments(Change.Id changeId) {
+ this.changeId = changeId;
+ this.psId = null;
+ this.storage = new StorageBackend();
+ }
+
+ public LocalComments(PatchSet.Id psId) {
+ this.changeId = psId.getParentKey();
+ this.psId = psId;
+ this.storage = new StorageBackend();
+ }
+
+ public String getReplyComment() {
+ String comment = storage.getItem(getReplyCommentName());
+ storage.removeItem(getReplyCommentName());
+ return comment;
+ }
+
+ public void setReplyComment(String comment) {
+ storage.setItem(getReplyCommentName(), comment.trim());
+ }
+
+ public boolean hasReplyComment() {
+ return storage.getKeys().contains(getReplyCommentName());
+ }
+
+ public void removeReplyComment() {
+ if (hasReplyComment()) {
+ storage.removeItem(getReplyCommentName());
+ }
+ }
+
+ private String getReplyCommentName() {
+ return "savedReplyComment-" + changeId.toString();
+ }
+
+ public static void saveInlineComments() {
+ final StorageBackend storage = new StorageBackend();
+ for (final String cookie : storage.getKeys()) {
+ if (isInlineComment(cookie)) {
+ GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
+ @Override
+ public void onSuccess(CommentInfo result) {
+ storage.removeItem(cookie);
+ }
+ };
+ InlineComment input = getInlineComment(cookie);
+ if (input.commentInfo.id() == null) {
+ CommentApi.createDraft(input.psId, input.commentInfo, cb);
+ } else {
+ CommentApi.updateDraft(input.psId, input.commentInfo.id(),
+ input.commentInfo, cb);
+ }
+ }
+ }
+ }
+
+ public void setInlineComment(CommentInfo comment) {
+ String name = getInlineCommentName(comment);
+ if (name == null) {
+ // Failed to get the store key -- so we can't continue.
+ return;
+ }
+ storage.setItem(name, comment.message().trim());
+ }
+
+ public boolean hasInlineComments() {
+ for (String cookie : storage.getKeys()) {
+ if (isInlineComment(cookie)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isInlineComment(String key) {
+ return key.startsWith("patchCommentEdit-") || key.startsWith("patchReply-")
+ || key.startsWith("patchComment-");
+ }
+
+ private static InlineComment getInlineComment(String key) {
+ String path;
+ Side side = Side.PARENT;
+ int line = 0;
+ CommentRange range;
+ StorageBackend storage = new StorageBackend();
+
+ String[] elements = key.split("-");
+ int offset = 1;
+ if (key.startsWith("patchReply-") || key.startsWith("patchCommentEdit-")) {
+ offset = 2;
+ }
+ Change.Id changeId = new Change.Id(Integer.parseInt(elements[offset + 0]));
+ PatchSet.Id psId =
+ new PatchSet.Id(changeId, Integer.parseInt(elements[offset + 1]));
+ path = atob(elements[offset + 2]);
+ side = (Side.PARENT.toString() == elements[offset + 3]) ? Side.PARENT
+ : Side.REVISION;
+ range = null;
+ if (elements[offset + 4].startsWith("R")) {
+ String rangeStart = elements[offset + 4].substring(1);
+ String rangeEnd = elements[offset + 5];
+ String[] split = rangeStart.split(",");
+ int sl = Integer.parseInt(split[0]);
+ int sc = Integer.parseInt(split[1]);
+ split = rangeEnd.split(",");
+ int el = Integer.parseInt(split[0]);
+ int ec = Integer.parseInt(split[1]);
+ range = CommentRange.create(sl, sc, el, ec);
+ line = sl;
+ } else {
+ line = Integer.parseInt(elements[offset + 4]);
+ }
+ CommentInfo info = CommentInfo.create(path, side, line, range);
+ info.message(storage.getItem(key));
+ if (key.startsWith("patchReply-")) {
+ info.inReplyTo(elements[1]);
+ } else if (key.startsWith("patchCommentEdit-")) {
+ info.id(elements[1]);
+ }
+ InlineComment inlineComment = new InlineComment(psId, info);
+ return inlineComment;
+ }
+
+ private String getInlineCommentName(CommentInfo comment) {
+ if (psId == null) {
+ return null;
+ }
+ String result = "patchComment-";
+ if (comment.id() != null) {
+ result = "patchCommentEdit-" + comment.id() + "-";
+ } else if (comment.inReplyTo() != null) {
+ result = "patchReply-" + comment.inReplyTo() + "-";
+ }
+ result += changeId + "-" + psId.getId() + "-" + btoa(comment.path()) + "-"
+ + comment.side() + "-";
+ if (comment.hasRange()) {
+ result += "R" + comment.range().startLine() + ","
+ + comment.range().startCharacter() + "-" + comment.range().endLine()
+ + "," + comment.range().endCharacter();
+ } else {
+ result += comment.line();
+ }
+ return result;
+ }
+
+ private static native String btoa(String a) /*-{ return btoa(a); }-*/;
+
+ private static native String atob(String b) /*-{ return atob(b); }-*/;
+}
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 2ec4b6b..2366e59 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
@@ -28,6 +28,7 @@
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.LabelValue;
@@ -75,7 +76,7 @@
import java.util.Set;
import java.util.TreeSet;
-class ReplyBox extends Composite {
+public class ReplyBox extends Composite {
interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
private static final Binder uiBinder = GWT.create(Binder.class);
@@ -90,6 +91,7 @@
private final String revision;
private ReviewInput in = ReviewInput.create();
private int labelHelpColumn;
+ private LocalComments lc;
@UiField Styles style;
@UiField TextArea message;
@@ -109,6 +111,7 @@
this.clp = clp;
this.psId = psId;
this.revision = revision;
+ this.lc = new LocalComments(psId.getParentKey());
initWidget(uiBinder.createAndBindUi(this));
List<String> names = new ArrayList<>(permitted.keySet());
@@ -140,6 +143,10 @@
protected void onLoad() {
commentsPanel.setVisible(false);
post.setEnabled(false);
+ if (lc.hasReplyComment()) {
+ message.setText(lc.getReplyComment());
+ lc.removeReplyComment();
+ }
ChangeApi.drafts(psId.getParentKey().get())
.get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
@Override
@@ -201,6 +208,13 @@
psId.getParentKey(),
String.valueOf(psId.get())));
}
+ @Override
+ public void onFailure(final Throwable caught) {
+ if (RestApi.isNotSignedIn(caught)) {
+ lc.setReplyComment(message.getText());
+ }
+ super.onFailure(caught);
+ }
});
hide();
}
@@ -214,7 +228,7 @@
void replyTo(MessageInfo msg) {
if (msg.message() != null) {
String t = message.getText();
- String m = quote(msg);
+ String m = quote(removePatchSetHeaderLine(msg.message()));
if (t == null || t.isEmpty()) {
t = m;
} else if (t.endsWith("\n\n")) {
@@ -224,20 +238,25 @@
} else {
t += "\n\n" + m;
}
- message.setText(t + "\n\n");
+ message.setText(t);
}
}
- private static String quote(MessageInfo msg) {
- String m = msg.message().trim();
- if (m.startsWith("Patch Set ")) {
- int i = m.indexOf('\n');
+ private static String removePatchSetHeaderLine(String msg) {
+ msg = msg.trim();
+ if (msg.startsWith("Patch Set ")) {
+ int i = msg.indexOf('\n');
if (i > 0) {
- m = m.substring(i + 1).trim();
+ msg = msg.substring(i + 1).trim();
}
}
+ return msg;
+ }
+
+ public static String quote(String msg) {
+ msg = msg.trim();
StringBuilder quotedMsg = new StringBuilder();
- for (String line : m.split("\\n")) {
+ for (String line : msg.split("\\n")) {
line = line.trim();
while (line.length() > 67) {
int i = line.lastIndexOf(' ', 67);
@@ -253,7 +272,8 @@
}
quotedMsg.append(" > ").append(line).append("\n");
}
- return quotedMsg.toString().substring(0, quotedMsg.length() - 1); // remove last '\n'
+ quotedMsg.append("\n");
+ return quotedMsg.toString();
}
private void hide() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
index 52f6b6a..6903b91 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -38,7 +38,10 @@
position: absolute;
bottom: 5px;
right: 5px;
+ background-color: #eee;
+ background-image: -webkit-linear-gradient(top, #eee, #eee);
}
+ .cancel div { color: #444; }
.comments {
max-height: 275px;
width: 526px;
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 3937ade..ae648dc 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
@@ -17,6 +17,7 @@
import com.google.gerrit.client.ConfirmationCallback;
import com.google.gerrit.client.ConfirmationDialog;
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;
@@ -50,7 +51,6 @@
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -177,10 +177,14 @@
@Override
public void onFailure(Throwable err) {
- UIObject.setVisible(error, true);
- error.setInnerText(err instanceof StatusCodeException
- ? ((StatusCodeException) err).getEncodedResponse()
- : err.getMessage());
+ if (isSigninFailure(err)) {
+ new NotSignedInDialog().center();
+ } else {
+ UIObject.setVisible(error, true);
+ error.setInnerText(err instanceof StatusCodeException
+ ? ((StatusCodeException) err).getEncodedResponse()
+ : err.getMessage());
+ }
}
});
}
@@ -209,20 +213,13 @@
cc.remove(i);
}
cc.remove(info.owner()._accountId());
-
- Set<Integer> removable = new HashSet<>();
- if (info.removableReviewers() != null) {
- for (AccountInfo a : Natives.asList(info.removableReviewers())) {
- removable.add(a._accountId());
- }
- }
-
+ Set<Integer> removable = info.removableReviewerIds();
Map<Integer, VotableInfo> votable = votable(info);
SafeHtml rHtml = Labels.formatUserList(style,
- r.values(), removable, votable);
+ r.values(), removable, null, votable);
SafeHtml ccHtml = Labels.formatUserList(style,
- cc.values(), removable, votable);
+ cc.values(), removable, null, votable);
reviewersText.setInnerSafeHtml(rHtml);
ccText.setInnerSafeHtml(ccHtml);
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 9cafeec..8314e3e 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
@@ -155,6 +155,10 @@
.addParameter("n", n);
}
+ public static RestApi vote(int id, int reviewer, String vote) {
+ return reviewer(id, reviewer).view("votes").id(vote);
+ }
+
public static RestApi reviewer(int id, int reviewer) {
return change(id).view("reviewers").id(reviewer);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index ef74a65..eb09657 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -36,11 +36,14 @@
String patchTableSize_Modify(int insertions, int deletions);
String patchTableSize_ModifyBinaryFiles(String bytesInserted,
String bytesDeleted);
+ String patchTableSize_ModifyBinaryFilesWithPercentages(String bytesInserted,
+ String percentageInserted, String bytesDeleted, String percentageDeleted);
String patchTableSize_LongModify(int insertions, int deletions);
String patchTableSize_Lines(@PluralCount int insertions);
String removeHashtag(String name);
String removeReviewer(String fullName);
+ String removeVote(String label);
String messageWrittenOn(String date);
String renamedFrom(String sourcePath);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 67ef2c3..b3c3980 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -18,11 +18,13 @@
patchTableDrafts = {0} drafts
patchTableSize_Modify = +{0}, -{1}
patchTableSize_ModifyBinaryFiles = +{0}, -{1}
+patchTableSize_ModifyBinaryFilesWithPercentages = +{0} (+{1}), -{2} (-{3})
patchTableSize_LongModify = {0} inserted, {1} deleted
patchTableSize_Lines = {0} lines
removeHashtag = Remove hashtag {0}
removeReviewer = Remove reviewer {0}
+removeVote = Remove vote {0}
messageWrittenOn = on {0}
renamedFrom = renamed from {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
index be64a3d..8750389 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -38,7 +38,7 @@
/** Handler that can receive notifications of a change's starred status. */
public static interface ChangeStarHandler {
- public void onChangeStar(ChangeStarEvent event);
+ void onChangeStar(ChangeStarEvent event);
}
/** Event fired when a star changes status. The new status is reported. */
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index 21b2f50..86d149a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -15,10 +15,12 @@
package com.google.gerrit.client.diff;
import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.change.LocalComments;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
@@ -190,7 +192,7 @@
setRangeHighlight(edit);
if (edit) {
String msg = comment.message() != null
- ? comment.message().trim()
+ ? comment.message()
: "";
editArea.setValue(msg);
cancel.setVisible(!isNew());
@@ -289,6 +291,7 @@
enableEdit(false);
pendingGroup = group;
+ final LocalComments lc = new LocalComments(psId);
GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
@Override
public void onSuccess(CommentInfo result) {
@@ -306,6 +309,11 @@
public void onFailure(Throwable e) {
enableEdit(true);
pendingGroup = null;
+ if (RestApi.isNotSignedIn(e)) {
+ CommentInfo saved = CommentInfo.copy(comment);
+ saved.message(editArea.getValue().trim());
+ lc.setInlineComment(saved);
+ }
super.onFailure(e);
}
};
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 eec3d0c..3d7607b 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
@@ -74,6 +74,8 @@
@UiField CheckBox reviewed;
@UiField Element project;
@UiField Element filePath;
+ @UiField Element fileNumber;
+ @UiField Element fileCount;
@UiField Element noDiff;
@UiField FlowPanel linkPanel;
@@ -143,6 +145,9 @@
public void onSuccess(NativeMap<FileInfo> result) {
JsArray<FileInfo> files = result.values();
FileInfo.sortFileInfoByPath(files);
+ fileNumber.setInnerText(
+ Integer.toString(Natives.asList(files).indexOf(result.get(path)) + 1));
+ fileCount.setInnerText(Integer.toString(files.length()));
int index = 0; // TODO: Maybe use patchIndex.
for (int i = 0; i < files.length(); i++) {
if (path.equals(files.get(i).path())) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
index f13c9a3..39eb6cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -34,6 +34,11 @@
.path {
white-space: nowrap;
}
+ .fileCount {
+ white-space: nowrap;
+ position: relative;
+ bottom: 4px;
+ }
.navigation {
position: absolute;
top: 0;
@@ -69,6 +74,9 @@
<div class='{style.navigation}'>
<span ui:field='noDiff' class='{style.nodiff}'><ui:msg>No Differences</ui:msg></span>
<g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+ <span class='{style.fileCount}'>
+ <ui:msg>File <span ui:field='fileNumber'/> of <span ui:field='fileCount'/></ui:msg>
+ </span>
<x:InlineHyperlink ui:field='prev' styleName='{res.style.goPrev}'/>
<x:InlineHyperlink ui:field='up'
styleName='{res.style.goUp}'
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 91baf90..f363f9b 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
@@ -20,7 +20,6 @@
import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING;
import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_NONE;
import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_TRAILING;
-
import static com.google.gwt.event.dom.client.KeyCodes.KEY_ESCAPE;
import com.google.gerrit.client.Gerrit;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index 48b4c3c..f5bcaa6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -18,6 +18,7 @@
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.change.ReplyBox;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.changes.Util;
@@ -143,17 +144,21 @@
replyBox.setEdit(true);
}
- void addReplyBox() {
+ void addReplyBox(boolean quote) {
+ CommentInfo commentReply = CommentInfo.createReply(comment);
+ if (quote) {
+ commentReply.message(ReplyBox.quote(comment.message()));
+ }
getCommentManager().addDraftBox(
getCm().side(),
- CommentInfo.createReply(comment)).setEdit(true);
+ commentReply).setEdit(true);
}
void doReply() {
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
} else if (replyBox == null) {
- addReplyBox();
+ addReplyBox(false);
} else {
openReplyBox();
}
@@ -165,6 +170,15 @@
doReply();
}
+ @UiHandler("quote")
+ void onQuote(ClickEvent e) {
+ e.stopPropagation();
+ if (!Gerrit.isSignedIn()) {
+ Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
+ }
+ addReplyBox(true);
+ }
+
@UiHandler("done")
void onReplyDone(ClickEvent e) {
e.stopPropagation();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index 46b76ca..cbea847 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -61,6 +61,11 @@
<ui:attribute name='title'/>
<div><ui:msg>Reply</ui:msg></div>
</g:Button>
+ <g:Button ui:field='quote' styleName=''
+ title='Reply to this comment with quoting it'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Quote</ui:msg></div>
+ </g:Button>
<g:Button ui:field='done' styleName=''
title='Reply "Done" to this comment'>
<ui:attribute name='title'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
index 383f278..26f8ff5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
@@ -27,12 +27,14 @@
.delete {
background-color: #faa;
+ min-width: 12px;
}
.insert {
background-color: #9f9;
+ min-width: 12px;
}
.edit {
- border-left: 3px solid #faa;
- width: 2px !important;
+ border-left: 6px solid #faa;
+ width: 6px !important;
background-color: #9f9;
}
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 a511550..ebce1c0 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
@@ -307,7 +307,15 @@
}
.searchPanel .searchTextBox {
font-size: 9pt;
- margin: 5.286px 3px 0 0;
+ margin: 8.286px 3px 0 0;
+}
+.searchPanel .searchDropdown {
+ font-size: 8pt;
+ border: 2px solid;
+ border-color: rgba(0, 0, 0, 0.15);
+ height: 16px;
+ border-radius: 2px;
+ box-sizing: content-box;
}
.searchPanel .searchButton {
text-align: center;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 81494c0..3cf29e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.patches;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.change.LocalComments;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.rpc.GerritCallback;
@@ -243,6 +244,7 @@
discard.setEnabled(false);
final PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
+ final LocalComments lc = new LocalComments(psId);
final boolean wasNew = isNew();
GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
@Override
@@ -264,6 +266,7 @@
save.setEnabled(true);
cancel.setEnabled(true);
discard.setEnabled(true);
+ lc.setInlineComment(toInput(comment));
super.onFailure(caught);
onSave.onFailure(caught);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index c7f9859..adfaa04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -26,6 +26,7 @@
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.Label;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.safehtml.client.SafeHtml;
@@ -35,7 +36,7 @@
class NavLinks extends Composite {
public enum Nav {
PREV (0, '[', PatchUtil.C.previousFileHelp(), 0),
- NEXT (3, ']', PatchUtil.C.nextFileHelp(), 1);
+ NEXT (4, ']', PatchUtil.C.nextFileHelp(), 1);
public int col; // Table Cell column to display link in
public int key; // key code shortcut to activate link
@@ -59,7 +60,7 @@
NavLinks(KeyCommandSet kcs, PatchSet.Id forPatch) {
patchSetId = forPatch;
keys = kcs;
- table = new Grid(1, 4);
+ table = new Grid(1, 5);
initWidget(table);
final CellFormatter fmt = table.getCellFormatter();
@@ -68,6 +69,7 @@
fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
+ fmt.setHorizontalAlignment(0, 4, HasHorizontalAlignment.ALIGN_RIGHT);
final ChangeLink up = new ChangeLink("", patchSetId);
SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink()));
@@ -77,6 +79,10 @@
void display(int patchIndex, PatchTable fileList,
List<InlineHyperlink> links, List<WebLinkInfo> webLinks) {
if (fileList != null) {
+ Label fileCountLabel =
+ new Label(Gerrit.M.fileCount(patchIndex + 1, fileList.size()));
+ fileCountLabel.setStyleName(Gerrit.RESOURCES.css().nowrap());
+ table.setWidget(0, 3, fileCountLabel);
setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex));
setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex));
} else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
index d84f799..6c500d8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
@@ -102,6 +102,10 @@
return i != null ? i : -1;
}
+ int size() {
+ return patchMap.size();
+ }
+
private Map<Patch.Key, Integer> patchMap() {
if (patchMap == null) {
patchMap = new HashMap<>();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index bccd237..daac7cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -38,15 +38,10 @@
}
public static void showFailure(Throwable caught) {
- if (isNotSignedIn(caught) || isInvalidXSRF(caught)) {
+ if (isSigninFailure(caught)) {
new NotSignedInDialog().center();
-
} else if (isNoSuchEntity(caught)) {
- if (Gerrit.isSignedIn()) {
- new ErrorDialog(Gerrit.C.notFoundBody()).center();
- } else {
- new NotSignedInDialog().center();
- }
+ new ErrorDialog(Gerrit.C.notFoundBody()).center();
} else if (isInactiveAccount(caught)) {
new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
@@ -77,12 +72,20 @@
}
}
- private static boolean isInvalidXSRF(final Throwable caught) {
+ public static boolean isSigninFailure(Throwable caught) {
+ if (isNotSignedIn(caught) || isInvalidXSRF(caught) ||
+ (isNoSuchEntity(caught) && !Gerrit.isSignedIn())) {
+ return true;
+ }
+ return false;
+ }
+
+ protected static boolean isInvalidXSRF(final Throwable caught) {
return caught instanceof InvocationException
&& caught.getMessage().equals(JsonConstants.ERROR_INVALID_XSRF);
}
- private static boolean isNotSignedIn(Throwable caught) {
+ protected static boolean isNotSignedIn(Throwable caught) {
return RestApi.isNotSignedIn(caught)
|| (caught instanceof RemoteJsonException
&& caught.getMessage().equals(NotSignedInException.MESSAGE));
@@ -99,17 +102,17 @@
&& caught.getMessage().startsWith(InactiveAccountException.MESSAGE);
}
- private static boolean isNoSuchAccount(final Throwable caught) {
+ protected static boolean isNoSuchAccount(final Throwable caught) {
return caught instanceof RemoteJsonException
&& caught.getMessage().startsWith(NoSuchAccountException.MESSAGE);
}
- private static boolean isNameAlreadyUsed(final Throwable caught) {
+ protected static boolean isNameAlreadyUsed(final Throwable caught) {
return caught instanceof RemoteJsonException
&& caught.getMessage().startsWith(NameAlreadyUsedException.MESSAGE);
}
- private static boolean isNoSuchGroup(final Throwable caught) {
+ protected static boolean isNoSuchGroup(final Throwable caught) {
return caught instanceof RemoteJsonException
&& caught.getMessage().startsWith(NoSuchGroupException.MESSAGE);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
index 8128afe..97ed559 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
@@ -44,12 +44,10 @@
@Override
public void onFailure(final Throwable caught) {
- if (isNoSuchEntity(caught)) {
- if (Gerrit.isSignedIn()) {
- Gerrit.display(screen.getToken(), new NotFoundScreen());
- } else {
- new NotSignedInDialog().center();
- }
+ if (isSigninFailure(caught)) {
+ new NotSignedInDialog().center();
+ } else if (isNoSuchEntity(caught)) {
+ Gerrit.display(screen.getToken(), new NotFoundScreen());
} else {
super.onFailure(caught);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
index 8b58403..36708dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -43,7 +43,8 @@
private HorizontalPanel filterPanel;
private String match;
private Query query;
- private Button close;
+ private Button closeTop;
+ private Button closeBottom;
private ScrollPanel sp;
private PopupPanel.PositionCallback popupPosition;
private int preferredTop;
@@ -55,10 +56,11 @@
createWidgets(popupText, currentPageLink);
final FlowPanel pfp = new FlowPanel();
pfp.add(filterPanel);
+ pfp.add(closeTop);
sp = new ScrollPanel(projectsTab);
sp.setSize("100%", "100%");
pfp.add(sp);
- pfp.add(close);
+ pfp.add(closeBottom);
popup.setWidget(pfp);
popup.setHeight("100%");
popupPosition = getPositionCallback();
@@ -147,17 +149,23 @@
};
projectsTab.setSavePointerId(currentPageLink);
- close = new Button(Util.C.projectsClose());
+ closeTop = createCloseButton();
+ closeBottom = createCloseButton();
+
+ popup = new DialogBox();
+ popup.setModal(false);
+ popup.setText(popupText);
+ }
+
+ private Button createCloseButton() {
+ Button close = new Button(Util.C.projectsClose());
close.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
closePopup();
}
});
-
- popup = new DialogBox();
- popup.setModal(false);
- popup.setText(popupText);
+ return close;
}
public void displayPopup() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
index a91becf..9a5eb03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
@@ -17,5 +17,5 @@
import com.google.gwt.event.shared.EventHandler;
public interface ScreenLoadHandler extends EventHandler {
- public void onScreenLoad(ScreenLoadEvent event);
+ void onScreenLoad(ScreenLoadEvent event);
}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 639e5e7..d0c6f9d 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -401,23 +401,23 @@
}
public interface EventHandler {
- public void handle(CodeMirror instance, NativeEvent event);
+ void handle(CodeMirror instance, NativeEvent event);
}
public interface RenderLineHandler {
- public void handle(CodeMirror instance, LineHandle handle, Element element);
+ void handle(CodeMirror instance, LineHandle handle, Element element);
}
public interface GutterClickHandler {
- public void handle(CodeMirror instance, int line, String gutter,
+ void handle(CodeMirror instance, int line, String gutter,
NativeEvent clickEvent);
}
public interface BeforeSelectionChangeHandler {
- public void handle(CodeMirror instance, Pos anchor, Pos head);
+ void handle(CodeMirror instance, Pos anchor, Pos head);
}
public interface ChangesHandler {
- public void handle(CodeMirror instance);
+ void handle(CodeMirror instance);
}
}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
index 6ce70db..4b97da1 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
@@ -18,6 +18,7 @@
@external .CodeMirror-linenumber;
@external .CodeMirror-overlayscroll-horizontal;
@external .CodeMirror-overlayscroll-vertical;
+@external .CodeMirror-scrollbar-filler;
@external .cm-tab;
@external .cm-searching;
@external .cm-trailingspace;
@@ -38,7 +39,17 @@
.CodeMirror-overlayscroll-vertical div {
min-height: 25px;
}
-
+/* Ensure the scrollbars are not too narrow */
+.CodeMirror-overlayscroll-horizontal {
+ min-height: 12px;
+}
+.CodeMirror-overlayscroll-vertical {
+ min-width: 12px;
+}
+.CodeMirror-scrollbar-filler {
+ min-height: 12px;
+ min-width: 12px;
+}
/* Stack the scrollbar so annotations can receive clicks. */
.CodeMirror-overlayscroll-vertical {
z-index: inherit;
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
index 0f659c9a..04dccdb 100644
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/FormatUtilTest.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client;
import static com.google.gerrit.client.FormatUtil.formatBytes;
+import static com.google.gerrit.client.FormatUtil.formatPercentage;
import static org.junit.Assert.assertEquals;
import com.googlecode.gwt.test.GwtModule;
@@ -45,4 +46,17 @@
assertEquals("-27 B", formatBytes(-27));
assertEquals("-1.7 MiB", formatBytes(-1728));
}
+
+ @Test
+ public void testFormatPercentage() {
+ assertEquals("N/A", formatPercentage(0, 10));
+ assertEquals("0%", formatPercentage(100, 0));
+ assertEquals("+25%", formatPercentage(100, 25));
+ assertEquals("-25%", formatPercentage(100, -25));
+ assertEquals("+50%", formatPercentage(100, 50));
+ assertEquals("-50%", formatPercentage(100, -50));
+ assertEquals("+100%", formatPercentage(100, 100));
+ assertEquals("-100%", formatPercentage(100, -100));
+ assertEquals("+500%", formatPercentage(100, 500));
+ }
}
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
index adfe86c..c1a0f44 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
@@ -14,20 +14,36 @@
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(boolean headless, boolean slave) {
+ 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;
+ 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 e1810ef..ab6cc90 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,6 +14,8 @@
package com.google.gerrit.httpd;
+import static com.google.gerrit.reviewdb.client.AuthType.OAUTH;
+
import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.DownloadConfig;
@@ -40,7 +42,11 @@
if (authConfig.isTrustContainerAuth()) {
authFilter = ContainerAuthFilter.class;
} else if (authConfig.isGitBasicAuth()) {
- authFilter = ProjectBasicAuthFilter.class;
+ if (authConfig.getAuthType() == OAUTH) {
+ authFilter = ProjectOAuthFilter.class;
+ } else {
+ authFilter = ProjectBasicAuthFilter.class;
+ }
} else {
authFilter = ProjectDigestFilter.class;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 89d62dd..13b1a48 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -23,15 +23,14 @@
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.ChangeCache;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ReceiveCommits;
-import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.UploadPackMetricsHook;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -50,8 +49,6 @@
import org.eclipse.jgit.http.server.resolver.AsIsFileService;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.PostReceiveHook;
-import org.eclipse.jgit.transport.PostReceiveHookChain;
import org.eclipse.jgit.transport.PreUploadHook;
import org.eclipse.jgit.transport.PreUploadHookChain;
import org.eclipse.jgit.transport.ReceivePack;
@@ -200,11 +197,15 @@
static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
private final TransferConfig config;
+ private final UploadPackMetricsHook uploadMetrics;
private final DynamicSet<PreUploadHook> preUploadHooks;
@Inject
- UploadFactory(TransferConfig tc, DynamicSet<PreUploadHook> preUploadHooks) {
+ UploadFactory(TransferConfig tc,
+ UploadPackMetricsHook uploadMetrics,
+ DynamicSet<PreUploadHook> preUploadHooks) {
this.config = tc;
+ this.uploadMetrics = uploadMetrics;
this.preUploadHooks = preUploadHooks;
}
@@ -215,6 +216,7 @@
up.setTimeout(config.getTimeout());
up.setPreUploadHook(PreUploadHookChain.newChain(
Lists.newArrayList(preUploadHooks)));
+ up.setPostUploadHook(uploadMetrics);
return up;
}
}
@@ -273,18 +275,10 @@
static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
private final AsyncReceiveCommits.Factory factory;
- private final TransferConfig config;
- private DynamicSet<ReceivePackInitializer> receivePackInitializers;
- private DynamicSet<PostReceiveHook> postReceiveHooks;
@Inject
- ReceiveFactory(AsyncReceiveCommits.Factory factory, TransferConfig config,
- DynamicSet<ReceivePackInitializer> receivePackInitializers,
- DynamicSet<PostReceiveHook> postReceiveHooks) {
+ ReceiveFactory(AsyncReceiveCommits.Factory factory) {
this.factory = factory;
- this.config = config;
- this.receivePackInitializers = receivePackInitializers;
- this.postReceiveHooks = postReceiveHooks;
}
@Override
@@ -297,24 +291,13 @@
throw new ServiceNotAuthorizedException();
}
- final IdentifiedUser user = pc.getUser().asIdentifiedUser();
- final ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
+ ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
+ rc.init();
+
ReceivePack rp = rc.getReceivePack();
- rp.setRefLogIdent(user.newRefLogIdent());
- rp.setTimeout(config.getTimeout());
- rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
- init(pc.getProject().getNameKey(), rp);
- rp.setPostReceiveHook(PostReceiveHookChain.newChain(
- Lists.newArrayList(postReceiveHooks)));
req.setAttribute(ATT_RC, rc);
return rp;
}
-
- private void init(Project.NameKey project, ReceivePack rp) {
- for (ReceivePackInitializer initializer : receivePackInitializers) {
- initializer.init(project, rp);
- }
- }
}
static class DisabledReceiveFactory implements
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
new file mode 100644
index 0000000..75f3b2c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -0,0 +1,336 @@
+// 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicMap.Entry;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+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.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Authenticates the current user with an OAuth2 server.
+ *
+ * @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
+ */
+@Singleton
+class ProjectOAuthFilter implements Filter {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectOAuthFilter.class);
+
+ private static final String REALM_NAME = "Gerrit Code Review";
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String BASIC = "Basic ";
+ private static final String GIT_COOKIE_PREFIX = "git-";
+
+ private final DynamicItem<WebSession> session;
+ private final DynamicMap<OAuthLoginProvider> loginProviders;
+ private final AccountCache accountCache;
+ private final AccountManager accountManager;
+ private final String gitOAuthProvider;
+ private final boolean userNameToLowerCase;
+
+ private String defaultAuthPlugin;
+ private String defaultAuthProvider;
+
+ @Inject
+ ProjectOAuthFilter(DynamicItem<WebSession> session,
+ DynamicMap<OAuthLoginProvider> pluginsProvider,
+ AccountCache accountCache,
+ AccountManager accountManager,
+ @GerritServerConfig Config gerritConfig) {
+ this.session = session;
+ this.loginProviders = pluginsProvider;
+ this.accountCache = accountCache;
+ this.accountManager = accountManager;
+ this.gitOAuthProvider =
+ gerritConfig.getString("auth", null, "gitOAuthProvider");
+ this.userNameToLowerCase =
+ gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ if (Strings.isNullOrEmpty(gitOAuthProvider)) {
+ pickOnlyProvider();
+ } else {
+ pickConfiguredProvider();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ Response rsp = new Response((HttpServletResponse) response);
+ if (verify(req, rsp)) {
+ chain.doFilter(req, rsp);
+ }
+ }
+
+ private boolean verify(HttpServletRequest req, Response rsp)
+ throws IOException {
+ AuthInfo authInfo = null;
+
+ // first check if there is a suitable git cookie; such cookies are
+ // expected to have names starting with the prefix git-
+ if (req.getCookies() != null) {
+ for (Cookie cookie: req.getCookies()) {
+ if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
+ authInfo = extractAuthInfo(cookie);
+ if (authInfo != null) {
+ break;
+ }
+ }
+ }
+ }
+
+ // if there is no suitable git cookie fall back to Basic authentication
+ if (authInfo == null) {
+ String hdr = req.getHeader(AUTHORIZATION);
+ if (hdr == null || !hdr.startsWith(BASIC)) {
+ // Allow an anonymous connection through, or it might be using a
+ // session cookie instead of basic authentication.
+ return true;
+ }
+
+ byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
+ String usernamePassword = new String(decoded, encoding(req));
+ int splitPos = usernamePassword.indexOf(':');
+ if (splitPos < 1) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ authInfo = new AuthInfo(usernamePassword.substring(0, splitPos),
+ usernamePassword.substring(splitPos + 1),
+ defaultAuthPlugin, defaultAuthProvider);
+ }
+
+ if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ AccountState who = accountCache.getByUsername(authInfo.username);
+ if (who == null || !who.getAccount().isActive()) {
+ log.warn("Authentication failed for " + authInfo.username
+ + ": account inactive or not provisioned in Gerrit");
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ AuthRequest authRequest = AuthRequest.forExternalUser(
+ authInfo.username);
+ authRequest.setEmailAddress(who.getAccount().getPreferredEmail());
+ authRequest.setDisplayName(who.getAccount().getFullName());
+ authRequest.setPassword(authInfo.tokenOrSecret);
+ authRequest.setAuthPlugin(authInfo.pluginName);
+ authRequest.setAuthProvider(authInfo.exportName);
+
+ try {
+ AuthResult authResult = accountManager.authenticate(authRequest);
+ WebSession ws = session.get();
+ ws.setUserAccountId(authResult.getAccountId());
+ ws.setAccessPathOk(AccessPath.GIT, true);
+ ws.setAccessPathOk(AccessPath.REST_API, true);
+ return true;
+ } catch (AccountException e) {
+ log.warn("Authentication failed for " + authInfo.username, e);
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ }
+
+ /**
+ * Picks the only installed OAuth provider. If there is a multiude
+ * of providers available, the actual provider must be determined
+ * from the authentication request.
+ *
+ * @throws ServletException if there is no {@code OAuthLoginProvider}
+ * installed at all.
+ */
+ private void pickOnlyProvider() throws ServletException {
+ try {
+ Entry<OAuthLoginProvider> loginProvider =
+ Iterables.getOnlyElement(loginProviders);
+ defaultAuthPlugin = loginProvider.getPluginName();
+ defaultAuthProvider = loginProvider.getExportName();
+ } catch (NoSuchElementException e) {
+ throw new ServletException("No OAuth login provider installed");
+ } catch (IllegalArgumentException e) {
+ // multiple providers found => do not pick any
+ }
+ }
+
+ /**
+ * Picks the {@code OAuthLoginProvider} configured with
+ * <tt>auth.gitOAuthProvider</tt>.
+ *
+ * @throws ServletException if the configured provider was not found.
+ */
+ private void pickConfiguredProvider() throws ServletException {
+ int splitPos = gitOAuthProvider.lastIndexOf(':');
+ if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
+ // no colon at all or leading/trailing colon: malformed providerId
+ throw new ServletException("OAuth login provider configuration is"
+ + " invalid: Must be of the form pluginName:providerName");
+ }
+ defaultAuthPlugin= gitOAuthProvider.substring(0, splitPos);
+ defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
+ OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin,
+ defaultAuthProvider);
+ if (provider == null) {
+ throw new ServletException("Configured OAuth login provider "
+ + gitOAuthProvider + " wasn't installed");
+ }
+ }
+
+ private AuthInfo extractAuthInfo(Cookie cookie)
+ throws UnsupportedEncodingException {
+ String username = URLDecoder.decode(cookie.getName()
+ .substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
+ String value = cookie.getValue();
+ int splitPos = value.lastIndexOf('@');
+ if (splitPos < 1 || splitPos == value.length() - 1) {
+ // no providerId in the cookie value => assume default provider
+ // note: a leading/trailing at sign is considered to belong to
+ // the access token rather than being a separator
+ return new AuthInfo(username, cookie.getValue(),
+ defaultAuthPlugin, defaultAuthProvider);
+ }
+ String token = value.substring(0, splitPos);
+ String providerId = value.substring(splitPos + 1);
+ splitPos = providerId.lastIndexOf(':');
+ if (splitPos < 1 || splitPos == providerId.length() - 1) {
+ // no colon at all or leading/trailing colon: malformed providerId
+ return null;
+ }
+ String pluginName = providerId.substring(0, splitPos);
+ String exportName = providerId.substring(splitPos + 1);
+ OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
+ if (provider == null) {
+ return null;
+ }
+ return new AuthInfo(username, token, pluginName, exportName);
+ }
+
+ private static String encoding(HttpServletRequest req) {
+ return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
+ }
+
+ private class AuthInfo {
+ private final String username;
+ private final String tokenOrSecret;
+ private final String pluginName;
+ private final String exportName;
+
+ private AuthInfo(String username, String tokenOrSecret,
+ String pluginName, String exportName) {
+ this.username = userNameToLowerCase
+ ? username.toLowerCase(Locale.US)
+ : username;
+ this.tokenOrSecret = tokenOrSecret;
+ this.pluginName = pluginName;
+ this.exportName = exportName;
+ }
+ }
+
+ private static class Response extends HttpServletResponseWrapper {
+ private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ Response(HttpServletResponse rsp) {
+ super(rsp);
+ }
+
+ private void status(int sc) {
+ if (sc == SC_UNAUTHORIZED) {
+ StringBuilder v = new StringBuilder();
+ v.append(BASIC);
+ v.append("realm=\"").append(REALM_NAME).append("\"");
+ setHeader(WWW_AUTHENTICATE, v.toString());
+ } else if (containsHeader(WWW_AUTHENTICATE)) {
+ setHeader(WWW_AUTHENTICATE, null);
+ }
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ status(sc);
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ status(sc);
+ super.sendError(sc);
+ }
+
+ @Override
+ @Deprecated
+ public void setStatus(int sc, String sm) {
+ status(sc);
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ status(sc);
+ super.setStatus(sc);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetrics.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetrics.java
new file mode 100644
index 0000000..ec193c9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetrics.java
@@ -0,0 +1,44 @@
+// 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;
+
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class RequestMetrics {
+ final Counter1<Integer> errors;
+ final Counter1<Integer> successes;
+
+ @Inject
+ public RequestMetrics(MetricMaker metricMaker) {
+ errors = metricMaker.newCounter(
+ "http/server/error_count",
+ new Description("Rate of REST API error responses")
+ .setRate()
+ .setUnit("errors"),
+ Field.ofInteger("status", "HTTP status code"));
+ successes = metricMaker.newCounter(
+ "http/server/success_count",
+ new Description("Rate of REST API success responses")
+ .setRate()
+ .setUnit("successes"),
+ Field.ofInteger("status", "HTTP status code"));
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetricsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetricsFilter.java
new file mode 100644
index 0000000..48b2a2f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestMetricsFilter.java
@@ -0,0 +1,108 @@
+// 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;
+
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import java.io.IOException;
+
+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.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+@Singleton
+public class RequestMetricsFilter implements Filter {
+ public static Module module() {
+ return new ServletModule() {
+ @Override
+ protected void configureServlets() {
+ filter("/*").through(RequestMetricsFilter.class);
+ }
+ };
+ }
+
+ private final RequestMetrics metrics;
+
+ @Inject
+ RequestMetricsFilter(RequestMetrics metrics) {
+ this.metrics = metrics;
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ Response rsp = new Response((HttpServletResponse) response, metrics);
+
+ chain.doFilter(request, rsp);
+ }
+
+ @Override
+ public void init(FilterConfig cfg) throws ServletException {
+ }
+
+ private static class Response extends HttpServletResponseWrapper {
+ private final RequestMetrics metrics;
+
+ Response(HttpServletResponse response, RequestMetrics metrics) {
+ super(response);
+ this.metrics = metrics;
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ status(sc);
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ status(sc);
+ super.sendError(sc);
+ }
+
+ @Override
+ @Deprecated
+ public void setStatus(int sc, String sm) {
+ status(sc);
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ status(sc);
+ super.setStatus(sc);
+ }
+
+ private void status(int sc) {
+ if (sc >= SC_BAD_REQUEST) {
+ metrics.errors.increment(sc);
+ } else {
+ metrics.successes.increment(sc);
+ }
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
index 92809c0..4cb8e92 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
@@ -34,11 +34,20 @@
/** Requires the connection to use SSL, redirects if not. */
@Singleton
-class RequireSslFilter implements Filter {
- static class Module extends ServletModule {
+public class RequireSslFilter implements Filter {
+ public static class Module extends ServletModule {
+ private final boolean wantSsl;
+
+ @Inject
+ Module(@Nullable @CanonicalWebUrl String canonicalUrl) {
+ this.wantSsl = canonicalUrl != null && canonicalUrl.startsWith("https:");
+ }
+
@Override
protected void configureServlets() {
- filter("/*").through(RequireSslFilter.class);
+ if (wantSsl) {
+ filter("/*").through(RequireSslFilter.class);
+ }
}
}
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 4b3eca7..f3874be 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
@@ -22,7 +22,6 @@
import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
-import com.google.gerrit.httpd.raw.StaticModule;
import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
@@ -64,6 +63,7 @@
bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
if (options.enableDefaultUi()) {
+ filter("/").through(XsrfCookieFilter.class);
serve("/").with(HostPageServlet.class);
serve("/Gerrit").with(LegacyGerritServlet.class);
serve("/Gerrit/*").with(legacyGerritScreen());
@@ -103,8 +103,6 @@
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
filter("/Documentation/").through(QueryDocumentationFilter.class);
-
- install(new StaticModule());
}
private Key<HttpServlet> notFound() {
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 9e425e9..422a83d 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
@@ -16,7 +16,6 @@
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.httpd.auth.become.BecomeAnyAccountModule;
@@ -28,7 +27,6 @@
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.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.GitwebCgiConfig;
import com.google.gerrit.server.git.AsyncReceiveCommits;
@@ -42,17 +40,14 @@
public class WebModule extends LifecycleModule {
private final AuthConfig authConfig;
- private final boolean wantSSL;
private final GitwebCgiConfig gitwebCgiConfig;
private final GerritOptions options;
@Inject
WebModule(AuthConfig authConfig,
- @CanonicalWebUrl @Nullable String canonicalUrl,
GerritOptions options,
GitwebCgiConfig gitwebCgiConfig) {
this.authConfig = authConfig;
- this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
this.options = options;
this.gitwebCgiConfig = gitwebCgiConfig;
}
@@ -62,9 +57,6 @@
bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
bind(HttpRequestContext.class);
- if (wantSSL) {
- install(new RequireSslFilter.Module());
- }
install(new RunAsFilter.Module());
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 b2d32fc..327aaa3 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
@@ -21,18 +21,18 @@
import com.google.gerrit.server.account.AuthResult;
public interface WebSession {
- public boolean isSignedIn();
- public String getXGerritAuth();
- public boolean isValidXGerritAuth(String keyIn);
- public AccountExternalId.Key getLastLoginExternalId();
- public CurrentUser getUser();
- public void login(AuthResult res, boolean rememberMe);
+ boolean isSignedIn();
+ String getXGerritAuth();
+ boolean isValidXGerritAuth(String keyIn);
+ AccountExternalId.Key getLastLoginExternalId();
+ CurrentUser getUser();
+ void login(AuthResult res, boolean rememberMe);
/** Set the user account for this current request only. */
- public void setUserAccountId(Account.Id id);
- public boolean isAccessPathOk(AccessPath path);
- public void setAccessPathOk(AccessPath path, boolean ok);
+ void setUserAccountId(Account.Id id);
+ boolean isAccessPathOk(AccessPath path);
+ void setAccessPathOk(AccessPath path, boolean ok);
- public void logout();
- public String getSessionId();
+ void logout();
+ String getSessionId();
}
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
new file mode 100644
index 0000000..c14d043
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/XsrfCookieFilter.java
@@ -0,0 +1,81 @@
+// 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;
+
+import com.google.gerrit.common.data.HostPageData;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+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.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class XsrfCookieFilter implements Filter {
+ private final Provider<CurrentUser> user;
+ private final DynamicItem<WebSession> session;
+
+ @Inject
+ XsrfCookieFilter(
+ Provider<CurrentUser> user,
+ DynamicItem<WebSession> session) {
+ this.user = user;
+ this.session = session;
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse rsp,
+ FilterChain chain) throws IOException, ServletException {
+ WebSession s = user.get().isIdentifiedUser() ? session.get() : null;
+ setXsrfTokenCookie(
+ (HttpServletRequest) req, (HttpServletResponse) rsp, s);
+ chain.doFilter(req, rsp);
+ }
+
+ private static void setXsrfTokenCookie(HttpServletRequest req,
+ HttpServletResponse rsp, WebSession session) {
+ String v = session != null ? session.getXGerritAuth() : "";
+ Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, v);
+ c.setPath("/");
+ c.setSecure(isSecure(req));
+ c.setMaxAge(session != null
+ ? -1 // Set the cookie for this browser session.
+ : 0); // Remove the cookie (expire immediately).
+ rsp.addCookie(c);
+ }
+
+ private static boolean isSecure(HttpServletRequest req) {
+ return req.isSecure() || "https".equals(req.getScheme());
+ }
+
+ @Override
+ public void init(FilterConfig config) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
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
new file mode 100644
index 0000000..86bbf98
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BowerComponentsServlet.java
@@ -0,0 +1,48 @@
+// 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.Path;
+
+class BowerComponentsServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ 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");
+ }
+
+ private final Path zip;
+
+ BowerComponentsServlet(Cache<Path, Resource> cache, Path buckOut) {
+ super(cache, true);
+ this.zip = getZipPath(buckOut);
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) throws IOException {
+ return GerritLauncher.getZipFileSystem(zip)
+ .getPath("bower_components/" + pathInfo);
+ }
+}
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
new file mode 100644
index 0000000..0b4a02e
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/BuckUtils.java
@@ -0,0 +1,118 @@
+// 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/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index ab3728e..43c66db 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -25,13 +25,11 @@
import com.google.gerrit.common.Version;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
@@ -67,7 +65,6 @@
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -83,7 +80,6 @@
private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000;
private final Provider<CurrentUser> currentUser;
- private final DynamicItem<WebSession> session;
private final DynamicSet<WebUiPlugin> plugins;
private final DynamicSet<MessageOfTheDay> messages;
private final HostPageData.Theme signedOutTheme;
@@ -101,7 +97,6 @@
@Inject
HostPageServlet(
Provider<CurrentUser> cu,
- DynamicItem<WebSession> w,
SitePaths sp,
ThemeFactory themeFactory,
ServletContext servletContext,
@@ -113,7 +108,6 @@
GetDiffPreferences diffPref)
throws IOException, ServletException {
currentUser = cu;
- session = w;
plugins = webUiPlugins;
messages = motd;
signedOutTheme = themeFactory.getSignedOutTheme();
@@ -193,7 +187,6 @@
StringWriter w = new StringWriter();
CurrentUser user = currentUser.get();
if (user.isIdentifiedUser()) {
- setXGerritAuthCookie(req, rsp, session.get());
w.write(HPD_ID + ".accountDiffPref=");
json(getDiffPreferences(user.asIdentifiedUser()), w);
w.write(";");
@@ -202,7 +195,6 @@
json(signedInTheme, w);
w.write(";");
} else {
- setXGerritAuthCookie(req, rsp, null);
w.write(HPD_ID + ".theme=");
json(signedOutTheme, w);
w.write(";");
@@ -229,23 +221,6 @@
}
}
- private static void setXGerritAuthCookie(HttpServletRequest req,
- HttpServletResponse rsp, WebSession session) {
- String v = session != null ? session.getXGerritAuth() : "";
- Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, v);
- c.setPath("/");
- c.setHttpOnly(false);
- c.setSecure(isSecure(req));
- c.setMaxAge(session != null
- ? -1 // Set the cookie for this browser session.
- : 0); // Remove the cookie (expire immediately).
- rsp.addCookie(c);
- }
-
- private static boolean isSecure(HttpServletRequest req) {
- return req.isSecure() || "https".equals(req.getScheme());
- }
-
private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) {
try {
return getDiff.apply(new AccountResource(user));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java
new file mode 100644
index 0000000..3b225c9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java
@@ -0,0 +1,35 @@
+// 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 java.nio.file.Path;
+
+class PolyGerritUiIndexServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final Path index;
+
+ PolyGerritUiIndexServlet(Cache<Path, Resource> cache, Path ui) {
+ super(cache, true);
+ index = ui.resolve("index.html");
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) {
+ return index;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
new file mode 100644
index 0000000..4ca8b1c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiServlet.java
@@ -0,0 +1,35 @@
+// 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 java.nio.file.Path;
+
+class PolyGerritUiServlet extends ResourceServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final Path ui;
+
+ PolyGerritUiServlet(Cache<Path, Resource> cache, Path ui) {
+ super(cache, true);
+ this.ui = ui;
+ }
+
+ @Override
+ protected Path getResourcePath(String pathInfo) {
+ return ui.resolve(pathInfo);
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RebuildBowerComponentsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RebuildBowerComponentsFilter.java
new file mode 100644
index 0000000..38eaa0a
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RebuildBowerComponentsFilter.java
@@ -0,0 +1,74 @@
+// 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 java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.httpd.raw.BuckUtils.BuildFailureException;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.nio.file.Files;
+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.HttpServletResponse;
+
+@Singleton
+class RebuildBowerComponentsFilter implements Filter {
+ private static final String TARGET = "//polygerrit-ui:polygerrit_components";
+
+ private final Path gen;
+ private final Path root;
+ private final Path zip;
+
+ RebuildBowerComponentsFilter(Path buckOut) {
+ gen = buckOut.resolve("gen");
+ root = buckOut.getParent();
+ zip = BowerComponentsServlet.getZipPath(buckOut);
+ }
+
+ @Override
+ public synchronized void doFilter(ServletRequest sreq, ServletResponse sres,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletResponse res = (HttpServletResponse) sres;
+ try {
+ BuckUtils.build(root, gen, TARGET);
+ } catch (BuildFailureException e) {
+ BuckUtils.displayFailure(TARGET, e.why, res);
+ return;
+ }
+ if (!Files.exists(zip)) {
+ String msg = "`buck build` did not produce " + zip.toAbsolutePath();
+ BuckUtils.displayFailure(TARGET, msg.getBytes(UTF_8), res);
+ }
+ GerritLauncher.reloadZipFileSystem(zip);
+ chain.doFilter(sreq, sres);
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
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 a5bc6c6..1984cbb 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,33 +14,16 @@
package com.google.gerrit.httpd.raw;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.MoreObjects;
-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.gerrit.httpd.raw.BuckUtils.BuildFailureException;
import com.google.gwtexpui.linker.server.UserAgentRule;
-import com.google.gwtexpui.server.CacheHeaders;
-
-import org.eclipse.jgit.util.RawParseUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InterruptedIOException;
-import java.io.PrintWriter;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashSet;
-import java.util.Properties;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -55,9 +38,6 @@
import javax.servlet.http.HttpServletResponse;
class RecompileGwtUiFilter implements Filter {
- private static final Logger log =
- LoggerFactory.getLogger(RecompileGwtUiFilter.class);
-
private final boolean gwtuiRecompile =
System.getProperty("gerrit.disable-gwtui-recompile") == null;
private final UserAgentRule rule = new UserAgentRule();
@@ -92,9 +72,9 @@
synchronized (this) {
try {
- build(root, gen, rule);
+ BuckUtils.build(root, gen, rule);
} catch (BuildFailureException e) {
- displayFailure(rule, e.why, (HttpServletResponse) res);
+ BuckUtils.displayFailure(rule, e.why, (HttpServletResponse) res);
return;
}
@@ -109,24 +89,6 @@
chain.doFilter(request, res);
}
- private void displayFailure(String rule, byte[] why, HttpServletResponse res)
- throws IOException {
- res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- res.setContentType("text/html");
- res.setCharacterEncoding(UTF_8.name());
- CacheHeaders.setNotCacheable(res);
-
- Escaper html = HtmlEscapers.htmlEscaper();
- try (PrintWriter w = res.getWriter()) {
- w.write("<html><title>BUILD FAILED</title><body>");
- w.format("<h1>%s FAILED</h1>", html.escape(rule));
- w.write("<pre>");
- w.write(html.escape(RawParseUtils.decode(why)));
- w.write("</pre>");
- w.write("</body></html>");
- }
- }
-
@Override
public void init(FilterConfig config) {
}
@@ -166,59 +128,6 @@
}
}
- private static void build(Path root, Path gen, String target)
- throws IOException, BuildFailureException {
- log.info("buck build " + target);
- Properties properties = loadBuckProperties(gen);
- String buck = MoreObjects.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 FileNotFoundException, IOException {
- Properties properties = new Properties();
- try (InputStream in = new FileInputStream(
- gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
- properties.load(in);
- }
- return properties;
- }
-
- @SuppressWarnings("serial")
- private static class BuildFailureException extends Exception {
- final byte[] why;
-
- BuildFailureException(byte[] why) {
- this.why = why;
- }
- }
-
private static void mkdir(File dir) throws IOException {
if (!dir.isDirectory()) {
mkdir(dir.getParentFile());
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 86916c0..d9aab24 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
@@ -118,8 +118,9 @@
*
* @param pathInfo result of {@link HttpServletRequest#getPathInfo()}.
* @return path where static content can be found.
+ * @throws IOException if an error occurred resolving the resource.
*/
- protected abstract Path getResourcePath(String pathInfo);
+ protected abstract Path getResourcePath(String pathInfo) throws IOException;
protected FileTime getLastModifiedTime(Path p) throws IOException {
return Files.getLastModifiedTime(p);
@@ -205,7 +206,7 @@
try {
Path p = getResourcePath(name);
return cache.get(p, newLoader(p));
- } catch (ExecutionException e) {
+ } catch (ExecutionException | IOException e) {
log.warn(String.format("Cannot load static resource %s", name), e);
return null;
}
@@ -292,7 +293,7 @@
};
}
- static class Resource {
+ public static class Resource {
static final Resource NOT_FOUND =
new Resource(FileTime.fromMillis(0), "", new byte[] {});
@@ -321,7 +322,7 @@
}
}
- static class Weigher
+ public static class Weigher
implements com.google.common.cache.Weigher<Path, Resource> {
@Override
public int weigh(Path p, Resource r) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
index cf99d3c..6365306 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SiteStaticDirectoryServlet.java
@@ -51,15 +51,6 @@
@Override
protected Path getResourcePath(String pathInfo) {
- Path p = staticBase.resolve(pathInfo);
- try {
- p = p.toRealPath().normalize();
- if (!p.startsWith(staticBase)) {
- return null;
- }
- return p;
- } catch (IOException e) {
- return null;
- }
+ return staticBase.resolve(pathInfo);
}
}
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 e530710f..3ae36ff 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
@@ -14,15 +14,20 @@
package com.google.gerrit.httpd.raw;
+import static com.google.common.base.Preconditions.checkArgument;
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.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.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provides;
import com.google.inject.ProvisionException;
@@ -49,35 +54,46 @@
private static final Logger log =
LoggerFactory.getLogger(StaticModule.class);
+ public static final String CACHE = "static_content";
+
+ 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/*");
+
private static final String DOC_SERVLET = "DocServlet";
private static final String FAVICON_SERVLET = "FaviconServlet";
private static final String GWT_UI_SERVLET = "GwtUiServlet";
private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
- static final String CACHE = "static_content";
+ private final GerritOptions options;
+ private Paths paths;
- private final FileSystem warFs;
- private final Path buckOut;
- private final Path unpackedWar;
+ @Inject
+ public StaticModule(GerritOptions options) {
+ this.options = options;
+ }
- public StaticModule() {
- warFs = getDistributionArchive();
- if (warFs == null) {
- buckOut = getDeveloperBuckOut();
- unpackedWar = makeWarTempDir();
- } else {
- buckOut = null;
- unpackedWar = null;
+ private Paths getPaths() {
+ if (paths == null) {
+ paths = new Paths();
}
+ return paths;
}
@Override
protected void configureServlets() {
serveRegex("^/Documentation/(.+)$").with(named(DOC_SERVLET));
serve("/static/*").with(SiteStaticDirectoryServlet.class);
- serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
- serve("/favicon.ico").with(named(FAVICON_SERVLET));
- serveGwtUi();
install(new CacheModule() {
@Override
protected void configure() {
@@ -86,13 +102,12 @@
.weigher(ResourceServlet.Weigher.class);
}
});
- }
-
- private void serveGwtUi() {
- serveRegex("^/gerrit_ui/(?!rpc/)(.*)$")
- .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
- if (warFs == null) {
- filter("/").through(new RecompileGwtUiFilter(buckOut, unpackedWar));
+ if (options.enablePolyGerrit()) {
+ install(new CoreStaticModule());
+ install(new PolyGerritUiModule());
+ } else if (options.enableDefaultUi()) {
+ install(new CoreStaticModule());
+ install(new GwtUiModule());
}
}
@@ -100,8 +115,9 @@
@Singleton
@Named(DOC_SERVLET)
HttpServlet getDocServlet(@Named(CACHE) Cache<Path, Resource> cache) {
- if (warFs != null) {
- return new WarDocServlet(cache, warFs);
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new WarDocServlet(cache, p.warFs);
} else {
return new HttpServlet() {
private static final long serialVersionUID = 1L;
@@ -115,107 +131,222 @@
}
}
- @Provides
- @Singleton
- @Named(GWT_UI_SERVLET)
- HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache)
- throws IOException {
- if (warFs != null) {
- return new WarGwtUiServlet(cache, warFs);
- } else {
- return new DeveloperGwtUiServlet(cache, unpackedWar);
+ private class CoreStaticModule extends ServletModule {
+ @Override
+ public void configureServlets() {
+ serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
+ serve("/favicon.ico").with(named(FAVICON_SERVLET));
}
- }
- @Provides
- @Singleton
- @Named(ROBOTS_TXT_SERVLET)
- HttpServlet getRobotsTxtServlet(@GerritServerConfig Config cfg,
- SitePaths sitePaths, @Named(CACHE) Cache<Path, Resource> cache) {
- Path configPath = sitePaths.resolve(
- cfg.getString("httpd", null, "robotsFile"));
- if (configPath != null) {
- if (exists(configPath) && isReadable(configPath)) {
- return new SingleFileServlet(cache, configPath, true);
+ @Provides
+ @Singleton
+ @Named(ROBOTS_TXT_SERVLET)
+ HttpServlet getRobotsTxtServlet(@GerritServerConfig Config cfg,
+ SitePaths sitePaths, @Named(CACHE) Cache<Path, Resource> cache) {
+ Path configPath = sitePaths.resolve(
+ cfg.getString("httpd", null, "robotsFile"));
+ if (configPath != null) {
+ if (exists(configPath) && isReadable(configPath)) {
+ return new SingleFileServlet(cache, configPath, true);
+ } else {
+ log.warn("Cannot read httpd.robotsFile, using default");
+ }
+ }
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new SingleFileServlet(
+ cache, p.warFs.getPath("/robots.txt"), false);
} else {
- log.warn("Cannot read httpd.robotsFile, using default");
+ return new SingleFileServlet(
+ cache, webappSourcePath("robots.txt"), true);
}
}
- if (warFs != null) {
- return new SingleFileServlet(cache, warFs.getPath("/robots.txt"), false);
- } else {
- return new SingleFileServlet(cache, webappSourcePath("robots.txt"), true);
- }
- }
- @Provides
- @Singleton
- @Named(FAVICON_SERVLET)
- HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
- if (warFs != null) {
- return new SingleFileServlet(cache, warFs.getPath("/favicon.ico"), false);
- } else {
- return new SingleFileServlet(
- cache, webappSourcePath("favicon.ico"), true);
- }
- }
-
- private Path webappSourcePath(String name) {
- return buckOut.resolveSibling("gerrit-war").resolve("src").resolve("main")
- .resolve("webapp").resolve(name);
- }
-
- private static Key<HttpServlet> named(String name) {
- return Key.get(HttpServlet.class, Names.named(name));
- }
-
- private static FileSystem getDistributionArchive() {
- try {
- return GerritLauncher.getDistributionArchiveFileSystem();
- } catch (IOException e) {
- if ((e instanceof FileNotFoundException)
- && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
- return null;
+ @Provides
+ @Singleton
+ @Named(FAVICON_SERVLET)
+ HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new SingleFileServlet(
+ cache, p.warFs.getPath("/favicon.ico"), false);
} else {
+ return new SingleFileServlet(
+ cache, webappSourcePath("favicon.ico"), true);
+ }
+ }
+
+ private Path webappSourcePath(String name) {
+ return getPaths().buckOut.resolveSibling("gerrit-war").resolve("src")
+ .resolve("main").resolve("webapp").resolve(name);
+ }
+ }
+
+ private class GwtUiModule extends ServletModule {
+ @Override
+ public void configureServlets() {
+ serveRegex("^/gerrit_ui/(?!rpc/)(.*)$")
+ .with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
+ Paths p = getPaths();
+ if (p.warFs == null && p.buckOut != null) {
+ filter("/").through(new RecompileGwtUiFilter(p.buckOut, p.unpackedWar));
+ }
+ }
+
+ @Provides
+ @Singleton
+ @Named(GWT_UI_SERVLET)
+ HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache)
+ throws IOException {
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new WarGwtUiServlet(cache, p.warFs);
+ } else {
+ return new DeveloperGwtUiServlet(cache, p.unpackedWar);
+ }
+ }
+ }
+
+ private class PolyGerritUiModule extends ServletModule {
+ @Override
+ public void configureServlets() {
+ Path buckOut = getPaths().buckOut;
+ if (buckOut != null) {
+ RebuildBowerComponentsFilter rebuildFilter =
+ new RebuildBowerComponentsFilter(buckOut);
+ for (String p : POLYGERRIT_INDEX_PATHS) {
+ // Rebuilding bower_components once per load on the index request,
+ // is sufficient, since it will finish building before attempting to
+ // access any bower_components resources. Plus it saves contention and
+ // extraneous buck builds.
+ filter(p).through(rebuildFilter);
+ }
+ serve("/bower_components/*").with(BowerComponentsServlet.class);
+ } else {
+ // In the war case, bower_components are either inlined by vulcanize, or
+ // live under /polygerrit_ui in the war file, so we don't need a
+ // separate servlet.
+ }
+
+ for (String p : POLYGERRIT_INDEX_PATHS) {
+ filter(p).through(XsrfCookieFilter.class);
+ serve(p).with(PolyGerritUiIndexServlet.class);
+ }
+ serve("/*").with(PolyGerritUiServlet.class);
+ }
+
+ @Provides
+ @Singleton
+ PolyGerritUiIndexServlet getPolyGerritUiIndexServlet(
+ @Named(CACHE) Cache<Path, Resource> cache) {
+ return new PolyGerritUiIndexServlet(cache, polyGerritBasePath());
+ }
+
+ @Provides
+ @Singleton
+ PolyGerritUiServlet getPolyGerritUiServlet(
+ @Named(CACHE) Cache<Path, Resource> cache) {
+ return new PolyGerritUiServlet(cache, polyGerritBasePath());
+ }
+
+ @Provides
+ @Singleton
+ BowerComponentsServlet getBowerComponentsServlet(
+ @Named(CACHE) Cache<Path, Resource> cache) {
+ return new BowerComponentsServlet(cache, getPaths().buckOut);
+ }
+
+ private Path polyGerritBasePath() {
+ Paths p = getPaths();
+ boolean forceDev = options.forcePolyGerritDev();
+ if (forceDev) {
+ checkArgument(p.buckOut != null,
+ "no buck-out directory found for PolyGerrit developer mode");
+ }
+ return forceDev || p.warFs == null
+ ? p.buckOut.getParent().resolve("polygerrit-ui").resolve("app")
+ : p.warFs.getPath("/polygerrit_ui");
+ }
+ }
+
+ private class Paths {
+ private final FileSystem warFs;
+ private final Path buckOut;
+ private final Path unpackedWar;
+
+ private Paths() {
+ try {
+ warFs = getDistributionArchive();
+ if (warFs == null) {
+ buckOut = getDeveloperBuckOut();
+ unpackedWar = makeWarTempDir();
+ } else if (options.forcePolyGerritDev()) {
+ buckOut = getDeveloperBuckOut();
+ unpackedWar = null;
+ } else {
+ buckOut = null;
+ unpackedWar = null;
+ }
+ } catch (IOException e) {
+ throw new ProvisionException(
+ "Error initializing static content paths", e);
+ }
+ }
+
+ private FileSystem getDistributionArchive() throws IOException {
+ File war;
+ try {
+ war = GerritLauncher.getDistributionArchive();
+ } catch (IOException e) {
+ if ((e instanceof FileNotFoundException)
+ && GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
+ return null;
+ } else {
+ ProvisionException pe =
+ new ProvisionException("Error reading gerrit.war");
+ pe.initCause(e);
+ throw pe;
+ }
+ }
+ return GerritLauncher.getZipFileSystem(war.toPath());
+ }
+
+ 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.
+ //
+ try {
+ File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
+ if (!dstwar.delete() || !dstwar.mkdir()) {
+ throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
+ }
+
+ // Jetty normally refuses to serve out of a symlinked directory, as
+ // a security feature. Try to resolve out any symlinks in the path.
+ //
+ try {
+ return dstwar.getCanonicalFile().toPath();
+ } catch (IOException e) {
+ return dstwar.getAbsoluteFile().toPath();
+ }
+ } catch (IOException e) {
ProvisionException pe =
- new ProvisionException("Error reading gerrit.war");
+ new ProvisionException("Cannot create war tempdir");
pe.initCause(e);
throw pe;
}
}
}
- private static Path getDeveloperBuckOut() {
- try {
- return GerritLauncher.getDeveloperBuckOut();
- } catch (FileNotFoundException e) {
- return null;
- }
- }
-
- private static 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.
- //
- try {
- File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
- if (!dstwar.delete() || !dstwar.mkdir()) {
- throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
- }
-
- // Jetty normally refuses to serve out of a symlinked directory, as
- // a security feature. Try to resolve out any symlinks in the path.
- //
- try {
- return dstwar.getCanonicalFile().toPath();
- } catch (IOException e) {
- return dstwar.getAbsoluteFile().toPath();
- }
- } catch (IOException e) {
- ProvisionException pe =
- new ProvisionException("Cannot create war tempdir");
- pe.initCause(e);
- throw pe;
- }
+ private static Key<HttpServlet> named(String name) {
+ return Key.get(HttpServlet.class, Names.named(name));
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
index ef5a1df..182ee7e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
@@ -15,5 +15,5 @@
package com.google.gerrit.httpd.resources;
public interface ResourceKey {
- public int weigh();
+ int weigh();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
new file mode 100644
index 0000000..2a01b77
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiMetrics.java
@@ -0,0 +1,87 @@
+// 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.restapi;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.restapi.RestApiServlet.ViewData;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram1;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer1;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class RestApiMetrics {
+ private static final String[] PKGS = {
+ "com.google.gerrit.server.",
+ "com.google.gerrit.",
+ };
+
+ final Counter1<String> count;
+ final Counter2<String, Integer> errorCount;
+ final Timer1<String> serverLatency;
+ final Histogram1<String> responseBytes;
+
+ @Inject
+ RestApiMetrics(MetricMaker metrics) {
+ Field<String> view = Field.ofString("view", "view implementation class");
+ count = metrics.newCounter(
+ "http/server/rest_api/count",
+ new Description("REST API calls by view")
+ .setRate(),
+ view);
+
+ errorCount = metrics.newCounter(
+ "http/server/rest_api/error_count",
+ new Description("REST API calls by view")
+ .setRate(),
+ view,
+ Field.ofInteger("error_code", "HTTP status code"));
+
+ serverLatency = metrics.newTimer(
+ "http/server/rest_api/server_latency",
+ new Description("REST API call latency by view")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ view);
+
+ responseBytes = metrics.newHistogram(
+ "http/server/rest_api/response_bytes",
+ new Description("Size of response on network (may be gzip compressed)")
+ .setCumulative()
+ .setUnit(Units.BYTES),
+ view);
+ }
+
+ String view(ViewData viewData) {
+ String impl = viewData.view.getClass().getName().replace('$', '.');
+ for (String p : PKGS) {
+ if (impl.startsWith(p)) {
+ impl = impl.substring(p.length());
+ break;
+ }
+ }
+ if (!Strings.isNullOrEmpty(viewData.pluginName)
+ && !"gerrit".equals(viewData.pluginName)) {
+ impl = viewData.pluginName + '-' + impl;
+ }
+ return impl;
+ }
+}
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 7347cd9..f2ca49d 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
@@ -43,6 +43,7 @@
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
+import com.google.common.io.CountingOutputStream;
import com.google.common.math.IntMath;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
@@ -125,6 +126,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
@@ -164,16 +166,19 @@
final DynamicItem<WebSession> webSession;
final Provider<ParameterParser> paramParser;
final AuditService auditService;
+ final RestApiMetrics metrics;
@Inject
Globals(Provider<CurrentUser> currentUser,
DynamicItem<WebSession> webSession,
Provider<ParameterParser> paramParser,
- AuditService auditService) {
+ AuditService auditService,
+ RestApiMetrics metrics) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
this.auditService = auditService;
+ this.metrics = metrics;
}
}
@@ -197,10 +202,12 @@
@Override
protected final void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
+ final long startNanos = System.nanoTime();
long auditStartTs = TimeUtil.nowMs();
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
+ long responseBytes = -1;
Object result = null;
Multimap<String, String> params = LinkedHashMultimap.create();
Object inputRequestBody = null;
@@ -348,47 +355,62 @@
if (result != Response.none()) {
result = Response.unwrap(result);
if (result instanceof BinaryResult) {
- replyBinaryResult(req, res, (BinaryResult) result);
+ responseBytes = replyBinaryResult(req, res, (BinaryResult) result);
} else {
- replyJson(req, res, config, result);
+ responseBytes = replyJson(req, res, config, result);
}
}
} catch (MalformedJsonException e) {
- replyError(req, res, status = SC_BAD_REQUEST,
+ responseBytes = replyError(req, res, status = SC_BAD_REQUEST,
"Invalid " + JSON_TYPE + " in request", e);
} catch (JsonParseException e) {
- replyError(req, res, status = SC_BAD_REQUEST,
+ responseBytes = replyError(req, res, status = SC_BAD_REQUEST,
"Invalid " + JSON_TYPE + " in request", e);
} catch (BadRequestException e) {
- replyError(req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"),
- e.caching(), e);
+ responseBytes = replyError(req, res, status = SC_BAD_REQUEST,
+ messageOr(e, "Bad Request"), e.caching(), e);
} catch (AuthException e) {
- replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"),
- e.caching(), e);
+ responseBytes = replyError(req, res, status = SC_FORBIDDEN,
+ messageOr(e, "Forbidden"), e.caching(), e);
} catch (AmbiguousViewException e) {
- replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
+ responseBytes = replyError(req, res, status = SC_NOT_FOUND,
+ messageOr(e, "Ambiguous"), e);
} catch (ResourceNotFoundException e) {
- replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"),
- e.caching(), e);
+ responseBytes = replyError(req, res, status = SC_NOT_FOUND,
+ messageOr(e, "Not Found"), e.caching(), e);
} catch (MethodNotAllowedException e) {
- replyError(req, res, status = SC_METHOD_NOT_ALLOWED,
+ responseBytes = replyError(req, res, status = SC_METHOD_NOT_ALLOWED,
messageOr(e, "Method Not Allowed"), e.caching(), e);
} catch (ResourceConflictException e) {
- replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"),
- e.caching(), e);
+ responseBytes = replyError(req, res, status = SC_CONFLICT,
+ messageOr(e, "Conflict"), e.caching(), e);
} catch (PreconditionFailedException e) {
- replyError(req, res, status = SC_PRECONDITION_FAILED,
+ responseBytes = replyError(req, res, status = SC_PRECONDITION_FAILED,
messageOr(e, "Precondition Failed"), e.caching(), e);
} catch (UnprocessableEntityException e) {
- replyError(req, res, status = 422, messageOr(e, "Unprocessable Entity"),
- e.caching(), e);
+ responseBytes = replyError(req, res, status = 422,
+ messageOr(e, "Unprocessable Entity"), e.caching(), e);
} catch (NotImplementedException e) {
- replyError(req, res, status = SC_NOT_IMPLEMENTED,
+ responseBytes = replyError(req, res, status = SC_NOT_IMPLEMENTED,
messageOr(e, "Not Implemented"), e);
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
- handleException(e, req, res);
+ responseBytes = handleException(e, req, res);
} finally {
+ String metric = viewData != null && viewData.view != null
+ ? globals.metrics.view(viewData)
+ : "_unknown";
+ globals.metrics.count.increment(metric);
+ if (status >= SC_BAD_REQUEST) {
+ globals.metrics.errorCount.increment(metric, status);
+ }
+ if (responseBytes != -1) {
+ globals.metrics.responseBytes.record(metric, responseBytes);
+ }
+ globals.metrics.serverLatency.record(
+ metric,
+ System.nanoTime() - startNanos,
+ TimeUnit.NANOSECONDS);
globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get()
.getSessionId(), globals.currentUser.get(), req,
auditStartTs, params, inputRequestBody, status,
@@ -651,7 +673,7 @@
throw new InstantiationException("Cannot make " + type);
}
- public static void replyJson(@Nullable HttpServletRequest req,
+ public static long replyJson(@Nullable HttpServletRequest req,
HttpServletResponse res,
Multimap<String, String> config,
Object result)
@@ -667,7 +689,7 @@
}
w.write('\n');
w.flush();
- replyBinaryResult(req, res, asBinaryResult(buf)
+ return replyBinaryResult(req, res, asBinaryResult(buf)
.setContentType(JSON_TYPE)
.setCharacterEncoding(UTF_8));
}
@@ -736,7 +758,7 @@
}
@SuppressWarnings("resource")
- static void replyBinaryResult(
+ static long replyBinaryResult(
@Nullable HttpServletRequest req,
HttpServletResponse res,
BinaryResult bin) throws IOException {
@@ -767,10 +789,13 @@
}
if (req == null || !"HEAD".equals(req.getMethod())) {
- try (OutputStream dst = res.getOutputStream()) {
+ try (CountingOutputStream dst =
+ new CountingOutputStream(res.getOutputStream())) {
bin.writeTo(dst);
+ return dst.getCount();
}
}
+ return 0;
} finally {
appResult.close();
}
@@ -977,7 +1002,7 @@
viewData.pluginName, viewData.view.getClass());
}
- private static void handleException(Throwable err, HttpServletRequest req,
+ private static long handleException(Throwable err, HttpServletRequest req,
HttpServletResponse res) throws IOException {
String uri = req.getRequestURI();
if (!Strings.isNullOrEmpty(req.getQueryString())) {
@@ -987,16 +1012,17 @@
if (!res.isCommitted()) {
res.reset();
- replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
+ return replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
}
+ return 0;
}
- public static void replyError(HttpServletRequest req, HttpServletResponse res,
+ public static long replyError(HttpServletRequest req, HttpServletResponse res,
int statusCode, String msg, @Nullable Throwable err) throws IOException {
- replyError(req, res, statusCode, msg, CacheControl.NONE, err);
+ return replyError(req, res, statusCode, msg, CacheControl.NONE, err);
}
- public static void replyError(HttpServletRequest req,
+ public static long replyError(HttpServletRequest req,
HttpServletResponse res, int statusCode, String msg,
CacheControl c, @Nullable Throwable err) throws IOException {
if (err != null) {
@@ -1004,18 +1030,18 @@
}
configureCaching(req, res, null, null, c);
res.setStatus(statusCode);
- replyText(req, res, msg);
+ return replyText(req, res, msg);
}
- static void replyText(@Nullable HttpServletRequest req,
+ static long replyText(@Nullable HttpServletRequest req,
HttpServletResponse res, String text) throws IOException {
if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) {
- replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
+ return replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
} else {
if (!text.endsWith("\n")) {
text += "\n";
}
- replyBinaryResult(req, res,
+ return replyBinaryResult(req, res,
BinaryResult.create(text).setContentType("text/plain"));
}
}
@@ -1099,7 +1125,7 @@
}
}
- private static class ViewData {
+ static class ViewData {
String pluginName;
RestView<RestResource> view;
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 e0d63c8..3c1c12d 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
@@ -153,7 +153,7 @@
}
Account account = user.get().getAccount();
- hooks.doClaSignupHook(account, ca);
+ hooks.doClaSignupHook(account, ca.getName());
final AccountGroupMember.Key key =
new AccountGroupMember.Key(account.getId(), group.getId());
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 e877d1e..4496fc1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -24,6 +24,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -52,6 +53,7 @@
}
private final ChangeHooks hooks;
+ private final GitReferenceUpdated gitRefUpdated;
private final IdentifiedUser user;
private final ProjectAccessFactory.Factory projectAccessFactory;
private final ProjectCache projectCache;
@@ -63,7 +65,9 @@
MetaDataUpdate.User metaDataUpdateFactory,
AllProjectsNameProvider allProjects,
Provider<SetParent> setParent,
- ChangeHooks hooks, IdentifiedUser user,
+ ChangeHooks hooks,
+ GitReferenceUpdated gitRefUpdated,
+ IdentifiedUser user,
@Assisted("projectName") Project.NameKey projectName,
@Nullable @Assisted ObjectId base,
@Assisted List<AccessSection> sectionList,
@@ -75,6 +79,7 @@
this.projectAccessFactory = projectAccessFactory;
this.projectCache = projectCache;
this.hooks = hooks;
+ this.gitRefUpdated = gitRefUpdated;
this.user = user;
}
@@ -84,6 +89,8 @@
throws IOException, NoSuchProjectException, ConfigInvalidException {
RevCommit commit = config.commit(md);
+ gitRefUpdated.fire(config.getProject().getNameKey(), RefNames.REFS_CONFIG,
+ base, commit.getId());
hooks.doRefUpdatedHook(
new Branch.NameKey(config.getProject().getNameKey(), RefNames.REFS_CONFIG),
base, commit.getId(), user.getAccount());
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
index 687e02f..5be25fa 100644
--- a/gerrit-launcher/BUCK
+++ b/gerrit-launcher/BUCK
@@ -4,6 +4,7 @@
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',
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 fb54bcf..8c05a57 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
@@ -32,13 +32,16 @@
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
@@ -300,9 +303,10 @@
}
private static volatile File myArchive;
- private static volatile FileSystem myArchiveFs;
private static volatile File myHome;
+ private static final Map<Path, FileSystem> zipFileSystems = new HashMap<>();
+
/**
* Locate the JAR/WAR file we were launched from.
*
@@ -319,19 +323,54 @@
return result;
}
result = locateMyArchive();
- myArchiveFs = FileSystems.newFileSystem(
- URI.create("jar:" + result.toPath().toUri()),
- Collections.<String, String> emptyMap());
myArchive = result;
}
}
return result;
}
- public static FileSystem getDistributionArchiveFileSystem()
- throws FileNotFoundException, IOException {
- getDistributionArchive();
- return myArchiveFs;
+ public static synchronized FileSystem getZipFileSystem(Path zip)
+ throws IOException {
+ // FileSystems canonicalizes the path, so we should too.
+ zip = zip.toRealPath();
+ FileSystem zipFs = zipFileSystems.get(zip);
+ if (zipFs == null) {
+ zipFs = newZipFileSystem(zip);
+ zipFileSystems.put(zip, zipFs);
+ }
+ return zipFs;
+ }
+
+ /**
+ * Reload the zip {@link FileSystem} for a path.
+ * <p>
+ * <strong>Warning</strong>: This calls {@link FileSystem#close()} on any
+ * previously open instance of the filesystem at this path, which may cause
+ * {@code IOException}s in any open path handles created with the old
+ * filesystem. Use with caution.
+ *
+ * @param zip path to zip file.
+ * @return reloaded filesystem instance.
+ * @throws IOException if there was an error reading the zip file.
+ */
+ public static synchronized FileSystem reloadZipFileSystem(Path zip)
+ throws IOException {
+ // FileSystems canonicalizes the path, so we should too.
+ zip = zip.toRealPath();
+ @SuppressWarnings("resource") // Caching resource for later use.
+ FileSystem zipFs = zipFileSystems.get(zip);
+ if (zipFs != null) {
+ zipFs.close();
+ }
+ zipFs = newZipFileSystem(zip);
+ zipFileSystems.put(zip, zipFs);
+ return zipFs;
+ }
+
+ private static FileSystem newZipFileSystem(Path zip) throws IOException {
+ return FileSystems.newFileSystem(
+ URI.create("jar:" + zip.toUri()),
+ Collections.<String, String> emptyMap());
}
private static File locateMyArchive() throws FileNotFoundException {
@@ -562,58 +601,68 @@
}
/**
+ * Locate the path of the {@code eclipse-out} directory in a source tree.
+ *
+ * @throws FileNotFoundException if the directory cannot be found.
+ */
+ public static Path getDeveloperEclipseOut() throws FileNotFoundException {
+ return resolveInSourceRoot("eclipse-out");
+ }
+
+ /**
* Locate the path of the {@code buck-out} directory in a source tree.
*
* @throws FileNotFoundException if the directory cannot be found.
*/
public static Path getDeveloperBuckOut() throws FileNotFoundException {
- // Find ourselves in the CLASSPATH, we should be a loose class file.
+ return resolveInSourceRoot("buck-out");
+ }
+
+ private static Path resolveInSourceRoot(String name)
+ throws FileNotFoundException {
+ // Find ourselves in the classpath, as a loose class file or jar.
Class<GerritLauncher> self = GerritLauncher.class;
URL u = self.getResource(self.getSimpleName() + ".class");
if (u == null) {
throw new FileNotFoundException("Cannot find class " + self.getName());
- } else if (!"file".equals(u.getProtocol())) {
- throw new FileNotFoundException("Cannot find extract path from " + u);
- }
-
- // Pop up to the top level classes folder that contains us.
- Path dir = Paths.get(u.getPath());
- String myName = self.getName();
- for (;;) {
- int dot = myName.lastIndexOf('.');
- if (dot < 0) {
- dir = dir.getParent();
- break;
+ } else if ("jar".equals(u.getProtocol())) {
+ String p = u.getPath();
+ try {
+ u = new URL(p.substring(0, p.indexOf('!')));
+ } catch (MalformedURLException e) {
+ FileNotFoundException fnfe =
+ new FileNotFoundException("Not a valid jar file: " + u);
+ fnfe.initCause(e);
+ throw fnfe;
}
- myName = myName.substring(0, dot);
- dir = dir.getParent();
+ }
+ if (!"file".equals(u.getProtocol())) {
+ throw new FileNotFoundException("Cannot extract path from " + u);
}
- dir = popdir(u, dir, "classes");
- dir = popdir(u, dir, "eclipse");
- if (last(dir).equals("buck-out")) {
- return dir;
+ // Pop up to the top-level source folder by looking for .buckconfig.
+ Path dir = Paths.get(u.getPath());
+ while (!Files.isRegularFile(dir.resolve(".buckconfig"))) {
+ Path parent = dir.getParent();
+ if (parent == null) {
+ throw new FileNotFoundException("Cannot find source root from " + u);
+ }
+ dir = parent;
}
- throw new FileNotFoundException("Cannot find buck-out from " + u);
- }
- private static String last(Path dir) {
- return dir.getName(dir.getNameCount() - 1).toString();
- }
-
- private static Path popdir(URL u, Path dir, String name)
- throws FileNotFoundException {
- if (last(dir).equals(name)) {
- return dir.getParent();
+ Path ret = dir.resolve(name);
+ if (!Files.exists(ret)) {
+ throw new FileNotFoundException(
+ name + " not found in source root " + dir);
}
- throw new FileNotFoundException("Cannot find buck-out from " + u);
+ return ret;
}
private static ClassLoader useDevClasspath()
throws MalformedURLException, FileNotFoundException {
- Path out = getDeveloperBuckOut();
+ Path out = getDeveloperEclipseOut();
List<URL> dirs = new ArrayList<>();
- dirs.add(out.resolve("eclipse").resolve("classes").toUri().toURL());
+ dirs.add(out.resolve("classes").toUri().toURL());
ClassLoader cl = GerritLauncher.class.getClassLoader();
for (URL u : ((URLClassLoader) cl).getURLs()) {
if (includeJar(u)) {
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 5c3b629..c05c8f0 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
@@ -131,7 +131,6 @@
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 ID_FIELD = ChangeField.LEGACY_ID2.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 REVIEWEDBY_FIELD =
@@ -139,10 +138,6 @@
private static final String UPDATED_SORT_FIELD =
sortFieldName(ChangeField.UPDATED);
- private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
- ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
- MERGEABLE_FIELD, PATCH_SET_FIELD, REVIEWEDBY_FIELD);
-
private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
"_", " ", ".", " ");
@@ -438,10 +433,12 @@
List<ChangeData> result =
Lists.newArrayListWithCapacity(docs.scoreDocs.length);
+ Set<String> fields = fields(opts);
+ String idFieldName = idFieldName();
for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
ScoreDoc sd = docs.scoreDocs[i];
- Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
- result.add(toChangeData(doc));
+ Document doc = searchers[sd.shardIndex].doc(sd.doc, fields);
+ result.add(toChangeData(doc, fields, idFieldName));
}
final List<ChangeData> r = Collections.unmodifiableList(result);
@@ -477,19 +474,62 @@
}
}
- private ChangeData toChangeData(Document doc) {
+ @SuppressWarnings("deprecation")
+ private Set<String> fields(QueryOptions opts) {
+ if (schemaHasRequestedField(ChangeField.LEGACY_ID2, opts.fields())
+ || schemaHasRequestedField(ChangeField.CHANGE, opts.fields())
+ || schemaHasRequestedField(ChangeField.LEGACY_ID, opts.fields())) {
+ return opts.fields();
+ }
+ // Request the numeric ID field even if the caller did not request it,
+ // otherwise we can't actually construct a ChangeData.
+ return Sets.union(opts.fields(), ImmutableSet.of(idFieldName()));
+ }
+
+ private boolean schemaHasRequestedField(FieldDef<ChangeData, ?> field,
+ Set<String> requested) {
+ return schema.hasField(field) && requested.contains(field.getName());
+ }
+
+ @SuppressWarnings("deprecation")
+ private String idFieldName() {
+ return schema.getField(ChangeField.LEGACY_ID2, ChangeField.LEGACY_ID).get()
+ .getName();
+ }
+
+ private ChangeData toChangeData(Document doc, Set<String> fields,
+ String idFieldName) {
+ ChangeData cd;
+ // Either change or the ID field was guaranteed to be included in the call
+ // to fields() above.
BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
- if (cb == null) {
- int id = doc.getField(ID_FIELD).numericValue().intValue();
- return changeDataFactory.create(db.get(), new Change.Id(id));
+ if (cb != null) {
+ cd = changeDataFactory.create(db.get(),
+ ChangeProtoField.CODEC.decode(cb.bytes, cb.offset, cb.length));
+ } else {
+ int id = doc.getField(idFieldName).numericValue().intValue();
+ cd = changeDataFactory.create(db.get(), new Change.Id(id));
}
- // Change proto.
- Change change = ChangeProtoField.CODEC.decode(
- cb.bytes, cb.offset, cb.length);
- ChangeData cd = changeDataFactory.create(db.get(), change);
+ if (fields.contains(PATCH_SET_FIELD)) {
+ decodePatchSets(doc, cd);
+ }
+ if (fields.contains(APPROVAL_FIELD)) {
+ decodeApprovals(doc, cd);
+ }
+ if (fields.contains(ADDED_FIELD) && fields.contains(DELETED_FIELD)) {
+ decodeChangedLines(doc, cd);
+ }
+ if (fields.contains(MERGEABLE_FIELD)) {
+ decodeMergeable(doc, cd);
+ }
+ if (fields.contains(REVIEWEDBY_FIELD)) {
+ decodeReviewedBy(doc, cd);
+ }
+ return cd;
+ }
- // Patch sets.
+ private void decodePatchSets(Document doc, ChangeData cd) {
List<PatchSet> patchSets =
decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC);
if (!patchSets.isEmpty()) {
@@ -497,12 +537,14 @@
// this cannot be valid since a change needs at least one patch set.
cd.setPatchSets(patchSets);
}
+ }
- // Approvals.
+ private void decodeApprovals(Document doc, ChangeData cd) {
cd.setCurrentApprovals(
decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC));
+ }
- // Changed lines.
+ private void decodeChangedLines(Document doc, ChangeData cd) {
IndexableField added = doc.getField(ADDED_FIELD);
IndexableField deleted = doc.getField(DELETED_FIELD);
if (added != null && deleted != null) {
@@ -510,16 +552,18 @@
added.numericValue().intValue(),
deleted.numericValue().intValue());
}
+ }
- // Mergeable.
+ private void decodeMergeable(Document doc, ChangeData cd) {
String mergeable = doc.get(MERGEABLE_FIELD);
if ("1".equals(mergeable)) {
cd.setMergeable(true);
} else if ("0".equals(mergeable)) {
cd.setMergeable(false);
}
+ }
- // Reviewed-by.
+ private void decodeReviewedBy(Document doc, ChangeData cd) {
IndexableField[] reviewedBy = doc.getFields(REVIEWEDBY_FIELD);
if (reviewedBy.length > 0) {
Set<Account.Id> accounts =
@@ -533,8 +577,6 @@
}
cd.setReviewedBy(accounts);
}
-
- return cd;
}
private static <T> List<T> decodeProtos(Document doc, String fieldName,
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
index 78abce8..0a6363b 100644
--- a/gerrit-openid/BUCK
+++ b/gerrit-openid/BUCK
@@ -3,6 +3,9 @@
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',
@@ -12,13 +15,12 @@
'//gerrit-server:server',
'//lib:guava',
'//lib:gwtorm',
+ '//lib:servlet-api-3_1',
'//lib/commons:codec',
'//lib/guice:guice',
'//lib/guice:guice-servlet',
'//lib/jgit:jgit',
'//lib/log:api',
- '//lib/openid:consumer',
],
- provided_deps = ['//lib:servlet-api-3_1'],
visibility = ['PUBLIC'],
)
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index c57ec52..3a14ffd 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -3,23 +3,26 @@
INIT_API_SRCS = glob([SRCS + 'init/api/*.java'])
-DEPS = [
- '//gerrit-common:server',
- '//gerrit-extension-api:api',
- '//gerrit-gwtexpui:linker_server',
- '//gerrit-gwtexpui:server',
- '//gerrit-httpd:httpd',
- '//gerrit-server:server',
- '//gerrit-sshd:sshd',
- '//gerrit-reviewdb:server',
- '//lib:guava',
- '//lib/guice:guice',
- '//lib/guice:guice-assistedinject',
- '//lib/guice:guice-servlet',
- '//lib/jgit:jgit',
- '//lib/log:api',
- '//lib/log:jsonevent-layout',
- '//lib/log:log4j'
+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:jgit',
+ '//lib/log:api',
+ '//lib/log:log4j',
+]
+
+DEPS = BASE_JETTY_DEPS + [
+ '//gerrit-reviewdb:server',
+ '//lib/log:jsonevent-layout',
]
java_library(
@@ -83,24 +86,31 @@
name = 'util-nodep',
srcs = glob([SRCS + 'util/*.java']),
provided_deps = DEPS + REST_UTIL_DEPS,
- visibility = [
- '//gerrit-acceptance-framework/...',
- ],
+ visibility = ['//gerrit-acceptance-framework/...'],
)
+JETTY_DEPS = [
+ '//lib/jetty:jmx',
+ '//lib/jetty:server',
+ '//lib/jetty:servlet',
+]
+
java_library(
name = 'http',
- srcs = glob([SRCS + 'http/**/*.java']),
- deps = DEPS + [
- '//lib/jetty:jmx',
- '//lib/jetty:server',
- '//lib/jetty:servlet',
- ],
- provided_deps = [
+ 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-war:'],
+ visibility = ['//gerrit-acceptance-framework/...'],
)
REST_PGM_DEPS = [
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 ee1b111..2815099 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
@@ -28,13 +28,17 @@
import com.google.gerrit.httpd.H2CacheBasedWebSession;
import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
import com.google.gerrit.httpd.RequestContextFilter;
+import com.google.gerrit.httpd.RequestMetricsFilter;
+import com.google.gerrit.httpd.RequireSslFilter;
import com.google.gerrit.httpd.WebModule;
import com.google.gerrit.httpd.WebSshGlueModule;
import com.google.gerrit.httpd.auth.oauth.OAuthModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.gerrit.httpd.raw.StaticModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
import com.google.gerrit.pgm.http.jetty.JettyEnv;
import com.google.gerrit.pgm.http.jetty.JettyModule;
import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
@@ -138,6 +142,9 @@
@Option(name = "--headless", usage = "Don't start the UI frontend")
private boolean headless;
+ @Option(name = "--polygerrit-dev", usage = "Force PolyGerrit UI for development")
+ private boolean polyGerritDev;
+
@Option(name = "--init", aliases = {"-i"},
usage = "Init site before starting the daemon")
private boolean doInit;
@@ -273,7 +280,7 @@
@VisibleForTesting
public void start() throws IOException {
if (dbInjector == null) {
- dbInjector = createDbInjector(MULTI_USER);
+ dbInjector = createDbInjector(true /* enableMetrics */, MULTI_USER);
}
cfgInjector = createCfgInjector();
config = cfgInjector.getInstance(
@@ -324,6 +331,7 @@
private Injector createSysInjector() {
final List<Module> modules = new ArrayList<>();
modules.add(SchemaVersionCheck.module());
+ modules.add(new DropWizardMetricMaker.RestModule());
modules.add(new LogFileCompressor.Module());
modules.add(new WorkQueue.Module());
modules.add(new ChangeHookRunner.Module());
@@ -367,7 +375,8 @@
modules.add(new AbstractModule() {
@Override
protected void configure() {
- bind(GerritOptions.class).toInstance(new GerritOptions(headless, slave));
+ bind(GerritOptions.class).toInstance(
+ new GerritOptions(config, headless, slave, polyGerritDev));
if (test) {
bind(String.class).annotatedWith(SecureStoreClassName.class)
.toInstance(DefaultSecureStore.class.getName());
@@ -446,9 +455,12 @@
}
modules.add(RequestContextFilter.module());
modules.add(AllRequestFilter.module());
+ modules.add(RequestMetricsFilter.module());
modules.add(H2CacheBasedWebSession.module());
modules.add(sysInjector.getInstance(GitOverHttpModule.class));
modules.add(sysInjector.getInstance(WebModule.class));
+ modules.add(sysInjector.getInstance(StaticModule.class));
+ modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
modules.add(new HttpPluginModule());
if (sshd) {
modules.add(sshInjector.getInstance(WebSshGlueModule.class));
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 dd5fe0c..1a43c38 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
@@ -117,11 +117,12 @@
sysInjector.getInstance(AllUsersName.class);
try (Repository allUsersRepo =
repoManager.openMetadataRepository(allUsersName)) {
- deleteDraftRefs(allUsersRepo);
+ deleteRefs(RefNames.REFS_DRAFT_COMMENTS, allUsersRepo);
+ deleteRefs(RefNames.REFS_STARRED_CHANGES, allUsersRepo);
for (final Project.NameKey project : changesByProject.keySet()) {
try (Repository repo = repoManager.openMetadataRepository(project)) {
final BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
- final BatchRefUpdate bruForDrafts =
+ final BatchRefUpdate bruAllUsers =
allUsersRepo.getRefDatabase().newBatchUpdate();
List<ListenableFuture<?>> futures = Lists.newArrayList();
@@ -136,7 +137,7 @@
for (final Change c : changesByProject.get(project)) {
final ListenableFuture<?> future = rebuilder.rebuildAsync(c,
- executor, bru, bruForDrafts, repo, allUsersRepo);
+ executor, bru, bruAllUsers, repo, allUsersRepo);
futures.add(future);
future.addListener(
new RebuildListener(c.getId(), future, ok, doneTask, failedTask),
@@ -149,7 +150,7 @@
public ListenableFuture<Void> apply(List<?> input)
throws Exception {
execute(bru, repo);
- execute(bruForDrafts, allUsersRepo);
+ execute(bruAllUsers, allUsersRepo);
mpm.end();
return Futures.immediateFuture(null);
}
@@ -173,15 +174,22 @@
try (RevWalk rw = new RevWalk(repo)) {
bru.execute(rw, NullProgressMonitor.INSTANCE);
}
+ for (ReceiveCommand command : bru.getCommands()) {
+ if (command.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException(String.format("Command %s failed: %s",
+ command.toString(), command.getResult()));
+ }
+ }
}
- private void deleteDraftRefs(Repository allUsersRepo) throws IOException {
+ private void deleteRefs(String prefix, Repository allUsersRepo)
+ throws IOException {
RefDatabase refDb = allUsersRepo.getRefDatabase();
- Map<String, Ref> allRefs = refDb.getRefs(RefNames.REFS_DRAFT_COMMENTS);
+ Map<String, Ref> allRefs = refDb.getRefs(prefix);
BatchRefUpdate bru = refDb.newBatchUpdate();
for (Map.Entry<String, Ref> ref : allRefs.entrySet()) {
bru.addCommand(new ReceiveCommand(ref.getValue().getObjectId(),
- ObjectId.zeroId(), RefNames.REFS_DRAFT_COMMENTS + ref.getKey()));
+ ObjectId.zeroId(), prefix + ref.getKey()));
}
execute(bru, allUsersRepo);
}
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 0684650..adfea594 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
@@ -23,6 +23,7 @@
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;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
@@ -127,11 +128,14 @@
private boolean reverseProxy;
@Inject
- JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
- final JettyEnv env, final HttpLogFactory httpLogFactory) {
+ JettyServer(@GerritServerConfig Config cfg,
+ ThreadSettingsConfig threadSettingsConfig,
+ SitePaths site,
+ JettyEnv env,
+ HttpLogFactory httpLogFactory) {
this.site = site;
- httpd = new Server(threadPool(cfg));
+ httpd = new Server(threadPool(cfg, threadSettingsConfig));
httpd.setConnectors(listen(httpd, cfg));
Handler app = makeContext(env, cfg);
@@ -315,8 +319,8 @@
return site.resolve(path);
}
- private ThreadPool threadPool(Config cfg) {
- int maxThreads = cfg.getInt("httpd", null, "maxthreads", 25);
+ private ThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+ int maxThreads = threadSettingsConfig.getHttpdMaxThreads();
int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
int idleTimeout = (int)MILLISECONDS.convert(60, SECONDS);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
index c30edf8..bc9ce8c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
@@ -23,5 +23,5 @@
* Performs database platform specific configuration steps and writes
* configuration parameters into the given database section
*/
- public void initConfig(Section databaseSection);
+ void initConfig(Section databaseSection);
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java
index 6b7386d..68af83f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PluginsDistribution.java
@@ -33,7 +33,7 @@
* IOException caused by dealing with the InputStream back to the
* caller
*/
- public void process(String pluginName, InputStream in) throws IOException;
+ void process(String pluginName, InputStream in) throws IOException;
}
/**
@@ -45,7 +45,7 @@
* @throws IOException in case of any other IO error caused by reading the
* plugin input stream
*/
- public void foreach(Processor processor) throws FileNotFoundException, IOException;
+ void foreach(Processor processor) throws FileNotFoundException, IOException;
/**
* List plugins included in the Gerrit distribution
@@ -53,5 +53,5 @@
* @throws FileNotFoundException if the location of the plugins couldn't be
* determined
*/
- public List<String> listPluginNames() throws FileNotFoundException;
+ List<String> listPluginNames() throws FileNotFoundException;
}
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 250cf59..fd28399 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
@@ -16,8 +16,8 @@
/** A single step in the site initialization process. */
public interface InitStep {
- public void run() throws Exception;
+ void run() throws Exception;
/** Executed after the site has been initialized */
- public void postRun() throws Exception;
+ void postRun() throws Exception;
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
index 048c2ee..6443e21 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -15,8 +15,10 @@
package com.google.gerrit.pgm.util;
import com.google.gerrit.common.SiteLibraryLoaderUtil;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.inject.Inject;
@@ -37,9 +39,11 @@
@Inject
SiteLibraryBasedDataSourceProvider(SitePaths site,
@GerritServerConfig Config cfg,
+ MetricMaker metrics,
+ ThreadSettingsConfig tsc,
DataSourceProvider.Context ctx,
DataSourceType dst) {
- super(cfg, ctx, dst);
+ super(cfg, metrics, tsc, ctx, dst);
libdir = site.lib_dir;
}
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 a6f1f93..bf69d9b 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
@@ -22,6 +22,9 @@
import com.google.gerrit.common.Die;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.SitePath;
@@ -93,7 +96,13 @@
}
/** @return provides database connectivity and site path. */
- protected Injector createDbInjector(final DataSourceProvider.Context context) {
+ protected Injector createDbInjector(DataSourceProvider.Context context) {
+ return createDbInjector(false, context);
+ }
+
+ /** @return provides database connectivity and site path. */
+ protected Injector createDbInjector(final boolean enableMetrics,
+ final DataSourceProvider.Context context) {
final Path sitePath = getSitePath();
final List<Module> modules = new ArrayList<>();
@@ -107,6 +116,17 @@
};
modules.add(sitePathModule);
+ if (enableMetrics) {
+ modules.add(new DropWizardMetricMaker.ApiModule());
+ } else {
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
+ }
+ });
+ }
+
modules.add(new LifecycleModule() {
@Override
protected void configure() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
index 44361aa..dfe0403 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -15,7 +15,7 @@
package com.google.gerrit.pgm.util;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.inject.Injector;
import com.google.inject.Key;
@@ -34,14 +34,15 @@
return limitThreads(
dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class)),
dbInjector.getInstance(DataSourceType.class),
+ dbInjector.getInstance(ThreadSettingsConfig.class),
threads);
}
- private static int limitThreads(Config cfg, DataSourceType dst, int threads) {
+ private static int limitThreads(Config cfg, DataSourceType dst,
+ ThreadSettingsConfig threadSettingsConfig, int threads) {
boolean usePool = cfg.getBoolean("database", "connectionpool",
dst.usePool());
- int poolLimit = cfg.getInt("database", "poollimit",
- DataSourceProvider.DEFAULT_POOL_LIMIT);
+ int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
if (usePool && threads > poolLimit) {
log.warn("Limiting program to " + poolLimit
+ " threads due to database.poolLimit");
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index abcacf9..b58c10a 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -28,6 +28,7 @@
'//gerrit-gwtexpui:server',
'//gerrit-reviewdb:server',
'//lib:args4j',
+ '//lib/dropwizard:dropwizard-core',
'//lib:guava',
'//lib:gwtorm',
'//lib:jsch',
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 86d2e4b9..337f5e6 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.12</version>
+ <version>2.13-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 10564de..55de829 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-archetype</artifactId>
- <version>2.12</version>
+ <version>2.13-SNAPSHOT</version>
<name>Gerrit Code Review - Plugin Archetype</name>
<description>Maven Archetype for Gerrit Plugins</description>
<url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 0d7bfb6..91e4b69 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-gwt-archetype</artifactId>
- <version>2.12</version>
+ <version>2.13-SNAPSHOT</version>
<name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
<description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
<url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 1e2c7b9..c02098c 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.12</version>
+ <version>2.13-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin GWT UI</name>
<description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
index 6d4e719..1fff691 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/extension/Panel.java
@@ -56,7 +56,7 @@
*
* @param panel panel that will contain the panel widget.
*/
- public void onLoad(Panel panel);
+ void onLoad(Panel panel);
}
static final class Context extends JavaScriptObject {
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
index 2a872fd..f91464f 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/screen/Screen.java
@@ -58,7 +58,7 @@
*
* @param screen panel that will contain the screen widget.
*/
- public void onLoad(Screen screen);
+ void onLoad(Screen screen);
}
static final class Context extends JavaScriptObject {
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index dc7e832..0565b80 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-js-archetype</artifactId>
- <version>2.12</version>
+ <version>2.13-SNAPSHOT</version>
<name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
<description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
<url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
index 0c2af36..3fd34bf 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/SparseHtmlFile.java
@@ -18,14 +18,14 @@
public interface SparseHtmlFile {
/** @return the line of formatted HTML. */
- public SafeHtml getSafeHtmlLine(int lineNo);
+ SafeHtml getSafeHtmlLine(int lineNo);
/** @return the number of lines in this sparse list. */
- public int size();
+ int size();
/** @return true if the line is valid in this sparse list. */
- public boolean contains(int idx);
+ boolean contains(int idx);
/** @return true if this line ends in the middle of a character edit span. */
- public boolean hasTrailingEdit(int idx);
+ boolean hasTrailingEdit(int idx);
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 2e33575..83a6ca8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -76,6 +76,12 @@
UNIFIED_DIFF
}
+ public static enum EmailStrategy {
+ ENABLED,
+ CC_ON_OWN_COMMENTS,
+ DISABLED
+ }
+
public static enum TimeFormat {
/** 12-hour clock: 1:15 am, 2:13 pm */
HHMM_12("h:mm a"),
@@ -120,9 +126,7 @@
@Column(id = 6, length = 20, notNull = false)
protected String downloadCommand;
- /** If true we CC the user on their own changes. */
- @Column(id = 7)
- protected boolean copySelfOnEmail;
+ // DELETED: id = 7 (copySelfOnEmail)
@Column(id = 8, length = 10, notNull = false)
protected String dateFormat;
@@ -155,6 +159,9 @@
@Column(id = 19)
protected boolean muteCommonPathPrefixes;
+ @Column(id = 20, length = 30, notNull = false)
+ protected String emailStrategy;
+
public AccountGeneralPreferences() {
}
@@ -242,14 +249,6 @@
}
}
- public boolean isCopySelfOnEmails() {
- return copySelfOnEmail;
- }
-
- public void setCopySelfOnEmails(boolean includeSelfOnEmail) {
- copySelfOnEmail = includeSelfOnEmail;
- }
-
public boolean isShowInfoInReviewCategory() {
return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
}
@@ -307,6 +306,17 @@
this.diffView = diffView.name();
}
+ public EmailStrategy getEmailStrategy() {
+ if (emailStrategy == null) {
+ return EmailStrategy.ENABLED;
+ }
+ return EmailStrategy.valueOf(emailStrategy);
+ }
+
+ public void setEmailStrategy(EmailStrategy strategy) {
+ this.emailStrategy = strategy.name();
+ }
+
public boolean isSizeBarInChangeTable() {
return sizeBarInChangeTable;
}
@@ -336,7 +346,6 @@
maximumPageSize = DEFAULT_PAGESIZE;
showSiteHeader = true;
useFlashClipboard = true;
- copySelfOnEmail = false;
reviewCategoryStrategy = null;
downloadUrl = null;
downloadCommand = null;
@@ -347,5 +356,6 @@
sizeBarInChangeTable = true;
legacycidInChangeTable = false;
muteCommonPathPrefixes = true;
+ emailStrategy = null;
}
}
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 5d2a1fd..d6ba008 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
@@ -40,8 +40,12 @@
/** Configurations of project-specific dashboards (canned search queries). */
public static final String REFS_DASHBOARDS = "refs/meta/dashboards/";
+ /** Draft inline comments of a user on a change */
public static final String REFS_DRAFT_COMMENTS = "refs/draft-comments/";
+ /** A change starred by a user */
+ public static final String REFS_STARRED_CHANGES = "refs/starred-changes/";
+
/**
* Prefix applied to merge commit base nodes.
* <p>
@@ -88,8 +92,30 @@
public static String refsDraftComments(Account.Id accountId,
Change.Id changeId) {
+ StringBuilder r = buildRefsPrefix(REFS_DRAFT_COMMENTS, accountId);
+ r.append(changeId.get());
+ return r.toString();
+ }
+
+ public static String refsDraftCommentsPrefix(Account.Id accountId) {
+ return buildRefsPrefix(REFS_DRAFT_COMMENTS, accountId).toString();
+ }
+
+ public static String refsStarredChanges(Account.Id accountId,
+ Change.Id changeId) {
+ StringBuilder r = buildRefsPrefix(REFS_STARRED_CHANGES, accountId);
+ r.append(changeId.get());
+ return r.toString();
+ }
+
+ public static String refsStarredChangesPrefix(Account.Id accountId) {
+ return buildRefsPrefix(REFS_STARRED_CHANGES, accountId).toString();
+ }
+
+ private static StringBuilder buildRefsPrefix(String prefix,
+ Account.Id accountId) {
StringBuilder r = new StringBuilder();
- r.append(REFS_DRAFT_COMMENTS);
+ r.append(prefix);
int n = accountId.get() % 100;
if (n < 10) {
r.append('0');
@@ -97,9 +123,8 @@
r.append(n);
r.append('/');
r.append(accountId.get());
- r.append('-');
- r.append(changeId.get());
- return r.toString();
+ r.append('/');
+ return r;
}
/**
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 b0981a7..844f893 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
@@ -39,7 +39,25 @@
@Test
public void refsDraftComments() throws Exception {
assertThat(RefNames.refsDraftComments(accountId, changeId))
- .isEqualTo("refs/draft-comments/23/1011123-67473");
+ .isEqualTo("refs/draft-comments/23/1011123/67473");
+ }
+
+ @Test
+ public void refsDraftCommentsPrefix() throws Exception {
+ assertThat(RefNames.refsDraftCommentsPrefix(accountId))
+ .isEqualTo("refs/draft-comments/23/1011123/");
+ }
+
+ @Test
+ public void refsStarredChanges() throws Exception {
+ assertThat(RefNames.refsStarredChanges(accountId, changeId))
+ .isEqualTo("refs/starred-changes/23/1011123/67473");
+ }
+
+ @Test
+ public void refsStarredChangesPrefix() throws Exception {
+ assertThat(RefNames.refsStarredChangesPrefix(accountId))
+ .isEqualTo("refs/starred-changes/23/1011123/");
}
@Test
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index c264779..b8fb4d2 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -53,6 +53,7 @@
'//lib/commons:lang',
'//lib/commons:net',
'//lib/commons:validator',
+ '//lib/dropwizard:dropwizard-core',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
@@ -97,6 +98,7 @@
'//lib/guice:guice-servlet',
'//lib/jgit:jgit',
'//lib/jgit:junit',
+ '//lib/joda:joda-time',
'//lib/log:api',
'//lib/log:impl_log4j',
'//lib/log:log4j',
@@ -137,6 +139,7 @@
srcs = PROLOG_TEST_CASE,
deps = [
':server',
+ ':testutil',
'//gerrit-common:server',
'//gerrit-extension-api:api',
'//lib:guava',
@@ -173,7 +176,6 @@
'//gerrit-common:annotations',
'//gerrit-server/src/main/prolog:common',
'//lib/antlr:java_runtime',
- '//lib/joda:joda-time',
],
source_under_test = [':server'],
)
@@ -193,8 +195,8 @@
'//lib:args4j',
'//lib:grappa',
'//lib:guava',
+ '//lib/dropwizard:dropwizard-core',
'//lib/guice:guice-assistedinject',
- '//lib/joda:joda-time',
'//lib/prolog:runtime',
],
source_under_test = [':server'],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 889f008..d9934f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -16,7 +16,6 @@
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.events.LifecycleListener;
@@ -715,12 +714,12 @@
}
@Override
- public void doClaSignupHook(Account account, ContributorAgreement cla) {
+ public void doClaSignupHook(Account account, String claName) {
if (account != null) {
List<String> args = new ArrayList<>();
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--user-id", account.getId().toString());
- addArg(args, "--cla-name", cla.getName());
+ addArg(args, "--cla-name", claName);
runHook(claSignedHook, args);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index b16a8a5..a70df51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -15,7 +15,6 @@
package com.google.gerrit.common;
import com.google.gerrit.common.ChangeHookRunner.HookResult;
-import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@@ -39,7 +38,7 @@
* @param patchSet The Patchset that was created.
* @throws OrmException
*/
- public void doPatchsetCreatedHook(Change change, PatchSet patchSet,
+ void doPatchsetCreatedHook(Change change, PatchSet patchSet,
ReviewDb db) throws OrmException;
/**
@@ -49,7 +48,7 @@
* @param patchSet The Patchset that was published.
* @throws OrmException
*/
- public void doDraftPublishedHook(Change change, PatchSet patchSet,
+ void doDraftPublishedHook(Change change, PatchSet patchSet,
ReviewDb db) throws OrmException;
/**
@@ -62,7 +61,7 @@
* @param approvals Map of label IDs to scores
* @throws OrmException
*/
- public void doCommentAddedHook(Change change, Account account,
+ void doCommentAddedHook(Change change, Account account,
PatchSet patchSet, String comment,
Map<String, Short> approvals, ReviewDb db)
throws OrmException;
@@ -76,7 +75,7 @@
* @param mergeResultRev The SHA-1 of the merge result revision.
* @throws OrmException
*/
- public void doChangeMergedHook(Change change, Account account,
+ void doChangeMergedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
/**
@@ -88,7 +87,7 @@
* @param reason The reason that the change failed to merge.
* @throws OrmException
*/
- public void doMergeFailedHook(Change change, Account account,
+ void doMergeFailedHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
@@ -99,7 +98,7 @@
* @param reason Reason for abandoning the change.
* @throws OrmException
*/
- public void doChangeAbandonedHook(Change change, Account account,
+ void doChangeAbandonedHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
@@ -110,7 +109,7 @@
* @param reason Reason for restoring the change.
* @throws OrmException
*/
- public void doChangeRestoredHook(Change change, Account account,
+ void doChangeRestoredHook(Change change, Account account,
PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
@@ -120,7 +119,7 @@
* @param refUpdate An actual RefUpdate object
* @param account The gerrit user who moved the ref
*/
- public void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate,
+ void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate,
Account account);
/**
@@ -131,7 +130,7 @@
* @param newId The ref's new id
* @param account The gerrit user who moved the ref
*/
- public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
+ void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
ObjectId newId, Account account);
/**
@@ -141,7 +140,7 @@
* @param patchSet The patchset that the reviewer was added on.
* @param account The gerrit user who was added as reviewer.
*/
- public void doReviewerAddedHook(Change change, Account account,
+ void doReviewerAddedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db) throws OrmException;
/**
@@ -151,10 +150,10 @@
* @param account The gerrit user who changed the topic.
* @param oldTopic The old topic name.
*/
- public void doTopicChangedHook(Change change, Account account,
+ void doTopicChangedHook(Change change, Account account,
String oldTopic, ReviewDb db) throws OrmException;
- public void doClaSignupHook(Account account, ContributorAgreement cla);
+ void doClaSignupHook(Account account, String claName);
/**
* Fire the Ref update Hook
@@ -165,7 +164,7 @@
* @param oldId The ref's old id
* @param newId The ref's new id
*/
- public HookResult doRefUpdateHook(Project project, String refName,
+ HookResult doRefUpdateHook(Project project, String refName,
Account uploader, ObjectId oldId, ObjectId newId);
/**
@@ -178,7 +177,7 @@
* @param db The database
* @throws OrmException
*/
- public void doHashtagsChangedHook(Change change, Account account,
+ void doHashtagsChangedHook(Change change, Account account,
Set<String>added, Set<String> removed, Set<String> hashtags,
ReviewDb db) throws OrmException;
@@ -188,5 +187,5 @@
* @param project The project that was created
* @param headName The head name of the created project
*/
- public void doProjectCreatedHook(Project.NameKey project, String headName);
+ void doProjectCreatedHook(Project.NameKey project, String headName);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index bed77a7..1e0afd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -15,7 +15,6 @@
package com.google.gerrit.common;
import com.google.gerrit.common.ChangeHookRunner.HookResult;
-import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@@ -59,7 +58,7 @@
}
@Override
- public void doClaSignupHook(Account account, ContributorAgreement cla) {
+ public void doClaSignupHook(Account account, String claName) {
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
index b74771f8..f71b5c68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
@@ -31,7 +31,7 @@
* @param db The database
* @throws OrmException
*/
- public void postEvent(Change change, Event event, ReviewDb db)
+ void postEvent(Change change, Event event, ReviewDb db)
throws OrmException;
/**
@@ -40,5 +40,5 @@
* @param branchName The branch that the event is related to
* @param event The event to post
*/
- public void postEvent(Branch.NameKey branchName, Event event);
+ void postEvent(Branch.NameKey branchName, Event event);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
index 7e8a794..97be844 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
@@ -19,5 +19,5 @@
@ExtensionPoint
public interface EventListener {
- public void onEvent(Event event);
+ void onEvent(Event event);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
index e2c4b34..bde6f5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
@@ -18,7 +18,7 @@
/** Distributes Events to ChangeListeners. Register listeners here. */
public interface EventSource {
- public void addEventListener(EventListener listener, CurrentUser user);
+ void addEventListener(EventListener listener, CurrentUser user);
- public void removeEventListener(EventListener listener);
+ void removeEventListener(EventListener listener);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java
new file mode 100644
index 0000000..1714c7a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java
@@ -0,0 +1,28 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+
+/**
+ * Metric whose value is supplied when the trigger is invoked.
+ *
+ * @see CallbackMetric0
+ * @param <V> type of the metric value, typically Integer or Long.
+ */
+public interface CallbackMetric<V> extends RegistrationHandle {
+ void prune();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java
new file mode 100644
index 0000000..829dd22
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java
@@ -0,0 +1,43 @@
+// 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.metrics;
+
+/**
+ * Metric whose value is supplied when the trigger is invoked.
+ *
+ * <pre>
+ * CallbackMetric0<Long> hits = metricMaker.newCallbackMetric("hits", ...);
+ * CallbackMetric0<Long> total = metricMaker.newCallbackMetric("total", ...);
+ * metricMaker.newTrigger(hits, total, new Runnable() {
+ * public void run() {
+ * hits.set(1);
+ * total.set(5);
+ * }
+ * });
+ * </pre>
+ *
+ * @param <V> type of the metric value, typically Integer or Long.
+ */
+public abstract class CallbackMetric0<V> implements CallbackMetric<V> {
+ /**
+ * Supply the current value of the metric.
+ *
+ * @param value current value.
+ */
+ public abstract void set(V value);
+
+ @Override
+ public void prune() {}
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric1.java
new file mode 100644
index 0000000..0888e21
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric1.java
@@ -0,0 +1,39 @@
+// 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.metrics;
+
+/**
+ * Metric whose value is supplied when the trigger is invoked.
+ *
+ * @param <F1> type of the field.
+ * @param <V> type of the metric value, typically Integer or Long.
+ */
+public abstract class CallbackMetric1<F1, V> implements CallbackMetric<V> {
+ /**
+ * Supply the current value of the metric.
+ *
+ * @param field1 bucket to increment.
+ * @param value current value.
+ */
+ public abstract void set(F1 field1, V value);
+
+ /** Ensure a zeroed metric is created for the field value. */
+ public abstract void forceCreate(F1 field1);
+
+ /** Prune any submetrics that were not assigned during this trigger. */
+ @Override
+ public void prune() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java
new file mode 100644
index 0000000..a2af7e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java
@@ -0,0 +1,41 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ */
+public abstract class Counter0 implements RegistrationHandle {
+ /** Increment the counter by one event. */
+ public void increment() {
+ incrementBy(1);
+ }
+
+ /**
+ * Increment the counter by a specified amount.
+ *
+ * @param value value to increment by, must be >= 0.
+ */
+ public abstract void incrementBy(long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java
new file mode 100644
index 0000000..8627dcc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java
@@ -0,0 +1,44 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ */
+public abstract class Counter1<F1> implements RegistrationHandle {
+ /** Increment the counter by one event. */
+ public void increment(F1 field1) {
+ incrementBy(field1, 1);
+ }
+
+ /**
+ * Increment the counter by a specified amount.
+ *
+ * @param field1 bucket to increment.
+ * @param value value to increment by, must be >= 0.
+ */
+ public abstract void incrementBy(F1 field1, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java
new file mode 100644
index 0000000..40efe95
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java
@@ -0,0 +1,46 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ */
+public abstract class Counter2<F1, F2> implements RegistrationHandle {
+ /** Increment the counter by one event. */
+ public void increment(F1 field1, F2 field2) {
+ incrementBy(field1, field2, 1);
+ }
+
+ /**
+ * Increment the counter by a specified amount.
+ *
+ * @param field1 bucket to increment.
+ * @param field2 bucket to increment.
+ * @param value value to increment by, must be >= 0.
+ */
+ public abstract void incrementBy(F1 field1, F2 field2, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java
new file mode 100644
index 0000000..d722930
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java
@@ -0,0 +1,48 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ * @param <F3> type of the field.
+ */
+public abstract class Counter3<F1, F2, F3> implements RegistrationHandle {
+ /** Increment the counter by one event. */
+ public void increment(F1 field1, F2 field2, F3 field3) {
+ incrementBy(field1, field2, field3, 1);
+ }
+
+ /**
+ * Increment the counter by a specified amount.
+ *
+ * @param field1 bucket to increment.
+ * @param field2 bucket to increment.
+ * @param field3 bucket to increment.
+ * @param value value to increment by, must be >= 0.
+ */
+ public abstract void incrementBy(F1 field1, F2 field2, F3 field3, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
new file mode 100644
index 0000000..43e5c8a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.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.metrics;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/** Describes a metric created by {@link MetricMaker}. */
+public class Description {
+ public static final String DESCRIPTION = "DESCRIPTION";
+ public static final String UNIT = "UNIT";
+ public static final String CUMULATIVE = "CUMULATIVE";
+ public static final String RATE = "RATE";
+ public static final String GAUGE = "GAUGE";
+ public static final String CONSTANT = "CONSTANT";
+ public static final String FIELD_ORDERING = "FIELD_ORDERING";
+ public static final String TRUE_VALUE = "1";
+
+ public static class Units {
+ public static final String SECONDS = "seconds";
+ public static final String MILLISECONDS = "milliseconds";
+ public static final String MICROSECONDS = "microseconds";
+ public static final String NANOSECONDS = "nanoseconds";
+
+ public static final String BYTES = "bytes";
+
+ private Units() {
+ }
+ }
+
+ public static enum FieldOrdering {
+ /** Default ordering places fields at end of the parent metric name. */
+ AT_END,
+
+ /**
+ * Splits the metric name by inserting field values before the last '/' in
+ * the metric name. For example {@code "plugins/replication/push_latency"}
+ * with a {@code Field.ofString("remote")} will create submetrics named
+ * {@code "plugins/replication/some-server/push_latency"}.
+ */
+ PREFIX_FIELDS_BASENAME;
+ }
+
+ private final Map<String, String> annotations;
+
+ /**
+ * Describe a metric.
+ *
+ * @param helpText a short one-sentence string explaining the values captured
+ * by the metric. This may be made available to administrators as
+ * documentation in the reporting tools.
+ */
+ public Description(String helpText) {
+ annotations = Maps.newLinkedHashMapWithExpectedSize(4);
+ annotations.put(DESCRIPTION, helpText);
+ }
+
+ /** Unit used to describe the value, e.g. "requests", "seconds", etc. */
+ public Description setUnit(String unitName) {
+ annotations.put(UNIT, unitName);
+ return this;
+ }
+
+ /**
+ * Mark the value as constant for the life of this process. Typically used for
+ * software versions, command line arguments, etc. that cannot change without
+ * a process restart.
+ */
+ public Description setConstant() {
+ annotations.put(CONSTANT, TRUE_VALUE);
+ return this;
+ }
+
+ /**
+ * Indicates the metric may be usefully interpreted as a count over short
+ * periods of time, such as request arrival rate. May only be applied to a
+ * {@link Counter0}.
+ */
+ public Description setRate() {
+ annotations.put(RATE, TRUE_VALUE);
+ return this;
+ }
+
+ /**
+ * Instantaneously sampled value that may increase or decrease at a later
+ * time. Memory allocated or open network connections are examples of gauges.
+ */
+ public Description setGauge() {
+ annotations.put(GAUGE, TRUE_VALUE);
+ return this;
+ }
+
+ /**
+ * Indicates the metric accumulates over the lifespan of the process. A
+ * {@link Counter0} like total requests handled accumulates over the process
+ * and should be {@code setCumulative()}.
+ */
+ public Description setCumulative() {
+ annotations.put(CUMULATIVE, TRUE_VALUE);
+ return this;
+ }
+
+ /** Configure how fields are ordered into submetric names. */
+ public Description setFieldOrdering(FieldOrdering ordering) {
+ annotations.put(FIELD_ORDERING, ordering.name());
+ return this;
+ }
+
+ /** True if the metric value never changes after startup. */
+ public boolean isConstant() {
+ return TRUE_VALUE.equals(annotations.get(CONSTANT));
+ }
+
+ /** True if the metric may be interpreted as a rate over time. */
+ public boolean isRate() {
+ return TRUE_VALUE.equals(annotations.get(RATE));
+ }
+
+ /** True if the metric is an instantaneous sample. */
+ public boolean isGauge() {
+ return TRUE_VALUE.equals(annotations.get(GAUGE));
+ }
+
+ /** True if the metric accumulates over the lifespan of the process. */
+ public boolean isCumulative() {
+ return TRUE_VALUE.equals(annotations.get(CUMULATIVE));
+ }
+
+ /** Get the suggested field ordering. */
+ public FieldOrdering getFieldOrdering() {
+ String o = annotations.get(FIELD_ORDERING);
+ return o != null ? FieldOrdering.valueOf(o) : FieldOrdering.AT_END;
+ }
+
+ /**
+ * Decode the unit as a unit of time.
+ *
+ * @return valid time unit.
+ * @throws IllegalArgumentException if the unit is not a valid unit of time.
+ */
+ public TimeUnit getTimeUnit() {
+ return getTimeUnit(annotations.get(UNIT));
+ }
+
+ private static final ImmutableMap<String, TimeUnit> TIME_UNITS = ImmutableMap.of(
+ Units.NANOSECONDS, TimeUnit.NANOSECONDS,
+ Units.MICROSECONDS, TimeUnit.MICROSECONDS,
+ Units.MILLISECONDS, TimeUnit.MILLISECONDS,
+ Units.SECONDS, TimeUnit.SECONDS);
+
+ public static TimeUnit getTimeUnit(String unit) {
+ if (Strings.isNullOrEmpty(unit)) {
+ throw new IllegalArgumentException("no unit configured");
+ }
+ TimeUnit u = TIME_UNITS.get(unit);
+ if (u == null) {
+ throw new IllegalArgumentException(String.format(
+ "unit %s not TimeUnit", unit));
+ }
+ return u;
+ }
+
+ /** Immutable copy of all annotations (configurable properties). */
+ public ImmutableMap<String, String> getAnnotations() {
+ return ImmutableMap.copyOf(annotations);
+ }
+
+ @Override
+ public String toString() {
+ return annotations.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/DisabledMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/DisabledMetricMaker.java
new file mode 100644
index 0000000..1b05e2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/DisabledMetricMaker.java
@@ -0,0 +1,155 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/** Exports no metrics, useful for running batch programs. */
+public class DisabledMetricMaker extends MetricMaker {
+ @Override
+ public Counter0 newCounter(String name, Description desc) {
+ return new Counter0() {
+ @Override public void incrementBy(long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1> Counter1<F1> newCounter(String name, Description desc,
+ Field<F1> field1) {
+ return new Counter1<F1>() {
+ @Override public void incrementBy(F1 field1, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2> Counter2<F1, F2> newCounter(String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ return new Counter2<F1, F2>() {
+ @Override public void incrementBy(F1 field1, F2 field2, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(String name,
+ Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ return new Counter3<F1, F2, F3>() {
+ @Override public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public Timer0 newTimer(String name, Description desc) {
+ return new Timer0() {
+ @Override public void record(long value, TimeUnit unit) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1> Timer1<F1> newTimer(String name, Description desc,
+ Field<F1> field1) {
+ return new Timer1<F1>() {
+ @Override public void record(F1 field1, long value, TimeUnit unit) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2> Timer2<F1, F2> newTimer(String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ return new Timer2<F1, F2>() {
+ @Override public void record(F1 field1, F2 field2, long value, TimeUnit unit) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(String name,
+ Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ return new Timer3<F1, F2, F3>() {
+ @Override public void record(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public Histogram0 newHistogram(String name, Description desc) {
+ return new Histogram0() {
+ @Override public void record(long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1> Histogram1<F1> newHistogram(String name, Description desc,
+ Field<F1> field1) {
+ return new Histogram1<F1>() {
+ @Override public void record(F1 field1, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2> Histogram2<F1, F2> newHistogram(String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ return new Histogram2<F1, F2>() {
+ @Override public void record(F1 field1, F2 field2, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(String name,
+ Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ return new Histogram3<F1, F2, F3>() {
+ @Override public void record(F1 field1, F2 field2, F3 field3, long value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <V> CallbackMetric0<V> newCallbackMetric(String name,
+ Class<V> valueClass, Description desc) {
+ return new CallbackMetric0<V>() {
+ @Override public void set(V value) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public <F1, V> CallbackMetric1<F1, V> newCallbackMetric(String name,
+ Class<V> valueClass, Description desc, Field<F1> field1) {
+ return new CallbackMetric1<F1, V>() {
+ @Override public void set(F1 field1, V value) {}
+ @Override public void forceCreate(F1 field1) {}
+ @Override public void remove() {}
+ };
+ }
+
+ @Override
+ public RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics,
+ Runnable trigger) {
+ return new RegistrationHandle() {
+ @Override public void remove() {}
+ };
+ }
+}
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
new file mode 100644
index 0000000..a91e428
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
@@ -0,0 +1,136 @@
+// 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.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+/** Describes a bucketing field used by a metric. */
+public class Field<T> {
+ /** Break down metrics by boolean true/false. */
+ public static Field<Boolean> ofBoolean(String name) {
+ return ofBoolean(name, null);
+ }
+
+ /** Break down metrics by boolean true/false. */
+ public static Field<Boolean> ofBoolean(String name, String description) {
+ return new Field<>(name, Boolean.class, description);
+ }
+
+ /** Break down metrics by cases of an enum. */
+ public static <E extends Enum<E>> Field<E> ofEnum(Class<E> enumType,
+ String name) {
+ return ofEnum(enumType, name, null);
+ }
+
+ /** Break down metrics by cases of an enum. */
+ public static <E extends Enum<E>> Field<E> ofEnum(Class<E> enumType,
+ String name, String description) {
+ return new Field<>(name, enumType, description);
+ }
+
+ /**
+ * Break down metrics by string.
+ * <p>
+ * Each unique string will allocate a new submetric. <b>Do not use user
+ * content as a field value</b> as field values are never reclaimed.
+ */
+ public static Field<String> ofString(String name) {
+ return ofString(name, null);
+ }
+
+ /**
+ * Break down metrics by string.
+ * <p>
+ * Each unique string will allocate a new submetric. <b>Do not use user
+ * content as a field value</b> as field values are never reclaimed.
+ */
+ public static Field<String> ofString(String name, String description) {
+ return new Field<>(name, String.class, description);
+ }
+
+ /**
+ * Break down metrics by integer.
+ * <p>
+ * Each unique integer will allocate a new submetric. <b>Do not use user
+ * content as a field value</b> as field values are never reclaimed.
+ */
+ public static Field<Integer> ofInteger(String name) {
+ return ofInteger(name, null);
+ }
+
+ /**
+ * Break down metrics by integer.
+ * <p>
+ * Each unique integer will allocate a new submetric. <b>Do not use user
+ * content as a field value</b> as field values are never reclaimed.
+ */
+ public static Field<Integer> ofInteger(String name, String description) {
+ return new Field<>(name, Integer.class, description);
+ }
+
+ private final String name;
+ private final Class<T> keyType;
+ private final Function<T, String> formatter;
+ private final String description;
+
+ private Field(String name, Class<T> keyType, String description) {
+ checkArgument(name.matches("^[a-z_]+$"), "name must match [a-z_]");
+ this.name = name;
+ this.keyType = keyType;
+ this.formatter = initFormatter(keyType);
+ this.description = description;
+ }
+
+ /** Name of this field within the metric. */
+ public String getName() {
+ return name;
+ }
+
+ /** Type of value used within the field. */
+ public Class<T> getType() {
+ return keyType;
+ }
+
+ /** Description text for the field explaining its range of values. */
+ public String getDescription() {
+ return description;
+ }
+
+ public Function<T, String> formatter() {
+ return formatter;
+ }
+
+ @SuppressWarnings("unchecked")
+ 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();
+ }
+ };
+ }
+ throw new IllegalStateException("unsupported type " + keyType.getName());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram0.java
new file mode 100644
index 0000000..3a6fbd9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram0.java
@@ -0,0 +1,27 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Measures the statistical distribution of values in a stream of data.
+ * <p>
+ * Suitable uses are "response size in bytes", etc.
+ */
+public abstract class Histogram0 implements RegistrationHandle {
+ /** Record a sample of a specified amount. */
+ public abstract void record(long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram1.java
new file mode 100644
index 0000000..16df8e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram1.java
@@ -0,0 +1,29 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Measures the statistical distribution of values in a stream of data.
+ * <p>
+ * Suitable uses are "response size in bytes", etc.
+ *
+ * @param <F1> type of the field.
+ */
+public abstract class Histogram1<F1> implements RegistrationHandle {
+ /** Record a sample of a specified amount. */
+ public abstract void record(F1 field1, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram2.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram2.java
new file mode 100644
index 0000000..2eca402
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram2.java
@@ -0,0 +1,30 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Measures the statistical distribution of values in a stream of data.
+ * <p>
+ * Suitable uses are "response size in bytes", etc.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ */
+public abstract class Histogram2<F1, F2> implements RegistrationHandle {
+ /** Record a sample of a specified amount. */
+ public abstract void record(F1 field1, F2 field2, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram3.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram3.java
new file mode 100644
index 0000000..09a756d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Histogram3.java
@@ -0,0 +1,31 @@
+// 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.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Measures the statistical distribution of values in a stream of data.
+ * <p>
+ * Suitable uses are "response size in bytes", etc.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ * @param <F3> type of the field.
+ */
+public abstract class Histogram3<F1, F2, F3> implements RegistrationHandle {
+ /** Record a sample of a specified amount. */
+ public abstract void record(F1 field1, F2 field2, F3 field3, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
new file mode 100644
index 0000000..2122594
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
@@ -0,0 +1,137 @@
+// 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.metrics;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.Set;
+
+/** Factory to create metrics for monitoring. */
+public abstract class MetricMaker {
+ /** Metric whose value increments during the life of the process. */
+ public abstract Counter0 newCounter(String name, Description desc);
+ public abstract <F1> Counter1<F1> newCounter(
+ String name, Description desc,
+ Field<F1> field1);
+ public abstract <F1, F2> Counter2<F1, F2> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2);
+ public abstract <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3);
+
+ /** Metric recording time spent on an operation. */
+ public abstract Timer0 newTimer(String name, Description desc);
+ public abstract <F1> Timer1<F1> newTimer(
+ String name, Description desc,
+ Field<F1> field1);
+ public abstract <F1, F2> Timer2<F1, F2> newTimer(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2);
+ public abstract <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3);
+
+ /** Metric statistical distribution of values. */
+ public abstract Histogram0 newHistogram(String name, Description desc);
+ public abstract <F1> Histogram1<F1> newHistogram(
+ String name, Description desc,
+ Field<F1> field1);
+ public abstract <F1, F2> Histogram2<F1, F2> newHistogram(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2);
+ public abstract <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3);
+
+ /**
+ * Constant value that does not change.
+ *
+ * @param name unique name of the metric.
+ * @param value only value of the metric.
+ * @param desc description of the metric.
+ */
+ public <V> void newConstantMetric(String name, final V value, Description desc) {
+ desc.setConstant();
+
+ @SuppressWarnings("unchecked")
+ Class<V> type = (Class<V>) value.getClass();
+ final CallbackMetric0<V> metric = newCallbackMetric(name, type, desc);
+ newTrigger(metric, new Runnable() {
+ @Override
+ public void run() {
+ metric.set(value);
+ }
+ });
+ }
+
+ /**
+ * Instantaneous reading of a value.
+ *
+ * <pre>
+ * metricMaker.newCallbackMetric("memory",
+ * new Description("Total bytes of memory used")
+ * .setGauge()
+ * .setUnit(Units.BYTES),
+ * new Supplier<Long>() {
+ * public Long get() {
+ * return Runtime.getRuntime().totalMemory();
+ * }
+ * });
+ * </pre>
+ *
+ * @param name unique name of the metric.
+ * @param valueClass type of value recorded by the metric.
+ * @param desc description of the metric.
+ * @param trigger function to compute the value of the metric.
+ */
+ public <V> void newCallbackMetric(String name,
+ Class<V> valueClass, Description desc, final Supplier<V> trigger) {
+ final CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc);
+ newTrigger(metric, new Runnable() {
+ @Override
+ public void run() {
+ metric.set(trigger.get());
+ }
+ });
+ }
+
+ /** Instantaneous reading of a single value. */
+ public abstract <V> CallbackMetric0<V> newCallbackMetric(
+ String name, Class<V> valueClass, Description desc);
+ public abstract <F1, V> CallbackMetric1<F1, V> newCallbackMetric(
+ String name, Class<V> valueClass, Description desc,
+ Field<F1> field1);
+
+ /** Connect logic to populate a previously created {@link CallbackMetric}. */
+ public RegistrationHandle newTrigger(CallbackMetric<?> metric1, Runnable trigger) {
+ return newTrigger(ImmutableSet.<CallbackMetric<?>>of(metric1), trigger);
+ }
+
+ public RegistrationHandle newTrigger(CallbackMetric<?> metric1,
+ CallbackMetric<?> metric2, Runnable trigger) {
+ return newTrigger(ImmutableSet.of(metric1, metric2), trigger);
+ }
+
+ public RegistrationHandle newTrigger(CallbackMetric<?> metric1,
+ CallbackMetric<?> metric2, CallbackMetric<?> metric3, Runnable trigger) {
+ return newTrigger(ImmutableSet.of(metric1, metric2, metric3), trigger);
+ }
+
+ public abstract RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics,
+ Runnable trigger);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
new file mode 100644
index 0000000..ff0735e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.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.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer.Context ctx = timer.start()) {
+ * }
+ * </pre>
+ */
+public abstract class Timer0 implements RegistrationHandle {
+ public static class Context extends TimerContext {
+ private final Timer0 timer;
+
+ Context(Timer0 timer) {
+ this.timer = timer;
+ }
+
+ @Override
+ public void record(long elapsed) {
+ timer.record(elapsed, NANOSECONDS);
+ }
+ }
+
+ /** Begin a timer for the current block, value will be recorded when closed. */
+ public Context start() {
+ return new Context(this);
+ }
+
+ /** Record a value in the distribution. */
+ public abstract void record(long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java
new file mode 100644
index 0000000..e0bc4fc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java
@@ -0,0 +1,59 @@
+// 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.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer1.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ */
+public abstract class Timer1<F1> implements RegistrationHandle {
+ public static class Context extends TimerContext {
+ private final Timer1<Object> timer;
+ private final Object field1;
+
+ @SuppressWarnings("unchecked")
+ <F1> Context(Timer1<F1> timer, F1 field1) {
+ this.timer = (Timer1<Object>) timer;
+ this.field1 = field1;
+ }
+
+ @Override
+ public void record(long elapsed) {
+ timer.record(field1, elapsed, NANOSECONDS);
+ }
+ }
+
+ /** Begin a timer for the current block, value will be recorded when closed. */
+ public Context start(F1 field1) {
+ return new Context(this, field1);
+ }
+
+ /** Record a value in the distribution. */
+ public abstract void record(F1 field1, long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java
new file mode 100644
index 0000000..5bfaba0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java
@@ -0,0 +1,62 @@
+// 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.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer2.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ */
+public abstract class Timer2<F1, F2> implements RegistrationHandle {
+ public static class Context extends TimerContext {
+ private final Timer2<Object, Object> timer;
+ private final Object field1;
+ private final Object field2;
+
+ @SuppressWarnings("unchecked")
+ <F1, F2> Context(Timer2<F1, F2> timer, F1 field1, F2 field2) {
+ this.timer = (Timer2<Object, Object>) timer;
+ this.field1 = field1;
+ this.field2 = field2;
+ }
+
+ @Override
+ public void record(long elapsed) {
+ timer.record(field1, field2, elapsed, NANOSECONDS);
+ }
+ }
+
+ /** Begin a timer for the current block, value will be recorded when closed. */
+ public Context start(F1 field1, F2 field2) {
+ return new Context(this, field1, field2);
+ }
+
+ /** Record a value in the distribution. */
+ public abstract void record(F1 field1, F2 field2, long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java
new file mode 100644
index 0000000..c564d42
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java
@@ -0,0 +1,66 @@
+// 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.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer3.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ * @param <F3> type of the field.
+ */
+public abstract class Timer3<F1, F2, F3> implements RegistrationHandle {
+ public static class Context extends TimerContext {
+ private final Timer3<Object, Object, Object> timer;
+ private final Object field1;
+ private final Object field2;
+ private final Object field3;
+
+ @SuppressWarnings("unchecked")
+ <F1, F2, F3> Context(Timer3<F1, F2, F3> timer, F1 f1, F2 f2, F3 f3) {
+ this.timer = (Timer3<Object, Object, Object>) timer;
+ this.field1 = f1;
+ this.field2 = f2;
+ this.field3 = f3;
+ }
+
+ @Override
+ public void record(long elapsed) {
+ timer.record(field1, field2, field3, elapsed, NANOSECONDS);
+ }
+ }
+
+ /** Begin a timer for the current block, value will be recorded when closed. */
+ public Context start(F1 field1, F2 field2, F3 field3) {
+ return new Context(this, field1, field2, field3);
+ }
+
+ /** Record a value in the distribution. */
+ public abstract void record(F1 field1, F2 field2, F3 field3,
+ long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/TimerContext.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/TimerContext.java
new file mode 100644
index 0000000..32ef753
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/TimerContext.java
@@ -0,0 +1,59 @@
+// 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.metrics;
+
+abstract class TimerContext implements AutoCloseable {
+ private final long startNanos;
+ private boolean stopped;
+
+ TimerContext() {
+ this.startNanos = System.nanoTime();
+ }
+
+ /**
+ * Record the elapsed time to the timer.
+ *
+ * @param elapsed Elapsed time in nanoseconds.
+ */
+ public abstract void record(long elapsed);
+
+ /** Get the start time in system time nanoseconds. */
+ public long getStartTime() {
+ return startNanos;
+ }
+
+ /**
+ * Stop the timer and record the elapsed time.
+ *
+ * @return the elapsed time in nanoseconds.
+ * @throws IllegalStateException if the timer is already stopped.
+ */
+ public long stop() {
+ if (!stopped) {
+ stopped = true;
+ long elapsed = System.nanoTime() - startNanos;
+ record(elapsed);
+ return elapsed;
+ }
+ throw new IllegalStateException("Already stopped");
+ }
+
+ @Override
+ public void close() {
+ if (!stopped) {
+ stop();
+ }
+ }
+}
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
new file mode 100644
index 0000000..629cf96
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
@@ -0,0 +1,149 @@
+// 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.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;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract callback metric broken down into buckets. */
+abstract class BucketedCallback<V> implements BucketedMetric {
+ private final DropWizardMetricMaker metrics;
+ private final MetricRegistry registry;
+ private final String name;
+ private final Description.FieldOrdering ordering;
+ protected final Field<?>[] fields;
+ private final V zero;
+ private final Map<Object, ValueGauge> cells;
+ protected volatile Runnable trigger;
+
+ BucketedCallback(DropWizardMetricMaker metrics, MetricRegistry registry,
+ String name, Class<V> valueType, Description desc, Field<?>... fields) {
+ this.metrics = metrics;
+ this.registry = registry;
+ this.name = name;
+ this.ordering = desc.getFieldOrdering();
+ this.fields = fields;
+ this.zero = CallbackMetricImpl0.zeroFor(valueType);
+ this.cells = new ConcurrentHashMap<>();
+ }
+
+ void doRemove() {
+ for (Object key : cells.keySet()) {
+ registry.remove(submetric(key));
+ }
+ metrics.remove(name);
+ }
+
+ void doBeginSet() {
+ for (ValueGauge g : cells.values()) {
+ g.set = false;
+ }
+ }
+
+ void doPrune() {
+ Iterator<Map.Entry<Object, ValueGauge>> i = cells.entrySet().iterator();
+ while (i.hasNext()) {
+ if (!i.next().getValue().set) {
+ i.remove();
+ }
+ }
+ }
+
+ void doEndSet() {
+ for (ValueGauge g : cells.values()) {
+ if (!g.set) {
+ g.value = zero;
+ }
+ }
+ }
+
+ ValueGauge getOrCreate(Object f1, Object f2) {
+ return getOrCreate(ImmutableList.of(f1, f2));
+ }
+
+ ValueGauge getOrCreate(Object f1, Object f2, Object f3) {
+ return getOrCreate(ImmutableList.of(f1, f2, f3));
+ }
+
+ ValueGauge getOrCreate(Object key) {
+ ValueGauge c = cells.get(key);
+ if (c != null) {
+ return c;
+ }
+
+ synchronized (cells) {
+ c = cells.get(key);
+ if (c == null) {
+ c = new ValueGauge();
+ registry.register(submetric(key), c);
+ cells.put(key, c);
+ }
+ return c;
+ }
+ }
+
+ private String submetric(Object key) {
+ return DropWizardMetricMaker.name(ordering, name, name(key));
+ }
+
+ abstract String name(Object key);
+
+ @Override
+ public Metric getTotal() {
+ return null;
+ }
+
+ @Override
+ public Field<?>[] getFields() {
+ return fields;
+ }
+
+ @Override
+ public Map<Object, Metric> getCells() {
+ return Maps.transformValues(
+ cells,
+ new Function<ValueGauge, Metric> () {
+ @Override
+ public Metric apply(ValueGauge in) {
+ return in;
+ }
+ });
+ }
+
+ final class ValueGauge implements Gauge<V> {
+ volatile V value = zero;
+ boolean set;
+
+ @Override
+ public V getValue() {
+ Runnable t = trigger;
+ if (t != null) {
+ t.run();
+ }
+ return value;
+ }
+ }
+}
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
new file mode 100644
index 0000000..22af5ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
@@ -0,0 +1,109 @@
+// 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.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;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker.CounterImpl;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract counter broken down into buckets by {@link Field} values. */
+abstract class BucketedCounter implements BucketedMetric {
+ private final DropWizardMetricMaker metrics;
+ private final String name;
+ private final boolean isRate;
+ private final Description.FieldOrdering ordering;
+ protected final Field<?>[] fields;
+ protected final CounterImpl total;
+ private final Map<Object, CounterImpl> cells;
+
+ BucketedCounter(DropWizardMetricMaker metrics,
+ String name, Description desc, Field<?>... fields) {
+ this.metrics = metrics;
+ this.name = name;
+ this.isRate = desc.isRate();
+ this.ordering = desc.getFieldOrdering();
+ this.fields = fields;
+ this.total = metrics.newCounterImpl(name + "_total", isRate);
+ this.cells = new ConcurrentHashMap<>();
+ }
+
+ void doRemove() {
+ for (CounterImpl c : cells.values()) {
+ c.remove();
+ }
+ total.remove();
+ metrics.remove(name);
+ }
+
+ CounterImpl forceCreate(Object f1, Object f2) {
+ return forceCreate(ImmutableList.of(f1, f2));
+ }
+
+ CounterImpl forceCreate(Object f1, Object f2, Object f3) {
+ return forceCreate(ImmutableList.of(f1, f2, f3));
+ }
+
+ CounterImpl forceCreate(Object key) {
+ CounterImpl c = cells.get(key);
+ if (c != null) {
+ return c;
+ }
+
+ synchronized (cells) {
+ c = cells.get(key);
+ if (c == null) {
+ c = metrics.newCounterImpl(submetric(key), isRate);
+ cells.put(key, c);
+ }
+ return c;
+ }
+ }
+
+ private String submetric(Object key) {
+ return DropWizardMetricMaker.name(ordering, name, name(key));
+ }
+
+ abstract String name(Object key);
+
+ @Override
+ public Metric getTotal() {
+ return total.metric;
+ }
+
+ @Override
+ public Field<?>[] getFields() {
+ return fields;
+ }
+
+ @Override
+ public Map<Object, Metric> getCells() {
+ return Maps.transformValues(
+ cells,
+ new Function<CounterImpl, Metric> () {
+ @Override
+ public Metric apply(CounterImpl in) {
+ return in.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
new file mode 100644
index 0000000..51c5fea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
@@ -0,0 +1,107 @@
+// 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.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;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker.HistogramImpl;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract histogram broken down into buckets by {@link Field} values. */
+abstract class BucketedHistogram implements BucketedMetric {
+ private final DropWizardMetricMaker metrics;
+ private final String name;
+ private final Description.FieldOrdering ordering;
+ protected final Field<?>[] fields;
+ protected final HistogramImpl total;
+ private final Map<Object, HistogramImpl> cells;
+
+ BucketedHistogram(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<?>... fields) {
+ this.metrics = metrics;
+ this.name = name;
+ this.ordering = desc.getFieldOrdering();
+ this.fields = fields;
+ this.total = metrics.newHistogramImpl(name + "_total");
+ this.cells = new ConcurrentHashMap<>();
+ }
+
+ void doRemove() {
+ for (HistogramImpl c : cells.values()) {
+ c.remove();
+ }
+ total.remove();
+ metrics.remove(name);
+ }
+
+ HistogramImpl forceCreate(Object f1, Object f2) {
+ return forceCreate(ImmutableList.of(f1, f2));
+ }
+
+ HistogramImpl forceCreate(Object f1, Object f2, Object f3) {
+ return forceCreate(ImmutableList.of(f1, f2, f3));
+ }
+
+ HistogramImpl forceCreate(Object key) {
+ HistogramImpl c = cells.get(key);
+ if (c != null) {
+ return c;
+ }
+
+ synchronized (cells) {
+ c = cells.get(key);
+ if (c == null) {
+ c = metrics.newHistogramImpl(submetric(key));
+ cells.put(key, c);
+ }
+ return c;
+ }
+ }
+
+ private String submetric(Object key) {
+ return DropWizardMetricMaker.name(ordering, name, name(key));
+ }
+
+ abstract String name(Object key);
+
+ @Override
+ public Metric getTotal() {
+ return total.metric;
+ }
+
+ @Override
+ public Field<?>[] getFields() {
+ return fields;
+ }
+
+ @Override
+ public Map<Object, Metric> getCells() {
+ return Maps.transformValues(
+ cells,
+ new Function<HistogramImpl, Metric> () {
+ @Override
+ public Metric apply(HistogramImpl in) {
+ return in.metric;
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java
new file mode 100644
index 0000000..799e594
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java
@@ -0,0 +1,29 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+
+/** Metric broken down into buckets by {@link Field} values. */
+interface BucketedMetric extends Metric {
+ @Nullable Metric getTotal();
+ Field<?>[] getFields();
+ Map<?, Metric> getCells();
+}
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
new file mode 100644
index 0000000..ec12e00
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
@@ -0,0 +1,107 @@
+// 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.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;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker.TimerImpl;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract timer broken down into buckets by {@link Field} values. */
+abstract class BucketedTimer implements BucketedMetric {
+ private final DropWizardMetricMaker metrics;
+ private final String name;
+ private final Description.FieldOrdering ordering;
+ protected final Field<?>[] fields;
+ protected final TimerImpl total;
+ private final Map<Object, TimerImpl> cells;
+
+ BucketedTimer(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<?>... fields) {
+ this.metrics = metrics;
+ this.name = name;
+ this.ordering = desc.getFieldOrdering();
+ this.fields = fields;
+ this.total = metrics.newTimerImpl(name + "_total");
+ this.cells = new ConcurrentHashMap<>();
+ }
+
+ void doRemove() {
+ for (TimerImpl c : cells.values()) {
+ c.remove();
+ }
+ total.remove();
+ metrics.remove(name);
+ }
+
+ TimerImpl forceCreate(Object f1, Object f2) {
+ return forceCreate(ImmutableList.of(f1, f2));
+ }
+
+ TimerImpl forceCreate(Object f1, Object f2, Object f3) {
+ return forceCreate(ImmutableList.of(f1, f2, f3));
+ }
+
+ TimerImpl forceCreate(Object key) {
+ TimerImpl c = cells.get(key);
+ if (c != null) {
+ return c;
+ }
+
+ synchronized (cells) {
+ c = cells.get(key);
+ if (c == null) {
+ c = metrics.newTimerImpl(submetric(key));
+ cells.put(key, c);
+ }
+ return c;
+ }
+ }
+
+ private String submetric(Object key) {
+ return DropWizardMetricMaker.name(ordering, name, name(key));
+ }
+
+ abstract String name(Object key);
+
+ @Override
+ public Metric getTotal() {
+ return total.metric;
+ }
+
+ @Override
+ public Field<?>[] getFields() {
+ return fields;
+ }
+
+ @Override
+ public Map<Object, Metric> getCells() {
+ return Maps.transformValues(
+ cells,
+ new Function<TimerImpl, Metric> () {
+ @Override
+ public Metric apply(TimerImpl in) {
+ return in.metric;
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java
new file mode 100644
index 0000000..372bdcb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java
@@ -0,0 +1,70 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Run a user specified trigger only once every 2 seconds.
+ * <p>
+ * This allows the same Runnable trigger to be applied to several metrics. When
+ * a recorder is sampling the related metrics only the first access will perform
+ * recomputation. Reading other related metrics will rely on the already set
+ * values for the next several seconds.
+ */
+class CallbackGroup implements Runnable {
+ private static final long PERIOD = TimeUnit.SECONDS.toNanos(2);
+
+ private final AtomicLong reloadAt;
+ private final Runnable trigger;
+ private final ImmutableSet<CallbackMetricGlue> metrics;
+ private final Object reloadLock = new Object();
+
+ CallbackGroup(Runnable trigger, ImmutableSet<CallbackMetricGlue> metrics) {
+ this.reloadAt = new AtomicLong(0);
+ this.trigger = trigger;
+ this.metrics = metrics;
+ }
+
+ @Override
+ public void run() {
+ if (reload()) {
+ synchronized (reloadLock) {
+ for (CallbackMetricGlue m : metrics) {
+ m.beginSet();
+ }
+ trigger.run();
+ for (CallbackMetricGlue m : metrics) {
+ m.endSet();
+ }
+ }
+ }
+ }
+
+ private boolean reload() {
+ for (;;) {
+ long now = System.nanoTime();
+ long next = reloadAt.get();
+ if (next > now) {
+ return false;
+ } else if (reloadAt.compareAndSet(next, now + PERIOD)) {
+ return true;
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricGlue.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricGlue.java
new file mode 100644
index 0000000..4f5b7ad
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricGlue.java
@@ -0,0 +1,22 @@
+// 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.metrics.dropwizard;
+
+interface CallbackMetricGlue {
+ void beginSet();
+ void endSet();
+ void register(Runnable trigger);
+ void remove();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
new file mode 100644
index 0000000..dcab692
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
@@ -0,0 +1,86 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.metrics.CallbackMetric0;
+
+import com.codahale.metrics.MetricRegistry;
+
+class CallbackMetricImpl0<V>
+ extends CallbackMetric0<V>
+ implements CallbackMetricGlue {
+ @SuppressWarnings("unchecked")
+ static <V> V zeroFor(Class<V> valueClass) {
+ if (valueClass == Integer.class) {
+ return (V) Integer.valueOf(0);
+ } else if (valueClass == Long.class) {
+ return (V) Long.valueOf(0);
+ } else if (valueClass == Double.class) {
+ return (V) Double.valueOf(0);
+ } else if (valueClass == Float.class) {
+ return (V) Float.valueOf(0);
+ } else if (valueClass == String.class) {
+ return (V) "";
+ } else if (valueClass == Boolean.class) {
+ return (V) Boolean.FALSE;
+ } else {
+ throw new IllegalArgumentException("unsupported value type "
+ + valueClass.getName());
+ }
+ }
+
+ private final DropWizardMetricMaker metrics;
+ private final MetricRegistry registry;
+ private final String name;
+ private volatile V value;
+
+ CallbackMetricImpl0(DropWizardMetricMaker metrics, MetricRegistry registry,
+ String name, Class<V> valueType) {
+ this.metrics = metrics;
+ this.registry = registry;
+ this.name = name;
+ this.value = zeroFor(valueType);
+ }
+
+ @Override
+ public void beginSet() {
+ }
+
+ @Override
+ public void endSet() {
+ }
+
+ @Override
+ public void set(V value) {
+ this.value = value;
+ }
+
+ @Override
+ public void remove() {
+ metrics.remove(name);
+ registry.remove(name);
+ }
+
+ @Override
+ public void register(final Runnable trigger) {
+ registry.register(name, new com.codahale.metrics.Gauge<V>() {
+ @Override
+ public V getValue() {
+ trigger.run();
+ return value;
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java
new file mode 100644
index 0000000..81d5ff5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl1.java
@@ -0,0 +1,84 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.MetricRegistry;
+
+/** Optimized version of {@link BucketedCallback} for single dimension. */
+class CallbackMetricImpl1<F1, V> extends BucketedCallback<V> {
+ CallbackMetricImpl1(DropWizardMetricMaker metrics, MetricRegistry registry,
+ String name, Class<V> valueClass, Description desc, Field<F1> field1) {
+ super(metrics, registry, name, valueClass, desc, field1);
+ }
+
+ CallbackMetric1<F1, V> create() {
+ return new Impl1();
+ }
+
+ private final class Impl1
+ extends CallbackMetric1<F1, V>
+ implements CallbackMetricGlue {
+ @Override
+ public void beginSet() {
+ doBeginSet();
+ }
+
+ @Override
+ public void set(F1 field1, V value) {
+ BucketedCallback<V>.ValueGauge cell = getOrCreate(field1);
+ cell.value = value;
+ cell.set = true;
+ }
+
+ @Override
+ public void prune() {
+ doPrune();
+ }
+
+ @Override
+ public void endSet() {
+ doEndSet();
+ }
+
+ @Override
+ public void forceCreate(F1 field1) {
+ getOrCreate(field1);
+ }
+
+ @Override
+ public void register(Runnable t) {
+ trigger = t;
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ }
+
+ @Override
+ String name(Object field1) {
+ @SuppressWarnings("unchecked")
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[0].formatter();
+
+ return fmt.apply(field1).replace('/', '-');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
new file mode 100644
index 0000000..25647ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
@@ -0,0 +1,52 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+/** Optimized version of {@link BucketedCounter} for single dimension. */
+class CounterImpl1<F1> extends BucketedCounter {
+ CounterImpl1(DropWizardMetricMaker metrics, String name, Description desc,
+ Field<F1> field1) {
+ super(metrics, name, desc, field1);
+ }
+
+ Counter1<F1> counter() {
+ return new Counter1<F1>() {
+ @Override
+ public void incrementBy(F1 field1, long value) {
+ total.incrementBy(value);
+ forceCreate(field1).incrementBy(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @Override
+ String name(Object field1) {
+ @SuppressWarnings("unchecked")
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[0].formatter();
+
+ return fmt.apply(field1).replace('/', '-');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
new file mode 100644
index 0000000..a2f1f84
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
@@ -0,0 +1,75 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+/** Generalized implementation of N-dimensional counter metrics. */
+class CounterImplN extends BucketedCounter implements BucketedMetric {
+ CounterImplN(DropWizardMetricMaker metrics, String name, Description desc,
+ Field<?>... fields) {
+ super(metrics, name, desc, fields);
+ }
+
+ <F1, F2> Counter2<F1, F2> counter2() {
+ return new Counter2<F1, F2>() {
+ @Override
+ public void incrementBy(F1 field1, F2 field2, long value) {
+ total.incrementBy(value);
+ forceCreate(field1, field2).incrementBy(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ <F1, F2, F3> Counter3<F1, F2, F3> counter3() {
+ return new Counter3<F1, F2, F3>() {
+ @Override
+ public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {
+ total.incrementBy(value);
+ forceCreate(field1, field2, field3).incrementBy(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ String name(Object key) {
+ ImmutableList<Object> keyList = (ImmutableList<Object>) key;
+ String[] parts = new String[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[i].formatter();
+
+ parts[i] = fmt.apply(keyList.get(i)).replace('/', '-');
+ }
+ return Joiner.on('/').join(parts);
+ }
+}
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
new file mode 100644
index 0000000..8f6e5d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -0,0 +1,428 @@
+// 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.metrics.dropwizard;
+
+import static com.google.common.base.Preconditions.checkArgument;
+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;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.FieldOrdering;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram0;
+import com.google.gerrit.metrics.Histogram1;
+import com.google.gerrit.metrics.Histogram2;
+import com.google.gerrit.metrics.Histogram3;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.metrics.proc.JGitMetricModule;
+import com.google.gerrit.metrics.proc.ProcMetricModule;
+import com.google.gerrit.server.cache.CacheMetrics;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Connects Gerrit metric package onto DropWizard.
+ *
+ * @see <a href="http://www.dropwizard.io/">DropWizard</a>
+ */
+@Singleton
+public class DropWizardMetricMaker extends MetricMaker {
+ public static class ApiModule extends RestApiModule {
+ @Override
+ protected void configure() {
+ bind(MetricRegistry.class).in(Scopes.SINGLETON);
+ bind(DropWizardMetricMaker.class).in(Scopes.SINGLETON);
+ bind(MetricMaker.class).to(DropWizardMetricMaker.class);
+
+ install(new ProcMetricModule());
+ install(new JGitMetricModule());
+ }
+ }
+
+ public static class RestModule extends RestApiModule {
+ @Override
+ protected void configure() {
+ DynamicMap.mapOf(binder(), METRIC_KIND);
+ child(CONFIG_KIND, "metrics").to(MetricsCollection.class);
+ get(METRIC_KIND).to(GetMetric.class);
+ bind(CacheMetrics.class);
+ }
+ }
+
+ private final MetricRegistry registry;
+ private final Map<String, BucketedMetric> bucketed;
+ private final Map<String, ImmutableMap<String, String>> descriptions;
+
+ @Inject
+ DropWizardMetricMaker(MetricRegistry registry) {
+ this.registry = registry;
+ this.bucketed = new ConcurrentHashMap<>();
+ this.descriptions = new ConcurrentHashMap<>();
+ }
+
+ Iterable<String> getMetricNames() {
+ return descriptions.keySet();
+ }
+
+ /** Get the underlying metric implementation. */
+ public Metric getMetric(String name) {
+ Metric m = bucketed.get(name);
+ return m != null ? m : registry.getMetrics().get(name);
+ }
+
+ /** Lookup annotations from a metric's {@link Description}. */
+ public ImmutableMap<String, String> getAnnotations(String name) {
+ return descriptions.get(name);
+ }
+
+ @Override
+ public synchronized Counter0 newCounter(String name, Description desc) {
+ checkCounterDescription(name, desc);
+ define(name, desc);
+ return newCounterImpl(name, desc.isRate());
+ }
+
+ @Override
+ public synchronized <F1> Counter1<F1> newCounter(
+ String name, Description desc,
+ Field<F1> field1) {
+ checkCounterDescription(name, desc);
+ CounterImpl1<F1> m = new CounterImpl1<>(this, name, desc, field1);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.counter();
+ }
+
+ @Override
+ public synchronized <F1, F2> Counter2<F1, F2> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ checkCounterDescription(name, desc);
+ CounterImplN m = new CounterImplN(this, name, desc, field1, field2);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.counter2();
+ }
+
+ @Override
+ public synchronized <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ checkCounterDescription(name, desc);
+ CounterImplN m = new CounterImplN(this, name, desc, field1, field2, field3);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.counter3();
+ }
+
+ private static void checkCounterDescription(String name, Description desc) {
+ checkMetricName(name);
+ checkArgument(!desc.isConstant(), "counters must not be constant");
+ checkArgument(!desc.isGauge(), "counters must not be gauge");
+ }
+
+ CounterImpl newCounterImpl(String name, boolean isRate) {
+ if (isRate) {
+ final com.codahale.metrics.Meter m = registry.meter(name);
+ return new CounterImpl(name, m) {
+ @Override
+ public void incrementBy(long delta) {
+ checkArgument(delta >= 0, "counter delta must be >= 0");
+ m.mark(delta);
+ }
+ };
+ } else {
+ final com.codahale.metrics.Counter m = registry.counter(name);
+ return new CounterImpl(name, m) {
+ @Override
+ public void incrementBy(long delta) {
+ checkArgument(delta >= 0, "counter delta must be >= 0");
+ m.inc(delta);
+ }
+ };
+ }
+ }
+
+ @Override
+ public synchronized Timer0 newTimer(String name, Description desc) {
+ checkTimerDescription(name, desc);
+ define(name, desc);
+ return newTimerImpl(name);
+ }
+
+ @Override
+ public synchronized <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
+ checkTimerDescription(name, desc);
+ TimerImpl1<F1> m = new TimerImpl1<>(this, name, desc, field1);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.timer();
+ }
+
+ @Override
+ public synchronized <F1, F2> Timer2<F1, F2> newTimer(String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ checkTimerDescription(name, desc);
+ TimerImplN m = new TimerImplN(this, name, desc, field1, field2);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.timer2();
+ }
+
+ @Override
+ public synchronized <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ checkTimerDescription(name, desc);
+ TimerImplN m = new TimerImplN(this, name, desc, field1, field2, field3);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.timer3();
+ }
+
+ private static void checkTimerDescription(String name, Description desc) {
+ checkMetricName(name);
+ checkArgument(!desc.isConstant(), "timer must not be constant");
+ checkArgument(!desc.isGauge(), "timer must not be a gauge");
+ checkArgument(!desc.isRate(), "timer must not be a rate");
+ checkArgument(desc.isCumulative(), "timer must be cumulative");
+ checkArgument(desc.getTimeUnit() != null, "timer must have a unit");
+ }
+
+ TimerImpl newTimerImpl(String name) {
+ return new TimerImpl(name, registry.timer(name));
+ }
+
+ @Override
+ public synchronized Histogram0 newHistogram(String name, Description desc) {
+ checkHistogramDescription(name, desc);
+ define(name, desc);
+ return newHistogramImpl(name);
+ }
+
+ @Override
+ public synchronized <F1> Histogram1<F1> newHistogram(String name,
+ Description desc, Field<F1> field1) {
+ checkHistogramDescription(name, desc);
+ HistogramImpl1<F1> m = new HistogramImpl1<>(this, name, desc, field1);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.histogram1();
+ }
+
+ @Override
+ public synchronized <F1, F2> Histogram2<F1, F2> newHistogram(String name,
+ Description desc, Field<F1> field1, Field<F2> field2) {
+ checkHistogramDescription(name, desc);
+ HistogramImplN m = new HistogramImplN(this, name, desc, field1, field2);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.histogram2();
+ }
+
+ @Override
+ public synchronized <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ checkHistogramDescription(name, desc);
+ HistogramImplN m = new HistogramImplN(this, name, desc, field1, field2, field3);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.histogram3();
+ }
+
+ private static void checkHistogramDescription(String name, Description desc) {
+ checkMetricName(name);
+ checkArgument(!desc.isConstant(), "histogram must not be constant");
+ checkArgument(!desc.isGauge(), "histogram must not be a gauge");
+ checkArgument(!desc.isRate(), "histogram must not be a rate");
+ checkArgument(desc.isCumulative(), "histogram must be cumulative");
+ }
+
+ HistogramImpl newHistogramImpl(String name) {
+ return new HistogramImpl(name, registry.histogram(name));
+ }
+
+ @Override
+ public <V> CallbackMetric0<V> newCallbackMetric(
+ String name, Class<V> valueClass, Description desc) {
+ checkMetricName(name);
+ define(name, desc);
+ return new CallbackMetricImpl0<>(this, registry, name, valueClass);
+ }
+
+ @Override
+ public <F1, V> CallbackMetric1<F1, V> newCallbackMetric(
+ String name, Class<V> valueClass, Description desc, Field<F1> field1) {
+ checkMetricName(name);
+ CallbackMetricImpl1<F1, V> m = new CallbackMetricImpl1<>(this, registry,
+ name, valueClass, desc, field1);
+ define(name, desc);
+ bucketed.put(name, m);
+ return m.create();
+ }
+
+ @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;
+ }
+ })
+ .toSet();
+
+ trigger = new CallbackGroup(trigger, all);
+ for (CallbackMetricGlue m : all) {
+ m.register(trigger);
+ }
+ trigger.run();
+
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ for (CallbackMetricGlue m : all) {
+ m.remove();
+ }
+ }
+ };
+ }
+
+ synchronized void remove(String name) {
+ bucketed.remove(name);
+ descriptions.remove(name);
+ }
+
+ private synchronized void define(String name, Description desc) {
+ if (descriptions.containsKey(name)) {
+ throw new IllegalStateException(String.format(
+ "metric %s already defined", name));
+ }
+ descriptions.put(name, desc.getAnnotations());
+ }
+
+ private static final Pattern METRIC_NAME_PATTERN = Pattern
+ .compile("[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*");
+
+ private static void checkMetricName(String name) {
+ checkArgument(
+ METRIC_NAME_PATTERN.matcher(name).matches(),
+ "metric name must match %s", METRIC_NAME_PATTERN.pattern());
+ }
+
+ static String name(Description.FieldOrdering ordering,
+ String codeName,
+ String fieldValues) {
+ if (ordering == FieldOrdering.PREFIX_FIELDS_BASENAME) {
+ int s = codeName.lastIndexOf('/');
+ if (s > 0) {
+ String prefix = codeName.substring(0, s);
+ String metric = codeName.substring(s + 1);
+ return prefix + '/' + fieldValues + '/' + metric;
+ }
+ }
+ return codeName + '/' + fieldValues;
+ }
+
+ abstract class CounterImpl extends Counter0 {
+ private final String name;
+ final Metric metric;
+
+ CounterImpl(String name, Metric metric) {
+ this.name = name;
+ this.metric = metric;
+ }
+
+ @Override
+ public void remove() {
+ descriptions.remove(name);
+ registry.remove(name);
+ }
+ }
+
+ class TimerImpl extends Timer0 {
+ private final String name;
+ final com.codahale.metrics.Timer metric;
+
+ private TimerImpl(String name, com.codahale.metrics.Timer metric) {
+ this.name = name;
+ this.metric = metric;
+ }
+
+ @Override
+ public void record(long value, TimeUnit unit) {
+ checkArgument(value >= 0, "timer delta must be >= 0");
+ metric.update(value, unit);
+ }
+
+ @Override
+ public void remove() {
+ descriptions.remove(name);
+ registry.remove(name);
+ }
+ }
+
+ class HistogramImpl extends Histogram0 {
+ private final String name;
+ final com.codahale.metrics.Histogram metric;
+
+ private HistogramImpl(String name, com.codahale.metrics.Histogram metric) {
+ this.name = name;
+ this.metric = metric;
+ }
+
+ @Override
+ public void record(long value) {
+ metric.update(value);
+ }
+
+ @Override
+ public void remove() {
+ descriptions.remove(name);
+ registry.remove(name);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
new file mode 100644
index 0000000..47064df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
@@ -0,0 +1,47 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+class GetMetric implements RestReadView<MetricResource> {
+ private final CurrentUser user;
+ private final DropWizardMetricMaker metrics;
+
+ @Option(name = "--data-only", usage = "return only values")
+ boolean dataOnly;
+
+ @Inject
+ GetMetric(CurrentUser user, DropWizardMetricMaker metrics) {
+ this.user = user;
+ this.metrics = metrics;
+ }
+
+ @Override
+ public MetricJson apply(MetricResource resource) throws AuthException {
+ if (!user.getCapabilities().canViewCaches()) {
+ throw new AuthException("restricted to viewCaches");
+ }
+ return new MetricJson(
+ resource.getMetric(),
+ metrics.getAnnotations(resource.getName()),
+ dataOnly);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
new file mode 100644
index 0000000..e3f9e1c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
@@ -0,0 +1,52 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram1;
+
+/** Optimized version of {@link BucketedHistogram} for single dimension. */
+class HistogramImpl1<F1> extends BucketedHistogram implements BucketedMetric {
+ HistogramImpl1(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<F1> field1) {
+ super(metrics, name, desc, field1);
+ }
+
+ Histogram1<F1> histogram1() {
+ return new Histogram1<F1>() {
+ @Override
+ public void record(F1 field1, long value) {
+ total.record(value);
+ forceCreate(field1).record(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @Override
+ String name(Object field1) {
+ @SuppressWarnings("unchecked")
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[0].formatter();
+
+ return fmt.apply(field1).replace('/', '-');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
new file mode 100644
index 0000000..d832c60
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
@@ -0,0 +1,75 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram2;
+import com.google.gerrit.metrics.Histogram3;
+
+/** Generalized implementation of N-dimensional Histogram metrics. */
+class HistogramImplN extends BucketedHistogram implements BucketedMetric {
+ HistogramImplN(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<?>... fields) {
+ super(metrics, name, desc, fields);
+ }
+
+ <F1, F2> Histogram2<F1, F2> histogram2() {
+ return new Histogram2<F1, F2>() {
+ @Override
+ public void record(F1 field1, F2 field2, long value) {
+ total.record(value);
+ forceCreate(field1, field2).record(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ <F1, F2, F3> Histogram3<F1, F2, F3> histogram3() {
+ return new Histogram3<F1, F2, F3>() {
+ @Override
+ public void record(F1 field1, F2 field2, F3 field3, long value) {
+ total.record(value);
+ forceCreate(field1, field2, field3).record(value);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ String name(Object key) {
+ ImmutableList<Object> keyList = (ImmutableList<Object>) key;
+ String[] parts = new String[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[i].formatter();
+
+ parts[i] = fmt.apply(keyList.get(i)).replace('/', '-');
+ }
+ return Joiner.on('/').join(parts);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
new file mode 100644
index 0000000..04d10a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -0,0 +1,96 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+
+import com.codahale.metrics.Metric;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+class ListMetrics implements RestReadView<ConfigResource> {
+ private final CurrentUser user;
+ private final DropWizardMetricMaker metrics;
+
+ @Option(name = "--data-only", usage = "return only values")
+ boolean dataOnly;
+
+ @Option(name = "--prefix", aliases = {"-p"}, metaVar = "PREFIX",
+ usage = "match metric by exact match or prefix")
+ List<String> query = new ArrayList<>();
+
+ @Inject
+ ListMetrics(CurrentUser user, DropWizardMetricMaker metrics) {
+ this.user = user;
+ this.metrics = metrics;
+ }
+
+ @Override
+ public Map<String, MetricJson> apply(ConfigResource resource)
+ throws AuthException {
+ if (!user.getCapabilities().canViewCaches()) {
+ throw new AuthException("restricted to viewCaches");
+ }
+
+ SortedMap<String, MetricJson> out = new TreeMap<>();
+ List<String> prefixes = new ArrayList<>(query.size());
+ for (String q : query) {
+ if (q.endsWith("/")) {
+ prefixes.add(q);
+ } else {
+ Metric m = metrics.getMetric(q);
+ if (m != null) {
+ out.put(q, toJson(q, m));
+ }
+ }
+ }
+
+ if (query.isEmpty() || !prefixes.isEmpty()) {
+ for (String name : metrics.getMetricNames()) {
+ if (include(prefixes, name)) {
+ out.put(name, toJson(name, metrics.getMetric(name)));
+ }
+ }
+ }
+
+ return out;
+ }
+
+ private MetricJson toJson(String q, Metric m) {
+ return new MetricJson(m, metrics.getAnnotations(q), dataOnly);
+ }
+
+ private static boolean include(List<String> prefixes, String name) {
+ if (prefixes.isEmpty()) {
+ return true;
+ }
+ for (String p : prefixes) {
+ if (name.startsWith(p)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
new file mode 100644
index 0000000..b332262
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -0,0 +1,210 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+class MetricJson {
+ String description;
+ String unit;
+ Boolean constant;
+ Boolean rate;
+ Boolean gauge;
+ Boolean cumulative;
+
+ Long count;
+ Object value;
+
+ Double rate_1m;
+ Double rate_5m;
+ Double rate_15m;
+ Double rate_mean;
+
+ Double p50;
+ Double p75;
+ Double p95;
+ Double p98;
+ Double p99;
+ Double p99_9;
+
+ Double min;
+ Double avg;
+ Double max;
+ Double sum;
+ Double std_dev;
+
+ List<FieldJson> fields;
+ Map<String, Object> buckets;
+
+ MetricJson(Metric metric, ImmutableMap<String, String> atts, boolean dataOnly) {
+ if (!dataOnly) {
+ description = atts.get(Description.DESCRIPTION);
+ unit = atts.get(Description.UNIT);
+ constant = toBool(atts, Description.CONSTANT);
+ rate = toBool(atts, Description.RATE);
+ gauge = toBool(atts, Description.GAUGE);
+ cumulative = toBool(atts, Description.CUMULATIVE);
+ }
+ init(metric, atts);
+ }
+
+ private void init(Metric metric, ImmutableMap<String, String> atts) {
+ if (metric instanceof BucketedMetric) {
+ BucketedMetric m = (BucketedMetric) metric;
+ if (m.getTotal() != null) {
+ init(m.getTotal(), atts);
+ }
+
+ Field<?>[] fieldList = m.getFields();
+ fields = new ArrayList<>(fieldList.length);
+ for (Field<?> f : fieldList) {
+ fields.add(new FieldJson(f));
+ }
+ buckets = makeBuckets(fieldList, m.getCells(), atts);
+
+ } else if (metric instanceof Counter) {
+ Counter c = (Counter) metric;
+ count = c.getCount();
+
+ } else if (metric instanceof Gauge) {
+ Gauge<?> g = (Gauge<?>) metric;
+ value = g.getValue();
+
+ } else if (metric instanceof Meter) {
+ Meter m = (Meter) metric;
+ count = m.getCount();
+ rate_1m = m.getOneMinuteRate();
+ rate_5m = m.getFiveMinuteRate();
+ rate_15m = m.getFifteenMinuteRate();
+
+ } else if (metric instanceof Timer) {
+ Timer m = (Timer) metric;
+ Snapshot s = m.getSnapshot();
+ count = m.getCount();
+ rate_1m = m.getOneMinuteRate();
+ rate_5m = m.getFiveMinuteRate();
+ rate_15m = m.getFifteenMinuteRate();
+
+ double div =
+ Description.getTimeUnit(atts.get(Description.UNIT)).toNanos(1);
+ p50 = s.getMedian() / div;
+ p75 = s.get75thPercentile() / div;
+ p95 = s.get95thPercentile() / div;
+ p98 = s.get98thPercentile() / div;
+ p99 = s.get99thPercentile() / div;
+ p99_9 = s.get999thPercentile() / div;
+
+ min = s.getMin() / div;
+ max = s.getMax() / div;
+ std_dev = s.getStdDev() / div;
+
+ } else if (metric instanceof Histogram) {
+ Histogram m = (Histogram) metric;
+ Snapshot s = m.getSnapshot();
+ count = m.getCount();
+
+ p50 = s.getMedian();
+ p75 = s.get75thPercentile();
+ p95 = s.get95thPercentile();
+ p98 = s.get98thPercentile();
+ p99 = s.get99thPercentile();
+ p99_9 = s.get999thPercentile();
+
+ min = (double) s.getMin();
+ avg = (double) s.getMean();
+ max = (double) s.getMax();
+ sum = s.getMean() * m.getCount();
+ std_dev = s.getStdDev();
+ }
+ }
+
+ private static Boolean toBool(ImmutableMap<String, String> atts, String key) {
+ return Description.TRUE_VALUE.equals(atts.get(key)) ? true : null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> makeBuckets(
+ Field<?>[] fields,
+ Map<?, Metric> metrics,
+ ImmutableMap<String, String> atts) {
+ if (fields.length == 1) {
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[0].formatter();
+ Map<String, Object> out = new TreeMap<>();
+ for (Map.Entry<?, Metric> e : metrics.entrySet()) {
+ out.put(
+ fmt.apply(e.getKey()),
+ new MetricJson(e.getValue(), atts, true));
+ }
+ return out;
+ }
+
+ Map<String, Object> out = new TreeMap<>();
+ for (Map.Entry<?, Metric> e : metrics.entrySet()) {
+ ImmutableList<Object> keys = (ImmutableList<Object>) e.getKey();
+ Map<String, Object> dst = out;
+
+ for (int i = 0; i < fields.length - 1; i++) {
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[i].formatter();
+ String key = fmt.apply(keys.get(i));
+ Map<String, Object> t = (Map<String, Object>) dst.get(key);
+ if (t == null) {
+ t = new TreeMap<>();
+ dst.put(key, t);
+ }
+ dst = t;
+ }
+
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[fields.length - 1].formatter();
+ dst.put(
+ fmt.apply(keys.get(fields.length - 1)),
+ new MetricJson(e.getValue(), atts, true));
+ }
+ return out;
+ }
+
+ static class FieldJson {
+ String name;
+ String type;
+ String description;
+
+ FieldJson(Field<?> field) {
+ this.name = field.getName();
+ this.description = field.getDescription();
+ this.type = Enum.class.isAssignableFrom(field.getType())
+ ? field.getType().getSimpleName()
+ : null;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
new file mode 100644
index 0000000..d073f37
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
@@ -0,0 +1,42 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.TypeLiteral;
+
+import com.codahale.metrics.Metric;
+
+class MetricResource extends ConfigResource {
+ static final TypeLiteral<RestView<MetricResource>> METRIC_KIND =
+ new TypeLiteral<RestView<MetricResource>>() {};
+
+ private final String name;
+ private final Metric metric;
+
+ MetricResource(String name, Metric metric) {
+ this.name = name;
+ this.metric = metric;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ Metric getMetric() {
+ return metric;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
new file mode 100644
index 0000000..81945f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
@@ -0,0 +1,72 @@
+// 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.metrics.dropwizard;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.codahale.metrics.Metric;
+
+@Singleton
+class MetricsCollection implements
+ ChildCollection<ConfigResource, MetricResource> {
+ private final DynamicMap<RestView<MetricResource>> views;
+ private final Provider<ListMetrics> list;
+ private final Provider<CurrentUser> user;
+ private final DropWizardMetricMaker metrics;
+
+ @Inject
+ MetricsCollection(DynamicMap<RestView<MetricResource>> views,
+ Provider<ListMetrics> list, Provider<CurrentUser> user,
+ DropWizardMetricMaker metrics) {
+ this.views = views;
+ this.list = list;
+ this.user = user;
+ this.metrics = metrics;
+ }
+
+ @Override
+ public DynamicMap<RestView<MetricResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<ConfigResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public MetricResource parse(ConfigResource parent, IdString id)
+ throws ResourceNotFoundException, AuthException {
+ if (!user.get().getCapabilities().canViewCaches()) {
+ throw new AuthException("restricted to viewCaches");
+ }
+
+ Metric metric = metrics.getMetric(id.get());
+ if (metric == null) {
+ throw new ResourceNotFoundException(id.get());
+ }
+ return new MetricResource(id.get(), metric);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
new file mode 100644
index 0000000..0164f6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Timer1;
+
+import java.util.concurrent.TimeUnit;
+
+/** Optimized version of {@link BucketedTimer} for single dimension. */
+class TimerImpl1<F1> extends BucketedTimer implements BucketedMetric {
+ TimerImpl1(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<F1> field1) {
+ super(metrics, name, desc, field1);
+ }
+
+ Timer1<F1> timer() {
+ return new Timer1<F1>() {
+ @Override
+ public void record(F1 field1, long value, TimeUnit unit) {
+ total.record(value, unit);
+ forceCreate(field1).record(value, unit);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @Override
+ String name(Object field1) {
+ @SuppressWarnings("unchecked")
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[0].formatter();
+
+ return fmt.apply(field1).replace('/', '-');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
new file mode 100644
index 0000000..49c9f14
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
@@ -0,0 +1,78 @@
+// 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.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+
+import java.util.concurrent.TimeUnit;
+
+/** Generalized implementation of N-dimensional timer metrics. */
+class TimerImplN extends BucketedTimer implements BucketedMetric {
+ TimerImplN(DropWizardMetricMaker metrics, String name,
+ Description desc, Field<?>... fields) {
+ super(metrics, name, desc, fields);
+ }
+
+ <F1, F2> Timer2<F1, F2> timer2() {
+ return new Timer2<F1, F2>() {
+ @Override
+ public void record(F1 field1, F2 field2, long value, TimeUnit unit) {
+ total.record(value, unit);
+ forceCreate(field1, field2).record(value, unit);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ <F1, F2, F3> Timer3<F1, F2, F3> timer3() {
+ return new Timer3<F1, F2, F3>() {
+ @Override
+ public void record(F1 field1, F2 field2, F3 field3,
+ long value, TimeUnit unit) {
+ total.record(value, unit);
+ forceCreate(field1, field2, field3).record(value, unit);
+ }
+
+ @Override
+ public void remove() {
+ doRemove();
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ String name(Object key) {
+ ImmutableList<Object> keyList = (ImmutableList<Object>) key;
+ String[] parts = new String[fields.length];
+ for (int i = 0; i < fields.length; i++) {
+ Function<Object, String> fmt =
+ (Function<Object, String>) fields[i].formatter();
+
+ parts[i] = fmt.apply(keyList.get(i)).replace('/', '-');
+ }
+ return Joiner.on('/').join(parts);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/JGitMetricModule.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
new file mode 100644
index 0000000..b5a2fcc8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/JGitMetricModule.java
@@ -0,0 +1,53 @@
+// 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.metrics.proc;
+
+import com.google.common.base.Supplier;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+
+import org.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
+
+public class JGitMetricModule extends MetricModule {
+ @Override
+ protected void configure(MetricMaker metrics) {
+ metrics.newCallbackMetric(
+ "jgit/block_cache/cache_used",
+ Long.class,
+ new Description("Bytes of memory retained in JGit block cache.")
+ .setGauge()
+ .setUnit(Units.BYTES),
+ new Supplier<Long>() {
+ @Override
+ public Long get() {
+ return WindowCacheStatAccessor.getOpenBytes();
+ }
+ });
+
+ metrics.newCallbackMetric(
+ "jgit/block_cache/open_files",
+ Integer.class,
+ new Description("File handles held open by JGit block cache.")
+ .setGauge()
+ .setUnit("fds"),
+ new Supplier<Integer>() {
+ @Override
+ public Integer get() {
+ return WindowCacheStatAccessor.getOpenFiles();
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/MetricModule.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/MetricModule.java
new file mode 100644
index 0000000..c556ee4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/MetricModule.java
@@ -0,0 +1,43 @@
+// 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.metrics.proc;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+
+/** Guice module to configure metrics on server startup. */
+public abstract class MetricModule extends LifecycleModule {
+ /** Configure metrics during server startup. */
+ protected abstract void configure(MetricMaker metrics);
+
+ @Override
+ protected void configure() {
+ listener().toInstance(new LifecycleListener() {
+ @Inject
+ MetricMaker metrics;
+
+ @Override
+ public void start() {
+ configure(metrics);
+ }
+
+ @Override
+ public void stop() {
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
new file mode 100644
index 0000000..53b860c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -0,0 +1,226 @@
+// 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.metrics.proc;
+
+import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.CallbackMetric1;
+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.sun.management.OperatingSystemMXBean;
+import com.sun.management.UnixOperatingSystemMXBean;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryUsage;
+import java.lang.management.ThreadMXBean;
+import java.util.concurrent.TimeUnit;
+
+@SuppressWarnings("restriction")
+public class ProcMetricModule extends MetricModule {
+ @Override
+ protected void configure(MetricMaker metrics) {
+ buildLabel(metrics);
+ procUptime(metrics);
+ procCpuUsage(metrics);
+ procJvmGc(metrics);
+ procJvmMemory(metrics);
+ procJvmThread(metrics);
+ }
+
+ private void buildLabel(MetricMaker metrics) {
+ metrics.newConstantMetric(
+ "build/label",
+ Strings.nullToEmpty(Version.getVersion()),
+ new Description("Version of Gerrit server software"));
+ }
+
+ private void procUptime(MetricMaker metrics) {
+ metrics.newConstantMetric(
+ "proc/birth_timestamp",
+ Long.valueOf(TimeUnit.MILLISECONDS.toMicros(
+ System.currentTimeMillis())),
+ new Description("Time at which the process started")
+ .setUnit(Units.MICROSECONDS));
+
+ metrics.newCallbackMetric(
+ "proc/uptime",
+ Long.class,
+ new Description("Uptime of this process")
+ .setUnit(Units.MILLISECONDS),
+ new Supplier<Long>() {
+ @Override
+ public Long get() {
+ return ManagementFactory.getRuntimeMXBean().getUptime();
+ }
+ });
+ }
+
+ private void procCpuUsage(MetricMaker metrics) {
+ final OperatingSystemMXBean sys =
+ (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
+ if (sys.getProcessCpuTime() != -1) {
+ metrics.newCallbackMetric(
+ "proc/cpu/usage",
+ Double.class,
+ new Description("CPU time used by the process")
+ .setCumulative()
+ .setUnit(Units.SECONDS),
+ new Supplier<Double>() {
+ @Override
+ public Double get() {
+ return sys.getProcessCpuTime() / 1e9;
+ }
+ });
+ }
+ if (sys instanceof UnixOperatingSystemMXBean) {
+ final UnixOperatingSystemMXBean unix = (UnixOperatingSystemMXBean) sys;
+ if (unix.getOpenFileDescriptorCount() != -1) {
+ metrics.newCallbackMetric(
+ "proc/num_open_fds",
+ Long.class,
+ new Description("Number of open file descriptors")
+ .setGauge()
+ .setUnit("fds"),
+ new Supplier<Long>() {
+ @Override
+ public Long get() {
+ return unix.getOpenFileDescriptorCount();
+ }
+ });
+ }
+ }
+ }
+
+ private void procJvmMemory(MetricMaker metrics) {
+ final CallbackMetric0<Long> heapCommitted = metrics.newCallbackMetric(
+ "proc/jvm/memory/heap_committed",
+ Long.class,
+ new Description("Amount of memory guaranteed for user objects.")
+ .setGauge()
+ .setUnit(Units.BYTES));
+
+ final CallbackMetric0<Long> heapUsed = metrics.newCallbackMetric(
+ "proc/jvm/memory/heap_used",
+ Long.class,
+ new Description("Amount of memory holding user objects.")
+ .setGauge()
+ .setUnit(Units.BYTES));
+
+ final CallbackMetric0<Long> nonHeapCommitted = metrics.newCallbackMetric(
+ "proc/jvm/memory/non_heap_committed",
+ Long.class,
+ new Description("Amount of memory guaranteed for classes, etc.")
+ .setGauge()
+ .setUnit(Units.BYTES));
+
+ final CallbackMetric0<Long> nonHeapUsed = metrics.newCallbackMetric(
+ "proc/jvm/memory/non_heap_used",
+ Long.class,
+ new Description("Amount of memory holding classes, etc.")
+ .setGauge()
+ .setUnit(Units.BYTES));
+
+ final CallbackMetric0<Integer> objectPendingFinalizationCount =
+ metrics.newCallbackMetric(
+ "proc/jvm/memory/object_pending_finalization_count",
+ Integer.class,
+ new Description("Approximate number of objects needing finalization.")
+ .setGauge()
+ .setUnit("objects"));
+
+ final MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
+ metrics.newTrigger(
+ ImmutableSet.<CallbackMetric<?>> of(
+ heapCommitted, heapUsed, nonHeapCommitted,
+ nonHeapUsed, objectPendingFinalizationCount),
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ MemoryUsage stats = memory.getHeapMemoryUsage();
+ heapCommitted.set(stats.getCommitted());
+ heapUsed.set(stats.getUsed());
+ } catch (IllegalArgumentException e) {
+ // MXBean may throw due to a bug in Java 7; ignore.
+ }
+
+ MemoryUsage stats = memory.getNonHeapMemoryUsage();
+ nonHeapCommitted.set(stats.getCommitted());
+ nonHeapUsed.set(stats.getUsed());
+
+ objectPendingFinalizationCount.set(
+ memory.getObjectPendingFinalizationCount());
+ }
+ });
+ }
+
+ private void procJvmGc(MetricMaker metrics) {
+ final CallbackMetric1<String, Long> gcCount = metrics.newCallbackMetric(
+ "proc/jvm/gc/count",
+ Long.class,
+ new Description("Number of GCs").setCumulative(),
+ Field.ofString("gc_name", "The name of the garbage collector"));
+
+ final CallbackMetric1<String, Long> gcTime = metrics.newCallbackMetric(
+ "proc/jvm/gc/time",
+ Long.class,
+ new Description("Approximate accumulated GC elapsed time")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ Field.ofString("gc_name", "The name of the garbage collector"));
+
+ metrics.newTrigger(gcCount, gcTime, new Runnable() {
+ @Override
+ public void run() {
+ for (GarbageCollectorMXBean gc : ManagementFactory
+ .getGarbageCollectorMXBeans()) {
+ long count = gc.getCollectionCount();
+ if (count != -1) {
+ gcCount.set(gc.getName(), count);
+ }
+ long time = gc.getCollectionTime();
+ if (time != -1) {
+ gcTime.set(gc.getName(), time);
+ }
+ }
+ }
+ });
+ }
+
+ private void procJvmThread(MetricMaker metrics) {
+ final ThreadMXBean thread = ManagementFactory.getThreadMXBean();
+ metrics.newCallbackMetric(
+ "proc/jvm/thread/num_live",
+ Integer.class,
+ new Description("Current live thread count")
+ .setGauge()
+ .setUnit("threads"),
+ new Supplier<Integer>() {
+ @Override
+ public Integer get() {
+ return thread.getThreadCount();
+ }
+ });
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java
index 4bd9423..9d32e38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java
@@ -30,5 +30,5 @@
@ExtensionPoint
public interface PredicateProvider {
/** Return set of packages that contain Prolog predicates */
- public ImmutableSet<String> getPackages();
+ ImmutableSet<String> getPackages();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index 5dd26f9..f6df335a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -75,20 +75,9 @@
*/
@Singleton
public class RulesCache {
- /** Maximum size of a dynamic Prolog script, in bytes. */
- private static final int SRC_LIMIT = 128 * 1024;
-
- /** Default size of the internal Prolog database within each interpreter. */
- private static final int DB_MAX = 256;
-
private static final List<String> PACKAGE_LIST = ImmutableList.of(
Prolog.BUILTIN, "gerrit");
- private final Map<ObjectId, MachineRef> machineCache = new HashMap<>();
-
- private final ReferenceQueue<PrologMachineCopy> dead =
- new ReferenceQueue<>();
-
private static final class MachineRef extends WeakReference<PrologMachineCopy> {
final ObjectId key;
@@ -100,17 +89,25 @@
}
private final boolean enableProjectRules;
+ private final int maxDbSize;
+ private final int maxSrcBytes;
private final Path cacheDir;
private final Path rulesDir;
private final GitRepositoryManager gitMgr;
private final DynamicSet<PredicateProvider> predicateProviders;
private final ClassLoader systemLoader;
private final PrologMachineCopy defaultMachine;
+ private final Map<ObjectId, MachineRef> machineCache = new HashMap<>();
+ private final ReferenceQueue<PrologMachineCopy> dead =
+ new ReferenceQueue<>();
@Inject
protected RulesCache(@GerritServerConfig Config config, SitePaths site,
GitRepositoryManager gm, DynamicSet<PredicateProvider> predicateProviders) {
- enableProjectRules = config.getBoolean("rules", null, "enable", true);
+ maxDbSize = config.getInt("rules", null, "maxPrologDatabaseSize", 256);
+ maxSrcBytes = config.getInt("rules", null, "maxSourceBytes", 128 << 10);
+ enableProjectRules = config.getBoolean("rules", null, "enable", true)
+ && maxSrcBytes > 0;
cacheDir = site.resolve(config.getString("cache", null, "directory"));
rulesDir = cacheDir != null ? cacheDir.resolve("rules") : null;
gitMgr = gm;
@@ -267,7 +264,7 @@
try (Repository git = gitMgr.openRepository(project)) {
try {
ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
- byte[] raw = ldr.getCachedBytes(SRC_LIMIT);
+ byte[] raw = ldr.getCachedBytes(maxSrcBytes);
return RawParseUtils.decode(raw);
} catch (LargeObjectException e) {
throw new CompileException("rules of " + project + " are too large", e);
@@ -281,7 +278,7 @@
private BufferingPrologControl newEmptyMachine(ClassLoader cl) {
BufferingPrologControl ctl = new BufferingPrologControl();
- ctl.setMaxDatabaseSize(DB_MAX);
+ ctl.setMaxDatabaseSize(maxDbSize);
ctl.setPrologClassLoader(new PrologClassLoader(new PredicateClassLoader(
predicateProviders, cl)));
ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
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 31058bc..7551d5f 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
@@ -15,6 +15,8 @@
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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
@@ -44,7 +46,7 @@
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.notedb.ReviewerState;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -114,10 +116,10 @@
* @param notes change notes.
* @return multimap of reviewers keyed by state, where each account appears
* exactly once in {@link SetMultimap#values()}, and
- * {@link ReviewerState#REMOVED} is not present.
+ * {@link ReviewerStateInternal#REMOVED} is not present.
* @throws OrmException if reviewers for the change could not be read.
*/
- public ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers(
+ public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
ReviewDb db, ChangeNotes notes) throws OrmException {
if (!migration.readChanges()) {
return getReviewers(db.patchSetApprovals().byChange(notes.getChangeId()));
@@ -132,9 +134,9 @@
* change.
* @return multimap of reviewers keyed by state, where each account appears
* exactly once in {@link SetMultimap#values()}, and
- * {@link ReviewerState#REMOVED} is not present.
+ * {@link ReviewerStateInternal#REMOVED} is not present.
*/
- public ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers(
+ public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
throws OrmException {
if (!migration.readChanges()) {
@@ -143,10 +145,10 @@
return notes.load().getReviewers();
}
- private static ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers(
+ private static ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers(
Iterable<PatchSetApproval> allApprovals) {
PatchSetApproval first = null;
- SetMultimap<ReviewerState, Account.Id> reviewers =
+ SetMultimap<ReviewerStateInternal, Account.Id> reviewers =
LinkedHashMultimap.create();
for (PatchSetApproval psa : allApprovals) {
if (first == null) {
@@ -159,10 +161,10 @@
}
Account.Id id = psa.getAccountId();
if (psa.getValue() != 0) {
- reviewers.put(ReviewerState.REVIEWER, id);
- reviewers.remove(ReviewerState.CC, id);
- } else if (!reviewers.containsEntry(ReviewerState.REVIEWER, id)) {
- reviewers.put(ReviewerState.CC, id);
+ reviewers.put(REVIEWER, id);
+ reviewers.remove(CC, id);
+ } else if (!reviewers.containsEntry(REVIEWER, id)) {
+ reviewers.put(CC, id);
}
}
return ImmutableSetMultimap.copyOf(reviewers);
@@ -215,7 +217,7 @@
cells.add(new PatchSetApproval(
new PatchSetApproval.Key(psId, account, labelId),
(short) 0, TimeUtil.nowTs()));
- update.putReviewer(account, ReviewerState.REVIEWER);
+ update.putReviewer(account, REVIEWER);
}
db.patchSetApprovals().insert(cells);
return Collections.unmodifiableList(cells);
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 f31a65b..94973f2 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
@@ -54,7 +54,7 @@
return
sortChangeMessages(db.changeMessages().byChange(notes.getChangeId()));
} else {
- return sortChangeMessages(notes.load().getChangeMessages().values());
+ return notes.load().getChangeMessages();
}
}
@@ -63,7 +63,7 @@
if (!migration.readChanges()) {
return db.changeMessages().byPatchSet(psId);
}
- return notes.load().getChangeMessages().get(psId);
+ return notes.load().getChangeMessagesByPatchSet().get(psId);
}
public void addChangeMessage(ReviewDb db, ChangeUpdate update,
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 6c80d26..8cb0c9c 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,15 +14,14 @@
package com.google.gerrit.server;
-import static com.google.gerrit.server.query.change.ChangeData.asChanges;
-
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
+import com.google.common.primitives.Ints;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -32,17 +31,16 @@
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeTriplet;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.BatchUpdate;
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.index.ChangeIndexer;
import com.google.gerrit.server.mail.RevertedSender;
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.RefControl;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gwtorm.server.OrmConcurrencyException;
@@ -54,25 +52,22 @@
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.NullProgressMonitor;
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.RefDatabase;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.ChangeIdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.MessageFormat;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -177,38 +172,35 @@
return subject;
}
- private final Provider<IdentifiedUser> user;
+ private final Provider<CurrentUser> user;
private final Provider<ReviewDb> db;
private final Provider<InternalChangeQuery> queryProvider;
+ private final ChangeControl.GenericFactory changeControlFactory;
private final RevertedSender.Factory revertedSenderFactory;
private final ChangeInserter.Factory changeInserterFactory;
private final GitRepositoryManager gitManager;
- private final GitReferenceUpdated gitRefUpdated;
- private final ChangeIndexer indexer;
private final BatchUpdate.Factory updateFactory;
private final ChangeMessagesUtil changeMessagesUtil;
private final ChangeUpdate.Factory changeUpdateFactory;
@Inject
- ChangeUtil(Provider<IdentifiedUser> user,
+ ChangeUtil(Provider<CurrentUser> user,
Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider,
+ ChangeControl.GenericFactory changeControlFactory,
RevertedSender.Factory revertedSenderFactory,
ChangeInserter.Factory changeInserterFactory,
GitRepositoryManager gitManager,
- GitReferenceUpdated gitRefUpdated,
- ChangeIndexer indexer,
BatchUpdate.Factory updateFactory,
ChangeMessagesUtil changeMessagesUtil,
ChangeUpdate.Factory changeUpdateFactory) {
this.user = user;
this.db = db;
this.queryProvider = queryProvider;
+ this.changeControlFactory = changeControlFactory;
this.revertedSenderFactory = revertedSenderFactory;
this.changeInserterFactory = changeInserterFactory;
this.gitManager = gitManager;
- this.gitRefUpdated = gitRefUpdated;
- this.indexer = indexer;
this.updateFactory = updateFactory;
this.changeMessagesUtil = changeMessagesUtil;
this.changeUpdateFactory = changeUpdateFactory;
@@ -232,7 +224,7 @@
RevCommit commitToRevert =
revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
- PersonIdent authorIdent = user.get()
+ PersonIdent authorIdent = user.get().asIdentifiedUser()
.newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
if (commitToRevert.getParentCount() == 0) {
@@ -338,137 +330,59 @@
}
}
- public void deleteDraftChange(Change change)
- throws NoSuchChangeException, OrmException, IOException {
- Change.Id changeId = change.getId();
- if (change.getStatus() != Change.Status.DRAFT) {
- throw new NoSuchChangeException(changeId);
- }
-
- ReviewDb db = this.db.get();
- db.changes().beginTransaction(change.getId());
- try {
- List<PatchSet> patchSets = db.patchSets().byChange(changeId).toList();
- for (PatchSet ps : patchSets) {
- if (!ps.isDraft()) {
- throw new NoSuchChangeException(changeId);
- }
- db.accountPatchReviews().delete(
- db.accountPatchReviews().byPatchSet(ps.getId()));
- }
-
- // No need to delete from notedb; draft patch sets will be filtered out.
- db.patchComments().delete(db.patchComments().byChange(changeId));
-
- db.patchSetApprovals().delete(db.patchSetApprovals().byChange(changeId));
- db.patchSets().delete(patchSets);
- db.changeMessages().delete(db.changeMessages().byChange(changeId));
- db.starredChanges().delete(db.starredChanges().byChange(changeId));
- db.changes().delete(Collections.singleton(change));
-
- // Delete all refs at once.
- try (Repository repo = gitManager.openRepository(change.getProject());
- RevWalk rw = new RevWalk(repo)) {
- String prefix = new PatchSet.Id(changeId, 1).toRefName();
- prefix = prefix.substring(0, prefix.length() - 1);
- BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
- for (Ref ref : repo.getRefDatabase().getRefs(prefix).values()) {
- ru.addCommand(
- new ReceiveCommand(
- ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
- }
- ru.execute(rw, NullProgressMonitor.INSTANCE);
- for (ReceiveCommand cmd : ru.getCommands()) {
- if (cmd.getResult() != ReceiveCommand.Result.OK) {
- throw new IOException("failed: " + cmd + ": " + cmd.getResult());
- }
- }
- }
-
- db.commit();
- indexer.delete(change.getId());
- } finally {
- db.rollback();
- }
- }
-
- public void deleteOnlyDraftPatchSet(PatchSet patch, Change change)
- throws NoSuchChangeException, OrmException, IOException {
- PatchSet.Id patchSetId = patch.getId();
- if (!patch.isDraft()) {
- throw new NoSuchChangeException(patchSetId.getParentKey());
- }
-
- try (Repository repo = gitManager.openRepository(change.getProject())) {
- RefUpdate update = repo.updateRef(patch.getRefName());
- update.setForceUpdate(true);
- update.disableRefLog();
- switch (update.delete()) {
- case NEW:
- case FAST_FORWARD:
- case FORCED:
- case NO_CHANGE:
- // Successful deletion.
- break;
- default:
- throw new IOException("Failed to delete ref " + patch.getRefName() +
- " in " + repo.getDirectory() + ": " + update.getResult());
- }
- gitRefUpdated.fire(change.getProject(), update, ReceiveCommand.Type.DELETE);
- }
-
- deleteOnlyDraftPatchSetPreserveRef(this.db.get(), patch);
- }
-
/**
* Find changes matching the given identifier.
*
* @param id change identifier, either a numeric ID, a Change-Id, or
* project~branch~id triplet.
- * @return all matching changes, even if they are not visible to the current
- * user.
+ * @param user user to wrap in controls.
+ * @return possibly-empty list of controls for all matching changes,
+ * corresponding to the given user; may or may not be visible.
+ * @throws OrmException if an error occurred querying the database.
*/
- public List<Change> findChanges(String id)
- throws OrmException, ResourceNotFoundException {
+ public List<ChangeControl> findChanges(String id, CurrentUser user)
+ throws OrmException {
// Try legacy id
- if (id.matches("^[1-9][0-9]*$")) {
- Change c = db.get().changes().get(Change.Id.parse(id));
- if (c != null) {
- return ImmutableList.of(c);
+ if (!id.isEmpty() && id.charAt(0) != '0') {
+ Integer n = Ints.tryParse(id);
+ try {
+ if (n != null) {
+ return ImmutableList.of(
+ changeControlFactory.controlFor(new Change.Id(n), user));
+ }
+ } catch (NoSuchChangeException e) {
+ return Collections.emptyList();
}
- return Collections.emptyList();
}
+ // Use the index to search for changes, but don't return any stored fields,
+ // to force rereading in case the index is stale.
+ InternalChangeQuery query = queryProvider.get()
+ .setRequestedFields(ImmutableSet.<String> of());
+
// Try isolated changeId
if (!id.contains("~")) {
- return asChanges(queryProvider.get().byKeyPrefix(id));
+ return asChangeControls(query.byKeyPrefix(id));
}
// Try change triplet
Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id);
if (triplet.isPresent()) {
- return asChanges(queryProvider.get().byBranchKey(
+ return asChangeControls(query.byBranchKey(
triplet.get().branch(),
triplet.get().id()));
}
- throw new ResourceNotFoundException(id);
+ return Collections.emptyList();
}
- private static void deleteOnlyDraftPatchSetPreserveRef(ReviewDb db,
- PatchSet patch) throws NoSuchChangeException, OrmException {
- PatchSet.Id patchSetId = patch.getId();
- if (!patch.isDraft()) {
- throw new NoSuchChangeException(patchSetId.getParentKey());
+ private List<ChangeControl> asChangeControls(List<ChangeData> cds)
+ throws OrmException {
+ List<ChangeControl> ctls = new ArrayList<>(cds.size());
+ for (ChangeData cd : cds) {
+ ctls.add(cd.changeControl(user.get()));
}
-
- db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(patchSetId));
- db.changeMessages().delete(db.changeMessages().byPatchSet(patchSetId));
- // No need to delete from notedb; draft patch sets will be filtered out.
- db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
- db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
-
- db.patchSets().delete(Collections.singleton(patch));
+ return ctls;
}
public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
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 2768733..8d31c11 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,7 +14,6 @@
package com.google.gerrit.server;
-import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -23,7 +22,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.StarredChange;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
@@ -68,6 +66,7 @@
@Singleton
public static class GenericFactory {
private final CapabilityControl.Factory capabilityControlFactory;
+ private final StarredChangesUtil starredChangesUtil;
private final AuthConfig authConfig;
private final Realm realm;
private final String anonymousCowardName;
@@ -79,6 +78,7 @@
@Inject
public GenericFactory(
@Nullable CapabilityControl.Factory capabilityControlFactory,
+ @Nullable StarredChangesUtil starredChangesUtil,
AuthConfig authConfig,
Realm realm,
@AnonymousCowardName String anonymousCowardName,
@@ -87,6 +87,7 @@
AccountCache accountCache,
GroupBackend groupBackend) {
this.capabilityControlFactory = capabilityControlFactory;
+ this.starredChangesUtil = starredChangesUtil;
this.authConfig = authConfig;
this.realm = realm;
this.anonymousCowardName = anonymousCowardName;
@@ -101,23 +102,23 @@
}
public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
- return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
- anonymousCowardName, canonicalUrl, accountCache, groupBackend,
- disableReverseDnsLookup, null, db, id, null);
+ return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
+ authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
+ groupBackend, disableReverseDnsLookup, null, db, id, null);
}
public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
- return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
- anonymousCowardName, canonicalUrl, accountCache, groupBackend,
- disableReverseDnsLookup, Providers.of(remotePeer), null,
+ return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
+ authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
+ groupBackend, disableReverseDnsLookup, Providers.of(remotePeer), null,
id, null);
}
public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
@Nullable CurrentUser caller) {
- return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
- anonymousCowardName, canonicalUrl, accountCache, groupBackend,
- disableReverseDnsLookup, Providers.of(remotePeer), null,
+ return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
+ authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
+ groupBackend, disableReverseDnsLookup, Providers.of(remotePeer), null,
id, caller);
}
}
@@ -131,6 +132,7 @@
@Singleton
public static class RequestFactory {
private final CapabilityControl.Factory capabilityControlFactory;
+ private final StarredChangesUtil starredChangesUtil;
private final AuthConfig authConfig;
private final Realm realm;
private final String anonymousCowardName;
@@ -145,6 +147,7 @@
@Inject
RequestFactory(
CapabilityControl.Factory capabilityControlFactory,
+ StarredChangesUtil starredChangesUtil,
final AuthConfig authConfig,
Realm realm,
@AnonymousCowardName final String anonymousCowardName,
@@ -155,6 +158,7 @@
@RemotePeer final Provider<SocketAddress> remotePeerProvider,
final Provider<ReviewDb> dbProvider) {
this.capabilityControlFactory = capabilityControlFactory;
+ this.starredChangesUtil = starredChangesUtil;
this.authConfig = authConfig;
this.realm = realm;
this.anonymousCowardName = anonymousCowardName;
@@ -167,16 +171,16 @@
}
public IdentifiedUser create(Account.Id id) {
- return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
- anonymousCowardName, canonicalUrl, accountCache, groupBackend,
- disableReverseDnsLookup, remotePeerProvider, dbProvider,
+ return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
+ authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
+ groupBackend, disableReverseDnsLookup, remotePeerProvider, dbProvider,
id, null);
}
public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
- return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
- anonymousCowardName, canonicalUrl, accountCache, groupBackend,
- disableReverseDnsLookup, remotePeerProvider, dbProvider,
+ return new IdentifiedUser(capabilityControlFactory, starredChangesUtil,
+ authConfig, realm, anonymousCowardName, canonicalUrl, accountCache,
+ groupBackend, disableReverseDnsLookup, remotePeerProvider, dbProvider,
id, caller);
}
}
@@ -189,6 +193,9 @@
SystemGroupBackend.ANONYMOUS_USERS,
SystemGroupBackend.REGISTERED_USERS));
+ @Nullable
+ private final StarredChangesUtil starredChangesUtil;
+
private final Provider<String> canonicalUrl;
private final AccountCache accountCache;
private final AuthConfig authConfig;
@@ -212,12 +219,13 @@
private Set<String> invalidEmails;
private GroupMembership effectiveGroups;
private Set<Change.Id> starredChanges;
- private ResultSet<StarredChange> starredQuery;
+ private ResultSet<Change.Id> starredQuery;
private Collection<AccountProjectWatch> notificationFilters;
private CurrentUser realUser;
private IdentifiedUser(
CapabilityControl.Factory capabilityControlFactory,
+ @Nullable StarredChangesUtil starredChangesUtil,
final AuthConfig authConfig,
Realm realm,
final String anonymousCowardName,
@@ -230,6 +238,7 @@
final Account.Id id,
@Nullable CurrentUser realUser) {
super(capabilityControlFactory);
+ this.starredChangesUtil = starredChangesUtil;
this.canonicalUrl = canonicalUrl;
this.accountCache = accountCache;
this.groupBackend = groupBackend;
@@ -322,13 +331,16 @@
@Override
public Set<Change.Id> getStarredChanges() {
if (starredChanges == null) {
- checkRequestScope();
+ if (starredChangesUtil == null) {
+ throw new IllegalStateException("StarredChangesUtil is missing");
+ }
try {
- starredChanges = starredChangeIds(
- starredQuery != null ? starredQuery : starredQuery());
- } catch (OrmException | RuntimeException e) {
- log.warn("Cannot query starred changes", e);
- starredChanges = Collections.emptySet();
+ starredChanges =
+ FluentIterable.from(
+ starredQuery != null
+ ? starredQuery
+ : starredChangesUtil.query(accountId))
+ .toSet();
} finally {
starredQuery = null;
}
@@ -344,14 +356,8 @@
}
public void asyncStarredChanges() {
- if (starredChanges == null && dbProvider != null) {
- try {
- starredQuery = starredQuery();
- } catch (OrmException e) {
- log.warn("Cannot query starred by user changes", e);
- starredQuery = null;
- starredChanges = Collections.emptySet();
- }
+ if (starredChanges == null && starredChangesUtil != null) {
+ starredQuery = starredChangesUtil.query(accountId);
}
}
@@ -371,21 +377,6 @@
}
}
- private ResultSet<StarredChange> starredQuery() throws OrmException {
- return dbProvider.get().starredChanges().byAccount(getAccountId());
- }
-
- private static ImmutableSet<Change.Id> starredChangeIds(
- Iterable<StarredChange> scs) {
- return FluentIterable.from(scs)
- .transform(new Function<StarredChange, Change.Id>() {
- @Override
- public Change.Id apply(StarredChange in) {
- return in.getChangeId();
- }
- }).toSet();
- }
-
@Override
public Collection<AccountProjectWatch> getNotificationFilters() {
if (notificationFilters == null) {
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
index 7b182b1..c4086e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -275,14 +275,10 @@
return sort(db.patchComments().draftByAuthor(author).toList());
}
- // TODO(dborowitz): Just scan author space.
- Set<String> refNames = getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
+ Set<String> refNames =
+ getRefNamesAllUsers(RefNames.refsDraftCommentsPrefix(author));
List<PatchLineComment> comments = Lists.newArrayList();
for (String refName : refNames) {
- Account.Id id = Account.Id.fromRefPart(refName);
- if (!author.equals(id)) {
- continue;
- }
Change.Id changeId = Change.Id.parse(refName);
comments.addAll(
draftFactory.create(changeId, author).load().getComments().values());
@@ -364,7 +360,7 @@
}
private Set<String> getRefNamesAllUsers(String prefix) throws OrmException {
- try (Repository repo = repoManager.openRepository(allUsers)) {
+ try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
RefDatabase refDb = repo.getRefDatabase();
return refDb.getRefs(prefix).keySet();
} catch (IOException e) {
@@ -375,7 +371,7 @@
private Iterable<String> getDraftRefs(final Change.Id changeId)
throws OrmException {
Set<String> refNames = getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
- final String suffix = "-" + changeId.get();
+ final String suffix = "/" + changeId.get();
return Iterables.filter(refNames, new Predicate<String>() {
@Override
public boolean apply(String input) {
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
new file mode 100644
index 0000000..d410ee9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -0,0 +1,277 @@
+// 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;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.StarredChange;
+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.NotesMigration;
+import com.google.gwtorm.server.ListResultSet;
+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;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+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.RefDatabase;
+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.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;
+
+@Singleton
+public class StarredChangesUtil {
+ private static final Logger log =
+ LoggerFactory.getLogger(StarredChangesUtil.class);
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+ private final NotesMigration migration;
+ private final Provider<ReviewDb> dbProvider;
+ private final PersonIdent serverIdent;
+
+ @Inject
+ StarredChangesUtil(GitRepositoryManager repoManager,
+ AllUsersName allUsers,
+ NotesMigration migration,
+ Provider<ReviewDb> dbProvider,
+ @GerritPersonIdent PersonIdent serverIdent) {
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ this.migration = migration;
+ this.dbProvider = dbProvider;
+ this.serverIdent = serverIdent;
+ }
+
+ public void star(Account.Id accountId, Change.Id changeId)
+ throws OrmException {
+ dbProvider.get().starredChanges()
+ .insert(Collections.singleton(new StarredChange(
+ new StarredChange.Key(accountId, changeId))));
+ if (!migration.writeChanges()) {
+ return;
+ }
+ try (Repository repo = repoManager.openMetadataRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RefUpdate u = repo.updateRef(
+ RefNames.refsStarredChanges(accountId, changeId));
+ u.setExpectedOldObjectId(ObjectId.zeroId());
+ u.setNewObjectId(emptyTree(repo));
+ u.setRefLogIdent(serverIdent);
+ u.setRefLogMessage("Star change " + changeId.get(), false);
+ RefUpdate.Result result = u.update(rw);
+ switch (result) {
+ case NEW:
+ return;
+ default:
+ throw new OrmException(
+ String.format("Star change %d for account %d failed: %s",
+ changeId.get(), accountId.get(), result.name()));
+ }
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format("Star change %d for account %d failed",
+ changeId.get(), accountId.get()), e);
+ }
+ }
+
+ private static ObjectId emptyTree(Repository repo) throws IOException {
+ try (ObjectInserter oi = repo.newObjectInserter()) {
+ ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+ oi.flush();
+ return id;
+ }
+ }
+
+ public void unstar(Account.Id accountId, Change.Id changeId)
+ throws OrmException {
+ dbProvider.get().starredChanges()
+ .delete(Collections.singleton(new StarredChange(
+ new StarredChange.Key(accountId, changeId))));
+ if (!migration.writeChanges()) {
+ return;
+ }
+ try (Repository repo = repoManager.openMetadataRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ RefUpdate u = repo.updateRef(
+ RefNames.refsStarredChanges(accountId, changeId));
+ u.setForceUpdate(true);
+ u.setRefLogIdent(serverIdent);
+ u.setRefLogMessage("Unstar change " + changeId.get(), true);
+ RefUpdate.Result result = u.delete();
+ switch (result) {
+ case FORCED:
+ return;
+ default:
+ throw new OrmException(
+ String.format("Unstar change %d for account %d failed: %s",
+ changeId.get(), accountId.get(), result.name()));
+ }
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format("Unstar change %d for account %d failed",
+ changeId.get(), accountId.get()), e);
+ }
+ }
+
+ public void unstarAll(Change.Id changeId) throws OrmException {
+ dbProvider.get().starredChanges().delete(
+ dbProvider.get().starredChanges().byChange(changeId));
+ if (!migration.writeChanges()) {
+ return;
+ }
+ try (Repository repo = repoManager.openMetadataRepository(allUsers);
+ RevWalk rw = new RevWalk(repo)) {
+ BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.setRefLogIdent(serverIdent);
+ batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
+ for (Account.Id accountId : byChange(changeId)) {
+ String refName = RefNames.refsStarredChanges(accountId, changeId);
+ Ref ref = repo.getRefDatabase().getRef(refName);
+ batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(),
+ ObjectId.zeroId(), refName));
+ }
+ batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ for (ReceiveCommand command : batchUpdate.getCommands()) {
+ if (command.getResult() != ReceiveCommand.Result.OK) {
+ throw new IOException(String.format(
+ "Unstar change %d failed, ref %s could not be deleted: %s",
+ changeId.get(), command.getRefName(), command.getResult()));
+ }
+ }
+ } catch (IOException e) {
+ throw new OrmException(
+ String.format("Unstar change %d failed", changeId.get()), e);
+ }
+ }
+
+ public Iterable<Account.Id> byChange(final Change.Id changeId)
+ throws OrmException {
+ if (!migration.readChanges()) {
+ return FluentIterable
+ .from(dbProvider.get().starredChanges().byChange(changeId))
+ .transform(new Function<StarredChange, Account.Id>() {
+ @Override
+ public Account.Id apply(StarredChange in) {
+ return in.getAccountId();
+ }
+ });
+ }
+ return FluentIterable.from(getRefNames(RefNames.REFS_STARRED_CHANGES))
+ .filter(new Predicate<String>() {
+ @Override
+ public boolean apply(String refPart) {
+ return refPart.endsWith("/" + changeId.get());
+ }
+ })
+ .transform(new Function<String, Account.Id>() {
+ @Override
+ public Account.Id apply(String refPart) {
+ return Account.Id.fromRefPart(refPart);
+ }
+ });
+ }
+
+ public ResultSet<Change.Id> query(Account.Id accountId) {
+ try {
+ if (!migration.readChanges()) {
+ return new ChangeIdResultSet(
+ dbProvider.get().starredChanges().byAccount(accountId));
+ }
+
+ return new ListResultSet<>(FluentIterable
+ .from(getRefNames(RefNames.refsStarredChangesPrefix(accountId)))
+ .transform(new Function<String, Change.Id>() {
+ @Override
+ public Change.Id apply(String changeId) {
+ return Change.Id.parse(changeId);
+ }
+ }).toList());
+ } catch (OrmException | RuntimeException e) {
+ log.warn(String.format("Cannot query starred changes for account %d",
+ accountId.get()), e);
+ List<Change.Id> empty = Collections.emptyList();
+ return new ListResultSet<>(empty);
+ }
+ }
+
+ private Set<String> getRefNames(String prefix) throws OrmException {
+ try (Repository repo = repoManager.openMetadataRepository(allUsers)) {
+ RefDatabase refDb = repo.getRefDatabase();
+ return refDb.getRefs(prefix).keySet();
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ private static class ChangeIdResultSet implements ResultSet<Change.Id> {
+ private static final Function<StarredChange, Change.Id>
+ STARRED_CHANGE_TO_CHANGE_ID =
+ new Function<StarredChange, Change.Id>() {
+ @Override
+ public Change.Id apply(StarredChange starredChange) {
+ return starredChange.getChangeId();
+ }
+ };
+
+ private final ResultSet<StarredChange> starredChangesResultSet;
+
+ ChangeIdResultSet(ResultSet<StarredChange> starredChangesResultSet) {
+ this.starredChangesResultSet = starredChangesResultSet;
+ }
+
+ @Override
+ public Iterator<Change.Id> iterator() {
+ return Iterators.transform(starredChangesResultSet.iterator(),
+ STARRED_CHANGE_TO_CHANGE_ID);
+ }
+
+ @Override
+ public List<Change.Id> toList() {
+ return Lists.transform(starredChangesResultSet.toList(),
+ STARRED_CHANGE_TO_CHANGE_ID);
+ }
+
+ @Override
+ public void close() {
+ starredChangesResultSet.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
index 406ab52..9001ea5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
@@ -20,7 +20,7 @@
/** Translates an email address to a set of matching accounts. */
public interface AccountByEmailCache {
- public Set<Account.Id> get(String email);
+ Set<Account.Id> get(String email);
- public void evict(String email);
+ void evict(String email);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
index 86308a9..d7d418d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
@@ -18,13 +18,13 @@
/** Caches important (but small) account state to avoid database hits. */
public interface AccountCache {
- public AccountState get(Account.Id accountId);
+ AccountState get(Account.Id accountId);
- public AccountState getIfPresent(Account.Id accountId);
+ AccountState getIfPresent(Account.Id accountId);
- public AccountState getByUsername(String username);
+ AccountState getByUsername(String username);
- public void evict(Account.Id accountId);
+ void evict(Account.Id accountId);
- public void evictByUsername(String username);
+ void evictByUsername(String username);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index 35ab3af..35d7c70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -38,6 +39,15 @@
return r;
}
+ /** Create a request for an external username. */
+ public static AuthRequest forExternalUser(String username) {
+ AccountExternalId.Key i =
+ new AccountExternalId.Key(SCHEME_EXTERNAL, username);
+ AuthRequest r = new AuthRequest(i.get());
+ r.setUserName(username);
+ return r;
+ }
+
/**
* Create a request for an email address registration.
* <p>
@@ -58,6 +68,8 @@
private String emailAddress;
private String userName;
private boolean skipAuthentication;
+ private String authPlugin;
+ private String authProvider;
public AuthRequest(final String externalId) {
this.externalId = externalId;
@@ -125,4 +137,20 @@
public void setSkipAuthentication(boolean skip) {
skipAuthentication = skip;
}
+
+ public String getAuthPlugin() {
+ return authPlugin;
+ }
+
+ public void setAuthPlugin(String authPlugin) {
+ this.authPlugin = authPlugin;
+ }
+
+ public String getAuthProvider() {
+ return authProvider;
+ }
+
+ public void setAuthProvider(String authProvider) {
+ this.authProvider = authProvider;
+ }
}
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 7bb00c5..9d580f5 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
@@ -40,7 +40,7 @@
/** Access control management for server-wide capabilities. */
public class CapabilityControl {
public static interface Factory {
- public CapabilityControl create(CurrentUser user);
+ CapabilityControl create(CurrentUser user);
}
private final CapabilityCollection capabilities;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
index f60492e..166f97e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
@@ -19,9 +19,9 @@
* Expands user name to a local email address, usually by adding a domain.
*/
public interface EmailExpander {
- public boolean canExpand(String user);
+ boolean canExpand(String user);
- public String expand(String user);
+ String expand(String user);
public static class None implements EmailExpander {
public static final None INSTANCE = new None();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index 08bf83e..1ab8928 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -24,6 +24,7 @@
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.EmailStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -104,7 +105,6 @@
Boolean useFlashClipboard;
String downloadScheme;
DownloadCommand downloadCommand;
- Boolean copySelfOnEmail;
DateFormat dateFormat;
TimeFormat timeFormat;
Boolean relativeDateInChangeTable;
@@ -113,6 +113,7 @@
Boolean muteCommonPathPrefixes;
ReviewCategoryStrategy reviewCategoryStrategy;
DiffView diffView;
+ EmailStrategy emailStrategy;
List<TopMenu.MenuItem> my;
Map<String, String> urlAliases;
@@ -124,7 +125,6 @@
useFlashClipboard = p.isUseFlashClipboard() ? true : null;
downloadScheme = p.getDownloadUrl();
downloadCommand = p.getDownloadCommand();
- copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
dateFormat = p.getDateFormat();
timeFormat = p.getTimeFormat();
relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
@@ -133,6 +133,7 @@
muteCommonPathPrefixes = p.isMuteCommonPathPrefixes() ? true : null;
reviewCategoryStrategy = p.getReviewCategoryStrategy();
diffView = p.getDiffView();
+ emailStrategy = p.getEmailStrategy();
}
loadFromAllUsers(v, allUsers);
}
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 c1a4e0f..c7a2241 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
@@ -19,9 +19,9 @@
/** Tracks group objects in memory for efficient access. */
public interface GroupCache {
- public AccountGroup get(AccountGroup.Id groupId);
+ AccountGroup get(AccountGroup.Id groupId);
- public AccountGroup get(AccountGroup.NameKey name);
+ AccountGroup get(AccountGroup.NameKey name);
/**
* Lookup a group definition by its UUID. The returned definition may be null
@@ -29,16 +29,16 @@
* copied from another server.
*/
@Nullable
- public AccountGroup get(AccountGroup.UUID uuid);
+ AccountGroup get(AccountGroup.UUID uuid);
/** @return sorted iteration of groups. */
- public abstract Iterable<AccountGroup> all();
+ Iterable<AccountGroup> all();
/** Notify the cache that a new group was constructed. */
- public void onCreateGroup(AccountGroup.NameKey newGroupName);
+ void onCreateGroup(AccountGroup.NameKey newGroupName);
- public void evict(AccountGroup group);
+ void evict(AccountGroup group);
- public void evictAfterRename(final AccountGroup.NameKey oldName,
+ void evictAfterRename(final AccountGroup.NameKey oldName,
final AccountGroup.NameKey newName);
}
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 6ba6bf0..9971301 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
@@ -21,14 +21,14 @@
/** Tracks group inclusions in memory for efficient access. */
public interface GroupIncludeCache {
/** @return groups directly a member of the passed group. */
- public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group);
+ Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group);
/** @return any groups the passed group belongs to. */
- public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
+ Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
/** @return set of any UUIDs that are not internal groups. */
- public Set<AccountGroup.UUID> allExternalMembers();
+ Set<AccountGroup.UUID> allExternalMembers();
- public void evictSubgroupsOf(AccountGroup.UUID groupId);
- public void evictParentGroupsOf(AccountGroup.UUID groupId);
+ void evictSubgroupsOf(AccountGroup.UUID groupId);
+ void evictParentGroupsOf(AccountGroup.UUID groupId);
}
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 056fa85b..6fd76d4 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
@@ -22,26 +22,26 @@
public interface Realm {
/** Can the end-user modify this field of their own account? */
- public boolean allowsEdit(Account.FieldName field);
+ boolean allowsEdit(Account.FieldName field);
/** Returns the account fields that the end-user can modify. */
- public Set<Account.FieldName> getEditableFields();
+ Set<Account.FieldName> getEditableFields();
- public AuthRequest authenticate(AuthRequest who) throws AccountException;
+ AuthRequest authenticate(AuthRequest who) throws AccountException;
- public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
+ AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
throws AccountException;
- public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+ AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
throws AccountException;
- public void onCreateAccount(AuthRequest who, Account account);
+ void onCreateAccount(AuthRequest who, Account account);
/** @return true if the user has the given email address. */
- public boolean hasEmailAddress(IdentifiedUser who, String email);
+ boolean hasEmailAddress(IdentifiedUser who, String email);
/** @return all known email addresses for the identified user. */
- public Set<String> getEmailAddresses(IdentifiedUser who);
+ Set<String> getEmailAddresses(IdentifiedUser who);
/**
* Locate an account whose local username is the given account name.
@@ -51,5 +51,5 @@
* how to convert the accountName into an email address, and then locate the
* user by that email address.
*/
- public Account.Id lookup(String accountName);
+ Account.Id lookup(String accountName);
}
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 569d12801..7042076 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
@@ -34,6 +34,7 @@
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.EmailStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -64,7 +65,6 @@
public Boolean useFlashClipboard;
public String downloadScheme;
public DownloadCommand downloadCommand;
- public Boolean copySelfOnEmail;
public DateFormat dateFormat;
public TimeFormat timeFormat;
public Boolean relativeDateInChangeTable;
@@ -73,6 +73,7 @@
public Boolean muteCommonPathPrefixes;
public ReviewCategoryStrategy reviewCategoryStrategy;
public DiffView diffView;
+ public EmailStrategy emailStrategy;
public List<TopMenu.MenuItem> my;
public Map<String, String> urlAliases;
}
@@ -146,9 +147,6 @@
if (i.downloadCommand != null) {
p.setDownloadCommand(i.downloadCommand);
}
- if (i.copySelfOnEmail != null) {
- p.setCopySelfOnEmails(i.copySelfOnEmail);
- }
if (i.dateFormat != null) {
p.setDateFormat(i.dateFormat);
}
@@ -173,6 +171,9 @@
if (i.diffView != null) {
p.setDiffView(i.diffView);
}
+ if (i.emailStrategy != null) {
+ p.setEmailStrategy(i.emailStrategy);
+ }
db.get().accounts().update(Collections.singleton(a));
db.get().commit();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
index a3c0d37..e69bc0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -27,10 +27,9 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ChangesCollection;
import com.google.gerrit.server.query.change.QueryChanges;
@@ -43,8 +42,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collections;
-
@Singleton
public class StarredChanges implements
ChildCollection<AccountResource, AccountResource.StarredChange>,
@@ -72,7 +69,7 @@
user.asyncStarredChanges();
ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
- if (user.getStarredChanges().contains(change.getChange().getId())) {
+ if (user.getStarredChanges().contains(change.getId())) {
return new AccountResource.StarredChange(user, change);
}
throw new ResourceNotFoundException(id);
@@ -117,13 +114,13 @@
@Singleton
public static class Create implements RestModifyView<AccountResource, EmptyInput> {
private final Provider<CurrentUser> self;
- private final Provider<ReviewDb> dbProvider;
+ private final StarredChangesUtil starredChangesUtil;
private ChangeResource change;
@Inject
- Create(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+ Create(Provider<CurrentUser> self, StarredChangesUtil starredChangesUtil) {
this.self = self;
- this.dbProvider = dbProvider;
+ this.starredChangesUtil = starredChangesUtil;
}
public Create setChange(ChangeResource change) {
@@ -138,10 +135,7 @@
throw new AuthException("not allowed to add starred change");
}
try {
- dbProvider.get().starredChanges().insert(Collections.singleton(
- new StarredChange(new StarredChange.Key(
- rsrc.getUser().getAccountId(),
- change.getChange().getId()))));
+ starredChangesUtil.star(self.get().getAccountId(), change.getId());
} catch (OrmDuplicateKeyException e) {
return Response.none();
}
@@ -173,12 +167,12 @@
public static class Delete implements
RestModifyView<AccountResource.StarredChange, EmptyInput> {
private final Provider<CurrentUser> self;
- private final Provider<ReviewDb> dbProvider;
+ private final StarredChangesUtil starredChangesUtil;
@Inject
- Delete(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+ Delete(Provider<CurrentUser> self, StarredChangesUtil starredChangesUtil) {
this.self = self;
- this.dbProvider = dbProvider;
+ this.starredChangesUtil = starredChangesUtil;
}
@Override
@@ -187,10 +181,8 @@
if (self.get() != rsrc.getUser()) {
throw new AuthException("not allowed remove starred change");
}
- dbProvider.get().starredChanges().delete(Collections.singleton(
- new StarredChange(new StarredChange.Key(
- rsrc.getUser().getAccountId(),
- rsrc.getChange().getId()))));
+ starredChangesUtil.unstar(self.get().getAccountId(),
+ rsrc.getChange().getId());
return Response.none();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
index 837e1ed..1cc3c94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
@@ -15,8 +15,6 @@
package com.google.gerrit.server.account;
import com.google.common.base.Strings;
-import com.google.common.collect.ComparisonChain;
-import com.google.common.collect.Ordering;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -24,6 +22,7 @@
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.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -33,7 +32,6 @@
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -140,15 +138,7 @@
}
List<AccountInfo> m = new ArrayList<>(matches.values());
- Collections.sort(m, new Comparator<AccountInfo>() {
- @Override
- public int compare(AccountInfo a, AccountInfo b) {
- return ComparisonChain.start()
- .compare(a.name, b.name, Ordering.natural().nullsLast())
- .compare(a.email, b.email, Ordering.natural().nullsLast())
- .result();
- }
- });
+ Collections.sort(m, AccountInfoComparator.ORDER_NULLS_LAST);
return m;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
new file mode 100644
index 0000000..3794701
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoComparator.java
@@ -0,0 +1,56 @@
+// 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.api.accounts;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.common.AccountInfo;
+
+import java.util.Comparator;
+
+public class AccountInfoComparator extends Ordering<AccountInfo>
+ implements Comparator<AccountInfo> {
+ public static final AccountInfoComparator ORDER_NULLS_FIRST =
+ new AccountInfoComparator();
+ public static final AccountInfoComparator ORDER_NULLS_LAST =
+ new AccountInfoComparator().setNullsLast();
+
+ private boolean nullsLast;
+
+ private AccountInfoComparator() {
+ }
+
+ private AccountInfoComparator setNullsLast() {
+ this.nullsLast = true;
+ return this;
+ }
+
+ @Override
+ public int compare(AccountInfo a, AccountInfo b) {
+ return ComparisonChain.start()
+ .compare(a.name, b.name, createOrdering())
+ .compare(a.email, b.email, createOrdering())
+ .compare(a._accountId, b._accountId, createOrdering())
+ .result();
+ }
+
+ private <S extends Comparable<?>> Ordering<S> createOrdering() {
+ if (nullsLast) {
+ return Ordering.natural().nullsLast();
+ } else {
+ return Ordering.natural().nullsFirst();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 7be8299..3bd7634 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -61,6 +61,11 @@
}
@Override
+ public AccountApi id(int id) throws RestApiException {
+ return id(String.valueOf(id));
+ }
+
+ @Override
public AccountApi self() throws RestApiException {
if (!self.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
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 b55642c..d7f8cde 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
@@ -22,6 +22,7 @@
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.RestoreInput;
import com.google.gerrit.extensions.api.changes.RevertInput;
+import com.google.gerrit.extensions.api.changes.ReviewerApi;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -46,6 +47,7 @@
import com.google.gerrit.server.change.PutTopic;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.change.Revert;
+import com.google.gerrit.server.change.Reviewers;
import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestReviewers;
@@ -68,7 +70,9 @@
private final Provider<CurrentUser> user;
private final Changes changeApi;
+ private final Reviewers reviewers;
private final Revisions revisions;
+ private final ReviewerApiImpl.Factory reviewerApi;
private final RevisionApiImpl.Factory revisionApi;
private final Provider<SuggestReviewers> suggestReviewers;
private final ChangeResource change;
@@ -90,7 +94,9 @@
@Inject
ChangeApiImpl(Provider<CurrentUser> user,
Changes changeApi,
+ Reviewers reviewers,
Revisions revisions,
+ ReviewerApiImpl.Factory reviewerApi,
RevisionApiImpl.Factory revisionApi,
Provider<SuggestReviewers> suggestReviewers,
Abandon abandon,
@@ -111,7 +117,9 @@
this.user = user;
this.changeApi = changeApi;
this.revert = revert;
+ this.reviewers = reviewers;
this.revisions = revisions;
+ this.reviewerApi = reviewerApi;
this.revisionApi = revisionApi;
this.suggestReviewers = suggestReviewers;
this.abandon = abandon;
@@ -132,7 +140,7 @@
@Override
public String id() {
- return Integer.toString(change.getChange().getId().get());
+ return Integer.toString(change.getId().get());
}
@Override
@@ -156,6 +164,16 @@
}
@Override
+ public ReviewerApi reviewer(String id) throws RestApiException {
+ try {
+ return reviewerApi.create(
+ reviewers.parse(change, IdString.fromDecoded(id)));
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot parse reviewer", e);
+ }
+ }
+
+ @Override
public void abandon() throws RestApiException {
abandon(new AbandonInput());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index acda1ee..bb01dea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -28,6 +28,7 @@
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.Change;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangesCollection;
import com.google.gerrit.server.change.CreateChange;
@@ -93,8 +94,7 @@
try {
ChangeInfo out = createChange.apply(
TopLevelResource.INSTANCE, in).value();
- return api.create(changes.parse(TopLevelResource.INSTANCE,
- IdString.fromUrl(out.changeId)));
+ return api.create(changes.parse(new Change.Id(out._number)));
} catch (OrmException | IOException | InvalidChangeOperationException
| UpdateException e) {
throw new RestApiException("Cannot create change", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
index 647f577..2e2dfcc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -22,12 +22,11 @@
import com.google.gerrit.server.change.DraftCommentResource;
import com.google.gerrit.server.change.GetDraftComment;
import com.google.gerrit.server.change.PutDraftComment;
+import com.google.gerrit.server.git.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-
class DraftApiImpl implements DraftApi {
interface Factory {
DraftApiImpl create(DraftCommentResource d);
@@ -62,7 +61,7 @@
public CommentInfo update(DraftInput in) throws RestApiException {
try {
return putDraft.apply(draft, in).value();
- } catch (IOException | OrmException e) {
+ } catch (UpdateException | OrmException e) {
throw new RestApiException("Cannot update draft", e);
}
}
@@ -71,7 +70,7 @@
public void delete() throws RestApiException {
try {
deleteDraft.apply(draft, null);
- } catch (IOException | OrmException e) {
+ } catch (UpdateException e) {
throw new RestApiException("Cannot delete draft", 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 a5e584e..228dad6 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
@@ -27,5 +27,6 @@
factory(DraftApiImpl.Factory.class);
factory(RevisionApiImpl.Factory.class);
factory(FileApiImpl.Factory.class);
+ factory(ReviewerApiImpl.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
new file mode 100644
index 0000000..49b3432
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -0,0 +1,65 @@
+// 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.api.changes;
+
+import com.google.gerrit.extensions.api.changes.ReviewerApi;
+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 ReviewerApiImpl implements ReviewerApi {
+ interface Factory {
+ ReviewerApiImpl create(ReviewerResource r);
+ }
+
+ private final ReviewerResource reviewer;
+ private final Votes.List listVotes;
+ private final DeleteVote deleteVote;
+
+ @Inject
+ ReviewerApiImpl(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);
+ }
+ }
+}
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 0926142..0ca43f5 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
@@ -183,7 +183,7 @@
public void delete() throws RestApiException {
try {
deleteDraft.apply(revision, null);
- } catch (OrmException | IOException e) {
+ } catch (UpdateException e) {
throw new RestApiException("Cannot delete draft ps", e);
}
}
@@ -347,7 +347,7 @@
return changes.id(revision.getChange().getId().get())
.revision(revision.getPatchSet().getId().get())
.draft(id);
- } catch (IOException | OrmException e) {
+ } catch (UpdateException | OrmException e) {
throw new RestApiException("Cannot create draft", e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
index 0bb395f..ec3d9ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -17,12 +17,16 @@
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.project.BranchesCollection;
import com.google.gerrit.server.project.CreateBranch;
import com.google.gerrit.server.project.DeleteBranch;
+import com.google.gerrit.server.project.FileResource;
+import com.google.gerrit.server.project.FilesCollection;
+import com.google.gerrit.server.project.GetContent;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -38,6 +42,8 @@
private final BranchesCollection branches;
private final CreateBranch.Factory createBranchFactory;
private final DeleteBranch deleteBranch;
+ private final FilesCollection filesCollection;
+ private final GetContent getContent;
private final String ref;
private final ProjectResource project;
@@ -45,11 +51,15 @@
BranchApiImpl(BranchesCollection branches,
CreateBranch.Factory createBranchFactory,
DeleteBranch deleteBranch,
+ FilesCollection filesCollection,
+ GetContent getContent,
@Assisted ProjectResource project,
@Assisted String ref) {
this.branches = branches;
this.createBranchFactory = createBranchFactory;
this.deleteBranch = deleteBranch;
+ this.filesCollection = filesCollection;
+ this.getContent = getContent;
this.project = project;
this.ref = ref;
}
@@ -85,6 +95,17 @@
}
}
+ @Override
+ public BinaryResult file(String path) throws RestApiException {
+ try {
+ FileResource resource = filesCollection.parse(resource(),
+ IdString.fromDecoded(path));
+ return getContent.apply(resource);
+ } catch (IOException e) {
+ throw new RestApiException("Cannot retrieve file", e);
+ }
+ }
+
private BranchResource resource() throws RestApiException, IOException {
return branches.parse(project, IdString.fromDecoded(ref));
}
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
new file mode 100644
index 0000000..582cc38
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -0,0 +1,121 @@
+// 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.auth.oauth;
+
+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.registration.DynamicMap;
+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.account.AbstractRealm;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class OAuthRealm extends AbstractRealm {
+ private final DynamicMap<OAuthLoginProvider> loginProviders;
+
+ @Inject
+ OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders) {
+ this.loginProviders = loginProviders;
+ }
+
+ @Override
+ public boolean allowsEdit(FieldName field) {
+ return false;
+ }
+
+ /**
+ * Authenticates with the {@link OAuthLoginProvider} specified
+ * in the authentication request.
+ *
+ * {@link AccountManager} calls this method without password
+ * if authenticity of the user has already been established.
+ * In that case the {@link AuthRequest} is supposed to contain
+ * a resolved email address and we can skip the authentication
+ * request to the {@code OAuthLoginService}.
+ *
+ * @param who the authentication request.
+ *
+ * @return the authentication request with resolved email address
+ * and display name in case the authenticity of the user could
+ * be established; otherwise {@code who} is returned unchanged.
+ *
+ * @throws AccountException if the authentication request with
+ * the OAuth2 server failed or no {@code OAuthLoginProvider} was
+ * available to handle the request.
+ */
+ @Override
+ public AuthRequest authenticate(AuthRequest who) throws AccountException {
+ if (Strings.isNullOrEmpty(who.getPassword()) &&
+ !Strings.isNullOrEmpty(who.getEmailAddress())) {
+ return who;
+ }
+
+ if (Strings.isNullOrEmpty(who.getAuthPlugin())
+ || Strings.isNullOrEmpty(who.getAuthProvider())) {
+ throw new AccountException("Cannot authenticate");
+ }
+ OAuthLoginProvider loginProvider =
+ loginProviders.get(who.getAuthPlugin(), who.getAuthProvider());
+ if (loginProvider == null) {
+ throw new AccountException("Cannot authenticate");
+ }
+
+ OAuthUserInfo userInfo;
+ try {
+ userInfo = loginProvider.login(who.getUserName(), who.getPassword());
+ } catch (IOException e) {
+ throw new AccountException("Cannot authenticate", e);
+ }
+ if (userInfo == null) {
+ throw new AccountException("Cannot authenticate");
+ }
+ if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())) {
+ who.setEmailAddress(userInfo.getEmailAddress());
+ }
+ if (!Strings.isNullOrEmpty(userInfo.getDisplayName())) {
+ who.setDisplayName(userInfo.getDisplayName());
+ }
+ return who;
+ }
+
+ @Override
+ public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
+ return who;
+ }
+
+ @Override
+ public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+ throws AccountException {
+ return who;
+ }
+
+ @Override
+ public void onCreateAccount(AuthRequest who, Account account) {
+ }
+
+ @Override
+ public Account.Id lookup(String accountName) {
+ return null;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
index 4b30a8d..571a7e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
@@ -37,7 +37,7 @@
* {@code null} is acceptable, and results in the server responding
* with a 404. This will hide the avatar image in the web UI.
*/
- public String getUrl(IdentifiedUser forUser, int imageSize);
+ String getUrl(IdentifiedUser forUser, int imageSize);
/**
* Gets a URL for a user to modify their avatar image.
@@ -46,5 +46,5 @@
* @return a URL the user should visit to modify their avatar, or null if
* modification is not possible.
*/
- public String getChangeAvatarUrl(IdentifiedUser forUser);
+ String getChangeAvatarUrl(IdentifiedUser forUser);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
new file mode 100644
index 0000000..41a48af
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -0,0 +1,90 @@
+package com.google.gerrit.server.cache;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Set;
+
+@Singleton
+public class CacheMetrics {
+ @Inject
+ public CacheMetrics(MetricMaker metrics,
+ final DynamicMap<Cache<?, ?>> cacheMap) {
+ Field<String> F_NAME = Field.ofString("cache_name");
+
+ final CallbackMetric1<String, Long> memEnt =
+ metrics.newCallbackMetric("caches/memory_cached", Long.class,
+ new Description("Memory entries").setGauge().setUnit("entries"),
+ F_NAME);
+ final CallbackMetric1<String, Double> memHit =
+ metrics.newCallbackMetric("caches/memory_hit_ratio", Double.class,
+ new Description("Memory hit ratio").setGauge().setUnit("percent"),
+ F_NAME);
+ final CallbackMetric1<String, Long> memEvict =
+ metrics.newCallbackMetric("caches/memory_eviction_count", Long.class,
+ new Description("Memory eviction count").setGauge()
+ .setUnit("evicted entries"),
+ F_NAME);
+ final CallbackMetric1<String, Long> perDiskEnt =
+ metrics.newCallbackMetric("caches/disk_cached", Long.class,
+ new Description("Disk entries used by persistent cache").setGauge()
+ .setUnit("entries"),
+ F_NAME);
+ final CallbackMetric1<String, Double> perDiskHit =
+ metrics.newCallbackMetric("caches/disk_hit_ratio", Double.class,
+ new Description("Disk hit ratio for persistent cache").setGauge()
+ .setUnit("percent"),
+ F_NAME);
+
+ final Set<CallbackMetric<?>> cacheMetrics =
+ ImmutableSet.<CallbackMetric<?>> of(memEnt, memHit, memEvict,
+ perDiskEnt, perDiskHit);
+
+ metrics.newTrigger(cacheMetrics, new Runnable() {
+ @Override
+ public void run() {
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ Cache<?, ?> c = e.getProvider().get();
+ String name = metricNameOf(e);
+ CacheStats cstats = c.stats();
+ memEnt.set(name, c.size());
+ memHit.set(name, cstats.hitRate() * 100);
+ memEvict.set(name, cstats.evictionCount());
+ if (c instanceof PersistentCache) {
+ PersistentCache.DiskStats d = ((PersistentCache) c).diskStats();
+ perDiskEnt.set(name, d.size());
+ perDiskHit.set(name, hitRatio(d));
+ }
+ }
+ for (CallbackMetric<?> cbm : cacheMetrics) {
+ cbm.prune();
+ }
+ }
+ });
+ }
+
+ private static double hitRatio(PersistentCache.DiskStats d) {
+ if (d.requestCount() <= 0) {
+ return 100;
+ }
+ return ((double) d.hitCount() / d.requestCount() * 100);
+ }
+
+ private static String metricNameOf(DynamicMap.Entry<Cache<?, ?>> e) {
+ if ("gerrit".equals(e.getPluginName())) {
+ return e.getExportName();
+ } else {
+ return String.format("plugin/%s/%s", e.getPluginName(),
+ e.getExportName());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
index 078f2dc..bdc1220 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
@@ -17,7 +17,7 @@
import com.google.common.cache.RemovalNotification;
public interface CacheRemovalListener<K,V> {
- public void onRemoval(String pluginName,
+ void onRemoval(String pluginName,
String cacheName,
RemovalNotification<K, V> notification);
-}
\ No newline at end of file
+}
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 f2d40c8..ff10351 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
@@ -82,7 +82,7 @@
throws RestApiException, UpdateException, OrmException {
ChangeControl control = req.getControl();
IdentifiedUser caller = control.getUser().asIdentifiedUser();
- if (!control.canAbandon()) {
+ if (!control.canAbandon(dbProvider.get())) {
throw new AuthException("abandon not permitted");
}
Change change = abandon(control, input.message, caller.getAccount());
@@ -174,12 +174,18 @@
@Override
public UiAction.Description getDescription(ChangeResource resource) {
+ boolean canAbandon = false;
+ try {
+ canAbandon = resource.getControl().canAbandon(dbProvider.get());
+ } catch (OrmException e) {
+ log.error("Cannot check canAbandon status. Assuming false.", e);
+ }
return new UiAction.Description()
.setLabel("Abandon")
.setTitle("Abandon the change")
.setVisible(resource.getChange().getStatus().isOpen()
&& resource.getChange().getStatus() != Change.Status.DRAFT
- && resource.getControl().canAbandon());
+ && canAbandon);
}
private static String status(Change change) {
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 cb3729b..2d06e93 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
@@ -177,7 +177,7 @@
if (edit.isPresent()) {
throw new ResourceConflictException(String.format(
"edit already exists for the change %s",
- resource.getChange().getChangeId()));
+ resource.getId()));
}
edit = createEdit();
if (!Strings.isNullOrEmpty(path)) {
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 90d6aae..1217f34 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
@@ -21,6 +21,7 @@
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.LabelTypes;
+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;
@@ -46,7 +47,6 @@
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.InvalidChangeOperationException;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.ssh.NoSshInfo;
import com.google.gerrit.server.util.RequestScopePropagator;
@@ -228,7 +228,7 @@
@Override
public void updateRepo(RepoContext ctx)
- throws InvalidChangeOperationException, IOException {
+ throws ResourceConflictException, IOException {
validate(ctx);
patchSetInfo = patchSetInfoFactory.get(
ctx.getRevWalk(), commit, patchSet.getId());
@@ -250,6 +250,7 @@
}
db.patchSets().insert(Collections.singleton(patchSet));
db.changes().insert(Collections.singleton(change));
+ update.setTopic(change.getTopic());
LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
approvalsUtil.addReviewers(db, update, labelTypes, change,
patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
@@ -307,7 +308,7 @@
}
private void validate(RepoContext ctx)
- throws IOException, InvalidChangeOperationException {
+ throws IOException, ResourceConflictException {
if (validatePolicy == CommitValidators.Policy.NONE) {
return;
}
@@ -339,7 +340,7 @@
break;
}
} catch (CommitValidationException e) {
- throw new InvalidChangeOperationException(e.getMessage());
+ throw new ResourceConflictException(e.getFullMessage());
}
}
}
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 733d7a2..7d64591 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
@@ -90,10 +90,12 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gerrit.server.api.accounts.GpgApiAdapter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LabelNormalizer;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
@@ -119,6 +121,7 @@
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.Set;
@@ -140,7 +143,6 @@
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
private final MergeUtil.Factory mergeUtilFactory;
- private final Submit submit;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeData.Factory changeDataFactory;
private final FileInfoJson fileInfoJson;
@@ -166,7 +168,6 @@
GitRepositoryManager repoManager,
ProjectCache projectCache,
MergeUtil.Factory mergeUtilFactory,
- Submit submit,
IdentifiedUser.GenericFactory uf,
ChangeData.Factory cdf,
FileInfoJson fileInfoJson,
@@ -187,7 +188,6 @@
this.repoManager = repoManager;
this.userFactory = uf;
this.projectCache = projectCache;
- this.submit = submit;
this.mergeUtilFactory = mergeUtilFactory;
this.fileInfoJson = fileInfoJson;
this.accountLoaderFactory = ailf;
@@ -407,7 +407,7 @@
// the response and avoid making a request to /submit_type from the UI.
out.mergeable = in.getStatus() == Change.Status.MERGED
? null : cd.isMergeable();
- out.submittable = submit.submittable(cd);
+ out.submittable = Submit.submittable(cd);
ChangedLines changedLines = cd.changedLines();
if (changedLines != null) {
out.insertions = changedLines.insertions;
@@ -437,6 +437,13 @@
out.permittedLabels = permittedLabels(ctl, cd);
}
out.removableReviewers = removableReviewers(ctl, out.labels.values());
+
+ out.reviewers = new HashMap<>();
+ for (Map.Entry<ReviewerStateInternal, Collection<Account.Id>> e
+ : cd.reviewers().asMap().entrySet()) {
+ out.reviewers.put(e.getKey().asReviewerState(),
+ toAccountInfo(e.getValue()));
+ }
}
boolean needMessages = has(MESSAGES);
@@ -828,6 +835,18 @@
return result;
}
+ 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);
+ }
+
private Map<String, RevisionInfo> revisions(ChangeControl ctl,
Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
GpgException, OrmException, IOException {
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 7b55b63..5148f9a 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
@@ -29,8 +29,8 @@
* implementation changes, which might invalidate old entries).
*/
public interface ChangeKindCache {
- public ChangeKind getChangeKind(ProjectState project, Repository repo,
+ ChangeKind getChangeKind(ProjectState project, Repository repo,
ObjectId prior, ObjectId next);
- public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
+ ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index 03d189f..2a69d46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -22,7 +22,9 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectState;
@@ -49,10 +51,22 @@
return control;
}
+ public IdentifiedUser getUser() {
+ return getControl().getUser().asIdentifiedUser();
+ }
+
+ public Change.Id getId() {
+ return getControl().getId();
+ }
+
public Change getChange() {
return getControl().getChange();
}
+ public Project.NameKey getProject() {
+ return getChange().getProject();
+ }
+
public ChangeNotes getNotes() {
return getControl().getNotes();
}
@@ -90,7 +104,7 @@
public String getETag() {
CurrentUser user = control.getUser();
Hasher h = Hashing.md5().newHasher()
- .putBoolean(user.getStarredChanges().contains(getChange().getId()));
+ .putBoolean(user.getStarredChanges().contains(getId()));
prepareETag(h, user);
return h.hash().toString();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index 1648a5d..96f2e13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.index.ChangeIndexer;
@@ -42,6 +43,7 @@
public class ChangesCollection implements
RestCollection<TopLevelResource, ChangeResource>,
AcceptsPost<TopLevelResource> {
+ private final Provider<ReviewDb> db;
private final Provider<CurrentUser> user;
private final ChangeControl.GenericFactory changeControlFactory;
private final Provider<QueryChanges> queryFactory;
@@ -52,6 +54,7 @@
@Inject
ChangesCollection(
+ Provider<ReviewDb> db,
Provider<CurrentUser> user,
ChangeControl.GenericFactory changeControlFactory,
Provider<QueryChanges> queryFactory,
@@ -59,6 +62,7 @@
ChangeUtil changeUtil,
CreateChange createChange,
ChangeIndexer changeIndexer) {
+ this.db = db;
this.user = user;
this.changeControlFactory = changeControlFactory;
this.queryFactory = queryFactory;
@@ -81,8 +85,8 @@
@Override
public ChangeResource parse(TopLevelResource root, IdString id)
throws ResourceNotFoundException, OrmException {
- List<Change> changes = changeUtil.findChanges(id.encoded());
- if (changes.isEmpty()) {
+ List<ChangeControl> ctls = changeUtil.findChanges(id.encoded(), user.get());
+ if (ctls.isEmpty()) {
Integer changeId = Ints.tryParse(id.get());
if (changeId != null) {
try {
@@ -92,23 +96,35 @@
}
}
}
- if (changes.size() != 1) {
+ if (ctls.isEmpty()) {
throw new ResourceNotFoundException(id);
}
+ if (ctls.size() != 1) {
+ throw new ResourceNotFoundException("Multiple changes found for " + id);
+ }
- ChangeControl control;
- try {
- control = changeControlFactory.validateFor(changes.get(0), user.get());
- } catch (NoSuchChangeException e) {
+ ChangeControl ctl = ctls.get(0);
+ if (!ctl.isVisible(db.get())) {
throw new ResourceNotFoundException(id);
}
- return new ChangeResource(control);
+ return new ChangeResource(ctl);
}
public ChangeResource parse(Change.Id id)
throws ResourceNotFoundException, OrmException {
- return parse(TopLevelResource.INSTANCE,
- IdString.fromUrl(Integer.toString(id.get())));
+ try {
+ ChangeControl ctl = changeControlFactory.controlFor(id, user.get());
+ if (!ctl.isVisible(db.get())) {
+ throw new ResourceNotFoundException(toIdString(id));
+ }
+ return new ChangeResource(ctl);
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException(toIdString(id));
+ }
+ }
+
+ private static IdString toIdString(Change.Id id) {
+ return IdString.fromDecoded(id.toString());
}
public ChangeResource parse(ChangeControl control) {
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 ddeb5c0..349117f 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
@@ -45,8 +45,12 @@
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.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeUpdate;
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.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
@@ -114,6 +118,9 @@
private final PatchSetInfoFactory patchSetInfoFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
private final BatchUpdate.Factory updateFactory;
+ private final ChangeIndexer indexer;
+ private final ChangeControl.GenericFactory changeControlFactory;
+ private final ChangeUpdate.Factory changeUpdateFactory;
private FixInput fix;
private Change change;
@@ -135,7 +142,10 @@
ProjectControl.GenericFactory projectControlFactory,
PatchSetInfoFactory patchSetInfoFactory,
PatchSetInserter.Factory patchSetInserterFactory,
- BatchUpdate.Factory updateFactory) {
+ BatchUpdate.Factory updateFactory,
+ ChangeIndexer indexer,
+ ChangeControl.GenericFactory changeControlFactory,
+ ChangeUpdate.Factory changeUpdateFactory) {
this.db = db;
this.repoManager = repoManager;
this.user = user;
@@ -144,6 +154,9 @@
this.patchSetInfoFactory = patchSetInfoFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
this.updateFactory = updateFactory;
+ this.indexer = indexer;
+ this.changeControlFactory = changeControlFactory;
+ this.changeUpdateFactory = changeUpdateFactory;
reset();
}
@@ -507,9 +520,15 @@
return c;
}
});
+ ChangeUpdate changeUpdate =
+ changeUpdateFactory.create(
+ changeControlFactory.controlFor(change, user.get()));
+ changeUpdate.fixStatus(Change.Status.MERGED);
+ changeUpdate.commit();
+ indexer.index(db.get(), change);
p.status = Status.FIXED;
p.outcome = "Marked change as merged";
- } catch (OrmException e) {
+ } catch (OrmException | IOException | NoSuchChangeException e) {
log.warn("Error marking " + change.getId() + "as merged", e);
p.status = Status.FIX_FAILED;
p.outcome = "Error updating status to merged";
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 3768738..576ae76 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
@@ -44,6 +44,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.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectsCollection;
@@ -154,19 +155,19 @@
ObjectId parentCommit;
List<String> groups;
if (input.baseChange != null) {
- List<Change> changes = changeUtil.findChanges(input.baseChange);
- if (changes.size() != 1) {
+ List<ChangeControl> ctls = changeUtil.findChanges(
+ input.baseChange, rsrc.getControl().getUser());
+ if (ctls.size() != 1) {
throw new InvalidChangeOperationException(
"Base change not found: " + input.baseChange);
}
- Change change = Iterables.getOnlyElement(changes);
- if (!rsrc.getControl().controlFor(change).isVisible(db.get())) {
+ ChangeControl ctl = Iterables.getOnlyElement(ctls);
+ if (!ctl.isVisible(db.get())) {
throw new InvalidChangeOperationException(
"Base change not found: " + input.baseChange);
}
- PatchSet ps = db.get().patchSets().get(
- new PatchSet.Id(change.getId(),
- change.currentPatchSetId().get()));
+ PatchSet ps =
+ db.get().patchSets().get(ctl.getChange().currentPatchSetId());
parentCommit = ObjectId.fromString(ps.getRevision().get());
groups = ps.getGroups();
} else {
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 a503721..0e7d96c 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
@@ -22,36 +22,39 @@
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;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+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.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.io.IOException;
-import java.sql.Timestamp;
import java.util.Collections;
@Singleton
public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
private final Provider<ReviewDb> db;
- private final ChangeUpdate.Factory updateFactory;
+ private final BatchUpdate.Factory updateFactory;
private final Provider<CommentJson> commentJson;
private final PatchLineCommentsUtil plcUtil;
private final PatchListCache patchListCache;
@Inject
CreateDraftComment(Provider<ReviewDb> db,
- ChangeUpdate.Factory updateFactory,
+ BatchUpdate.Factory updateFactory,
Provider<CommentJson> commentJson,
PatchLineCommentsUtil plcUtil,
PatchListCache patchListCache) {
@@ -64,7 +67,7 @@
@Override
public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in)
- throws BadRequestException, OrmException, IOException {
+ throws RestApiException, UpdateException, OrmException {
if (Strings.isNullOrEmpty(in.path)) {
throw new BadRequestException("path must be non-empty");
} else if (in.message == null || in.message.trim().isEmpty()) {
@@ -75,24 +78,50 @@
throw new BadRequestException("range endLine must be on the same line as the comment");
}
- int line = in.line != null
- ? in.line
- : in.range != null ? in.range.endLine : 0;
+ try (BatchUpdate bu = updateFactory.create(
+ db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ 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));
+ }
+ }
- Timestamp now = TimeUtil.nowTs();
- ChangeUpdate update = updateFactory.create(rsrc.getControl(), now);
+ private class Op extends BatchUpdate.Op {
+ private final PatchSet.Id psId;
+ private final DraftInput in;
- PatchLineComment c = new PatchLineComment(
- new PatchLineComment.Key(
- new Patch.Key(rsrc.getPatchSet().getId(), in.path),
- ChangeUtil.messageUUID(db.get())),
- line, rsrc.getAccountId(), Url.decode(in.inReplyTo), now);
- c.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
- c.setMessage(in.message.trim());
- c.setRange(in.range);
- setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
- plcUtil.insertComments(db.get(), update, Collections.singleton(c));
- update.commit();
- return Response.created(commentJson.get().setFillAccounts(false).format(c));
+ private PatchLineComment comment;
+
+ private Op(PatchSet.Id psId, DraftInput in) {
+ this.psId = psId;
+ this.in = in;
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws ResourceNotFoundException, OrmException {
+ PatchSet ps = ctx.getDb().patchSets().get(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.getUser().getAccountId(), Url.decode(in.inReplyTo),
+ ctx.getWhen());
+ comment.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+ comment.setMessage(in.message.trim());
+ comment.setRange(in.range);
+ setCommentRevId(
+ comment, patchListCache, ctx.getChange(), ps);
+ plcUtil.insertComments(
+ ctx.getDb(), ctx.getChangeUpdate(), Collections.singleton(comment));
+ }
}
}
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
index b276aae..1dce829 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -14,19 +14,18 @@
package com.google.gerrit.server.change;
-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.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.ChangeUtil;
import com.google.gerrit.server.change.DeleteDraftChange.Input;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.NoSuchChangeException;
+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;
@@ -34,50 +33,38 @@
import org.eclipse.jgit.lib.Config;
-import java.io.IOException;
-
@Singleton
public class DeleteDraftChange implements
RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> {
public static class Input {
}
- protected final Provider<ReviewDb> dbProvider;
- private final ChangeUtil changeUtil;
+ private final Provider<ReviewDb> db;
+ private final BatchUpdate.Factory updateFactory;
+ private final Provider<DeleteDraftChangeOp> opProvider;
private final boolean allowDrafts;
@Inject
- public DeleteDraftChange(Provider<ReviewDb> dbProvider,
- ChangeUtil changeUtil,
+ public DeleteDraftChange(Provider<ReviewDb> db,
+ BatchUpdate.Factory updateFactory,
+ Provider<DeleteDraftChangeOp> opProvider,
@GerritServerConfig Config cfg) {
- this.dbProvider = dbProvider;
- this.changeUtil = changeUtil;
- this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true);
+ this.db = db;
+ this.updateFactory = updateFactory;
+ this.opProvider = opProvider;
+ this.allowDrafts = DeleteDraftChangeOp.allowDrafts(cfg);
}
@Override
public Response<?> apply(ChangeResource rsrc, Input input)
- throws ResourceConflictException, AuthException,
- ResourceNotFoundException, MethodNotAllowedException,
- OrmException, IOException {
- if (rsrc.getChange().getStatus() != Status.DRAFT) {
- throw new ResourceConflictException("Change is not a draft");
+ 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();
}
-
- if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
- throw new AuthException("Not permitted to delete this draft change");
- }
-
- if (!allowDrafts) {
- throw new MethodNotAllowedException("draft workflow is disabled");
- }
-
- try {
- changeUtil.deleteDraftChange(rsrc.getChange());
- } catch (NoSuchChangeException e) {
- throw new ResourceNotFoundException(e.getMessage());
- }
-
return Response.none();
}
@@ -85,11 +72,10 @@
public UiAction.Description getDescription(ChangeResource rsrc) {
try {
return new UiAction.Description()
- .setTitle(String.format("Delete draft change %d",
- rsrc.getChange().getChangeId()))
+ .setTitle("Delete draft change " + rsrc.getId())
.setVisible(allowDrafts
&& rsrc.getChange().getStatus() == Status.DRAFT
- && rsrc.getControl().canDeleteDraft(dbProvider.get()));
+ && 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
new file mode 100644
index 0000000..41b9736
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
@@ -0,0 +1,112 @@
+// 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.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.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.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.Collections;
+import java.util.List;
+
+class DeleteDraftChangeOp extends BatchUpdate.Op {
+ static boolean allowDrafts(Config cfg) {
+ return cfg.getBoolean("change", "allowDrafts", true);
+ }
+
+ private final StarredChangesUtil starredChangesUtil;
+ private final boolean allowDrafts;
+
+ private Change.Id id;
+
+ @Inject
+ DeleteDraftChangeOp(StarredChangesUtil starredChangesUtil,
+ @GerritServerConfig Config cfg) {
+ this.starredChangesUtil = starredChangesUtil;
+ this.allowDrafts = allowDrafts(cfg);
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException {
+ 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 = 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.getChangeControl().canDeleteDraft(ctx.getDb())) {
+ throw new AuthException("Not permitted to delete this draft change");
+ }
+ List<PatchSet> patchSets = ctx.getDb().patchSets().byChange(id).toList();
+ for (PatchSet ps : patchSets) {
+ if (!ps.isDraft()) {
+ throw new ResourceConflictException("Cannot delete draft change " + id
+ + ": patch set " + ps.getPatchSetId() + " is not a draft");
+ }
+ db.accountPatchReviews().delete(
+ db.accountPatchReviews().byPatchSet(ps.getId()));
+ }
+
+ // No need to delete from notedb; draft patch sets will be filtered out.
+ db.patchComments().delete(db.patchComments().byChange(id));
+
+ db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
+ db.patchSets().delete(patchSets);
+ db.changeMessages().delete(db.changeMessages().byChange(id));
+ starredChangesUtil.unstarAll(id);
+ db.changes().delete(Collections.singleton(change));
+ ctx.markDeleted();
+ }
+
+ @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.getBatchRefUpdate().addCommand(
+ 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 c4270a9..72b4209 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
@@ -16,37 +16,44 @@
import static com.google.gerrit.server.PatchLineCommentsUtil.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.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gerrit.server.change.DeleteDraftComment.Input;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.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 DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> {
+public class DeleteDraftComment
+ implements RestModifyView<DraftCommentResource, Input> {
static class Input {
}
private final Provider<ReviewDb> db;
private final PatchLineCommentsUtil plcUtil;
- private final ChangeUpdate.Factory updateFactory;
+ private final BatchUpdate.Factory updateFactory;
private final PatchListCache patchListCache;
@Inject
DeleteDraftComment(Provider<ReviewDb> db,
PatchLineCommentsUtil plcUtil,
- ChangeUpdate.Factory updateFactory,
+ BatchUpdate.Factory updateFactory,
PatchListCache patchListCache) {
this.db = db;
this.plcUtil = plcUtil;
@@ -56,13 +63,41 @@
@Override
public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
- throws OrmException, IOException {
- ChangeUpdate update = updateFactory.create(rsrc.getControl());
-
- PatchLineComment c = rsrc.getComment();
- setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
- plcUtil.deleteComments(db.get(), update, Collections.singleton(c));
- update.commit();
+ throws RestApiException, UpdateException {
+ try (BatchUpdate bu = updateFactory.create(
+ db.get(), rsrc.getChange().getProject(), rsrc.getControl().getUser(),
+ TimeUtil.nowTs())) {
+ Op op = new Op(rsrc.getComment().getKey());
+ bu.addOp(rsrc.getChange().getId(), op);
+ bu.execute();
+ }
return Response.none();
}
+
+ private class Op extends BatchUpdate.Op {
+ private final PatchLineComment.Key key;
+
+ private Op(PatchLineComment.Key key) {
+ this.key = key;
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws ResourceNotFoundException, OrmException {
+ Optional<PatchLineComment> maybeComment =
+ plcUtil.get(ctx.getDb(), ctx.getChangeNotes(), key);
+ if (!maybeComment.isPresent()) {
+ return; // Nothing to do.
+ }
+ PatchSet.Id psId = key.getParentKey().getParentKey();
+ PatchSet ps = ctx.getDb().patchSets().get(psId);
+ if (ps == null) {
+ throw new ResourceNotFoundException("patch set not found: " + psId);
+ }
+ PatchLineComment c = maybeComment.get();
+ setCommentRevId(c, patchListCache, ctx.getChange(), ps);
+ plcUtil.deleteComments(
+ ctx.getDb(), ctx.getChangeUpdate(), Collections.singleton(c));
+ }
+ }
}
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 a266337..7edf483 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,11 +14,13 @@
package com.google.gerrit.server.change;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.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.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
@@ -28,19 +30,23 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.DeleteDraftPatchSet.Input;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.ChangeIndexer;
+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.UpdateException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
+import java.util.Collections;
@Singleton
public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Input>,
@@ -48,131 +54,134 @@
public static class Input {
}
- protected final Provider<ReviewDb> dbProvider;
+ private final Provider<ReviewDb> db;
+ private final BatchUpdate.Factory updateFactory;
private final PatchSetInfoFactory patchSetInfoFactory;
- private final ChangeUtil changeUtil;
- private final ChangeIndexer indexer;
+ private final Provider<DeleteDraftChangeOp> deleteChangeOpProvider;
private final boolean allowDrafts;
@Inject
- public DeleteDraftPatchSet(Provider<ReviewDb> dbProvider,
+ public DeleteDraftPatchSet(Provider<ReviewDb> db,
+ BatchUpdate.Factory updateFactory,
PatchSetInfoFactory patchSetInfoFactory,
- ChangeUtil changeUtil,
- ChangeIndexer indexer,
+ Provider<DeleteDraftChangeOp> deleteChangeOpProvider,
@GerritServerConfig Config cfg) {
- this.dbProvider = dbProvider;
+ this.db = db;
+ this.updateFactory = updateFactory;
this.patchSetInfoFactory = patchSetInfoFactory;
- this.changeUtil = changeUtil;
- this.indexer = indexer;
+ this.deleteChangeOpProvider = deleteChangeOpProvider;
this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true);
}
@Override
public Response<?> apply(RevisionResource rsrc, Input input)
- throws AuthException, ResourceNotFoundException,
- ResourceConflictException, MethodNotAllowedException,
- OrmException, IOException {
- PatchSet patchSet = rsrc.getPatchSet();
- PatchSet.Id patchSetId = patchSet.getId();
- Change change = rsrc.getChange();
-
- if (!patchSet.isDraft()) {
- throw new ResourceConflictException("Patch set is not a draft");
+ throws RestApiException, UpdateException {
+ try (BatchUpdate bu = updateFactory.create(
+ db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ bu.setOrder(BatchUpdate.Order.DB_BEFORE_REPO);
+ bu.addOp(rsrc.getChange().getId(), new Op(rsrc.getPatchSet().getId()));
+ bu.execute();
}
-
- if (!allowDrafts) {
- throw new MethodNotAllowedException("draft workflow is disabled");
- }
-
- if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
- throw new AuthException("Not permitted to delete this draft patch set");
- }
-
- deleteDraftPatchSet(patchSet, change);
- deleteOrUpdateDraftChange(patchSetId, change);
-
return Response.none();
}
+ private class Op extends BatchUpdate.Op {
+ private final PatchSet.Id psId;
+
+ private PatchSet patchSet;
+ private DeleteDraftChangeOp deleteChangeOp;
+
+ private Op(PatchSet.Id psId) {
+ this.psId = psId;
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws RestApiException, OrmException, IOException {
+ patchSet = ctx.getDb().patchSets().get(psId);
+ if (patchSet == null) {
+ return; // Nothing to do.
+ }
+ if (!patchSet.isDraft()) {
+ throw new ResourceConflictException("Patch set is not a draft");
+ }
+ if (!allowDrafts) {
+ throw new MethodNotAllowedException("Draft workflow is disabled");
+ }
+ if (!ctx.getChangeControl().canDeleteDraft(ctx.getDb())) {
+ throw new AuthException("Not permitted to delete this draft patch set");
+ }
+
+ deleteDraftPatchSet(patchSet, ctx);
+ deleteOrUpdateDraftChange(ctx);
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) throws IOException {
+ if (deleteChangeOp != null) {
+ deleteChangeOp.updateRepo(ctx);
+ return;
+ }
+ ctx.getBatchRefUpdate().addCommand(
+ new ReceiveCommand(
+ ObjectId.fromString(patchSet.getRevision().get()),
+ ObjectId.zeroId(),
+ patchSet.getRefName()));
+ }
+
+ private void deleteDraftPatchSet(PatchSet patchSet, ChangeContext ctx)
+ throws OrmException {
+ ReviewDb db = ctx.getDb();
+ db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(psId));
+ db.changeMessages().delete(db.changeMessages().byPatchSet(psId));
+ // No need to delete from notedb; draft patch sets will be filtered out.
+ db.patchComments().delete(db.patchComments().byPatchSet(psId));
+ db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(psId));
+ db.patchSets().delete(Collections.singleton(patchSet));
+ }
+
+ private void deleteOrUpdateDraftChange(ChangeContext ctx)
+ throws OrmException, RestApiException {
+ Change c = ctx.getChange();
+ if (Iterables.isEmpty(ctx.getDb().patchSets().byChange(c.getId()))) {
+ deleteChangeOp = deleteChangeOpProvider.get();
+ deleteChangeOp.updateChange(ctx);
+ return;
+ }
+ if (c.currentPatchSetId().equals(psId)) {
+ c.setCurrentPatchSet(previousPatchSetInfo(ctx));
+ }
+ ChangeUtil.updated(c);
+ ctx.getDb().changes().update(Collections.singleton(c));
+ }
+
+ private PatchSetInfo previousPatchSetInfo(ChangeContext ctx)
+ throws OrmException {
+ try {
+ // TODO(dborowitz): Get this in a way that doesn't involve re-opening
+ // the repo after the updateRepo phase.
+ return patchSetInfoFactory.get(ctx.getDb(),
+ new PatchSet.Id(psId.getParentKey(), psId.get() - 1));
+ } catch (PatchSetInfoNotAvailableException e) {
+ throw new OrmException(e);
+ }
+ }
+ }
+
@Override
public UiAction.Description getDescription(RevisionResource rsrc) {
try {
- int psCount = dbProvider.get().patchSets()
+ int psCount = db.get().patchSets()
.byChange(rsrc.getChange().getId()).toList().size();
return new UiAction.Description()
.setTitle(String.format("Delete draft revision %d",
rsrc.getPatchSet().getPatchSetId()))
.setVisible(allowDrafts
&& rsrc.getPatchSet().isDraft()
- && rsrc.getControl().canDeleteDraft(dbProvider.get())
+ && rsrc.getControl().canDeleteDraft(db.get())
&& psCount > 1);
} catch (OrmException e) {
throw new IllegalStateException(e);
}
}
-
- private void deleteDraftPatchSet(PatchSet patchSet, Change change)
- throws ResourceNotFoundException, OrmException, IOException {
- try {
- changeUtil.deleteOnlyDraftPatchSet(patchSet, change);
- } catch (NoSuchChangeException e) {
- throw new ResourceNotFoundException(e.getMessage());
- }
- }
-
- private void deleteOrUpdateDraftChange(PatchSet.Id patchSetId,
- Change change) throws OrmException, ResourceNotFoundException,
- IOException {
- if (dbProvider.get()
- .patchSets()
- .byChange(change.getId())
- .toList().size() == 0) {
- deleteDraftChange(change);
- } else {
- if (change.currentPatchSetId().equals(patchSetId)) {
- updateChange(dbProvider.get(), change,
- previousPatchSetInfo(patchSetId));
- } else {
- // TODO(davido): find a better way to enforce cache invalidation.
- updateChange(dbProvider.get(), change, null);
- }
- }
- }
-
- private void deleteDraftChange(Change change)
- throws OrmException, IOException, ResourceNotFoundException {
- try {
- changeUtil.deleteDraftChange(change);
- } catch (NoSuchChangeException e) {
- throw new ResourceNotFoundException(e.getMessage());
- }
- }
-
- private PatchSetInfo previousPatchSetInfo(PatchSet.Id patchSetId)
- throws OrmException {
- try {
- return patchSetInfoFactory.get(dbProvider.get(),
- new PatchSet.Id(patchSetId.getParentKey(),
- patchSetId.get() - 1));
- } catch (PatchSetInfoNotAvailableException e) {
- throw new OrmException(e);
- }
- }
-
- private void updateChange(final ReviewDb db,
- Change change, final PatchSetInfo psInfo)
- throws OrmException, IOException {
- change = db.changes().atomicUpdate(change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change c) {
- if (psInfo != null) {
- c.setCurrentPatchSet(psInfo);
- }
- ChangeUtil.updated(c);
- return c;
- }
- });
- indexer.index(db, change);
- }
}
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 6c37252..b1eb630 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
@@ -75,7 +75,7 @@
throws AuthException, ResourceNotFoundException, OrmException,
IOException {
ChangeControl control = rsrc.getControl();
- Change.Id changeId = rsrc.getChange().getId();
+ Change.Id changeId = rsrc.getChangeId();
ReviewDb db = dbProvider.get();
ChangeUpdate update = updateFactory.create(rsrc.getControl());
@@ -103,13 +103,13 @@
if (del.isEmpty()) {
throw new ResourceNotFoundException();
}
- ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+ ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChangeId(), db);
db.patchSetApprovals().delete(del);
- update.removeReviewer(rsrc.getUser().getAccountId());
+ update.removeReviewer(rsrc.getReviewerUser().getAccountId());
if (msg.length() > 0) {
ChangeMessage changeMessage =
- new ChangeMessage(new ChangeMessage.Key(rsrc.getChange().getId(),
+ new ChangeMessage(new ChangeMessage.Key(rsrc.getChangeId(),
ChangeUtil.messageUUID(db)),
control.getUser().getAccountId(),
TimeUtil.nowTs(), rsrc.getChange().currentPatchSetId());
@@ -136,9 +136,10 @@
private Iterable<PatchSetApproval> approvals(ReviewDb db,
ReviewerResource rsrc) throws OrmException {
- final Account.Id user = rsrc.getUser().getAccountId();
+ final Account.Id user = rsrc.getReviewerUser().getAccountId();
return Iterables.filter(
- approvalsUtil.byChange(db, rsrc.getNotes()).values(),
+ approvalsUtil.byChange(db, rsrc.getChangeResource().getNotes())
+ .values(),
new Predicate<PatchSetApproval>() {
@Override
public boolean apply(PatchSetApproval input) {
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
new file mode 100644
index 0000000..7392cdb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -0,0 +1,149 @@
+// 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.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.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.change.DeleteVote.Input;
+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.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 DeleteVote implements RestModifyView<VoteResource, Input> {
+ public static class Input {
+ }
+
+ private final Provider<ReviewDb> db;
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final ApprovalsUtil approvalsUtil;
+ private final ChangeMessagesUtil cmUtil;
+ private final IdentifiedUser.GenericFactory userFactory;
+
+ @Inject
+ DeleteVote(Provider<ReviewDb> db,
+ BatchUpdate.Factory batchUpdateFactory,
+ ApprovalsUtil approvalsUtil,
+ ChangeMessagesUtil cmUtil,
+ IdentifiedUser.GenericFactory userFactory) {
+ this.db = db;
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.approvalsUtil = approvalsUtil;
+ this.cmUtil = cmUtil;
+ this.userFactory = userFactory;
+ }
+
+ @Override
+ public Response<?> apply(VoteResource rsrc, Input input)
+ throws RestApiException, UpdateException {
+ ReviewerResource r = rsrc.getReviewer();
+ Change change = r.getChange();
+ try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
+ change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) {
+ bu.addOp(change.getId(),
+ new Op(r.getReviewerUser().getAccountId(), rsrc.getLabel()));
+ bu.execute();
+ }
+
+ return Response.none();
+ }
+
+ private class Op extends BatchUpdate.Op {
+ private final Account.Id accountId;
+ private final String label;
+
+ private Op(Account.Id accountId, String label) {
+ this.accountId = accountId;
+ this.label = label;
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws OrmException, AuthException, ResourceNotFoundException {
+ IdentifiedUser user = ctx.getUser().asIdentifiedUser();
+ Change change = ctx.getChange();
+ ChangeControl ctl = ctx.getChangeControl();
+ PatchSet.Id psId = change.currentPatchSetId();
+
+ PatchSetApproval psa = null;
+ StringBuilder msg = new StringBuilder();
+ for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+ ctx.getDb(), ctl, psId, accountId)) {
+ if (ctl.canRemoveReviewer(a)) {
+ if (a.getLabel().equals(label)) {
+ msg.append("Removed ")
+ .append(a.getLabel()).append(formatLabelValue(a.getValue()))
+ .append(" by ").append(userFactory.create(user.getAccountId())
+ .getNameEmail())
+ .append("\n");
+ psa = a;
+ a.setValue((short)0);
+ ctx.getChangeUpdate().setPatchSetId(psId);
+ ctx.getChangeUpdate().removeApproval(label);
+ break;
+ }
+ } else {
+ throw new AuthException("delete not permitted");
+ }
+ }
+ if (psa == null) {
+ throw new ResourceNotFoundException();
+ }
+ ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), ctx.getDb());
+ ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
+
+ if (msg.length() > 0) {
+ ChangeMessage changeMessage =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(),
+ ChangeUtil.messageUUID(ctx.getDb())),
+ user.getAccountId(),
+ ctx.getWhen(),
+ change.currentPatchSetId());
+ changeMessage.setMessage(msg.toString());
+ cmUtil.addChangeMessage(ctx.getDb(), ctx.getChangeUpdate(),
+ changeMessage);
+ }
+ }
+ }
+
+ private static String formatLabelValue(short value) {
+ if (value > 0) {
+ return "+" + value;
+ } else {
+ return Short.toString(value);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 71974c4..e31b11f 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
@@ -64,6 +64,7 @@
? e.getChangeType().getCode() : null;
d.oldPath = e.getOldName();
d.sizeDelta = e.getSizeDelta();
+ d.size = e.getSize();
if (e.getPatchType() == Patch.PatchType.BINARY) {
d.binary = true;
} else {
@@ -78,6 +79,7 @@
// a single record with data from both sides.
d.status = Patch.ChangeType.REWRITE.getCode();
d.sizeDelta = o.sizeDelta;
+ d.size = o.size;
if (o.binary != null && o.binary) {
d.binary = true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
index 547a500..c099e43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -39,9 +39,9 @@
}
}
- public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
+ boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db);
- public Boolean getIfPresent(ObjectId commit, Ref intoRef,
+ Boolean getIfPresent(ObjectId commit, Ref intoRef,
SubmitType submitType, String mergeStrategy);
}
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 9c588ed..0ef8b51 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.VoteResource.VOTE_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -37,6 +38,7 @@
bind(DraftComments.class);
bind(Comments.class);
bind(Files.class);
+ bind(Votes.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
@@ -45,6 +47,7 @@
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
+ DynamicMap.mapOf(binder(), VOTE_KIND);
get(CHANGE_KIND).to(GetChange.class);
get(CHANGE_KIND, "detail").to(GetDetail.class);
@@ -73,6 +76,8 @@
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
get(REVIEWER_KIND).to(GetReviewer.class);
delete(REVIEWER_KIND).to(DeleteReviewer.class);
+ child(REVIEWER_KIND, "votes").to(Votes.class);
+ delete(VOTE_KIND).to(DeleteVote.class);
child(CHANGE_KIND, "revisions").to(Revisions.class);
get(REVISION_KIND, "actions").to(GetRevisionActions.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index c34fb43..9a72b07 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
@@ -17,6 +17,8 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.ChangeHooks;
@@ -42,7 +44,7 @@
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.notedb.ReviewerState;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ChangeModifiedException;
@@ -106,7 +108,7 @@
private PatchSet patchSet;
private PatchSetInfo patchSetInfo;
private ChangeMessage changeMessage;
- private SetMultimap<ReviewerState, Account.Id> oldReviewers;
+ private SetMultimap<ReviewerStateInternal, Account.Id> oldReviewers;
@AssistedInject
public PatchSetInserter(ChangeHooks hooks,
@@ -235,8 +237,7 @@
if (message != null) {
changeMessage = new ChangeMessage(
- new ChangeMessage.Key(
- ctl.getChange().getId(), ChangeUtil.messageUUID(db)),
+ new ChangeMessage.Key(ctl.getId(), ChangeUtil.messageUUID(db)),
ctx.getUser().getAccountId(), ctx.getWhen(), patchSet.getId());
changeMessage.setMessage(message);
}
@@ -280,8 +281,8 @@
cm.setFrom(ctx.getUser().getAccountId());
cm.setPatchSet(patchSet, patchSetInfo);
cm.setChangeMessage(changeMessage);
- cm.addReviewers(oldReviewers.get(ReviewerState.REVIEWER));
- cm.addExtraCC(oldReviewers.get(ReviewerState.CC));
+ cm.addReviewers(oldReviewers.get(REVIEWER));
+ cm.addExtraCC(oldReviewers.get(CC));
cm.send();
} catch (Exception err) {
log.error("Cannot send email for new patch set on change "
@@ -328,7 +329,7 @@
break;
}
} catch (CommitValidationException e) {
- throw new ResourceConflictException(e.getMessage());
+ throw new ResourceConflictException(e.getFullMessage());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index a3fc2e1..6d7720d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -52,7 +52,7 @@
req.getChange().getProject(), req.getControl().getUser(),
TimeUtil.nowTs())) {
SetHashtagsOp op = hashtagsFactory.create(input);
- bu.addOp(req.getChange().getId(), op);
+ bu.addOp(req.getId(), op);
bu.execute();
return Response.<ImmutableSortedSet<String>> ok(op.getUpdatedHashtags());
}
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 e2473e7..f2d0ffa 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
@@ -17,6 +17,7 @@
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.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
@@ -349,7 +350,8 @@
}
@Override
- public void updateChange(ChangeContext ctx) throws OrmException {
+ public void updateChange(ChangeContext ctx)
+ throws OrmException, ResourceConflictException {
user = ctx.getUser().asIdentifiedUser();
change = ctx.getChange();
if (change.getLastUpdatedOn().before(ctx.getWhen())) {
@@ -500,7 +502,8 @@
return drafts;
}
- private boolean updateLabels(ChangeContext ctx) throws OrmException {
+ private boolean updateLabels(ChangeContext ctx)
+ throws OrmException, ResourceConflictException {
Map<String, Short> labels = in.labels;
if (labels == null) {
labels = Collections.emptyMap();
@@ -515,10 +518,6 @@
for (Map.Entry<String, Short> ent : labels.entrySet()) {
String name = ent.getKey();
LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
- if (ctx.getChange().getStatus().isClosed()) {
- // TODO Allow updating some labels even when closed.
- continue;
- }
PatchSetApproval c = current.remove(lt.getName());
String normName = lt.getName();
@@ -550,10 +549,16 @@
ups.add(c);
addLabelDelta(normName, c.getValue());
categories.put(normName, c.getValue());
+ update.putReviewer(user.getAccountId(), REVIEWER);
update.putApproval(ent.getKey(), ent.getValue());
}
}
+ if (!del.isEmpty() || !ups.isEmpty()) {
+ if (ctx.getChange().getStatus().isClosed()) {
+ throw new ResourceConflictException("change is closed");
+ }
+ }
forceCallerAsReviewer(ctx, current, ups, del);
ctx.getDb().patchSetApprovals().delete(del);
ctx.getDb().patchSetApprovals().upsert(ups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 3ab84ab..3330db5 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
@@ -149,11 +149,12 @@
private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
IOException {
- Account member = rsrc.getUser().getAccount();
- ChangeControl control = rsrc.getControl();
+ Account member = rsrc.getReviewerUser().getAccount();
+ ChangeControl control = rsrc.getReviewerControl();
PostResult result = new PostResult();
if (isValidReviewer(member, control)) {
- addReviewers(rsrc, result, ImmutableMap.of(member.getId(), control));
+ addReviewers(rsrc.getChangeResource(), result,
+ ImmutableMap.of(member.getId(), control));
}
return result;
}
@@ -230,9 +231,9 @@
ReviewDb db = dbProvider.get();
ChangeUpdate update = updateFactory.create(rsrc.getControl());
List<PatchSetApproval> added;
- db.changes().beginTransaction(rsrc.getChange().getId());
+ db.changes().beginTransaction(rsrc.getId());
try {
- ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+ ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getId(), db);
added = approvalsUtil.addReviewers(db, rsrc.getNotes(), update,
rsrc.getControl().getLabelTypes(), rsrc.getChange(),
reviewers.keySet());
@@ -243,7 +244,7 @@
update.commit();
CheckedFuture<?, IOException> indexFuture =
- indexer.indexAsync(rsrc.getChange().getId());
+ indexer.indexAsync(rsrc.getId());
result.reviewers = Lists.newArrayListWithCapacity(added.size());
for (PatchSetApproval psa : added) {
// New reviewers have value 0, don't bother normalizing.
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 a4a5e16..fe4744e 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
@@ -16,26 +16,31 @@
import static com.google.gerrit.server.PatchLineCommentsUtil.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.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+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.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.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
@@ -44,7 +49,7 @@
private final Provider<ReviewDb> db;
private final DeleteDraftComment delete;
private final PatchLineCommentsUtil plcUtil;
- private final ChangeUpdate.Factory updateFactory;
+ private final BatchUpdate.Factory updateFactory;
private final Provider<CommentJson> commentJson;
private final PatchListCache patchListCache;
@@ -52,7 +57,7 @@
PutDraftComment(Provider<ReviewDb> db,
DeleteDraftComment delete,
PatchLineCommentsUtil plcUtil,
- ChangeUpdate.Factory updateFactory,
+ BatchUpdate.Factory updateFactory,
Provider<CommentJson> commentJson,
PatchListCache patchListCache) {
this.db = db;
@@ -65,9 +70,7 @@
@Override
public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in) throws
- BadRequestException, OrmException, IOException {
- PatchLineComment c = rsrc.getComment();
- ChangeUpdate update = updateFactory.create(rsrc.getControl());
+ RestApiException, UpdateException, OrmException {
if (in == null || in.message == null || in.message.trim().isEmpty()) {
return delete.apply(rsrc, null);
} else if (in.id != null && !rsrc.getId().equals(in.id)) {
@@ -78,34 +81,74 @@
throw new BadRequestException("range endLine must be on the same line as the comment");
}
- if (in.path != null
- && !in.path.equals(c.getKey().getParentKey().getFileName())) {
- // Updating the path alters the primary key, which isn't possible.
- // Delete then recreate the comment instead of an update.
-
- plcUtil.deleteComments(db.get(), update, Collections.singleton(c));
- c = new PatchLineComment(
- new PatchLineComment.Key(
- new Patch.Key(rsrc.getPatchSet().getId(), in.path),
- c.getKey().get()),
- c.getLine(),
- rsrc.getAuthorId(),
- c.getParentUuid(), TimeUtil.nowTs());
- setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
- plcUtil.insertComments(db.get(), update,
- Collections.singleton(update(c, in)));
- } else {
- if (c.getRevId() == null) {
- setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
- }
- plcUtil.updateComments(db.get(), update,
- Collections.singleton(update(c, in)));
+ try (BatchUpdate bu = updateFactory.create(
+ db.get(), rsrc.getChange().getProject(), rsrc.getControl().getUser(),
+ TimeUtil.nowTs())) {
+ Op op = new Op(rsrc.getComment().getKey(), in);
+ bu.addOp(rsrc.getChange().getId(), op);
+ bu.execute();
+ return Response.ok(
+ commentJson.get().setFillAccounts(false).format(op.comment));
}
- update.commit();
- return Response.ok(commentJson.get().setFillAccounts(false).format(c));
}
- private PatchLineComment update(PatchLineComment e, DraftInput in) {
+ private class Op extends BatchUpdate.Op {
+ private final PatchLineComment.Key key;
+ private final DraftInput in;
+
+ private PatchLineComment comment;
+
+ private Op(PatchLineComment.Key key, DraftInput in) {
+ this.key = key;
+ this.in = in;
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx)
+ throws ResourceNotFoundException, OrmException {
+ Optional<PatchLineComment> maybeComment =
+ plcUtil.get(ctx.getDb(), ctx.getChangeNotes(), 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);
+ }
+ comment = maybeComment.get();
+
+ PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
+ PatchSet ps = ctx.getDb().patchSets().get(psId);
+ if (ps == null) {
+ throw new ResourceNotFoundException("patch set not found: " + psId);
+ }
+ if (in.path != null
+ && !in.path.equals(comment.getKey().getParentKey().getFileName())) {
+ // Updating the path alters the primary key, which isn't possible.
+ // Delete then recreate the comment instead of an update.
+
+ plcUtil.deleteComments(
+ ctx.getDb(), ctx.getChangeUpdate(), Collections.singleton(comment));
+ comment = new PatchLineComment(
+ new PatchLineComment.Key(
+ new Patch.Key(psId, in.path),
+ comment.getKey().get()),
+ comment.getLine(),
+ ctx.getUser().getAccountId(),
+ comment.getParentUuid(), ctx.getWhen());
+ setCommentRevId(comment, patchListCache, ctx.getChange(), ps);
+ plcUtil.insertComments(ctx.getDb(), ctx.getChangeUpdate(),
+ Collections.singleton(update(comment, in)));
+ } else {
+ if (comment.getRevId() == null) {
+ setCommentRevId(
+ comment, patchListCache, ctx.getChange(), ps);
+ }
+ plcUtil.updateComments(ctx.getDb(), ctx.getChangeUpdate(),
+ Collections.singleton(update(comment, in)));
+ }
+ }
+ }
+
+ private static PatchLineComment update(PatchLineComment e, DraftInput in) {
if (in.side != null) {
e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 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 1061d4b..105654b 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
@@ -77,7 +77,7 @@
Op op = new Op(ctl, input != null ? input : new Input());
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
- u.addOp(req.getChange().getId(), op);
+ u.addOp(req.getId(), op);
u.execute();
}
return Strings.isNullOrEmpty(op.newTopicName)
@@ -101,8 +101,8 @@
@Override
public void updateChange(ChangeContext ctx) throws OrmException {
change = ctx.getChange();
- String newTopicName = Strings.nullToEmpty(input.topic);
- String oldTopicName = Strings.nullToEmpty(change.getTopic());
+ newTopicName = Strings.nullToEmpty(input.topic);
+ oldTopicName = Strings.nullToEmpty(change.getTopic());
if (oldTopicName.equals(newTopicName)) {
return;
}
@@ -116,6 +116,7 @@
oldTopicName, newTopicName);
}
change.setTopic(Strings.emptyToNull(newTopicName));
+ ctx.getChangeUpdate().setTopic(change.getTopic());
ChangeUtil.updated(change);
ctx.getDb().changes().update(Collections.singleton(change));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 60f285f..5bcd23c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -90,7 +90,7 @@
ObjectInserter oi = repo.newObjectInserter();
BatchUpdate bu = updateFactory.create(dbProvider.get(),
change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
- if (!control.canRebase()) {
+ if (!control.canRebase(dbProvider.get())) {
throw new AuthException("rebase not permitted");
} else if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is "
@@ -209,9 +209,15 @@
public UiAction.Description getDescription(RevisionResource resource) {
PatchSet patchSet = resource.getPatchSet();
Branch.NameKey dest = resource.getChange().getDest();
+ boolean canRebase = false;
+ try {
+ canRebase = resource.getControl().canRebase(dbProvider.get());
+ } catch (OrmException e) {
+ log.error("Cannot check canRebase status. Assuming false.", e);
+ }
boolean visible = resource.getChange().getStatus().isOpen()
&& resource.isCurrent()
- && resource.getControl().canRebase();
+ && canRebase;
boolean enabled = true;
if (visible) {
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 5b0eb6d..421fced 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
@@ -80,14 +80,14 @@
public ChangeInfo apply(ChangeResource req, RestoreInput input)
throws RestApiException, UpdateException, OrmException {
ChangeControl ctl = req.getControl();
- if (!ctl.canRestore()) {
+ if (!ctl.canRestore(dbProvider.get())) {
throw new AuthException("restore not permitted");
}
Op op = new Op(input);
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
- u.addOp(req.getChange().getId(), op).execute();
+ u.addOp(req.getId(), op).execute();
}
return json.create(ChangeJson.NO_OPTIONS).format(op.change);
}
@@ -160,11 +160,17 @@
@Override
public UiAction.Description getDescription(ChangeResource resource) {
+ boolean canRestore = false;
+ try {
+ canRestore = resource.getControl().canRestore(dbProvider.get());
+ } catch (OrmException e) {
+ log.error("Cannot check canRestore status. Assuming false.", e);
+ }
return new UiAction.Description()
.setLabel("Restore")
.setTitle("Restore the change")
.setVisible(resource.getChange().getStatus() == Status.ABANDONED
- && resource.getControl().canRestore());
+ && canRestore);
}
private static String status(Change change) {
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 dc2ed5d..7e3845c 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
@@ -29,8 +29,8 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.git.UpdateException;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -59,9 +59,9 @@
public ChangeInfo apply(ChangeResource req, RevertInput input)
throws IOException, OrmException, RestApiException,
UpdateException {
- ChangeControl control = req.getControl();
+ RefControl refControl = req.getControl().getRefControl();
Change change = req.getChange();
- if (!control.canAddPatchSet()) {
+ if (!refControl.canUpload()) {
throw new AuthException("revert not permitted");
} else if (change.getStatus() != Status.MERGED) {
throw new ResourceConflictException("change is " + status(change));
@@ -69,7 +69,7 @@
Change.Id revertedChangeId;
try {
- revertedChangeId = changeUtil.revert(control,
+ revertedChangeId = changeUtil.revert(req.getControl(),
change.currentPatchSetId(),
Strings.emptyToNull(input.message),
new PersonIdent(myIdent, TimeUtil.nowTs()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 7c10b11..ec0f937 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -67,8 +67,8 @@
AccountLoader loader = accountLoaderFactory.create(true);
for (ReviewerResource rsrc : rsrcs) {
ReviewerInfo info = format(new ReviewerInfo(
- rsrc.getUser().getAccountId()),
- rsrc.getUserControl());
+ rsrc.getReviewerUser().getAccountId()),
+ rsrc.getReviewerControl());
loader.put(info);
infos.add(info);
}
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 f7b5228..d48151b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -14,48 +14,64 @@
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.Change;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
-public class ReviewerResource extends ChangeResource {
+public class ReviewerResource implements RestResource {
public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
new TypeLiteral<RestView<ReviewerResource>>() {};
public static interface Factory {
- ReviewerResource create(ChangeResource rsrc, IdentifiedUser user);
- ReviewerResource create(ChangeResource rsrc, Account.Id id);
+ ReviewerResource create(ChangeResource change, Account.Id id);
}
+ private final ChangeResource change;
private final IdentifiedUser user;
@AssistedInject
- ReviewerResource(@Assisted ChangeResource rsrc,
- @Assisted IdentifiedUser user) {
- super(rsrc);
- this.user = user;
- }
-
- @AssistedInject
ReviewerResource(IdentifiedUser.GenericFactory userFactory,
- @Assisted ChangeResource rsrc,
+ @Assisted ChangeResource change,
@Assisted Account.Id id) {
- this(rsrc, userFactory.create(id));
+ this.change = change;
+ this.user = userFactory.create(id);
}
- public IdentifiedUser getUser() {
+ public ChangeResource getChangeResource() {
+ return change;
+ }
+
+ public Change.Id getChangeId() {
+ return change.getId();
+ }
+
+ public Change getChange() {
+ return change.getChange();
+ }
+
+ public IdentifiedUser getReviewerUser() {
return user;
}
/**
+ * @return the control for the caller's user (as opposed to the reviewer's
+ * user as returned by {@link #getReviewerControl()}).
+ */
+ public ChangeControl getControl() {
+ return change.getControl();
+ }
+
+ /**
* @return the control for the reviewer's user (as opposed to the caller's
* user as returned by {@link #getControl()}).
*/
- public ChangeControl getUserControl() {
- return getControl().forUser(user);
+ public ChangeControl getReviewerControl() {
+ return change.getControl().forUser(user);
}
}
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 6731dd9..685ff7b 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
@@ -21,6 +21,7 @@
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.server.IdentifiedUser;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -63,6 +64,10 @@
return getControl().getChange();
}
+ public Project.NameKey getProject() {
+ return getChange().getProject();
+ }
+
public ChangeNotes getNotes() {
return getChangeResource().getNotes();
}
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 bb5775b..734ebb6 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
@@ -129,7 +129,7 @@
// Chance of collision rises; look at all patch sets on the change.
List<RevisionResource> out = Lists.newArrayList();
for (PatchSet ps : dbProvider.get().patchSets()
- .byChange(change.getChange().getId())) {
+ .byChange(change.getId())) {
if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
out.add(new RevisionResource(change, ps));
}
@@ -141,7 +141,7 @@
private List<RevisionResource> byLegacyPatchSetId(ChangeResource change,
String id) throws OrmException {
PatchSet ps = dbProvider.get().patchSets().get(new PatchSet.Id(
- change.getChange().getId(),
+ change.getId(),
Integer.parseInt(id)));
if (ps != null) {
return Collections.singletonList(new RevisionResource(change, ps));
@@ -161,8 +161,7 @@
throws AuthException, IOException {
Optional<ChangeEdit> edit = editUtil.byChange(change.getChange());
if (edit.isPresent()) {
- PatchSet ps = new PatchSet(new PatchSet.Id(
- change.getChange().getId(), 0));
+ PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0));
ps.setRevision(edit.get().getRevision());
if (revid == null || edit.get().getRevision().equals(revid)) {
return Collections.singletonList(
@@ -174,7 +173,7 @@
private static List<RevisionResource> toResources(final ChangeResource change,
Iterable<PatchSet> patchSets) {
- final Change.Id changeId = change.getChange().getId();
+ final Change.Id changeId = change.getId();
return FluentIterable.from(patchSets)
.filter(new Predicate<PatchSet>() {
@Override
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 1a92ec0..d22b1dd 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
@@ -259,7 +259,7 @@
* @param cd the change to check for submittability
* @return if the change has any problems for submission
*/
- public boolean submittable(ChangeData cd) {
+ public static boolean submittable(ChangeData cd) {
try {
MergeOp.checkSubmitRule(cd);
return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java
new file mode 100644
index 0000000..4dfaff0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/VoteResource.java
@@ -0,0 +1,40 @@
+// 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.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class VoteResource implements RestResource {
+ public static final TypeLiteral<RestView<VoteResource>> VOTE_KIND =
+ new TypeLiteral<RestView<VoteResource>>() {};
+
+ private final ReviewerResource reviewer;
+ private final String label;
+
+ public VoteResource(ReviewerResource reviewer, String label) {
+ this.reviewer = reviewer;
+ this.label = label;
+ }
+
+ public ReviewerResource getReviewer() {
+ return reviewer;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+}
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
new file mode 100644
index 0000000..3bba37e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -0,0 +1,89 @@
+// 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.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.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+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.Map;
+import java.util.TreeMap;
+
+@Singleton
+public class Votes implements ChildCollection<ReviewerResource, VoteResource> {
+ private final DynamicMap<RestView<VoteResource>> views;
+ private final List list;
+
+ @Inject
+ Votes(DynamicMap<RestView<VoteResource>> views,
+ List list) {
+ this.views = views;
+ this.list = list;
+ }
+
+ @Override
+ public DynamicMap<RestView<VoteResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<ReviewerResource> list() throws AuthException {
+ return list;
+ }
+
+ @Override
+ public VoteResource parse(ReviewerResource reviewer, IdString id)
+ throws ResourceNotFoundException, OrmException, AuthException {
+ return new VoteResource(reviewer, id.get());
+ }
+
+ @Singleton
+ public static class List implements RestReadView<ReviewerResource> {
+ private final Provider<ReviewDb> db;
+ private final ApprovalsUtil approvalsUtil;
+
+ @Inject
+ List(Provider<ReviewDb> db,
+ ApprovalsUtil approvalsUtil) {
+ this.db = db;
+ this.approvalsUtil = approvalsUtil;
+ }
+
+ @Override
+ public Map<String, Short> apply(ReviewerResource rsrc) throws OrmException {
+ Map<String, Short> votes = new TreeMap<>();
+ Iterable<PatchSetApproval> byPatchSetUser = approvalsUtil.byPatchSetUser(
+ db.get(),
+ rsrc.getControl(),
+ rsrc.getChange().currentPatchSetId(),
+ rsrc.getReviewerUser().getAccountId());
+ for (PatchSetApproval psa : byPatchSetUser) {
+ votes.put(psa.getLabel(), psa.getValue());
+ }
+ return votes;
+ }
+ }
+}
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 ca4a9d2..a662328 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
@@ -21,6 +21,7 @@
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.InternalAuthBackend;
import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.gerrit.server.auth.oauth.OAuthRealm;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
@@ -42,6 +43,10 @@
install(new LdapModule());
break;
+ case OAUTH:
+ bind(Realm.class).to(OAuthRealm.class);
+ break;
+
case CUSTOM_EXTENSION:
break;
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 ca3afad..564e0fb 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
@@ -19,6 +19,7 @@
import com.google.common.cache.Cache;
import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.EventListener;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.CloneCommand;
import com.google.gerrit.extensions.config.DownloadCommand;
@@ -75,6 +76,7 @@
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
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.BatchUpdate;
import com.google.gerrit.server.git.EmailMerge;
@@ -124,6 +126,7 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
import com.google.gerrit.server.ssh.SshAddressesModule;
import com.google.gerrit.server.tools.ToolsCatalog;
@@ -270,6 +273,7 @@
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
.to(ProjectConfigEntry.UpdateChecker.class);
+ DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
DynamicSet.setOf(binder(), EventListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
DynamicSet.setOf(binder(), RefOperationValidationListener.class);
@@ -293,10 +297,13 @@
DynamicSet.setOf(binder(), DiffWebLink.class);
DynamicSet.setOf(binder(), ProjectWebLink.class);
DynamicSet.setOf(binder(), BranchWebLink.class);
+ DynamicMap.mapOf(binder(), OAuthLoginProvider.class);
factory(UploadValidators.Factory.class);
DynamicSet.setOf(binder(), UploadValidationListener.class);
+ DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
+
bind(AnonymousUser.class);
factory(CommitValidators.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 9eca842..e5ac370 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
@@ -35,6 +35,7 @@
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.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -64,6 +65,7 @@
private final GitwebConfig gitwebConfig;
private final DynamicItem<AvatarProvider> avatar;
private final boolean enableSignedPush;
+ private final QueryDocumentationExecutor docSearcher;
@Inject
public GetServerInfo(
@@ -79,7 +81,8 @@
@AnonymousCowardName String anonymousCowardName,
GitwebConfig gitwebConfig,
DynamicItem<AvatarProvider> avatar,
- @EnableSignedPush boolean enableSignedPush) {
+ @EnableSignedPush boolean enableSignedPush,
+ QueryDocumentationExecutor docSearcher) {
this.config = config;
this.authConfig = authConfig;
this.realm = realm;
@@ -93,6 +96,7 @@
this.gitwebConfig = gitwebConfig;
this.avatar = avatar;
this.enableSignedPush = enableSignedPush;
+ this.docSearcher = docSearcher;
}
@Override
@@ -238,6 +242,7 @@
info.reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
info.reportBugText = cfg.getString("gerrit", null, "reportBugText");
info.docUrl = getDocUrl(cfg);
+ info.docSearch = docSearcher.isAvailable();
info.editGpgKeys = toBoolean(enableSignedPush
&& cfg.getBoolean("gerrit", null, "editGpgKeys", true));
return info;
@@ -366,10 +371,11 @@
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 Boolean editGpgKeys;
}
public static class GitwebInfo {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
index 382c8fd..653f5bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
@@ -58,15 +58,19 @@
this.cacheMap = cacheMap;
}
+ public Map<String, CacheInfo> getCacheInfos() {
+ Map<String, CacheInfo> cacheInfos = new TreeMap<>();
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ cacheInfos.put(cacheNameOf(e.getPluginName(), e.getExportName()),
+ new CacheInfo(e.getProvider().get()));
+ }
+ return cacheInfos;
+ }
+
@Override
public Object apply(ConfigResource rsrc) {
if (format == null) {
- Map<String, CacheInfo> cacheInfos = new TreeMap<>();
- for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
- cacheInfos.put(cacheNameOf(e.getPluginName(), e.getExportName()),
- new CacheInfo(e.getProvider().get()));
- }
- return cacheInfos;
+ return getCacheInfos();
} else {
List<String> cacheNames = new ArrayList<>();
for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 23615d6..34946ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -37,7 +37,7 @@
public class ProjectOwnerGroupsProvider extends GroupSetProvider {
public interface Factory {
- public ProjectOwnerGroupsProvider create(Project.NameKey project);
+ ProjectOwnerGroupsProvider create(Project.NameKey project);
}
@AssistedInject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index 519a4a4..d6693ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -48,13 +48,14 @@
IOException, ConfigInvalidException {
if (i.changesPerPage != null || i.showSiteHeader != null
|| i.useFlashClipboard != null || i.downloadScheme != null
- || i.downloadCommand != null || i.copySelfOnEmail != null
+ || i.downloadCommand != null
|| i.dateFormat != null || i.timeFormat != null
|| i.relativeDateInChangeTable != null
|| i.sizeBarInChangeTable != null
|| i.legacycidInChangeTable != null
|| i.muteCommonPathPrefixes != null
- || i.reviewCategoryStrategy != null) {
+ || i.reviewCategoryStrategy != null
+ || i.emailStrategy != null) {
throw new BadRequestException("unsupported option");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java
new file mode 100644
index 0000000..c62583e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ThreadSettingsConfig.java
@@ -0,0 +1,55 @@
+// 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class ThreadSettingsConfig {
+ private final int sshdThreads;
+ private final int httpdMaxThreads;
+ private final int sshdBatchThreads;
+ private final int databasePoolLimit;
+
+ @Inject
+ ThreadSettingsConfig(@GerritServerConfig Config cfg) {
+ int cores = Runtime.getRuntime().availableProcessors();
+ sshdThreads = cfg.getInt("sshd", "threads", 2 * cores);
+ httpdMaxThreads = cfg.getInt("httpd", "maxThreads", 25);
+ int defaultDatabasePoolLimit = sshdThreads + httpdMaxThreads + 2;
+ databasePoolLimit =
+ cfg.getInt("database", "poolLimit", defaultDatabasePoolLimit);
+ sshdBatchThreads = cores == 1 ? 1 : 2;
+ }
+
+ public int getDatabasePoolLimit() {
+ return databasePoolLimit;
+ }
+
+ public int getHttpdMaxThreads() {
+ return httpdMaxThreads;
+ }
+
+ public int getSshdThreads() {
+ return sshdThreads;
+ }
+
+ public int getSshdBatchTreads() {
+ return sshdBatchThreads;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index 446013e..438795f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -74,7 +74,7 @@
}
public List<DocResult> doQuery(String q) throws DocQueryException {
- if (parser == null || searcher == null) {
+ if (!isAvailable()) {
throw new DocQueryException("Documentation search not available");
}
try {
@@ -123,6 +123,10 @@
return dir;
}
+ public boolean isAvailable() {
+ return parser != null && searcher != null;
+ }
+
@SuppressWarnings("serial")
public static class DocQueryException extends Exception {
DocQueryException() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventsMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventsMetrics.java
new file mode 100644
index 0000000..eaaf1a83
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventsMetrics.java
@@ -0,0 +1,43 @@
+// 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.events;
+
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class EventsMetrics implements EventListener {
+ private final Counter1<String> events;
+
+ @Inject
+ public EventsMetrics(MetricMaker metricMaker) {
+ events = metricMaker.newCounter(
+ "events",
+ new Description("Triggered events")
+ .setRate()
+ .setUnit("triggered events"),
+ Field.ofString("type"));
+ }
+
+ @Override
+ public void onEvent(com.google.gerrit.server.events.Event event) {
+ events.increment(event.getType());
+ }
+}
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 c54fe26..90d1783 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
@@ -77,10 +77,30 @@
*/
public class BatchUpdate implements AutoCloseable {
public interface Factory {
- public BatchUpdate create(ReviewDb db, Project.NameKey project,
+ BatchUpdate create(ReviewDb db, Project.NameKey project,
CurrentUser user, Timestamp when);
}
+ /** Order of execution of the various phases. */
+ public static enum Order {
+ /**
+ * Update the repository and execute all ref updates before touching the
+ * database.
+ * <p>
+ * The default and most common, as Gerrit does not behave well when a patch
+ * set has no corresponding ref in the repo.
+ */
+ REPO_BEFORE_DB,
+
+ /**
+ * Update the database before touching the repository.
+ * <p>
+ * Generally only used when deleting patch sets, which should be deleted
+ * first from the database (for the same reason as above.)
+ */
+ DB_BEFORE_REPO;
+ }
+
public class Context {
public Project.NameKey getProject() {
return project;
@@ -97,6 +117,10 @@
public CurrentUser getUser() {
return user;
}
+
+ public Order getOrder() {
+ return order;
+ }
}
public class RepoContext extends Context {
@@ -135,6 +159,7 @@
public class ChangeContext extends Context {
private final ChangeControl ctl;
private final ChangeUpdate update;
+ private boolean deleted;
private ChangeContext(ChangeControl ctl) {
this.ctl = ctl;
@@ -156,6 +181,10 @@
public Change getChange() {
return update.getChange();
}
+
+ public void markDeleted() {
+ this.deleted = true;
+ }
}
public static class Op {
@@ -199,6 +228,7 @@
private RevWalk revWalk;
private BatchRefUpdate batchRefUpdate;
private boolean closeRepo;
+ private Order order;
@AssistedInject
BatchUpdate(GitRepositoryManager repoManager,
@@ -221,6 +251,7 @@
this.user = user;
this.when = when;
tz = serverIdent.getTimeZone();
+ order = Order.REPO_BEFORE_DB;
}
@Override
@@ -242,6 +273,11 @@
return this;
}
+ public BatchUpdate setOrder(Order order) {
+ this.order = order;
+ return this;
+ }
+
private void initRepository() throws IOException {
if (repo == null) {
this.repo = repoManager.openRepository(project);
@@ -287,8 +323,19 @@
public void execute() throws UpdateException, RestApiException {
try {
- executeRefUpdates();
- executeChangeOps();
+ switch (order) {
+ case REPO_BEFORE_DB:
+ executeRefUpdates();
+ executeChangeOps();
+ break;
+ case DB_BEFORE_REPO:
+ executeChangeOps();
+ executeRefUpdates();
+ break;
+ default:
+ throw new IllegalStateException("invalid execution order: " + order);
+ }
+
reindexChanges();
if (batchRefUpdate != null) {
@@ -356,7 +403,11 @@
db.rollback();
}
ctx.getChangeUpdate().commit();
- indexFutures.add(indexer.indexAsync(id));
+ if (ctx.deleted) {
+ indexFutures.add(indexer.deleteAsync(id));
+ } else {
+ indexFutures.add(indexer.indexAsync(id));
+ }
}
} catch (Exception e) {
Throwables.propagateIfPossible(e, RestApiException.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
index 391ccd0..209cbe0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
@@ -20,5 +20,5 @@
import java.util.List;
public interface ChangeCache {
- public List<Change> get(Project.NameKey name);
+ List<Change> get(Project.NameKey name);
}
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 dee2df0..ab6d66c 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
@@ -41,7 +41,7 @@
* repository.
* @throws IOException the name cannot be read as a repository.
*/
- public abstract Repository openRepository(Project.NameKey name)
+ Repository openRepository(Project.NameKey name)
throws RepositoryNotFoundException, IOException;
/**
@@ -58,7 +58,7 @@
* @throws RepositoryNotFoundException the name is invalid.
* @throws IOException the repository cannot be created.
*/
- public abstract Repository createRepository(Project.NameKey name)
+ Repository createRepository(Project.NameKey name)
throws RepositoryCaseMismatchException, RepositoryNotFoundException,
IOException;
@@ -76,11 +76,11 @@
* repository.
* @throws IOException the name cannot be read as a repository.
*/
- public abstract Repository openMetadataRepository(Project.NameKey name)
+ Repository openMetadataRepository(Project.NameKey name)
throws RepositoryNotFoundException, IOException;
/** @return set of all known projects, sorted by natural NameKey order. */
- public abstract SortedSet<Project.NameKey> list();
+ SortedSet<Project.NameKey> list();
/**
* Read the {@code GIT_DIR/description} file for gitweb.
@@ -94,7 +94,7 @@
* @throws IOException the description file exists, but is not readable by
* this process.
*/
- public abstract String getProjectDescription(Project.NameKey name)
+ String getProjectDescription(Project.NameKey name)
throws RepositoryNotFoundException, IOException;
/**
@@ -106,6 +106,6 @@
* @param name the repository name, relative to the base directory.
* @param description new description text for the repository.
*/
- public abstract void setProjectDescription(Project.NameKey name,
+ void setProjectDescription(Project.NameKey name,
final String description);
}
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 0dc7aa9..097ab6e 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
@@ -116,7 +116,7 @@
LabelTypes labelTypes = ctl.getLabelTypes();
for (PatchSetApproval psa : approvals) {
Change.Id changeId = psa.getKey().getParentKey().getParentKey();
- checkArgument(changeId.equals(ctl.getChange().getId()),
+ checkArgument(changeId.equals(ctl.getId()),
"Approval %s does not match change %s",
psa.getKey(), ctl.getChange().getKey());
if (psa.isSubmit()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java
new file mode 100644
index 0000000..ebfaae7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LazyPostReceiveHookChain.java
@@ -0,0 +1,40 @@
+// 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.git;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+
+import java.util.Collection;
+
+class LazyPostReceiveHookChain implements PostReceiveHook {
+ private final DynamicSet<PostReceiveHook> hooks;
+
+ @Inject
+ LazyPostReceiveHookChain(DynamicSet<PostReceiveHook> hooks) {
+ this.hooks = hooks;
+ }
+
+ @Override
+ public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+ for (PostReceiveHook h : hooks) {
+ h.onPostReceive(rp, commands);
+ }
+ }
+}
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 fc2f672..61689a1 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
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static org.eclipse.jgit.lib.RefDatabase.ALL;
import com.google.common.base.Optional;
@@ -167,7 +168,6 @@
private CodeReviewRevWalk rw;
private RevFlag canMergeFlag;
private ObjectInserter inserter;
- private PersonIdent refLogIdent;
private Map<Branch.NameKey, RefUpdate> pendingRefUpdates;
private Map<Branch.NameKey, CodeReviewCommit> openBranches;
private Map<Branch.NameKey, MergeTip> mergeTips;
@@ -427,8 +427,7 @@
for (Project.NameKey project : br.keySet()) {
openRepository(project);
for (Branch.NameKey branch : br.get(project)) {
-
- RefUpdate update = updateBranch(branch);
+ RefUpdate update = updateBranch(branch, caller);
pendingRefUpdates.remove(branch);
setDestProject(branch);
@@ -474,7 +473,6 @@
toMerge.add(commit);
}
MergeTip mergeTip = strategy.run(branchTip, toMerge);
- refLogIdent = strategy.getRefLogIdent();
logDebug("Produced {} new commits", strategy.getNewCommits().size());
commits.putAll(strategy.getNewCommits());
return mergeTip;
@@ -720,8 +718,8 @@
}
}
- private RefUpdate updateBranch(Branch.NameKey destBranch)
- throws IntegrationException {
+ private RefUpdate updateBranch(Branch.NameKey destBranch,
+ IdentifiedUser caller) throws IntegrationException {
RefUpdate branchUpdate = getPendingRefUpdate(destBranch);
CodeReviewCommit branchTip = getBranchTip(destBranch);
@@ -755,7 +753,8 @@
}
}
- branchUpdate.setRefLogIdent(refLogIdent);
+ branchUpdate.setRefLogIdent(
+ identifiedUserFactory.create(caller.getAccountId()).newRefLogIdent());
branchUpdate.setForceUpdate(false);
branchUpdate.setNewObjectId(currentTip);
branchUpdate.setRefLogMessage("merged", true);
@@ -859,6 +858,9 @@
// If mergeTip is null merge failed and mergeResultRev will not be read.
ObjectId mergeResultRev =
mergeTip != null ? mergeTip.getMergeResults().get(commit) : null;
+ // The change notes must be forcefully reloaded so that the SUBMIT
+ // approval that we added earlier is visible
+ commit.notes().reload();
try {
ChangeMessage msg;
switch (s) {
@@ -1050,6 +1052,7 @@
logDebug("Add approval for " + cd + " from user " + user);
ChangeUpdate update = updateFactory.create(control, timestamp);
+ update.putReviewer(user.getAccountId(), REVIEWER);
List<SubmitRecord> record = records.get(cd.change().getId());
if (record != null) {
update.merge(record);
@@ -1063,6 +1066,11 @@
// If the submit strategy created a new revision (rebase, cherry-pick)
// approve that as well
if (!psIdNewRev.equals(psId)) {
+ update.setPatchSetId(psId);
+ update.commit();
+ // Create a new ChangeUpdate instance because we need to store meta data
+ // on another patch set (psIdNewRev).
+ update = updateFactory.create(control, timestamp);
batch = approve(control, psIdNewRev, user,
update, timestamp);
// Write update commit after all normalized label commits.
@@ -1072,6 +1080,7 @@
} finally {
db.rollback();
}
+ update.commit();
indexer.index(db, cd.change());
}
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 addbc1b..246647c 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
@@ -141,7 +141,7 @@
private static final String KEY_CAN_OVERRIDE = "canOverride";
private static final String KEY_Branch = "branch";
private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
- "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+ "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp", "PatchSetLock");
private static final String PLUGIN = "plugin";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
index c7d925c..cd37c9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
@@ -19,5 +19,5 @@
INTERACTIVE, BATCH
}
- public WorkQueue.Executor getQueue(QueueType type);
+ WorkQueue.Executor getQueue(QueueType type);
}
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 5da4ec7..f73db1c 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
@@ -53,7 +53,6 @@
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.common.ChangeHookRunner.HookResult;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
@@ -65,6 +64,7 @@
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicMap.Entry;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
@@ -101,6 +101,7 @@
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -308,6 +309,7 @@
private final SshInfo sshInfo;
private final AllProjectsName allProjectsName;
private final ReceiveConfig receiveConfig;
+ private final DynamicSet<ReceivePackInitializer> initializers;
private final ChangeKindCache changeKindCache;
private final BatchUpdate.Factory batchUpdateFactory;
private final SetHashtagsOp.Factory hashtagsFactory;
@@ -378,7 +380,10 @@
final ChangeIndexer indexer,
final SshInfo sshInfo,
final AllProjectsName allProjectsName,
- ReceiveConfig config,
+ ReceiveConfig receiveConfig,
+ TransferConfig transferConfig,
+ DynamicSet<ReceivePackInitializer> initializers,
+ Provider<LazyPostReceiveHookChain> lazyPostReceive,
@Assisted final ProjectControl projectControl,
@Assisted final Repository repo,
final Provider<SubmoduleOp> subOpProvider,
@@ -420,7 +425,8 @@
this.indexer = indexer;
this.sshInfo = sshInfo;
this.allProjectsName = allProjectsName;
- this.receiveConfig = config;
+ this.receiveConfig = receiveConfig;
+ this.initializers = initializers;
this.changeKindCache = changeKindCache;
this.batchUpdateFactory = batchUpdateFactory;
this.hashtagsFactory = hashtagsFactory;
@@ -448,6 +454,10 @@
rp.setAllowCreates(true);
rp.setAllowDeletes(true);
rp.setAllowNonFastForwards(true);
+ rp.setRefLogIdent(user.newRefLogIdent());
+ rp.setTimeout(transferConfig.getTimeout());
+ rp.setMaxObjectSizeLimit(transferConfig.getEffectiveMaxObjectSizeLimit(
+ projectControl.getProjectState()));
rp.setCheckReceivedObjects(ps.getConfig().getCheckReceivedObjects());
rp.setRefFilter(new RefFilter() {
@Override
@@ -465,7 +475,7 @@
});
if (!projectControl.allRefsAreVisible()) {
- rp.setCheckReferencedObjectsAreReachable(config.checkReferencedObjectsAreReachable);
+ rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable);
rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false));
}
List<AdvertiseRefsHook> advHooks = new ArrayList<>(3);
@@ -497,6 +507,13 @@
db, queryProvider, projectControl.getProject().getNameKey()));
advHooks.add(new HackPushNegotiateHook());
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
+ rp.setPostReceiveHook(lazyPostReceive.get());
+ }
+
+ public void init() {
+ for (ReceivePackInitializer i : initializers) {
+ i.init(projectControl.getProject().getNameKey(), rp);
+ }
}
/** Add reviewers for new (or updated) changes. */
@@ -880,19 +897,6 @@
continue;
}
- HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(),
- user.getAccount(), cmd.getOldId(),
- cmd.getNewId());
-
- if (result != null) {
- final String message = result.toString().trim();
- if (result.getExitValue() != 0) {
- reject(cmd, message);
- continue;
- }
- rp.sendMessage(message);
- }
-
if (MagicBranch.isMagicBranch(cmd.getRefName())) {
parseMagicBranch(cmd);
continue;
@@ -1751,9 +1755,7 @@
insertChange(threadLocalDb);
}
}
- synchronized (newProgress) {
- newProgress.update(1);
- }
+ synchronizedIncrement(newProgress);
return null;
}
}));
@@ -1792,6 +1794,16 @@
ins.getChange().getId(),
hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags))
.setRunHooks(false));
+ if (!Strings.isNullOrEmpty(magicBranch.topic)) {
+ bu.addOp(
+ ins.getChange().getId(),
+ new BatchUpdate.Op() {
+ @Override
+ public void updateChange(ChangeContext ctx) throws Exception {
+ ctx.getChangeUpdate().setTopic(magicBranch.topic);
+ }
+ });
+ }
}
bu.execute();
}
@@ -1966,7 +1978,7 @@
}
}
- boolean validate(boolean autoClose) throws IOException {
+ boolean validate(boolean autoClose) throws IOException, OrmException {
if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
return false;
} else if (change == null) {
@@ -1989,8 +2001,12 @@
}
changeCtl = projectControl.controlFor(change);
- if (!changeCtl.canAddPatchSet()) {
- reject(inputCommand, "cannot replace " + ontoChange);
+ if (!changeCtl.canAddPatchSet(db)) {
+ String locked = ".";
+ if (changeCtl.isPatchSetLocked(db)) {
+ locked = ". Change is patch set locked.";
+ }
+ reject(inputCommand, "cannot replace " + ontoChange + locked);
return false;
} else if (change.getStatus().isClosed()) {
reject(inputCommand, "change " + ontoChange + " closed");
@@ -2156,9 +2172,7 @@
}
}
} finally {
- synchronized (replaceProgress) {
- replaceProgress.update(1);
- }
+ synchronizedIncrement(replaceProgress);
}
}
}));
@@ -2234,11 +2248,15 @@
recipients.add(magicBranch.getMailRecipients());
approvals = magicBranch.labels;
Set<String> hashtags = magicBranch.hashtags;
+ ChangeNotes notes = changeCtl.getNotes().load();
if (!hashtags.isEmpty()) {
- ChangeNotes notes = changeCtl.getNotes().load();
hashtags.addAll(notes.getHashtags());
update.setHashtags(hashtags);
}
+ if (magicBranch.topic != null
+ && !magicBranch.topic.equals(notes.getChange().getTopic())) {
+ update.setTopic(magicBranch.topic);
+ }
}
recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines));
recipients.remove(me);
@@ -2824,4 +2842,10 @@
private static boolean isConfig(final ReceiveCommand cmd) {
return cmd.getRefName().equals(RefNames.REFS_CONFIG);
}
+
+ private static void synchronizedIncrement(Task p) {
+ synchronized (p) {
+ p.update(1);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceivePackInitializer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceivePackInitializer.java
index ec0bdaf..ee229d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceivePackInitializer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceivePackInitializer.java
@@ -32,5 +32,5 @@
* @param project project for which the ReceivePack is created
* @param receivePack the ReceivePack instance which is being initialized
*/
- public void init(Project.NameKey project, ReceivePack receivePack);
+ void init(Project.NameKey project, ReceivePack receivePack);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
index 3cbac3b..d77c7e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TabFile.java
@@ -28,7 +28,7 @@
public class TabFile {
public interface Parser {
- public String parse(String str);
+ String parse(String str);
}
public static Parser TRIM = new Parser() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
new file mode 100644
index 0000000..fd4e7d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -0,0 +1,98 @@
+// 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.git;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram1;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer1;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.eclipse.jgit.transport.PostUploadHook;
+
+@Singleton
+public class UploadPackMetricsHook implements PostUploadHook {
+ enum Operation {
+ CLONE,
+ FETCH;
+ }
+
+ private final Counter1<Operation> requestCount;
+ private final Timer1<Operation> counting;
+ private final Timer1<Operation> compressing;
+ private final Timer1<Operation> writing;
+ private final Histogram1<Operation> packBytes;
+
+ @Inject
+ UploadPackMetricsHook(MetricMaker metricMaker) {
+ Field<Operation> operation = Field.ofEnum(Operation.class, "operation");
+ requestCount = metricMaker.newCounter(
+ "git/upload-pack/request_count",
+ new Description("Total number of git-upload-pack requests")
+ .setRate()
+ .setUnit("requests"),
+ operation);
+
+ counting = metricMaker.newTimer(
+ "git/upload-pack/phase_counting",
+ new Description("Time spenting in the 'Counting...' phase")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ operation);
+
+ compressing = metricMaker.newTimer(
+ "git/upload-pack/phase_compressing",
+ new Description("Time spenting in the 'Compressing...' phase")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ operation);
+
+ writing = metricMaker.newTimer(
+ "git/upload-pack/phase_writing",
+ new Description("Time spenting transferring bytes to client")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS),
+ operation);
+
+ packBytes = metricMaker.newHistogram(
+ "git/upload-pack/pack_bytes",
+ new Description("Distribution of sizes of packs sent to clients")
+ .setCumulative()
+ .setUnit(Units.BYTES),
+ operation);
+ }
+
+ @Override
+ public void onPostUpload(PackStatistics stats) {
+ Operation op = Operation.FETCH;
+ if (stats.getUninterestingObjects() == null
+ || stats.getUninterestingObjects().isEmpty()) {
+ op = Operation.CLONE;
+ }
+
+ requestCount.increment(op);
+ counting.record(op, stats.getTimeCounting(), MILLISECONDS);
+ compressing.record(op, stats.getTimeCompressing(), MILLISECONDS);
+ writing.record(op, stats.getTimeWriting(), MILLISECONDS);
+ packBytes.record(op, stats.getTotalBytes());
+ }
+}
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 4a8163b..954e412 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
@@ -246,7 +246,7 @@
/** Runnable needing to know it was canceled. */
public interface CancelableRunnable extends Runnable {
/** Notifies the runnable it was canceled. */
- public void cancel();
+ void cancel();
}
/** A wrapper around a scheduled Runnable, as maintained in the queue. */
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 61b1b00..2714015 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
@@ -176,6 +176,7 @@
// Merge conflict; don't update change.
return;
}
+ ctx.getChangeUpdate().setPatchSetId(psId);
PatchSet ps = new PatchSet(psId);
ps.setCreatedOn(ctx.getWhen());
ps.setUploader(args.caller.getAccountId());
@@ -191,6 +192,7 @@
for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
args.db, toMerge.getControl(), toMerge.getPatchsetId())) {
approvals.add(new PatchSetApproval(ps.getId(), a));
+ ctx.getChangeUpdate().putApproval(a.getLabel(), a.getValue());
}
args.db.patchSetApprovals().insert(approvals);
@@ -200,7 +202,6 @@
args.changeControlFactory.controlFor(toMerge.change(), args.caller));
mergeTip.moveTipTo(newCommit, newCommit);
newCommits.put(c.getId(), newCommit);
- setRefLogIdent();
}
}
@@ -240,7 +241,6 @@
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
- setRefLogIdent();
}
}
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 1709659..67162ea 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
@@ -44,17 +44,11 @@
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
newMergeTipCommit, args.alreadyAccepted);
- setRefLogIdent();
return mergeTip;
}
@Override
- public boolean retryOnLockFailure() {
- return false;
- }
-
- @Override
public boolean dryRun(CodeReviewCommit mergeTip,
CodeReviewCommit toMerge) throws IntegrationException {
return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
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 9f5f521..e7ad79c 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
@@ -55,7 +55,6 @@
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
- setRefLogIdent();
return mergeTip;
}
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 4ebe461..6894b57 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
@@ -61,7 +61,6 @@
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
args.alreadyAccepted);
- setRefLogIdent();
return mergeTip;
}
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 cd6b5bd..9e5fcd2 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
@@ -110,7 +110,6 @@
CommitMergeStatus.CLEAN_REBASE);
newCommits.put(newPatchSet.getId().getParentKey(),
mergeTip.getCurrentTip());
- setRefLogIdent();
} catch (MergeConflictException e) {
n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
throw new IntegrationException(
@@ -140,7 +139,6 @@
}
args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
mergeTip.getCurrentTip(), args.alreadyAccepted);
- setRefLogIdent();
} catch (IOException e) {
throw new IntegrationException("Cannot merge " + n.name(), e);
}
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 5215f55..c4c7e88 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
@@ -35,7 +35,6 @@
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
@@ -108,8 +107,6 @@
protected final Arguments args;
- private PersonIdent refLogIdent;
-
SubmitStrategy(Arguments args) {
this.args = args;
}
@@ -128,7 +125,6 @@
*/
public final MergeTip run(final CodeReviewCommit currentTip,
final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
- refLogIdent = null;
checkState(args.caller != null);
return _run(currentTip, toMerge);
}
@@ -153,19 +149,6 @@
CodeReviewCommit toMerge) throws IntegrationException;
/**
- * Returns the identity that should be used for reflog entries when updating
- * the destination branch.
- * <p>
- * The reflog identity may only be set during {@link #run(CodeReviewCommit,
- * Collection)}, and this method is invalid to call beforehand.
- *
- * @return the ref log identity, which may be {@code null}.
- */
- public final PersonIdent getRefLogIdent() {
- return refLogIdent;
- }
-
- /**
* Returns all commits that have been newly created for the changes that are
* getting merged.
* <p>
@@ -180,28 +163,4 @@
public Map<Change.Id, CodeReviewCommit> getNewCommits() {
return Collections.emptyMap();
}
-
- /**
- * Returns whether a merge that failed with {@link Result#LOCK_FAILURE} should
- * be retried.
- * <p>
- * May be overridden by subclasses.
- *
- * @return {@code true} if a merge that failed with
- * {@link Result#LOCK_FAILURE} should be retried, otherwise
- * {@code false}
- */
- public boolean retryOnLockFailure() {
- return true;
- }
-
- /**
- * Set the ref log identity if it wasn't set yet.
- */
- protected final void setRefLogIdent() {
- if (refLogIdent == null) {
- refLogIdent = args.identifiedUserFactory.create(
- args.caller.getAccountId()).newRefLogIdent();
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
index 029096e..91c6a14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
@@ -23,7 +23,8 @@
private static final long serialVersionUID = 1L;
private final List<CommitValidationMessage> messages;
- public CommitValidationException(String reason, List<CommitValidationMessage> messages) {
+ public CommitValidationException(String reason,
+ List<CommitValidationMessage> messages) {
super(reason);
this.messages = messages;
}
@@ -41,4 +42,16 @@
public List<CommitValidationMessage> getMessages() {
return messages;
}
+
+ /** @return the reason string along with all validation messages. */
+ public String getFullMessage() {
+ StringBuilder sb = new StringBuilder(getMessage());
+ if (!messages.isEmpty()) {
+ sb.append(':');
+ for (CommitValidationMessage msg : messages) {
+ sb.append("\n ").append(msg.getMessage());
+ }
+ }
+ return sb.toString();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
index e98c49b..9d7b3e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
@@ -34,6 +34,6 @@
* @return list of validation messages
* @throws CommitValidationException if validation fails
*/
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException;
}
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 6ac5707..b069ca9 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
@@ -131,6 +131,7 @@
validators.add(new ConfigValidator(refControl, repo));
validators.add(new BannedCommitsValidator(rejectCommits));
validators.add(new PluginCommitValidationListener(commitValidationListeners));
+ validators.add(new ChangeHookValidator(hooks));
List<CommitValidationMessage> messages = new LinkedList<>();
@@ -164,7 +165,7 @@
}
validators.add(new ConfigValidator(refControl, repo));
validators.add(new PluginCommitValidationListener(commitValidationListeners));
- validators.add(new ChangeHookValidator(refControl, hooks));
+ validators.add(new ChangeHookValidator(hooks));
List<CommitValidationMessage> messages = new LinkedList<>();
@@ -548,42 +549,37 @@
/** Reject commits that don't pass user-supplied ref-update hook. */
public static class ChangeHookValidator implements
CommitValidationListener {
- private final RefControl refControl;
private final ChangeHooks hooks;
- public ChangeHookValidator(RefControl refControl, ChangeHooks hooks) {
- this.refControl = refControl;
+ public ChangeHookValidator(ChangeHooks hooks) {
this.hooks = hooks;
}
@Override
public List<CommitValidationMessage> onCommitReceived(
CommitReceivedEvent receiveEvent) throws CommitValidationException {
+ IdentifiedUser user = receiveEvent.user;
+ String refname = receiveEvent.refName;
+ ObjectId old = ObjectId.zeroId();
+ if (receiveEvent.commit.getParentCount() > 0) {
+ old = receiveEvent.commit.getParent(0);
+ }
- if (refControl.getUser().isIdentifiedUser()) {
- IdentifiedUser user = refControl.getUser().asIdentifiedUser();
-
- String refname = receiveEvent.refName;
- ObjectId old = ObjectId.zeroId();
- if (receiveEvent.commit.getParentCount() > 0) {
- old = receiveEvent.commit.getParent(0);
- }
- if (receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
- /*
- * If the ref-update hook tries to distinguish behavior between pushes to
- * refs/heads/... and refs/for/..., make sure we send it the correct refname.
- * Also, if this is targetting refs/for/, make sure we behave the same as
- * what a push to refs/for/ would behave; in particular, setting oldrev to
- * 0000000000000000000000000000000000000000.
- */
- refname = refname.replace(R_HEADS, "refs/for/refs/heads/");
- old = ObjectId.zeroId();
- }
- HookResult result = hooks.doRefUpdateHook(receiveEvent.project, refname,
- user.getAccount(), old, receiveEvent.commit);
- if (result != null && result.getExitValue() != 0) {
- throw new CommitValidationException(result.toString().trim());
- }
+ if (receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
+ /*
+ * If the ref-update hook tries to distinguish behavior between pushes to
+ * refs/heads/... and refs/for/..., make sure we send it the correct refname.
+ * Also, if this is targetting refs/for/, make sure we behave the same as
+ * what a push to refs/for/ would behave; in particular, setting oldrev to
+ * 0000000000000000000000000000000000000000.
+ */
+ refname = refname.replace(R_HEADS, "refs/for/refs/heads/");
+ old = ObjectId.zeroId();
+ }
+ HookResult result = hooks.doRefUpdateHook(receiveEvent.project, refname,
+ user.getAccount(), old, receiveEvent.commit);
+ if (result != null && result.getExitValue() != 0) {
+ throw new CommitValidationException(result.toString().trim());
}
return Collections.emptyList();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
index 0a8d245..6e98223 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
@@ -39,7 +39,7 @@
* @param patchSetId the patch set ID
* @throws MergeValidationException if the commit fails to validate
*/
- public void onPreMerge(Repository repo,
+ void onPreMerge(Repository repo,
CodeReviewCommit commit,
ProjectState destProject,
Branch.NameKey destBranch,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
index 2c032c4..e6923c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidationListener.java
@@ -51,7 +51,7 @@
* @throws ValidationException to block the upload and send a message
* back to the end-user over the client's protocol connection.
*/
- public void onPreUpload(Repository repository, Project project,
+ void onPreUpload(Repository repository, Project project,
String remoteHost, UploadPack up, Collection<? extends ObjectId> wants,
Collection<? extends ObjectId> haves)
throws ValidationException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index 405cdf1..d623b31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -14,10 +14,8 @@
package com.google.gerrit.server.group;
-import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import com.google.common.collect.Ordering;
import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -30,13 +28,13 @@
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gerrit.server.api.accounts.AccountInfoComparator;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.kohsuke.args4j.Option;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -83,15 +81,7 @@
final Map<Account.Id, AccountInfo> members =
getMembers(groupId, new HashSet<AccountGroup.UUID>());
final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
- Collections.sort(memberInfos, new Comparator<AccountInfo>() {
- @Override
- public int compare(AccountInfo a, AccountInfo b) {
- return ComparisonChain.start()
- .compare(a.name, b.name, Ordering.natural().nullsFirst())
- .compare(a.email, b.email, Ordering.natural().nullsFirst())
- .compare(a._accountId, b._accountId, Ordering.natural().nullsFirst()).result();
- }
- });
+ Collections.sort(memberInfos, AccountInfoComparator.ORDER_NULLS_FIRST);
return memberInfos;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index 02e737a..258f1e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -35,10 +35,10 @@
*/
public interface ChangeIndex {
/** @return the schema version used by this index. */
- public Schema<ChangeData> getSchema();
+ Schema<ChangeData> getSchema();
/** Close this index. */
- public void close();
+ void close();
/**
* Update a change document in the index.
@@ -52,7 +52,7 @@
*
* @throws IOException
*/
- public void replace(ChangeData cd) throws IOException;
+ void replace(ChangeData cd) throws IOException;
/**
* Delete a change document from the index by id.
@@ -61,14 +61,14 @@
*
* @throws IOException
*/
- public void delete(Change.Id id) throws IOException;
+ void delete(Change.Id id) throws IOException;
/**
* Delete all change documents from the index.
*
* @throws IOException
*/
- public void deleteAll() throws IOException;
+ void deleteAll() throws IOException;
/**
* Convert the given operator predicate into a source searching the index and
@@ -90,7 +90,7 @@
* @throws QueryParseException if the predicate could not be converted to an
* indexed data source.
*/
- public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+ ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
throws QueryParseException;
/**
@@ -99,5 +99,5 @@
* @param ready whether the index is ready
* @throws IOException
*/
- public void markReady(boolean ready) throws IOException;
+ void markReady(boolean ready) throws IOException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 6c484d8..84edcbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -239,9 +239,7 @@
try {
ChangeData cd = changeDataFactory.create(
newCtx.getReviewDbProvider().get(), id);
- for (ChangeIndex i : getWriteIndexes()) {
- i.replace(cd);
- }
+ index(cd);
return null;
} finally {
context.setContext(oldCtx);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index 683f8cf..28fa9f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -50,7 +50,7 @@
int backendLimit = opts.config().maxLimit();
int limit = Ints.saturatedCast((long) opts.limit() + opts.start());
limit = Math.min(limit, backendLimit);
- return QueryOptions.create(opts.config(), 0, limit);
+ return QueryOptions.create(opts.config(), 0, limit, opts.fields());
}
private final ChangeIndex index;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
index 31fbb40..9d573e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -69,6 +69,11 @@
@Override
public void onGitReferenceUpdated(final Event event) {
+ if (event.getRefName().startsWith(RefNames.REFS_CHANGES)
+ || event.getRefName().startsWith(RefNames.REFS_DRAFT_COMMENTS)
+ || event.getRefName().startsWith(RefNames.REFS_USERS)) {
+ return;
+ }
Futures.transformAsync(
executor.submit(new GetChanges(event)),
new AsyncFunction<List<Change>, List<Void>>() {
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 df70292..a605461 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
@@ -61,6 +61,8 @@
}
private final ImmutableMap<String, FieldDef<T, ?>> fields;
+ private final ImmutableMap<String, FieldDef<T, ?>> storedFields;
+
private int version;
protected Schema(Iterable<FieldDef<T, ?>> fields) {
@@ -71,10 +73,15 @@
public Schema(int version, Iterable<FieldDef<T, ?>> fields) {
this.version = version;
ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
+ ImmutableMap.Builder<String, FieldDef<T, ?>> sb = ImmutableMap.builder();
for (FieldDef<T, ?> f : fields) {
b.put(f.getName(), f);
+ if (f.isStored()) {
+ sb.put(f.getName(), f);
+ }
}
this.fields = b.build();
+ this.storedFields = sb.build();
}
public final int getVersion() {
@@ -95,6 +102,14 @@
}
/**
+ * @return all fields in this schema where {@link FieldDef#isStored()} is
+ * true.
+ */
+ public final ImmutableMap<String, FieldDef<T, ?>> getStoredFields() {
+ return storedFields;
+ }
+
+ /**
* Look up fields in this schema.
*
* @param first the preferred field to look up.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
index 0f1e86e..f825d1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -25,9 +25,9 @@
public class AddKeySender extends OutgoingEmail {
public interface Factory {
- public AddKeySender create(IdentifiedUser user, AccountSshKey sshKey);
+ AddKeySender create(IdentifiedUser user, AccountSshKey sshKey);
- public AddKeySender create(IdentifiedUser user, List<String> gpgKey);
+ AddKeySender create(IdentifiedUser user, List<String> gpgKey);
}
private final IdentifiedUser callingUser;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 8948ce3..e8101de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.mail;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -23,10 +25,8 @@
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.StarredChange;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.mail.ProjectWatch.Watchers;
-import com.google.gerrit.server.notedb.ReviewerState;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -296,9 +296,9 @@
try {
// BCC anyone who has starred this change.
//
- for (StarredChange w : args.db.get().starredChanges().byChange(
- change.getId())) {
- super.add(RecipientType.BCC, w.getAccountId());
+ for (Account.Id accountId : args.starredChangesUtil
+ .byChange(change.getId())) {
+ super.add(RecipientType.BCC, accountId);
}
} catch (OrmException err) {
// Just don't BCC everyone. Better to send a partial message to those
@@ -329,7 +329,7 @@
/** Users who have non-zero approval codes on the change. */
protected void ccExistingReviewers() {
try {
- for (Account.Id id : changeData.reviewers().get(ReviewerState.REVIEWER)) {
+ for (Account.Id id : changeData.reviewers().get(REVIEWER)) {
add(RecipientType.CC, id);
}
} catch (OrmException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 3d0041c..3e5e167 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -52,7 +52,7 @@
.getLogger(CommentSender.class);
public static interface Factory {
- public CommentSender create(NotifyHandling notify, Change.Id id);
+ CommentSender create(NotifyHandling notify, Change.Id id);
}
private final NotifyHandling notify;
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
index 29895d9..e67b8ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -33,7 +33,7 @@
LoggerFactory.getLogger(CreateChangeSender.class);
public static interface Factory {
- public CreateChangeSender create(Change.Id id);
+ CreateChangeSender create(Change.Id id);
}
@Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 8e5fa6f..88c9199 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -22,6 +22,7 @@
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;
@@ -73,6 +74,7 @@
final RuntimeInstance velocityRuntime;
final EmailSettings settings;
final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners;
+ final StarredChangesUtil starredChangesUtil;
@Inject
EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -96,7 +98,8 @@
RuntimeInstance velocityRuntime,
EmailSettings settings,
@SshAdvertisedAddresses List<String> sshAddresses,
- DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners) {
+ DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners,
+ StarredChangesUtil starredChangesUtil) {
this.server = server;
this.projectCache = projectCache;
this.groupBackend = groupBackend;
@@ -122,5 +125,6 @@
this.settings = settings;
this.sshAddresses = sshAddresses;
this.outgoingEmailValidationListeners = outgoingEmailValidationListeners;
+ this.starredChangesUtil = starredChangesUtil;
}
}
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 58bdac1..b6fb006 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
@@ -28,7 +28,7 @@
* the string provides proof the user has the ability to read messages
* sent to that address. Must not be null.
*/
- public String encode(Account.Id accountId, String emailAddress);
+ String encode(Account.Id accountId, String emailAddress);
/**
* Decode a token previously created.
@@ -36,7 +36,7 @@
* @return a pair of account id and email address.
* @throws InvalidTokenException the token is invalid, expired, malformed, etc.
*/
- public ParsedToken decode(String tokenString) throws InvalidTokenException;
+ ParsedToken decode(String tokenString) throws InvalidTokenException;
/** Exception thrown when a token does not parse correctly. */
public static class InvalidTokenException extends Exception {
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
index dec3d2c..9bcabc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
@@ -18,7 +18,7 @@
/** Constructs an address to send email from. */
public interface FromAddressGenerator {
- public boolean isGenericAddress(Account.Id fromId);
+ boolean isGenericAddress(Account.Id fromId);
- public Address from(Account.Id fromId);
+ Address from(Account.Id fromId);
}
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 c6e59eb..a3a048a 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
@@ -14,13 +14,16 @@
package com.google.gerrit.server.mail;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+
import com.google.common.collect.Multimap;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.notedb.ReviewerState;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.revwalk.FooterKey;
@@ -56,10 +59,10 @@
}
public static MailRecipients getRecipientsFromReviewers(
- Multimap<ReviewerState, Account.Id> reviewers) {
+ Multimap<ReviewerStateInternal, Account.Id> reviewers) {
MailRecipients recipients = new MailRecipients();
- recipients.reviewers.addAll(reviewers.get(ReviewerState.REVIEWER));
- recipients.cc.addAll(reviewers.get(ReviewerState.CC));
+ recipients.reviewers.addAll(reviewers.get(REVIEWER));
+ recipients.cc.addAll(reviewers.get(CC));
return recipients;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index ba75723..f91b0fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -23,7 +23,7 @@
/** Send notice about a change failing to merged. */
public class MergeFailSender extends ReplyToChangeSender {
public static interface Factory {
- public MergeFailSender create(Change.Id id);
+ MergeFailSender create(Change.Id id);
}
@Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 7fbcf8d..9725e7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -31,7 +31,7 @@
/** Send notice about a change successfully merged. */
public class MergedSender extends ReplyToChangeSender {
public static interface Factory {
- public MergedSender create(Change.Id id);
+ MergedSender create(Change.Id id);
}
private final LabelTypes labelTypes;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 7dd51fe..5222544 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -19,6 +19,7 @@
import com.google.common.collect.Sets;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.EmailStrategy;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.mail.EmailHeader.AddressList;
@@ -95,28 +96,29 @@
if (shouldSendMessage()) {
if (fromId != null) {
final Account fromUser = args.accountCache.get(fromId).getAccount();
+ EmailStrategy strategy =
+ fromUser.getGeneralPreferences().getEmailStrategy();
- if (fromUser.getGeneralPreferences().isCopySelfOnEmails()) {
+ if (strategy == EmailStrategy.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.
//
- final String fromEmail = fromUser.getPreferredEmail();
- for (Iterator<Address> i = smtpRcptTo.iterator(); i.hasNext();) {
- if (i.next().email.equals(fromEmail)) {
- i.remove();
- }
- }
- for (EmailHeader hdr : headers.values()) {
- if (hdr instanceof AddressList) {
- ((AddressList) hdr).remove(fromEmail);
- }
+ 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();
+ if (thisUser.getGeneralPreferences().getEmailStrategy()
+ == EmailStrategy.DISABLED) {
+ removeUser(thisUser);
}
if (smtpRcptTo.isEmpty()) {
@@ -476,6 +478,20 @@
return r.toString();
}
+ private 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 (EmailHeader hdr : headers.values()) {
+ if (hdr instanceof AddressList) {
+ ((AddressList) hdr).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/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index c4d374f..c5d56b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -23,7 +23,7 @@
public class RegisterNewEmailSender extends OutgoingEmail {
public interface Factory {
- public RegisterNewEmailSender create(String address);
+ RegisterNewEmailSender create(String address);
}
private final EmailTokenVerifier tokenVerifier;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index 05c7933..7980845 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -31,7 +31,7 @@
/** Send notice of new patch sets for reviewers. */
public class ReplacePatchSetSender extends ReplyToChangeSender {
public static interface Factory {
- public ReplacePatchSetSender create(Change.Id id);
+ ReplacePatchSetSender create(Change.Id id);
}
private final Set<Account.Id> reviewers = new HashSet<>();
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
index 62a6c72..fbfbe61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -22,7 +22,7 @@
/** Alert a user to a reply to a change, usually commentary made during review. */
public abstract class ReplyToChangeSender extends ChangeEmail {
public static interface Factory<T extends ReplyToChangeSender> {
- public T create(Change.Id id);
+ T create(Change.Id id);
}
protected ReplyToChangeSender(EmailArguments ea, String mc, ChangeData cd)
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 43d53f0..96486e96 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}.
*/
- public abstract MimeType getMimeType(final String path, final byte[] content);
+ MimeType getMimeType(final String path, final 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).
*/
- public abstract boolean isSafeInline(final MimeType type);
+ boolean isSafeInline(final 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 b1ca9e7..d4555b4 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
@@ -63,6 +63,11 @@
return self();
}
+ public T reload() throws OrmException {
+ loaded = false;
+ return load();
+ }
+
public ObjectId loadRevision() throws OrmException {
if (loaded) {
return getRevision();
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 fb41027..22c8da4 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
@@ -86,8 +86,7 @@
}
public void setPatchSetId(PatchSet.Id psId) {
- checkArgument(psId == null
- || psId.getParentKey().equals(getChange().getId()));
+ checkArgument(psId == null || psId.getParentKey().equals(ctl.getId()));
this.psId = psId;
}
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 bd8f797..acd2852 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
@@ -98,7 +98,7 @@
IdentifiedUser user = ctl.getUser().asIdentifiedUser();
this.accountId = user.getAccountId();
this.changeNotes = getChangeNotes().load();
- this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(),
+ this.draftNotes = draftNotesFactory.create(ctl.getId(),
user.getAccountId());
this.upsertComments = Lists.newArrayList();
@@ -273,7 +273,7 @@
@Override
protected String getRefName() {
- return RefNames.refsDraftComments(accountId, getChange().getId());
+ return RefNames.refsDraftComments(accountId, ctl.getId());
}
@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 c561a0d..aeb8f46 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
@@ -33,6 +33,7 @@
static final FooterKey FOOTER_STATUS = new FooterKey("Status");
static final FooterKey FOOTER_SUBMITTED_WITH =
new FooterKey("Submitted-with");
+ static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
public static String changeRefName(Change.Id id) {
StringBuilder r = new StringBuilder();
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 063ff5a..b5019e6 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
@@ -18,6 +18,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
@@ -116,10 +117,11 @@
private final Change change;
private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
- private ImmutableSetMultimap<ReviewerState, Account.Id> reviewers;
+ private ImmutableSetMultimap<ReviewerStateInternal, Account.Id> reviewers;
private ImmutableList<Account.Id> allPastReviewers;
private ImmutableList<SubmitRecord> submitRecords;
- private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessages;
+ private ImmutableList<ChangeMessage> allChangeMessages;
+ private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
private ImmutableListMultimap<RevId, PatchLineComment> comments;
private ImmutableSet<String> hashtags;
NoteMap noteMap;
@@ -143,7 +145,7 @@
return approvals;
}
- public ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers() {
+ public ImmutableSetMultimap<ReviewerStateInternal, Account.Id> getReviewers() {
return reviewers;
}
@@ -170,9 +172,18 @@
return submitRecords;
}
- /** @return change messages by patch set, in chronological order. */
- public ImmutableListMultimap<PatchSet.Id, ChangeMessage> getChangeMessages() {
- return changeMessages;
+ /** @return all change messages, in chronological order, oldest first. */
+ public ImmutableList<ChangeMessage> getChangeMessages() {
+ return allChangeMessages;
+ }
+
+ /**
+ * @return change messages by patch set, in chronological order, oldest
+ * first.
+ */
+ public ImmutableListMultimap<PatchSet.Id, ChangeMessage>
+ getChangeMessagesByPatchSet() {
+ return changeMessagesByPatchSet;
}
/** @return inline comments on each revision. */
@@ -250,18 +261,20 @@
change.setStatus(parser.status);
}
approvals = parser.buildApprovals();
- changeMessages = parser.buildMessages();
+ changeMessagesByPatchSet = parser.buildMessagesByPatchSet();
+ allChangeMessages = parser.buildAllMessages();
comments = ImmutableListMultimap.copyOf(parser.comments);
noteMap = parser.commentNoteMap;
+ change.setTopic(Strings.emptyToNull(parser.topic));
if (parser.hashtags != null) {
hashtags = ImmutableSet.copyOf(parser.hashtags);
} else {
hashtags = ImmutableSet.of();
}
- ImmutableSetMultimap.Builder<ReviewerState, Account.Id> reviewers =
+ ImmutableSetMultimap.Builder<ReviewerStateInternal, Account.Id> reviewers =
ImmutableSetMultimap.builder();
- for (Map.Entry<Account.Id, ReviewerState> e
+ for (Map.Entry<Account.Id, ReviewerStateInternal> e
: parser.reviewers.entrySet()) {
reviewers.put(e.getValue(), e.getKey());
}
@@ -277,7 +290,8 @@
approvals = ImmutableListMultimap.of();
reviewers = ImmutableSetMultimap.of();
submitRecords = ImmutableList.of();
- changeMessages = ImmutableListMultimap.of();
+ allChangeMessages = ImmutableList.of();
+ changeMessagesByPatchSet = ImmutableListMultimap.of();
comments = ImmutableListMultimap.of();
hashtags = ImmutableSet.of();
}
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 6f8cb2b..9d759ed 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
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
import com.google.common.base.Enums;
@@ -26,6 +27,7 @@
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedListMultimap;
@@ -70,12 +72,13 @@
import java.util.Set;
class ChangeNotesParser implements AutoCloseable {
- final Map<Account.Id, ReviewerState> reviewers;
+ final Map<Account.Id, ReviewerStateInternal> reviewers;
final List<Account.Id> allPastReviewers;
final List<SubmitRecord> submitRecords;
final Multimap<RevId, PatchLineComment> comments;
NoteMap commentNoteMap;
Change.Status status;
+ String topic;
Set<String> hashtags;
private final Change.Id changeId;
@@ -84,7 +87,8 @@
private final Repository repo;
private final Map<PatchSet.Id,
Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
- private final Multimap<PatchSet.Id, ChangeMessage> changeMessages;
+ private final List<ChangeMessage> allChangeMessages;
+ private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
ChangeNotesParser(Change change, ObjectId tip, RevWalk walk,
GitRepositoryManager repoManager) throws RepositoryNotFoundException,
@@ -98,7 +102,8 @@
reviewers = Maps.newLinkedHashMap();
allPastReviewers = Lists.newArrayList();
submitRecords = Lists.newArrayListWithExpectedSize(1);
- changeMessages = LinkedListMultimap.create();
+ allChangeMessages = Lists.newArrayList();
+ changeMessagesByPatchSet = LinkedListMultimap.create();
comments = ArrayListMultimap.create();
}
@@ -133,11 +138,16 @@
return ImmutableListMultimap.copyOf(result);
}
- ImmutableListMultimap<PatchSet.Id, ChangeMessage> buildMessages() {
- for (Collection<ChangeMessage> v : changeMessages.asMap().values()) {
- Collections.sort((List<ChangeMessage>) v, ChangeNotes.MESSAGE_BY_TIME);
+ ImmutableList<ChangeMessage> buildAllMessages() {
+ return ImmutableList.copyOf(Lists.reverse(allChangeMessages));
+ }
+
+ ImmutableListMultimap<PatchSet.Id, ChangeMessage> buildMessagesByPatchSet() {
+ for (Collection<ChangeMessage> v :
+ changeMessagesByPatchSet.asMap().values()) {
+ Collections.reverse((List<ChangeMessage>) v);
}
- return ImmutableListMultimap.copyOf(changeMessages);
+ return ImmutableListMultimap.copyOf(changeMessagesByPatchSet);
}
private void parse(RevCommit commit) throws ConfigInvalidException {
@@ -147,6 +157,9 @@
PatchSet.Id psId = parsePatchSetId(commit);
Account.Id accountId = parseIdent(commit);
parseChangeMessage(psId, accountId, commit);
+ if (topic == null) {
+ topic = parseTopic(commit);
+ }
parseHashtags(commit);
@@ -160,13 +173,25 @@
parseApproval(psId, accountId, commit, line);
}
- for (ReviewerState state : ReviewerState.values()) {
+ for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
for (String line : commit.getFooterLines(state.getFooterKey())) {
parseReviewer(state, line);
}
}
}
+ private String parseTopic(RevCommit commit)
+ throws ConfigInvalidException {
+ List<String> topicLines = commit.getFooterLines(FOOTER_TOPIC);
+ if (topicLines.isEmpty()) {
+ return null;
+ } else if (topicLines.size() > 1) {
+ throw expectedOneFooter(FOOTER_TOPIC, topicLines);
+ }
+ return topicLines.get(0);
+ }
+
+
private void parseHashtags(RevCommit commit) throws ConfigInvalidException {
// Commits are parsed in reverse order and only the last set of hashtags should be used.
if (hashtags != null) {
@@ -267,7 +292,8 @@
new Timestamp(commit.getCommitterIdent().getWhen().getTime()),
psId);
changeMessage.setMessage(changeMsgString);
- changeMessages.put(psId, changeMessage);
+ changeMessagesByPatchSet.put(psId, changeMessage);
+ allChangeMessages.add(changeMessage);
}
private void parseComments()
@@ -385,7 +411,7 @@
GERRIT_PLACEHOLDER_HOST, email);
}
- private void parseReviewer(ReviewerState state, String line)
+ private void parseReviewer(ReviewerStateInternal state, String line)
throws ConfigInvalidException {
PersonIdent ident = RawParseUtils.parsePersonIdent(line);
if (ident == null) {
@@ -398,11 +424,11 @@
}
private void pruneReviewers() {
- Iterator<Map.Entry<Account.Id, ReviewerState>> rit =
+ Iterator<Map.Entry<Account.Id, ReviewerStateInternal>> rit =
reviewers.entrySet().iterator();
while (rit.hasNext()) {
- Map.Entry<Account.Id, ReviewerState> e = rit.next();
- if (e.getValue() == ReviewerState.REMOVED) {
+ Map.Entry<Account.Id, ReviewerStateInternal> e = rit.next();
+ if (e.getValue() == ReviewerStateInternal.REMOVED) {
rit.remove();
for (Table<Account.Id, ?, ?> curr : approvals.values()) {
curr.rowKeySet().remove(e.getKey());
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
index d715947..22ccac7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -30,6 +30,8 @@
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.client.StarredChange;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchLineCommentsUtil;
@@ -42,9 +44,12 @@
import com.google.inject.Provider;
import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
import java.io.IOException;
import java.sql.Timestamp;
@@ -95,7 +100,7 @@
}
public void rebuild(Change change, BatchRefUpdate bru,
- BatchRefUpdate bruForDrafts, Repository changeRepo,
+ BatchRefUpdate bruAllUsers, Repository changeRepo,
Repository allUsersRepo) throws NoSuchChangeException, IOException,
OrmException {
deleteRef(change, changeRepo);
@@ -169,15 +174,36 @@
draftUpdate = draftUpdateFactory.create(
controlFactory.controlFor(change, user), e.when);
draftUpdate.setPatchSetId(e.psId);
- batchForDrafts = draftUpdate.openUpdateInBatch(bruForDrafts);
+ batchForDrafts = draftUpdate.openUpdateInBatch(bruAllUsers);
}
e.applyDraft(draftUpdate);
}
writeToBatch(batchForDrafts, draftUpdate, allUsersRepo);
- synchronized(bruForDrafts) {
+ synchronized(bruAllUsers) {
batchForDrafts.commit();
}
}
+
+ createStarredChangesRefs(changeId, bruAllUsers, allUsersRepo);
+ }
+
+ private void createStarredChangesRefs(Change.Id changeId,
+ BatchRefUpdate bruAllUsers, Repository allUsersRepo)
+ throws IOException, OrmException {
+ ObjectId emptyTree = emptyTree(allUsersRepo);
+ for (StarredChange starred : dbProvider.get().starredChanges()
+ .byChange(changeId)) {
+ bruAllUsers.addCommand(new ReceiveCommand(ObjectId.zeroId(), emptyTree,
+ RefNames.refsStarredChanges(starred.getAccountId(), changeId)));
+ }
+ }
+
+ private static ObjectId emptyTree(Repository repo) throws IOException {
+ try (ObjectInserter oi = repo.newObjectInserter()) {
+ ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+ oi.flush();
+ return id;
+ }
}
private void deleteRef(Change change, Repository changeRepo)
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 cb35619..7c011b6 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
@@ -20,11 +20,13 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap;
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.Lists;
import com.google.common.collect.Maps;
@@ -86,12 +88,13 @@
private final AccountCache accountCache;
private final Map<String, Optional<Short>> approvals;
- private final Map<Account.Id, ReviewerState> reviewers;
+ private final Map<Account.Id, ReviewerStateInternal> reviewers;
private Change.Status status;
private String subject;
private List<SubmitRecord> submitRecords;
private final CommentsInNotesUtil commentsUtil;
private List<PatchLineComment> comments;
+ private String topic;
private Set<String> hashtags;
private String changeMessage;
private ChangeNotes notes;
@@ -165,7 +168,11 @@
public void setStatus(Change.Status status) {
checkArgument(status != Change.Status.MERGED,
- "use submit(Iterable<PatchSetApproval>)");
+ "use merge(Iterable<SubmitRecord>)");
+ this.status = status;
+ }
+
+ public void fixStatus(Change.Status status) {
this.status = status;
}
@@ -312,17 +319,21 @@
}
+ public void setTopic(String topic) {
+ this.topic = Strings.nullToEmpty(topic);
+ }
+
public void setHashtags(Set<String> hashtags) {
this.hashtags = hashtags;
}
- public void putReviewer(Account.Id reviewer, ReviewerState type) {
- checkArgument(type != ReviewerState.REMOVED, "invalid ReviewerType");
+ public void putReviewer(Account.Id reviewer, ReviewerStateInternal type) {
+ checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType");
reviewers.put(reviewer, type);
}
public void removeReviewer(Account.Id reviewer) {
- reviewers.put(reviewer, ReviewerState.REMOVED);
+ reviewers.put(reviewer, ReviewerStateInternal.REMOVED);
}
/** @return the tree id for the updated tree */
@@ -383,7 +394,7 @@
@Override
protected String getRefName() {
- return ChangeNoteUtil.changeRefName(getChange().getId());
+ return ChangeNoteUtil.changeRefName(ctl.getId());
}
@Override
@@ -414,11 +425,15 @@
addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
}
+ if (topic != null) {
+ addFooter(msg, FOOTER_TOPIC, topic);
+ }
+
if (hashtags != null) {
addFooter(msg, FOOTER_HASHTAGS, Joiner.on(",").join(hashtags));
}
- for (Map.Entry<Account.Id, ReviewerState> e : reviewers.entrySet()) {
+ for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
Account account = accountCache.get(e.getKey()).getAccount();
PersonIdent ident = newIdent(account, when);
addFooter(msg, e.getValue().getFooterKey())
@@ -477,7 +492,8 @@
&& status == null
&& subject == null
&& submitRecords == null
- && hashtags == null;
+ && hashtags == null
+ && topic == null;
}
private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
index e65f8e82..d0cb1df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -243,9 +243,6 @@
}
int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
- if (startChar == 0) {
- return null;
- }
if (note[ptr.value] == '-') {
range.setStartCharacter(startChar);
ptr.value += 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
new file mode 100644
index 0000000..3bf2135
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -0,0 +1,64 @@
+// 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.notedb;
+
+import com.google.gerrit.extensions.client.ReviewerState;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+import java.util.Arrays;
+
+/** State of a reviewer on a change. */
+public enum ReviewerStateInternal {
+ /** The user has contributed at least one nonzero vote on the change. */
+ REVIEWER(new FooterKey("Reviewer"), ReviewerState.REVIEWER),
+
+ /** The reviewer was added to the change, but has not voted. */
+ CC(new FooterKey("CC"), ReviewerState.CC),
+
+ /** The user was previously a reviewer on the change, but was removed. */
+ REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
+
+ static {
+ boolean ok = true;
+ if (ReviewerStateInternal.values().length != ReviewerState.values().length) {
+ ok = false;
+ }
+ for (ReviewerStateInternal s : ReviewerStateInternal.values()) {
+ ok &= s.name().equals(s.state.name());
+ }
+ if (!ok) {
+ throw new IllegalStateException("Mismatched reviewer state mapping: "
+ + Arrays.asList(ReviewerStateInternal.values()) + " != "
+ + Arrays.asList(ReviewerState.values()));
+ }
+ }
+
+ private final FooterKey footerKey;
+ private final ReviewerState state;
+
+ private ReviewerStateInternal(FooterKey footerKey, ReviewerState state) {
+ this.footerKey = footerKey;
+ this.state = state;
+ }
+
+ FooterKey getFooterKey() {
+ return footerKey;
+ }
+
+ public ReviewerState asReviewerState() {
+ return state;
+ }
+}
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 a6332f0..3680dc3 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
@@ -20,12 +20,12 @@
/** Provides a cached list of {@link PatchListEntry}. */
public interface PatchListCache {
- public PatchList get(PatchListKey key, Project.NameKey project)
+ PatchList get(PatchListKey key, Project.NameKey project)
throws PatchListNotAvailableException;
- public PatchList get(Change change, PatchSet patchSet)
+ PatchList get(Change change, PatchSet patchSet)
throws PatchListNotAvailableException;
- public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
+ IntraLineDiff getIntraLineDiff(IntraLineDiffKey key,
IntraLineDiffArgs args);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 5de28f3..d130175 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -51,7 +51,7 @@
static PatchListEntry empty(final String fileName) {
return new PatchListEntry(ChangeType.MODIFIED, PatchType.UNIFIED, null,
- fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0, 0);
+ fileName, EMPTY_HEADER, Collections.<Edit> emptyList(), 0, 0, 0, 0);
}
private final ChangeType changeType;
@@ -62,11 +62,13 @@
private final List<Edit> edits;
private final int insertions;
private final int deletions;
+ private final long size;
private final long sizeDelta;
// Note: When adding new fields, the serialVersionUID in PatchListKey must be
// incremented so that entries from the cache are automatically invalidated.
- PatchListEntry(FileHeader hdr, List<Edit> editList, long sizeDelta) {
+ PatchListEntry(FileHeader hdr, List<Edit> editList, long size,
+ long sizeDelta) {
changeType = toChangeType(hdr);
patchType = toPatchType(hdr);
@@ -111,12 +113,13 @@
}
insertions = ins;
deletions = del;
+ this.size = size;
this.sizeDelta = sizeDelta;
}
private PatchListEntry(ChangeType changeType, PatchType patchType,
String oldName, String newName, byte[] header, List<Edit> edits,
- int insertions, int deletions, long sizeDelta) {
+ int insertions, int deletions, long size, long sizeDelta) {
this.changeType = changeType;
this.patchType = patchType;
this.oldName = oldName;
@@ -125,6 +128,7 @@
this.edits = edits;
this.insertions = insertions;
this.deletions = deletions;
+ this.size = size;
this.sizeDelta = sizeDelta;
}
@@ -172,6 +176,10 @@
return deletions;
}
+ public long getSize() {
+ return size;
+ }
+
public long getSizeDelta() {
return sizeDelta;
}
@@ -208,6 +216,7 @@
writeBytes(out, header);
writeVarInt32(out, insertions);
writeVarInt32(out, deletions);
+ writeFixInt64(out, size);
writeFixInt64(out, sizeDelta);
writeVarInt32(out, edits.size());
@@ -227,6 +236,7 @@
byte[] hdr = readBytes(in);
int ins = readVarInt32(in);
int del = readVarInt32(in);
+ long size = readFixInt64(in);
long sizeDelta = readFixInt64(in);
int editCount = readVarInt32(in);
@@ -240,7 +250,7 @@
}
return new PatchListEntry(changeType, patchType, oldName, newName, hdr,
- toList(editArray), ins, del, sizeDelta);
+ toList(editArray), ins, del, size, sizeDelta);
}
private static List<Edit> toList(Edit[] l) {
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 15277b2..0200fa5 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
@@ -34,7 +34,7 @@
import java.io.Serializable;
public class PatchListKey implements Serializable {
- static final long serialVersionUID = 18L;
+ static final long serialVersionUID = 19L;
public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
Whitespace.IGNORE_NONE, 'N',
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 aa2a8a8..10ea61c 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
@@ -206,7 +206,7 @@
getFileSize(repo, reader, e.getOldMode(), e.getOldPath(), aTree);
long newSize =
getFileSize(repo, reader, e.getNewMode(), e.getNewPath(), bTree);
- entries.add(newEntry(aTree, fh, newSize - oldSize));
+ entries.add(newEntry(aTree, fh, newSize, newSize - oldSize));
}
}
return new PatchList(a, b, againstParent,
@@ -301,37 +301,38 @@
byte[] rawHdr = hdr.toString().getBytes(UTF_8);
byte[] aContent = aText.getContent();
byte[] bContent = bText.getContent();
+ long size = bContent.length;
long sizeDelta = bContent.length - aContent.length;
RawText aRawText = new RawText(aContent);
RawText bRawText = new RawText(bContent);
EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
- return new PatchListEntry(fh, edits, sizeDelta);
+ return new PatchListEntry(fh, edits, size, sizeDelta);
}
private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader,
- long sizeDelta) {
+ long size, long sizeDelta) {
final FileMode oldMode = fileHeader.getOldMode();
final FileMode newMode = fileHeader.getNewMode();
if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
- sizeDelta);
+ size, sizeDelta);
}
if (aTree == null // want combined diff
|| fileHeader.getPatchType() != PatchType.UNIFIED
|| fileHeader.getHunks().isEmpty()) {
return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
- sizeDelta);
+ size, sizeDelta);
}
List<Edit> edits = fileHeader.toEditList();
if (edits.isEmpty()) {
return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
- sizeDelta);
+ size, sizeDelta);
} else {
- return new PatchListEntry(fileHeader, edits, sizeDelta);
+ return new PatchListEntry(fileHeader, edits, size, sizeDelta);
}
}
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 6b458aa..5df364f 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
@@ -34,6 +34,7 @@
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.util.PluginRequestContext;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -77,6 +78,7 @@
private final List<StartPluginListener> onStart;
private final List<StopPluginListener> onStop;
private final List<ReloadPluginListener> onReload;
+ private final MetricMaker serverMetrics;
private Module sysModule;
private Module sshModule;
@@ -102,12 +104,14 @@
Injector sysInjector,
ThreadLocalRequestContext local,
ServerInformation srvInfo,
- CopyConfigModule ccm) {
+ CopyConfigModule ccm,
+ MetricMaker serverMetrics) {
this.sysInjector = sysInjector;
this.srvInfo = srvInfo;
this.local = local;
this.copyConfigModule = ccm;
this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
+ this.serverMetrics = serverMetrics;
onStart = new CopyOnWriteArrayList<>();
onStart.addAll(listeners(sysInjector, StartPluginListener.class));
@@ -127,6 +131,10 @@
return srvInfo;
}
+ MetricMaker getServerMetrics() {
+ return serverMetrics;
+ }
+
boolean hasDynamicItem(TypeLiteral<?> type) {
return sysItems.containsKey(type)
|| (sshItems != null && sshItems.containsKey(type))
@@ -424,6 +432,7 @@
}
}
}
+
private void reattachItem(
ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
Map<TypeLiteral<?>, DynamicItem<?>> items,
@@ -564,6 +573,9 @@
if (StopPluginListener.class.isAssignableFrom(type)) {
return false;
}
+ if (MetricMaker.class.isAssignableFrom(type)) {
+ return false;
+ }
if (type.getName().startsWith("com.google.inject.")) {
return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
new file mode 100644
index 0000000..724ebeb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
@@ -0,0 +1,204 @@
+// 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.plugins;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Histogram0;
+import com.google.gerrit.metrics.Histogram1;
+import com.google.gerrit.metrics.Histogram2;
+import com.google.gerrit.metrics.Histogram3;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+public class PluginMetricMaker extends MetricMaker implements LifecycleListener {
+ private final MetricMaker root;
+ private final String prefix;
+ private final Set<RegistrationHandle> cleanup;
+
+ public PluginMetricMaker(MetricMaker root, String pluginName) {
+ this.root = root;
+ this.prefix = String.format("plugins/%s/", pluginName);
+ cleanup = Collections.synchronizedSet(new HashSet<RegistrationHandle>());
+ }
+
+ @Override
+ public Counter0 newCounter(String name, Description desc) {
+ Counter0 m = root.newCounter(prefix + name, desc);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1> Counter1<F1> newCounter(
+ String name, Description desc,
+ Field<F1> field1) {
+ Counter1<F1> m = root.newCounter(prefix + name, desc, field1);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2> Counter2<F1, F2> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ Counter2<F1, F2> m = root.newCounter(prefix + name, desc, field1, field2);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ Counter3<F1, F2, F3> m =
+ root.newCounter(prefix + name, desc, field1, field2, field3);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public Timer0 newTimer(String name, Description desc) {
+ Timer0 m = root.newTimer(prefix + name, desc);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1> Timer1<F1> newTimer(
+ String name, Description desc,
+ Field<F1> field1) {
+ Timer1<F1> m = root.newTimer(prefix + name, desc, field1);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2> Timer2<F1, F2> newTimer(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ Timer2<F1, F2> m = root.newTimer(prefix + name, desc, field1, field2);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ Timer3<F1, F2, F3> m =
+ root.newTimer(prefix + name, desc, field1, field2, field3);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public Histogram0 newHistogram(String name, Description desc) {
+ Histogram0 m = root.newHistogram(prefix + name, desc);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1> Histogram1<F1> newHistogram(
+ String name, Description desc,
+ Field<F1> field1) {
+ Histogram1<F1> m = root.newHistogram(prefix + name, desc, field1);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2> Histogram2<F1, F2> newHistogram(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2) {
+ Histogram2<F1, F2> m = root.newHistogram(prefix + name, desc, field1, field2);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(
+ String name, Description desc,
+ Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+ Histogram3<F1, F2, F3> m =
+ root.newHistogram(prefix + name, desc, field1, field2, field3);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <V> CallbackMetric0<V> newCallbackMetric(
+ String name, Class<V> valueClass, Description desc) {
+ CallbackMetric0<V> m = root.newCallbackMetric(prefix + name, valueClass, desc);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public <F1, V> CallbackMetric1<F1, V> newCallbackMetric(String name,
+ Class<V> valueClass, Description desc, Field<F1> field1) {
+ CallbackMetric1<F1, V> m =
+ root.newCallbackMetric(prefix + name, valueClass, desc, field1);
+ cleanup.add(m);
+ return m;
+ }
+
+ @Override
+ public RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics,
+ Runnable trigger) {
+ final RegistrationHandle handle = root.newTrigger(metrics, trigger);
+ cleanup.add(handle);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ handle.remove();
+ cleanup.remove(handle);
+ }
+ };
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ synchronized (cleanup) {
+ Iterator<RegistrationHandle> itr = cleanup.iterator();
+ while (itr.hasNext()) {
+ itr.next().remove();
+ itr.remove();
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
index 72a499e..73fb9c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
@@ -16,5 +16,5 @@
/** Broadcasts event indicating a plugin was reloaded. */
public interface ReloadPluginListener {
- public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
+ void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
}
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 14c1185..ea96a56 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
@@ -236,7 +236,7 @@
if (getApiType() == ApiType.PLUGIN) {
modules.add(env.getSysModule());
}
- modules.add(new ServerPluginInfoModule(this));
+ modules.add(new ServerPluginInfoModule(this, env.getServerMetrics()));
return Guice.createInjector(modules);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
index b0e9453..a7f0087 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -17,6 +17,8 @@
import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.PluginUser;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
@@ -32,10 +34,12 @@
private final Path dataDir;
private volatile boolean ready;
+ private final MetricMaker serverMetrics;
- ServerPluginInfoModule(ServerPlugin plugin) {
+ ServerPluginInfoModule(ServerPlugin plugin, MetricMaker serverMetrics) {
this.plugin = plugin;
this.dataDir = plugin.getDataDir();
+ this.serverMetrics = serverMetrics;
}
@Override
@@ -47,6 +51,17 @@
bind(String.class)
.annotatedWith(PluginCanonicalWebUrl.class)
.toInstance(plugin.getPluginCanonicalWebUrl());
+
+ install(new LifecycleModule() {
+ @Override
+ public void configure() {
+ PluginMetricMaker metrics = new PluginMetricMaker(
+ serverMetrics,
+ plugin.getName());
+ bind(MetricMaker.class).toInstance(metrics);
+ listener().toInstance(metrics);
+ }
+ });
}
@Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
index aaad370..0d27c87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
@@ -16,5 +16,5 @@
/** Broadcasts event indicating a plugin was loaded. */
public interface StartPluginListener {
- public void onStartPlugin(Plugin plugin);
+ void onStartPlugin(Plugin plugin);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
index 24bd655..7ce53a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StopPluginListener.java
@@ -16,5 +16,5 @@
/** Broadcasts event indicating a plugin was unloaded. */
public interface StopPluginListener {
- public void onStopPlugin(Plugin plugin);
+ void onStopPlugin(Plugin plugin);
}
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 3ab6ff5..606ca78d 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
@@ -28,6 +28,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.query.change.ChangeData;
@@ -89,7 +90,7 @@
throws NoSuchChangeException, OrmException {
ChangeControl c = controlFor(change, user);
if (!c.isVisible(db.get())) {
- throw new NoSuchChangeException(c.getChange().getId());
+ throw new NoSuchChangeException(c.getId());
}
return c;
}
@@ -103,23 +104,27 @@
private final ChangeData.Factory changeDataFactory;
private final RefControl refControl;
private final ChangeNotes notes;
+ private final ApprovalsUtil approvalsUtil;
@AssistedInject
ChangeControl(
ChangeData.Factory changeDataFactory,
ChangeNotes.Factory notesFactory,
+ ApprovalsUtil approvalsUtil,
@Assisted RefControl refControl,
@Assisted Change change) {
- this(changeDataFactory, refControl,
+ this(changeDataFactory, approvalsUtil, refControl,
notesFactory.create(change));
}
@AssistedInject
ChangeControl(
ChangeData.Factory changeDataFactory,
+ ApprovalsUtil approvalsUtil,
@Assisted RefControl refControl,
@Assisted ChangeNotes notes) {
this.changeDataFactory = changeDataFactory;
+ this.approvalsUtil = approvalsUtil;
this.refControl = refControl;
this.notes = notes;
}
@@ -128,7 +133,7 @@
if (getUser().equals(who)) {
return this;
}
- return new ChangeControl(changeDataFactory,
+ return new ChangeControl(changeDataFactory, approvalsUtil,
getRefControl().forUser(who), notes);
}
@@ -148,6 +153,10 @@
return getProjectControl().getProject();
}
+ public Change.Id getId() {
+ return notes.getChangeId();
+ }
+
public Change getChange() {
return notes.getChange();
}
@@ -190,13 +199,13 @@
}
/** Can this user abandon this change? */
- public boolean canAbandon() {
- return isOwner() // owner (aka creator) of the change can abandon
+ public boolean canAbandon(ReviewDb db) throws OrmException {
+ return (isOwner() // owner (aka creator) of the change can abandon
|| getRefControl().isOwner() // branch owner can abandon
|| getProjectControl().isOwner() // project owner can abandon
|| getUser().getCapabilities().canAdministrateServer() // site administers are god
|| getRefControl().canAbandon() // user can abandon a specific ref
- ;
+ ) && !isPatchSetLocked(db);
}
/** Can this user publish this draft change or any draft patch set of this change? */
@@ -212,14 +221,14 @@
}
/** Can this user rebase this change? */
- public boolean canRebase() {
- return isOwner() || getRefControl().canSubmit()
- || getRefControl().canRebase();
+ public boolean canRebase(ReviewDb db) throws OrmException {
+ return (isOwner() || getRefControl().canSubmit()
+ || getRefControl().canRebase()) && !isPatchSetLocked(db);
}
/** Can this user restore this change? */
- public boolean canRestore() {
- return canAbandon() // Anyone who can abandon the change can restore it back
+ public boolean canRestore(ReviewDb db) throws OrmException {
+ return canAbandon(db) // Anyone who can abandon the change can restore it back
&& getRefControl().canUpload(); // as long as you can upload too
}
@@ -258,8 +267,25 @@
}
/** Can this user add a patch set to this change? */
- public boolean canAddPatchSet() {
- return getRefControl().canUpload();
+ public boolean canAddPatchSet(ReviewDb db) throws OrmException {
+ return getRefControl().canUpload() && !isPatchSetLocked(db);
+ }
+
+ /** Is the current patch set locked against state changes? */
+ public boolean isPatchSetLocked(ReviewDb db) throws OrmException {
+ if (getChange().getStatus() == Change.Status.MERGED) {
+ return false;
+ }
+
+ for (PatchSetApproval ap : approvalsUtil.byPatchSet(db, this,
+ getChange().currentPatchSetId())) {
+ LabelType type = getLabelTypes().byLabel(ap.getLabel());
+ if (type != null && ap.getValue() == 1
+ && type.getFunctionName().equalsIgnoreCase("PatchSetLock")) {
+ return true;
+ }
+ }
+ return false;
}
/** Is this user the owner of the change? */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index d451b46..67bdc88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -23,10 +23,10 @@
/** Cache of project information, including access rights. */
public interface ProjectCache {
/** @return the parent state for all projects on this server. */
- public ProjectState getAllProjects();
+ ProjectState getAllProjects();
/** @return the project state of the project storing meta data for all users. */
- public ProjectState getAllUsers();
+ ProjectState getAllUsers();
/**
* Get the cached data for a project by its unique name.
@@ -35,7 +35,7 @@
* @return the cached data; null if no such project exists or a error occurred.
* @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
*/
- public ProjectState get(Project.NameKey projectName);
+ ProjectState get(Project.NameKey projectName);
/**
* Get the cached data for a project by its unique name.
@@ -44,14 +44,14 @@
* @throws IOException when there was an error.
* @return the cached data; null if no such project exists.
*/
- public ProjectState checkedGet(Project.NameKey projectName)
+ ProjectState checkedGet(Project.NameKey projectName)
throws IOException;
/** Invalidate the cached information about the given project. */
- public void evict(Project p);
+ void evict(Project p);
/** Invalidate the cached information about the given project. */
- public void evict(Project.NameKey p);
+ void evict(Project.NameKey p);
/**
* Remove information about the given project from the cache. It will no
@@ -60,14 +60,14 @@
void remove(Project p);
/** @return sorted iteration of projects. */
- public abstract Iterable<Project.NameKey> all();
+ Iterable<Project.NameKey> all();
/**
* @return estimated set of relevant groups extracted from hot project access
* rules. If the cache is cold or too small for the entire project set
* of the server, this set may be incomplete.
*/
- public abstract Set<AccountGroup.UUID> guessRelevantGroupUUIDs();
+ Set<AccountGroup.UUID> guessRelevantGroupUUIDs();
/**
* Filter the set of registered project names by common prefix.
@@ -75,8 +75,8 @@
* @param prefix common prefix.
* @return sorted iteration of projects sharing the same prefix.
*/
- public abstract Iterable<Project.NameKey> byName(String prefix);
+ Iterable<Project.NameKey> byName(String prefix);
/** Notify the cache that a new project was constructed. */
- public void onCreateProject(Project.NameKey newProjectName);
+ void onCreateProject(Project.NameKey newProjectName);
}
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 76ad2f0..5f921b1 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
@@ -36,6 +36,7 @@
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
@@ -89,6 +90,7 @@
private final DynamicMap<RestView<ProjectResource>> views;
private final Provider<CurrentUser> user;
private final ChangeHooks hooks;
+ private final GitReferenceUpdated gitRefUpdated;
@Inject
PutConfig(@EnableSignedPush boolean serverEnableSignedPush,
@@ -102,6 +104,7 @@
AllProjectsNameProvider allProjects,
DynamicMap<RestView<ProjectResource>> views,
ChangeHooks hooks,
+ GitReferenceUpdated gitRefUpdated,
Provider<CurrentUser> user) {
this.serverEnableSignedPush = serverEnableSignedPush;
this.metaDataUpdateFactory = metaDataUpdateFactory;
@@ -114,6 +117,7 @@
this.allProjects = allProjects;
this.views = views;
this.hooks = hooks;
+ this.gitRefUpdated = gitRefUpdated;
this.user = user;
}
@@ -199,6 +203,8 @@
ObjectId commitRev = projectConfig.commit(md);
// Only fire hook if project was actually changed.
if (!Objects.equals(baseRev, commitRev)) {
+ gitRefUpdated.fire(projectName, RefNames.REFS_CONFIG,
+ baseRev, commitRev);
hooks.doRefUpdatedHook(
new Branch.NameKey(projectName, RefNames.REFS_CONFIG),
baseRev, commitRev, user.get().asIdentifiedUser().getAccount());
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 d589865..2d407bd 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
@@ -27,6 +27,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
@@ -46,15 +47,18 @@
private final MetaDataUpdate.Server updateFactory;
private final GitRepositoryManager gitMgr;
private final ChangeHooks hooks;
+ private final GitReferenceUpdated gitRefUpdated;
@Inject
PutDescription(ProjectCache cache,
MetaDataUpdate.Server updateFactory,
ChangeHooks hooks,
+ GitReferenceUpdated gitRefUpdated,
GitRepositoryManager gitMgr) {
this.cache = cache;
this.updateFactory = updateFactory;
this.hooks = hooks;
+ this.gitRefUpdated = gitRefUpdated;
this.gitMgr = gitMgr;
}
@@ -91,6 +95,8 @@
ObjectId commitRev = config.commit(md);
// Only fire hook if project was actually changed.
if (!Objects.equals(baseRev, commitRev)) {
+ gitRefUpdated.fire(resource.getNameKey(), RefNames.REFS_CONFIG,
+ baseRev, commitRev);
hooks.doRefUpdatedHook(
new Branch.NameKey(resource.getNameKey(), RefNames.REFS_CONFIG),
baseRev, commitRev, user.getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java
index 83cdc80..8a1718d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/DataSource.java
@@ -19,8 +19,8 @@
public interface DataSource<T> {
/** @return an estimate of the number of results from {@link #read()}. */
- public int getCardinality();
+ int getCardinality();
/** @return read from the database and return the results. */
- public ResultSet<T> read() throws OrmException;
+ ResultSet<T> read() throws OrmException;
}
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 c8f9972..3a21ce4 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
@@ -73,6 +73,11 @@
* @param <T> type of object the predicates can evaluate in memory.
*/
public abstract class QueryBuilder<T> {
+ /** Converts a value string passed to an operator into a {@link Predicate}. */
+ public interface OperatorFactory<T, Q extends QueryBuilder<T>> {
+ Predicate<T> create(Q builder, String value) throws QueryParseException;
+ }
+
/**
* Defines the operators known by a QueryBuilder.
*
@@ -162,7 +167,7 @@
protected final Definition<T, ? extends QueryBuilder<T>> builderDef;
@SuppressWarnings("rawtypes")
- private final Map<String, OperatorFactory> opFactories;
+ protected final Map<String, OperatorFactory> opFactories;
@SuppressWarnings({"unchecked", "rawtypes"})
protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
@@ -323,11 +328,6 @@
return new QueryParseException(msg, why);
}
- /** Converts a value string passed to an operator into a {@link Predicate}. */
- protected interface OperatorFactory<T, Q extends QueryBuilder<T>> {
- Predicate<T> create(Q builder, String value) throws QueryParseException;
- }
-
/** Denotes a method which is a query operator. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
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 9677f5f..a3fb523 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
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
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.SetMultimap;
import com.google.gerrit.common.data.SubmitRecord;
@@ -44,7 +45,7 @@
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.notedb.ReviewerState;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
@@ -415,7 +416,7 @@
this.patchListCache = patchListCache;
this.notesMigration = notesMigration;
this.mergeabilityCache = mergeabilityCache;
- legacyId = c.getChange().getId();
+ legacyId = c.getId();
change = c.getChange();
changeControl = c;
notes = c.getNotes();
@@ -543,6 +544,23 @@
return changeControl;
}
+ public ChangeControl changeControl(CurrentUser user) throws OrmException {
+ if (changeControl != null) {
+ throw new IllegalStateException(
+ "user already specified: " + changeControl.getUser());
+ }
+ try {
+ if (change != null) {
+ changeControl = changeControlFactory.controlFor(change, user);
+ } else {
+ changeControl = changeControlFactory.controlFor(legacyId, user);
+ }
+ } catch (NoSuchChangeException e) {
+ throw new OrmException(e);
+ }
+ return changeControl;
+ }
+
void cacheVisibleTo(ChangeControl ctl) {
visibleTo = ctl.getUser();
changeControl = ctl;
@@ -706,7 +724,7 @@
return allApprovals;
}
- public SetMultimap<ReviewerState, Account.Id> reviewers()
+ public SetMultimap<ReviewerStateInternal, Account.Id> reviewers()
throws OrmException {
return approvalsUtil.getReviewers(notes(), approvals().values());
}
@@ -808,10 +826,7 @@
events.add(ReviewedByEvent.create(msg));
}
}
- for (PatchSet ps : patchSets()) {
- events.add(ReviewedByEvent.create(ps));
- }
- Collections.sort(events, Collections.reverseOrder());
+ events = Lists.reverse(events);
reviewedBy = new LinkedHashSet<>();
Account.Id owner = c.getOwner();
for (ReviewedByEvent event : events) {
@@ -829,12 +844,7 @@
}
@AutoValue
- abstract static class ReviewedByEvent implements Comparable<ReviewedByEvent> {
- private static ReviewedByEvent create(PatchSet ps) {
- return new AutoValue_ChangeData_ReviewedByEvent(
- ps.getUploader(), ps.getCreatedOn());
- }
-
+ abstract static class ReviewedByEvent {
private static ReviewedByEvent create(ChangeMessage msg) {
return new AutoValue_ChangeData_ReviewedByEvent(
msg.getAuthor(), msg.getWrittenOn());
@@ -842,11 +852,6 @@
public abstract Account.Id author();
public abstract Timestamp ts();
-
- @Override
- public int compareTo(ReviewedByEvent other) {
- return ts().compareTo(other.ts());
- }
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
index 47bf82d..c32ff0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
@@ -18,5 +18,5 @@
public interface ChangeDataSource extends DataSource<ChangeData> {
/** @return true if all returned ChangeData.hasChange() will be true. */
- public boolean hasChange();
+ boolean hasChange();
}
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 a4cff07..440731e 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
@@ -25,6 +25,7 @@
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
@@ -86,6 +87,10 @@
* Parses a query string meant to be applied to change objects.
*/
public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
+ public interface ChangeOperatorFactory
+ extends OperatorFactory<ChangeData, ChangeQueryBuilder> {
+ }
+
private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
private static final Pattern PAT_CHANGE_ID =
Pattern.compile("^[iI][0-9a-f]{4,}.*$");
@@ -151,6 +156,7 @@
final Provider<ReviewDb> db;
final Provider<InternalChangeQuery> queryProvider;
final IndexRewriter rewriter;
+ final DynamicMap<ChangeOperatorFactory> opFactories;
final IdentifiedUser.GenericFactory userFactory;
final CapabilityControl.Factory capabilityControlFactory;
final ChangeControl.GenericFactory changeControlGenericFactory;
@@ -180,6 +186,7 @@
public Arguments(Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider,
IndexRewriter rewriter,
+ DynamicMap<ChangeOperatorFactory> opFactories,
IdentifiedUser.GenericFactory userFactory,
Provider<CurrentUser> self,
CapabilityControl.Factory capabilityControlFactory,
@@ -202,7 +209,7 @@
IndexConfig indexConfig,
Provider<ListMembers> listMembers,
@GerritServerConfig Config cfg) {
- this(db, queryProvider, rewriter, userFactory, self,
+ this(db, queryProvider, rewriter, opFactories, userFactory, self,
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
@@ -217,6 +224,7 @@
Provider<ReviewDb> db,
Provider<InternalChangeQuery> queryProvider,
IndexRewriter rewriter,
+ DynamicMap<ChangeOperatorFactory> opFactories,
IdentifiedUser.GenericFactory userFactory,
Provider<CurrentUser> self,
CapabilityControl.Factory capabilityControlFactory,
@@ -242,6 +250,7 @@
this.db = db;
this.queryProvider = queryProvider;
this.rewriter = rewriter;
+ this.opFactories = opFactories;
this.userFactory = userFactory;
this.self = self;
this.capabilityControlFactory = capabilityControlFactory;
@@ -267,7 +276,7 @@
}
Arguments asUser(CurrentUser otherUser) {
- return new Arguments(db, queryProvider, rewriter, userFactory,
+ return new Arguments(db, queryProvider, rewriter, opFactories, userFactory,
Providers.of(otherUser),
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
@@ -320,6 +329,7 @@
ChangeQueryBuilder(Arguments args) {
super(mydef);
this.args = args;
+ setupDynamicOperators();
}
@VisibleForTesting
@@ -330,6 +340,13 @@
this.args = args;
}
+ private void setupDynamicOperators() {
+ for (DynamicMap.Entry<ChangeOperatorFactory> e : args.opFactories) {
+ String name = e.getExportName() + "_" + e.getPluginName();
+ opFactories.put(name, e.getProvider().get());
+ }
+ }
+
public ChangeQueryBuilder asUser(CurrentUser user) {
return new ChangeQueryBuilder(builderDef, args.asUser(user));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCache.java
index bf7a5dd..e8b2fef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCache.java
@@ -18,8 +18,8 @@
public interface ConflictsCache {
- public void put(ConflictKey key, Boolean value);
+ void put(ConflictKey key, Boolean value);
@Nullable
- public Boolean getIfPresent(ConflictKey key);
+ Boolean getIfPresent(ConflictKey key);
}
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 34a2bdf..ff6f2bc 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
@@ -106,6 +106,11 @@
return this;
}
+ public InternalChangeQuery setRequestedFields(Set<String> fields) {
+ qp.setRequestedFields(fields);
+ return this;
+ }
+
public List<ChangeData> byKey(Change.Key key) throws OrmException {
return byKeyPrefix(key.get());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
index 1964fa5..a70f892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
@@ -17,29 +17,36 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.server.index.IndexConfig;
+import java.util.Set;
+
@AutoValue
public abstract class QueryOptions {
- public static QueryOptions create(IndexConfig config, int start, int limit) {
+ public static QueryOptions create(IndexConfig config, int start, int limit,
+ Set<String> fields) {
checkArgument(start >= 0, "start must be nonnegative: %s", start);
checkArgument(limit > 0, "limit must be positive: %s", limit);
- return new AutoValue_QueryOptions(config, start, limit);
+ return new AutoValue_QueryOptions(config, start, limit,
+ ImmutableSet.copyOf(fields));
}
public static QueryOptions oneResult() {
- return create(IndexConfig.createDefault(), 0, 1);
+ return create(IndexConfig.createDefault(), 0, 1,
+ ImmutableSet.<String> of());
}
public abstract IndexConfig config();
public abstract int start();
public abstract int limit();
+ public abstract ImmutableSet<String> fields();
public QueryOptions withLimit(int newLimit) {
- return create(config(), start(), newLimit);
+ return create(config(), start(), newLimit, fields());
}
public QueryOptions withStart(int newStart) {
- return create(config(), newStart, limit());
+ return create(config(), newStart, limit(), fields());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index a2c8b81..1a6ae02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -18,10 +18,16 @@
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.index.IndexRewriter;
@@ -32,32 +38,41 @@
import com.google.gwtorm.server.ResultSet;
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.Set;
public class QueryProcessor {
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> userProvider;
private final ChangeControl.GenericFactory changeControlFactory;
+ private final IndexCollection indexes;
private final IndexRewriter rewriter;
private final IndexConfig indexConfig;
+ private final Metrics metrics;
private int limitFromCaller;
private int start;
private boolean enforceVisibility = true;
+ private Set<String> requestedFields;
@Inject
QueryProcessor(Provider<ReviewDb> db,
Provider<CurrentUser> userProvider,
ChangeControl.GenericFactory changeControlFactory,
+ IndexCollection indexes,
IndexRewriter rewriter,
- IndexConfig indexConfig) {
+ IndexConfig indexConfig,
+ Metrics metrics) {
this.db = db;
this.userProvider = userProvider;
this.changeControlFactory = changeControlFactory;
+ this.indexes = indexes;
this.rewriter = rewriter;
this.indexConfig = indexConfig;
+ this.metrics = metrics;
}
public QueryProcessor enforceVisibility(boolean enforce) {
@@ -75,6 +90,11 @@
return this;
}
+ public QueryProcessor setRequestedFields(Set<String> fields) {
+ requestedFields = fields;
+ return this;
+ }
+
/**
* Query for changes that match a structured query.
*
@@ -114,6 +134,9 @@
private List<QueryResult> queryChanges(List<String> queryStrings,
List<Predicate<ChangeData>> queries)
throws OrmException, QueryParseException {
+ @SuppressWarnings("resource")
+ Timer0.Context context = metrics.executionTime.start();
+
Predicate<ChangeData> visibleToMe = enforceVisibility
? new IsVisibleToPredicate(db, changeControlFactory, userProvider.get())
: null;
@@ -140,7 +163,8 @@
"Cannot go beyond page " + indexConfig.maxPages() + "of results");
}
- QueryOptions opts = QueryOptions.create(indexConfig, start, limit + 1);
+ QueryOptions opts = QueryOptions.create(
+ indexConfig, start, limit + 1, getRequestedFields());
Predicate<ChangeData> s = rewriter.rewrite(q, opts);
if (!(s instanceof ChangeDataSource)) {
q = Predicate.and(open(), q);
@@ -170,9 +194,20 @@
limits.get(i),
matches.get(i).toList()));
}
+ context.close(); // only measure successful queries
return out;
}
+ private Set<String> getRequestedFields() {
+ if (requestedFields != null) {
+ return requestedFields;
+ }
+ ChangeIndex index = indexes.getSearchIndex();
+ return index != null
+ ? index.getSchema().getStoredFields().keySet()
+ : ImmutableSet.<String> of();
+ }
+
boolean isDisabled() {
return getPermittedLimit() <= 0;
}
@@ -203,4 +238,19 @@
}
return Ordering.natural().min(possibleLimits);
}
+
+ @Singleton
+ static class Metrics {
+ final Timer0 executionTime;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ executionTime = metricMaker.newTimer(
+ "change/query/query_latency",
+ new Description("Successful change query latency,"
+ + " accumulated over the life of the process")
+ .setCumulative()
+ .setUnit(Description.Units.MILLISECONDS));
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 30ebaa7..0c3bf67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -20,9 +20,14 @@
import com.google.common.base.Strings;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.persistence.DataSourceInterceptor;
+import com.google.gerrit.metrics.CallbackMetric1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.config.ConfigSection;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
import com.google.gwtorm.jdbc.SimpleDataSource;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -43,18 +48,22 @@
@Singleton
public class DataSourceProvider implements Provider<DataSource>,
LifecycleListener {
- public static final int DEFAULT_POOL_LIMIT = 8;
-
private final Config cfg;
+ private final MetricMaker metrics;
private final Context ctx;
private final DataSourceType dst;
+ private final ThreadSettingsConfig threadSettingsConfig;
private DataSource ds;
@Inject
protected DataSourceProvider(@GerritServerConfig Config cfg,
+ MetricMaker metrics,
+ ThreadSettingsConfig threadSettingsConfig,
Context ctx,
DataSourceType dst) {
this.cfg = cfg;
+ this.metrics = metrics;
+ this.threadSettingsConfig = threadSettingsConfig;
this.ctx = ctx;
this.dst = dst;
}
@@ -120,12 +129,15 @@
if (password != null && !password.isEmpty()) {
ds.setPassword(password);
}
- ds.setMaxActive(cfg.getInt("database", "poollimit", DEFAULT_POOL_LIMIT));
+ int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
+ ds.setMaxActive(poolLimit);
ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
- ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", 4));
+ ds.setMaxIdle(
+ cfg.getInt("database", "poolmaxidle", Math.min(poolLimit, 16)));
ds.setMaxWait(ConfigUtil.getTimeUnit(cfg, "database", null,
"poolmaxwait", MILLISECONDS.convert(30, SECONDS), MILLISECONDS));
ds.setInitialSize(ds.getMinIdle());
+ exportPoolMetrics(ds);
return intercept(interceptor, ds);
} else {
@@ -148,6 +160,25 @@
}
}
+ private void exportPoolMetrics(final BasicDataSource pool) {
+ final CallbackMetric1<Boolean, Integer> cnt = metrics.newCallbackMetric(
+ "sql/connection_pool/connections",
+ Integer.class,
+ new Description("SQL database connections")
+ .setGauge()
+ .setUnit("connections"),
+ Field.ofBoolean("active"));
+ metrics.newTrigger(cnt, new Runnable() {
+ @Override
+ public void run() {
+ synchronized (pool) {
+ cnt.set(true, pool.getNumActive());
+ cnt.set(false, pool.getNumIdle());
+ }
+ }
+ });
+ }
+
private DataSource intercept(String interceptor, DataSource ds) {
if (interceptor == null) {
return ds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
index ecaaf5e..6eaa540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
@@ -20,11 +20,11 @@
/** Abstraction of a supported database platform */
public interface DataSourceType {
- public String getDriver();
+ String getDriver();
- public String getUrl();
+ String getUrl();
- public boolean usePool();
+ boolean usePool();
/**
* Return a ScriptRunner that runs the index script. Must not return
@@ -32,5 +32,5 @@
*
* @throws IOException
*/
- public ScriptRunner getIndexScript() throws IOException;
+ ScriptRunner getIndexScript() throws IOException;
}
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 27df9a5c..78646a0 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
@@ -32,7 +32,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_115> C = Schema_115.class;
+ public static final Class<Schema_116> C = Schema_116.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java
new file mode 100644
index 0000000..c7c2a59
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java
@@ -0,0 +1,43 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_116 extends SchemaVersion {
+ @Inject
+ Schema_116(Provider<Schema_115> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+ ui.message("Migrate user preference copySelfOnEmail to emailStrategy");
+ try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
+ stmt.executeUpdate("UPDATE accounts SET "
+ + "EMAIL_STRATEGY='ENABLED' "
+ + "WHERE (COPY_SELF_ON_EMAIL='N')");
+ stmt.executeUpdate("UPDATE accounts SET "
+ + "EMAIL_STRATEGY='CC_ON_OWN_COMMENTS' "
+ + "WHERE (COPY_SELF_ON_EMAIL='Y')");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
index 13c5703..a3c0a5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
@@ -19,8 +19,8 @@
/** Permits controlling the contents of the SSH key cache area. */
public interface SshKeyCache {
- public void evict(String username);
+ void evict(String username);
- public AccountSshKey create(AccountSshKey.Id id, String encoded)
+ AccountSshKey create(AccountSshKey.Id id, String encoded)
throws InvalidSshKeyException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
index 1c68a75..cd559ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
@@ -20,9 +20,9 @@
public class TreeFormatter {
public static interface TreeNode {
- public String getDisplayName();
- public boolean isVisible();
- public SortedSet<? extends TreeNode> getChildren();
+ String getDisplayName();
+ boolean isVisible();
+ SortedSet<? extends TreeNode> getChildren();
}
public static final String NOT_VISIBLE_NODE = "(x)";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/GroupCreationValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/GroupCreationValidationListener.java
index 11db3ee..03bdf37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/GroupCreationValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/GroupCreationValidationListener.java
@@ -30,6 +30,6 @@
* @param args arguments for the group creation
* @throws ValidationException if validation fails
*/
- public void validateNewGroup(CreateGroupArgs args)
+ void validateNewGroup(CreateGroupArgs args)
throws ValidationException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java
index c1d509e..1baab7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java
@@ -32,6 +32,6 @@
* @param toRemove the hashtags to be removed
* @throws ValidationException if validation fails
*/
- public void validateHashtags(Change change, Set<String> toAdd,
+ void validateHashtags(Change change, Set<String> toAdd,
Set<String> toRemove) 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 5b3f158..398a303 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
@@ -55,6 +55,6 @@
* @param args E-mail properties. Some are mutable.
* @throws ValidationException if validation fails.
*/
- public void validateOutgoingEmail(OutgoingEmailValidationListener.Args args)
+ void validateOutgoingEmail(OutgoingEmailValidationListener.Args args)
throws ValidationException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/ProjectCreationValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/ProjectCreationValidationListener.java
index d3c69c1..6012328 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/ProjectCreationValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/ProjectCreationValidationListener.java
@@ -30,6 +30,6 @@
* @param args arguments for the project creation
* @throws ValidationException if validation fails
*/
- public void validateNewProject(CreateProjectArgs args)
+ void validateNewProject(CreateProjectArgs args)
throws ValidationException;
}
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 9a4e77c..59c926f 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -243,6 +243,7 @@
legacy_submit_rule('MaxNoBlock', Label, Min, Max, T) :- !, max_no_block(Label, Max, T).
legacy_submit_rule('NoBlock', Label, Min, Max, T) :- !, T = may(_).
legacy_submit_rule('NoOp', Label, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('PatchSetLock', Label, Min, Max, T) :- !, T = may(_).
legacy_submit_rule(Fun, Label, Min, Max, T) :- T = impossible(unsupported(Fun)).
%% max_with_block:
@@ -294,6 +295,10 @@
%%
%% - At least one maximum is used.
%%
+max_no_block(Max, Label, label(Label, S)) :-
+ number(Max), atom(Label),
+ !,
+ max_no_block(Label, Max, S).
max_no_block(Label, Max, ok(Who)) :-
check_label_range_permission(Label, Max, ok(Who)),
!
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index b537f26..aace2b3 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -63,6 +63,10 @@
AWK=/usr/xpg4/bin/awk
fi
+ # Get core.commentChar from git config or use default symbol
+ commentChar=`git config --get core.commentChar`
+ commentChar=${commentChar:-#}
+
# How this works:
# - parse the commit message as (textLine+ blankLine*)*
# - assume textLine+ to be a footer until proven otherwise
@@ -81,8 +85,8 @@
blankLines = 0
}
- # Skip lines starting with "#" without any spaces before it.
- /^#/ { next }
+ # Skip lines starting with commentChar without any spaces before it.
+ /^'"$commentChar"'/ { next }
# Skip the line starting with the diff command and everything after it,
# up to the end of the file, assuming it is only patch data.
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
new file mode 100644
index 0000000..aa4c4eb
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -0,0 +1,201 @@
+// 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.metrics.proc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.FieldOrdering;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class ProcMetricModuleTest {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Inject
+ MetricMaker metrics;
+
+ @Inject
+ MetricRegistry registry;
+
+ @Test
+ public void testConstantBuildLabel() {
+ Gauge<String> buildLabel = gauge("build/label");
+ assertThat(buildLabel.getValue()).isEqualTo(Version.getVersion());
+ }
+
+ @Test
+ public void testProcUptime() {
+ Gauge<Long> birth = gauge("proc/birth_timestamp");
+ assertThat(birth.getValue()).isAtMost(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()));
+
+ Gauge<Long> uptime = gauge("proc/uptime");
+ assertThat(uptime.getValue()).isAtLeast(1L);
+ }
+
+ @Test
+ public void testCounter0() {
+ Counter0 cntr = metrics.newCounter(
+ "test/count",
+ new Description("simple test")
+ .setCumulative());
+
+ Counter raw = get("test/count", Counter.class);
+ assertThat(raw.getCount()).isEqualTo(0);
+
+ cntr.increment();
+ assertThat(raw.getCount()).isEqualTo(1);
+
+ cntr.incrementBy(5);
+ assertThat(raw.getCount()).isEqualTo(6);
+ }
+
+ @Test
+ public void testCounter1() {
+ Counter1<String> cntr = metrics.newCounter(
+ "test/count",
+ new Description("simple test")
+ .setCumulative(),
+ Field.ofString("action"));
+
+ Counter total = get("test/count_total", Counter.class);
+ assertThat(total.getCount()).isEqualTo(0);
+
+ cntr.increment("passed");
+ Counter passed = get("test/count/passed", Counter.class);
+ assertThat(total.getCount()).isEqualTo(1);
+ assertThat(passed.getCount()).isEqualTo(1);
+
+ cntr.incrementBy("failed", 5);
+ Counter failed = get("test/count/failed", Counter.class);
+ assertThat(total.getCount()).isEqualTo(6);
+ assertThat(passed.getCount()).isEqualTo(1);
+ assertThat(failed.getCount()).isEqualTo(5);
+ }
+
+ @Test
+ public void testCounterPrefixFields() {
+ Counter1<String> cntr = metrics.newCounter(
+ "test/count",
+ new Description("simple test")
+ .setCumulative()
+ .setFieldOrdering(FieldOrdering.PREFIX_FIELDS_BASENAME),
+ Field.ofString("action"));
+
+ Counter total = get("test/count_total", Counter.class);
+ assertThat(total.getCount()).isEqualTo(0);
+
+ cntr.increment("passed");
+ Counter passed = get("test/passed/count", Counter.class);
+ assertThat(total.getCount()).isEqualTo(1);
+ assertThat(passed.getCount()).isEqualTo(1);
+
+ cntr.incrementBy("failed", 5);
+ Counter failed = get("test/failed/count", Counter.class);
+ assertThat(total.getCount()).isEqualTo(6);
+ assertThat(passed.getCount()).isEqualTo(1);
+ assertThat(failed.getCount()).isEqualTo(5);
+ }
+
+ @Test
+ public void testCallbackMetric0() {
+ final CallbackMetric0<Long> cntr = metrics.newCallbackMetric(
+ "test/count",
+ Long.class,
+ new Description("simple test")
+ .setCumulative());
+
+ final AtomicInteger invocations = new AtomicInteger(0);
+ metrics.newTrigger(cntr, new Runnable() {
+ @Override
+ public void run() {
+ invocations.getAndIncrement();
+ cntr.set(42L);
+ }
+ });
+
+ // Triggers run immediately with DropWizard binding.
+ assertThat(invocations.get()).isEqualTo(1);
+
+ Gauge<Long> raw = gauge("test/count");
+ assertThat(raw.getValue()).isEqualTo(42);
+
+ // Triggers are debounced to avoid being fired too frequently.
+ assertThat(invocations.get()).isEqualTo(1);
+ }
+
+ @Test
+ public void testInvalidName1() {
+ exception.expect(IllegalArgumentException.class);
+ metrics.newCounter("invalid name", new Description("fail"));
+ }
+
+ @Test
+ public void testInvalidName2() {
+ exception.expect(IllegalArgumentException.class);
+ metrics.newCounter("invalid/ name", new Description("fail"));
+ }
+
+ @SuppressWarnings({"unchecked", "cast"})
+ private <V> Gauge<V> gauge(String name) {
+ return (Gauge<V>) get(name, Gauge.class);
+ }
+
+ private <M extends Metric> M get(String name, Class<M> type) {
+ Metric m = registry.getMetrics().get(name);
+ assertThat(m).named(name).isNotNull();
+ assertThat(m).named(name).isInstanceOf(type);
+
+ @SuppressWarnings("unchecked")
+ M result = (M) m;
+ return result;
+ }
+
+ @Before
+ public void setup() {
+ Injector injector =
+ Guice.createInjector(new DropWizardMetricMaker.ApiModule());
+
+ LifecycleManager mgr = new LifecycleManager();
+ mgr.add(injector);
+ mgr.start();
+
+ injector.injectMembers(this);
+ }
+}
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 d6a5c67..0245b89 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
@@ -40,9 +40,7 @@
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import java.io.PushbackReader;
import java.io.StringReader;
@@ -62,9 +60,6 @@
private ProjectConfig local;
private Util util;
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
@Before
public void setUp() throws Exception {
util = new Util();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index 8956e8f..dc8004a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.testutil.GerritBaseTests;
import com.google.inject.Guice;
import com.google.inject.Module;
@@ -45,7 +46,7 @@
/** Base class for any tests written in Prolog. */
-public abstract class PrologTestCase {
+public abstract class PrologTestCase extends GerritBaseTests {
private static final SymbolTerm test_1 = SymbolTerm.intern("test", 1);
private String pkg;
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 039871e..6349be2 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
@@ -95,7 +95,8 @@
bind(CapabilityControl.Factory.class)
.toProvider(Providers.<CapabilityControl.Factory>of(null));
bind(Realm.class).toInstance(mockRealm);
-
+ bind(StarredChangesUtil.class)
+ .toProvider(Providers.<StarredChangesUtil> of(null));
}
};
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index f5e6d74..052b126 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -47,6 +47,7 @@
import com.google.gerrit.server.GerritPersonIdent;
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.AccountLoader;
import com.google.gerrit.server.account.CapabilityControl;
@@ -67,8 +68,8 @@
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeAccountCache;
+import com.google.gerrit.testutil.GerritServerTests;
import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.TestChanges;
import com.google.gwtorm.server.ListResultSet;
@@ -87,10 +88,7 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
import java.sql.Timestamp;
import java.util.Collections;
@@ -98,23 +96,10 @@
import java.util.Map;
import java.util.TimeZone;
-@RunWith(ConfigSuite.class)
-public class CommentsTest {
+public class CommentsTest extends GerritServerTests {
private static final TimeZone TZ =
TimeZone.getTimeZone("America/Los_Angeles");
- @ConfigSuite.Parameter
- public Config config;
-
- @ConfigSuite.Config
- @GerritServerConfig
- public static Config noteDbEnabled() {
- return NotesMigration.allEnabledConfig();
- }
-
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
private Injector injector;
private ReviewDb db;
private Project.NameKey project;
@@ -200,6 +185,8 @@
.toInstance(GitReferenceUpdated.DISABLED);
bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
.toInstance(serverIdent);
+ bind(StarredChangesUtil.class)
+ .toProvider(Providers.<StarredChangesUtil> of(null));
}
@Provides
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 743e43d..8cdd42b 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
@@ -21,10 +21,9 @@
import static org.junit.Assert.assertTrue;
import com.google.gerrit.server.util.HostPlatform;
+import com.google.gerrit.testutil.GerritBaseTests;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import java.io.IOException;
import java.nio.file.Files;
@@ -32,10 +31,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
-public class SitePathsTest {
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
+public class SitePathsTest extends GerritBaseTests {
@Test
public void testCreate_NotExisting() throws IOException {
final Path root = random();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 63ae818..dd03bb0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -26,8 +26,8 @@
new FakeQueryBuilder.Definition<>(
FakeQueryBuilder.class),
new ChangeQueryBuilder.Arguments(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, null, null, null, null, null,
+ null, null, indexes, null, null, null, null, null, null));
}
@Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
index 9ac83d5..f44d172 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
@@ -25,6 +25,7 @@
import static com.google.gerrit.server.query.Predicate.or;
import static org.junit.Assert.assertEquals;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.Predicate;
@@ -34,22 +35,18 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.OrSource;
import com.google.gerrit.server.query.change.QueryOptions;
+import com.google.gerrit.testutil.GerritBaseTests;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;
-public class IndexRewriterTest {
+public class IndexRewriterTest extends GerritBaseTests {
private static final IndexConfig CONFIG = IndexConfig.createDefault();
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
private FakeIndex index;
private IndexCollection indexes;
private ChangeQueryBuilder queryBuilder;
@@ -289,7 +286,8 @@
}
private static QueryOptions options(int start, int limit) {
- return QueryOptions.create(CONFIG, start, limit);
+ return QueryOptions.create(CONFIG, start, limit,
+ ImmutableSet.<String> of());
}
private Set<Change.Status> status(String query) throws QueryParseException {
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 145042c..d5f3132 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
@@ -15,15 +15,13 @@
package com.google.gerrit.server.mail;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
-import org.junit.Rule;
+import com.google.gerrit.testutil.GerritBaseTests;
+
import org.junit.Test;
-import org.junit.rules.ExpectedException;
-public class AddressTest {
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
+public class AddressTest extends GerritBaseTests {
@Test
public void testParse_NameEmail1() {
final Address a = Address.parse("A U Thor <author@example.com>");
@@ -100,9 +98,12 @@
}
private void assertInvalid(final String in) {
- exception.expect(IllegalArgumentException.class);
- exception.expectMessage("Invalid email address: " + in);
- Address.parse(in);
+ try {
+ Address.parse(in);
+ fail("Expected IllegalArgumentException for " + in);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).isEqualTo("Invalid email address: " + in);
+ }
}
@Test
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 8030f9f..07624a4 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
@@ -15,7 +15,6 @@
package com.google.gerrit.server.notedb;
import static com.google.inject.Scopes.SINGLETON;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableList;
@@ -32,6 +31,7 @@
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+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.FakeRealm;
@@ -49,8 +49,10 @@
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.ProjectCache;
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.TestTimeUtil;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.StandardKeyEncoder;
@@ -62,17 +64,13 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.After;
import org.junit.Before;
import java.sql.Timestamp;
import java.util.TimeZone;
-import java.util.concurrent.atomic.AtomicLong;
-public class AbstractChangeNotesTest {
+public class AbstractChangeNotesTest extends GerritBaseTests {
private static final TimeZone TZ =
TimeZone.getTimeZone("America/Los_Angeles");
@@ -91,7 +89,6 @@
private Injector injector;
private String systemTimeZone;
- private volatile long clockStepMs;
@Inject private AllUsersNameProvider allUsers;
@@ -139,6 +136,8 @@
.toInstance(serverIdent);
bind(GitReferenceUpdated.class)
.toInstance(GitReferenceUpdated.DISABLED);
+ bind(StarredChangesUtil.class)
+ .toProvider(Providers.<StarredChangesUtil> of(null));
}
});
@@ -151,21 +150,12 @@
private void setTimeForTesting() {
systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
- clockStepMs = MILLISECONDS.convert(1, SECONDS);
- final AtomicLong clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
- DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
- @Override
- public long getMillis() {
- return clockMs.getAndAdd(clockStepMs);
- }
- });
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
}
@After
public void resetTime() {
- DateTimeUtils.setCurrentMillisSystem();
+ TestTimeUtil.useSystemTime();
System.setProperty("user.timezone", systemTimeZone);
}
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 bac72f0..0f3fee4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.notedb;
+import static org.junit.Assert.fail;
+
import com.google.gerrit.common.TimeUtil;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -27,17 +29,12 @@
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
public class ChangeNotesParserTest extends AbstractChangeNotesTest {
private TestRepository<InMemoryRepository> testRepo;
private RevWalk walk;
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
@Before
public void setUpTestRepo() throws Exception {
testRepo = new TestRepository<>(repo);
@@ -171,6 +168,23 @@
+ "Reviewer: 1@gerrit\n");
}
+ @Test
+ public void parseTopic() throws Exception {
+ assertParseSucceeds("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Topic: Some Topic");
+ assertParseSucceeds("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Topic:");
+ assertParseFails("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Topic: Some Topic\n"
+ + "Topic: Other Topic");
+ }
+
private RevCommit writeCommit(String body) throws Exception {
return writeCommit(body, ChangeNoteUtil.newIdent(
changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,
@@ -205,8 +219,10 @@
private void assertParseFails(RevCommit commit) throws Exception {
try (ChangeNotesParser parser = newParser(commit)) {
- exception.expect(ConfigInvalidException.class);
parser.parseAll();
+ fail("Expected parse to fail:\n" + commit.getFullMessage());
+ } catch (ConfigInvalidException e) {
+ // Expected
}
}
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 f19987f..c68c283 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
@@ -15,8 +15,8 @@
package com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.notedb.ReviewerState.CC;
-import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
+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;
@@ -371,6 +371,43 @@
}
@Test
+ public void topicChangeNotes() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+
+ // initially topic is not set
+ ChangeNotes notes = newNotes(c);
+ notes = newNotes(c);
+ assertThat(notes.getChange().getTopic()).isNull();
+
+ // set topic
+ String topic = "myTopic";
+ update.setTopic(topic);
+ update.commit();
+ notes = newNotes(c);
+ assertThat(notes.getChange().getTopic()).isEqualTo(topic);
+
+ // clear topic by setting empty string
+ update.setTopic("");
+ update.commit();
+ notes = newNotes(c);
+ assertThat(notes.getChange().getTopic()).isNull();
+
+ // set other topic
+ topic = "otherTopic";
+ update.setTopic(topic);
+ update.commit();
+ notes = newNotes(c);
+ assertThat(notes.getChange().getTopic()).isEqualTo(topic);
+
+ // clear topic by setting null
+ update.setTopic(null);
+ update.commit();
+ notes = newNotes(c);
+ assertThat(notes.getChange().getTopic()).isNull();
+ }
+
+ @Test
public void emptyExceptSubject() throws Exception {
ChangeUpdate update = newUpdate(newChange(), changeOwner);
update.setSubject("Create change");
@@ -526,7 +563,7 @@
ChangeNotes notes = newNotes(c);
ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
- notes.getChangeMessages();
+ notes.getChangeMessagesByPatchSet();
assertThat(changeMessages.keySet()).containsExactly(ps1);
ChangeMessage cm = Iterables.getOnlyElement(changeMessages.get(ps1));
@@ -557,7 +594,7 @@
ChangeNotes notes = newNotes(c);
ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
- notes.getChangeMessages();
+ notes.getChangeMessagesByPatchSet();
assertThat(changeMessages).hasSize(1);
ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
@@ -579,7 +616,7 @@
ChangeNotes notes = newNotes(c);
ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
- notes.getChangeMessages();
+ notes.getChangeMessagesByPatchSet();
assertThat(changeMessages).hasSize(1);
ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
@@ -609,7 +646,7 @@
ChangeNotes notes = newNotes(c);
ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
- notes.getChangeMessages();
+ notes.getChangeMessagesByPatchSet();
assertThat(changeMessages).hasSize(2);
ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
@@ -642,7 +679,7 @@
ChangeNotes notes = newNotes(c);
ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
- notes.getChangeMessages();
+ notes.getChangeMessagesByPatchSet();
assertThat(changeMessages.keySet()).hasSize(1);
List<ChangeMessage> cm = changeMessages.get(ps1);
@@ -690,7 +727,7 @@
update.commit();
update = newUpdate(c, otherUser);
- CommentRange range3 = new CommentRange(3, 1, 4, 1);
+ CommentRange range3 = new CommentRange(3, 0, 4, 1);
PatchLineComment comment3 = newPublishedComment(psId, "file2",
uuid3, range3, range3.getEndLine(), otherUser, null, time3, message3,
(short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
@@ -729,7 +766,7 @@
+ "\n"
+ "File: file2\n"
+ "\n"
- + "3:1-4:1\n"
+ + "3:0-4:1\n"
+ CommentsInNotesUtil.formatTime(serverIdent, time3) + "\n"
+ "Author: Other Account <2@gerrit>\n"
+ "UUID: uuid3\n"
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 c206902..811cd8a 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
@@ -15,8 +15,8 @@
package com.google.gerrit.server.notedb;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.notedb.ReviewerState.CC;
-import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
+import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
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 a671523..b3c94ed 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
@@ -37,6 +37,7 @@
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.CurrentUser;
+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.FakeRealm;
@@ -96,6 +97,14 @@
value(-2, "This shall not be merged"));
}
+ public static final LabelType patchSetLock() {
+ LabelType label = category("Patch-Set-Lock",
+ value(1, "Patch Set Locked"),
+ value(0, "Patch Set Unlocked"));
+ label.setFunctionName("PatchSetLock");
+ return label;
+ }
+
public static LabelValue value(int value, String text) {
return new LabelValue((short) value, text);
}
@@ -317,6 +326,8 @@
bind(ChangeKindCache.class).to(ChangeKindCacheImpl.NoCache.class);
bind(MergeabilityCache.class)
.to(MergeabilityCache.NotImplemented.class);
+ bind(StarredChangesUtil.class)
+ .toProvider(Providers.<StarredChangesUtil> of(null));
}
});
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 382610f..47df2db 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
@@ -21,33 +21,13 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.junit.Test;
-import java.util.Collections;
import java.util.List;
public class AndPredicateTest extends PredicateTest {
- private static final class TestPredicate extends OperatorPredicate<String> {
- private TestPredicate(String name, String value) {
- super(name, value);
- }
-
- @Override
- public boolean match(String object) {
- return false;
- }
-
- @Override
- public int getCost() {
- return 0;
- }
- }
-
- private static TestPredicate f(final String name, final String value) {
- return new TestPredicate(name, value);
- }
-
@Test
public void testChildren() {
final TestPredicate a = f("author", "alice");
@@ -64,17 +44,29 @@
final TestPredicate b = f("author", "bob");
final Predicate<String> n = and(a, b);
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().clear();
+ try {
+ n.getChildren().clear();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
assertChildren("clear", n, of(a, b));
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().remove(0);
+ try {
+ n.getChildren().remove(0);
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
assertChildren("remove(0)", n, of(a, b));
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().iterator().remove();
- assertChildren("remove(0)", n, of(a, b));
+ try {
+ n.getChildren().iterator().remove();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
+ assertChildren("iterator().remove()", n, of(a, b));
}
private static void assertChildren(String o, Predicate<String> p,
@@ -129,11 +121,5 @@
assertNotSame(n2, n2.copy(s2));
assertEquals(s2, n2.copy(s2).getChildren());
assertEquals(s3, n2.copy(s3).getChildren());
-
- try {
- n2.copy(Collections.<Predicate<String>> emptyList());
- } catch (IllegalArgumentException e) {
- assertEquals("Need at least two predicates", e.getMessage());
- }
}
}
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 e31caaf..8f16670 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
@@ -23,27 +23,7 @@
import java.util.Collections;
-public class FieldPredicateTest {
- private static final class TestPredicate extends OperatorPredicate<String> {
- private TestPredicate(String name, String value) {
- super(name, value);
- }
-
- @Override
- public boolean match(String object) {
- return false;
- }
-
- @Override
- public int getCost() {
- return 0;
- }
- }
-
- private static TestPredicate f(final String name, final String value) {
- return new TestPredicate(name, value);
- }
-
+public class FieldPredicateTest extends PredicateTest {
@Test
public void testToString() {
assertEquals("author:bob", f("author", "bob").toString());
@@ -81,10 +61,8 @@
assertSame(f, f.copy(Collections.<Predicate<String>> emptyList()));
assertSame(f, f.copy(f.getChildren()));
- try {
- f.copy(Collections.singleton(f("owner", "bob")));
- } catch (IllegalArgumentException e) {
- assertEquals("Expected 0 children", e.getMessage());
- }
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Expected 0 children");
+ f.copy(Collections.singleton(f("owner", "bob")));
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
index 45a747c..0256081 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
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.junit.Test;
@@ -28,26 +29,6 @@
import java.util.List;
public class NotPredicateTest extends PredicateTest {
- private static final class TestPredicate extends OperatorPredicate<String> {
- private TestPredicate(String name, String value) {
- super(name, value);
- }
-
- @Override
- public boolean match(String object) {
- return false;
- }
-
- @Override
- public int getCost() {
- return 0;
- }
- }
-
- private static TestPredicate f(final String name, final String value) {
- return new TestPredicate(name, value);
- }
-
@Test
public void testNotNot() {
final TestPredicate p = f("author", "bob");
@@ -125,12 +106,14 @@
try {
n.copy(Collections.<Predicate> emptyList());
+ fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertEquals("Expected exactly one child", e.getMessage());
}
try {
n.copy(and(a, b).getChildren());
+ fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertEquals("Expected exactly one child", e.getMessage());
}
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 ee5e0b0..5640d1b 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
@@ -21,33 +21,13 @@
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import org.junit.Test;
-import java.util.Collections;
import java.util.List;
public class OrPredicateTest extends PredicateTest {
- private static final class TestPredicate extends OperatorPredicate<String> {
- private TestPredicate(String name, String value) {
- super(name, value);
- }
-
- @Override
- public boolean match(String object) {
- return false;
- }
-
- @Override
- public int getCost() {
- return 0;
- }
- }
-
- private static TestPredicate f(final String name, final String value) {
- return new TestPredicate(name, value);
- }
-
@Test
public void testChildren() {
final TestPredicate a = f("author", "alice");
@@ -64,17 +44,29 @@
final TestPredicate b = f("author", "bob");
final Predicate<String> n = or(a, b);
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().clear();
+ try {
+ n.getChildren().clear();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
assertChildren("clear", n, of(a, b));
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().remove(0);
+ try {
+ n.getChildren().remove(0);
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
assertChildren("remove(0)", n, of(a, b));
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().iterator().remove();
- assertChildren("remove(0)", n, of(a, b));
+ try {
+ n.getChildren().iterator().remove();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // Expected
+ }
+ assertChildren("iterator().remove()", n, of(a, b));
}
private static void assertChildren(String o, Predicate<String> p,
@@ -129,11 +121,5 @@
assertNotSame(n2, n2.copy(s2));
assertEquals(s2, n2.copy(s2).getChildren());
assertEquals(s3, n2.copy(s3).getChildren());
-
- try {
- n2.copy(Collections.<Predicate<String>> emptyList());
- } catch (IllegalArgumentException e) {
- assertEquals("Need at least two predicates", e.getMessage());
- }
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
index 865841e..9a601eb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/PredicateTest.java
@@ -14,10 +14,26 @@
package com.google.gerrit.server.query;
-import org.junit.Rule;
-import org.junit.rules.ExpectedException;
+import com.google.gerrit.testutil.GerritBaseTests;
-public class PredicateTest {
- @Rule
- public ExpectedException exception = ExpectedException.none();
+public class PredicateTest extends GerritBaseTests {
+ protected static final class TestPredicate extends OperatorPredicate<String> {
+ protected TestPredicate(String name, String value) {
+ super(name, value);
+ }
+
+ @Override
+ public boolean match(String object) {
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+ }
+
+ protected static TestPredicate f(String name, String value) {
+ return new TestPredicate(name, value);
+ }
}
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 d01fb03..6b91a12 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,6 +21,7 @@
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.fail;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
@@ -58,6 +59,7 @@
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
@@ -68,9 +70,11 @@
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.DisabledReviewDb;
+import com.google.gerrit.testutil.GerritServerTests;
import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testutil.TestTimeUtil;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
@@ -80,44 +84,24 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.concurrent.atomic.AtomicLong;
@Ignore
-@RunWith(ConfigSuite.class)
-public abstract class AbstractQueryChangesTest {
+public abstract class AbstractQueryChangesTest extends GerritServerTests {
@ConfigSuite.Default
public static Config defaultConfig() {
- return updateConfig(new Config());
- }
-
- @ConfigSuite.Config
- public static Config noteDbEnabled() {
- return updateConfig(NotesMigration.allEnabledConfig());
- }
-
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
- private static Config updateConfig(Config cfg) {
+ Config cfg = new Config();
cfg.setInt("index", null, "maxPages", 10);
return cfg;
}
- @ConfigSuite.Parameter public Config config;
@Inject protected AccountManager accountManager;
@Inject protected BatchUpdate.Factory updateFactory;
@Inject protected ChangeInserter.Factory changeFactory;
@@ -142,7 +126,6 @@
protected ReviewDb db;
protected Account.Id userId;
protected CurrentUser user;
- protected volatile long clockStepMs;
private String systemTimeZone;
@@ -198,21 +181,12 @@
@Before
public void setTimeForTesting() {
systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
- clockStepMs = 1;
- final AtomicLong clockMs = new AtomicLong(
- new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
- DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
- @Override
- public long getMillis() {
- return clockMs.getAndAdd(clockStepMs);
- }
- });
+ TestTimeUtil.resetWithClockStep(1, MILLISECONDS);
}
@After
public void resetTime() {
- DateTimeUtils.setCurrentMillisSystem();
+ TestTimeUtil.useSystemTime();
System.setProperty("user.timezone", systemTimeZone);
}
@@ -718,7 +692,7 @@
@Test
public void updateOrder() throws Exception {
- clockStepMs = MILLISECONDS.convert(2, MINUTES);
+ TestTimeUtil.resetWithClockStep(2, MINUTES);
TestRepository<Repo> repo = createProject("repo");
List<ChangeInserter> inserters = Lists.newArrayList();
List<Change> changes = Lists.newArrayList();
@@ -743,7 +717,7 @@
@Test
public void updatedOrderWithMinuteResolution() throws Exception {
- clockStepMs = MILLISECONDS.convert(2, MINUTES);
+ TestTimeUtil.resetWithClockStep(2, MINUTES);
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins1 = newChange(repo, null, null, null, null);
Change change1 = insert(ins1);
@@ -897,16 +871,17 @@
@Test
public void byAge() throws Exception {
- long thirtyHours = MILLISECONDS.convert(30, HOURS);
- clockStepMs = thirtyHours;
+ long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
+ TestTimeUtil.resetWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
Change change2 = insert(newChange(repo, null, null, null, null));
- clockStepMs = 0; // Queried by AgePredicate constructor.
+ // Queried by AgePredicate constructor.
+ TestTimeUtil.setClockStep(0, MILLISECONDS);
long now = TimeUtil.nowMs();
assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1))
- .isEqualTo(thirtyHours);
- assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHours);
+ .isEqualTo(thirtyHoursInMs);
+ assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHoursInMs);
assertThat(TimeUtil.nowMs()).isEqualTo(now);
assertQuery("-age:1d");
@@ -920,11 +895,11 @@
@Test
public void byBefore() throws Exception {
- clockStepMs = MILLISECONDS.convert(30, HOURS);
+ TestTimeUtil.resetWithClockStep(30, HOURS);
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
Change change2 = insert(newChange(repo, null, null, null, null));
- clockStepMs = 0;
+ TestTimeUtil.setClockStep(0, MILLISECONDS);
assertQuery("before:2009-09-29");
assertQuery("before:2009-09-30");
@@ -940,11 +915,11 @@
@Test
public void byAfter() throws Exception {
- clockStepMs = MILLISECONDS.convert(30, HOURS);
+ TestTimeUtil.resetWithClockStep(30, HOURS);
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
Change change2 = insert(newChange(repo, null, null, null, null));
- clockStepMs = 0;
+ TestTimeUtil.setClockStep(0, MILLISECONDS);
assertQuery("after:2009-10-03");
assertQuery("after:\"2009-10-01 20:59:59 -0400\"", change2);
@@ -1219,7 +1194,7 @@
@Test
public void reviewedBy() throws Exception {
- clockStepMs = MILLISECONDS.convert(2, MINUTES);
+ TestTimeUtil.resetWithClockStep(2, MINUTES);
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
Change change2 = insert(newChange(repo, null, null, null, null));
@@ -1333,6 +1308,30 @@
cd.messages();
}
+ @Test
+ public void prepopulateOnlyRequestedFields() throws Exception {
+ assume().that(notesMigration.enabled()).isFalse();
+ TestRepository<Repo> repo = createProject("repo");
+ Change change = insert(newChange(repo, null, null, null, null));
+
+ db = new DisabledReviewDb();
+ requestContext.setContext(newRequestContext(userId));
+ // Use QueryProcessor directly instead of API so we get ChangeDatas back.
+ List<ChangeData> cds = queryProcessor
+ .setRequestedFields(ImmutableSet.of(
+ ChangeField.PATCH_SET.getName(),
+ ChangeField.CHANGE.getName()))
+ .queryChanges(queryBuilder.parse(change.getId().toString()))
+ .changes();
+ assertThat(cds).hasSize(1);
+
+ ChangeData cd = cds.get(0);
+ cd.change();
+ cd.patchSets();
+
+ exception.expect(DisabledReviewDb.Disabled.class);
+ cd.currentApprovals();
+ }
protected ChangeInserter newChange(
TestRepository<Repo> repo,
@@ -1414,8 +1413,12 @@
}
protected void assertBadQuery(QueryRequest query) throws Exception {
- exception.expect(BadRequestException.class);
- query.get();
+ try {
+ query.get();
+ fail("expected BadRequestException for query: " + query);
+ } catch (BadRequestException e) {
+ // Expected.
+ }
}
protected TestRepository<Repo> createProject(String name) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
index 7e7899b..ed4e520 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.query.change;
import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -25,6 +24,7 @@
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.testutil.InMemoryModule;
import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testutil.TestTimeUtil;
import com.google.inject.Guice;
import com.google.inject.Injector;
@@ -78,9 +78,16 @@
// Ignore.
}
+ @Override
+ @Ignore
+ @Test
+ public void prepopulateOnlyRequestedFields() throws Exception {
+ // Ignore.
+ }
+
@Test
public void isReviewed() throws Exception {
- clockStepMs = MILLISECONDS.convert(2, MINUTES);
+ TestTimeUtil.resetWithClockStep(2, MINUTES);
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
Change change2 = insert(newChange(repo, null, null, null, null));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
index c060aaf..3e3c13e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SocketUtilTest.java
@@ -24,9 +24,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import org.junit.Rule;
+import com.google.gerrit.testutil.GerritBaseTests;
+
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -34,10 +34,7 @@
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
-public class SocketUtilTest {
- @Rule
- public ExpectedException exception = ExpectedException.none();
-
+public class SocketUtilTest extends GerritBaseTests {
@Test
public void testIsIPv6() throws UnknownHostException {
final InetAddress ipv6 = getByName("1:2:3:4:5:6:7:8");
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
new file mode 100644
index 0000000..c7eb899
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java
@@ -0,0 +1,23 @@
+// 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 org.junit.Rule;
+import org.junit.rules.ExpectedException;
+
+public class GerritBaseTests {
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
new file mode 100644
index 0000000..39989a9
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
@@ -0,0 +1,69 @@
+// 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.server.notedb.NotesMigration;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.Arrays;
+
+@RunWith(ConfigSuite.class)
+public class GerritServerTests extends GerritBaseTests {
+ @ConfigSuite.Parameter
+ public Config config;
+
+ @ConfigSuite.Name
+ private String configName;
+
+ public static boolean isNoteDbTestEnabled() {
+ final String[] RUN_FLAGS = {"yes", "y", "true"};
+ String value = System.getenv("GERRIT_ENABLE_NOTEDB");
+ return value != null &&
+ Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
+ }
+
+ @Rule
+ public TestRule testRunner = new TestRule() {
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ beforeTest();
+ try {
+ base.evaluate();
+ } finally {
+ afterTest();
+ }
+ }
+ };
+ }
+ };
+
+ public void beforeTest() throws Exception {
+ if (isNoteDbTestEnabled()) {
+ NotesMigration.setAllEnabledConfig(config);
+ }
+ }
+
+ public void afterTest() {
+ }
+}
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 e5cd619..9d9ccb5 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
@@ -22,6 +22,8 @@
import com.google.gerrit.common.DisabledChangeHooks;
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;
@@ -132,6 +134,7 @@
.toInstance(cfg);
}
});
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
install(cfgInjector.getInstance(GerritGlobalModule.class));
install(new ChangeCacheImplModule(false));
factory(GarbageCollection.Factory.class);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 8b5e85a..f35dbea 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -102,12 +102,13 @@
GitRepositoryManager repoManager, NotesMigration migration,
Change c, AllUsersNameProvider allUsers,
IdentifiedUser user) throws OrmException {
- ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
+ ChangeControl ctl = EasyMock.createMock(ChangeControl.class);
expect(ctl.getChange()).andStubReturn(c);
expect(ctl.getUser()).andStubReturn(user);
ChangeNotes notes = new ChangeNotes(repoManager, migration, allUsers, c)
.load();
expect(ctl.getNotes()).andStubReturn(notes);
+ expect(ctl.getId()).andStubReturn(c.getId());
EasyMock.replay(ctl);
return ctl;
}
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
new file mode 100644
index 0000000..4c71c57
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestTimeUtil.java
@@ -0,0 +1,75 @@
+// 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 static com.google.common.base.Preconditions.checkState;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.joda.time.DateTimeZone;
+
+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 {
+ private static Long clockStepMs;
+ private static AtomicLong clockMs;
+
+ /**
+ * Reset the clock to a known start point, then set the clock step.
+ * <p>
+ * The clock is initially set to 2009/09/30 17:00:00 -0400.
+ *
+ * @param clockStep amount to increment clock by on each lookup.
+ * @param clockStepUnit time unit for {@code clockStep}.
+ */
+ 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());
+ setClockStep(clockStep, clockStepUnit);
+ }
+
+ /**
+ * Set the clock step used by {@link com.google.gerrit.common.TimeUtil}.
+ *
+ * @param clockStep amount to increment clock by on each lookup.
+ * @param clockStepUnit time unit for {@code clockStep}.
+ */
+ public static synchronized void setClockStep(
+ long clockStep, TimeUnit clockStepUnit) {
+ checkState(clockMs != null, "call resetWithClockStep first");
+ clockStepMs = MILLISECONDS.convert(clockStep, clockStepUnit);
+ DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+ @Override
+ public long getMillis() {
+ return clockMs.getAndAdd(clockStepMs);
+ }
+ });
+ }
+
+ /** Reset the clock to use the actual system clock. */
+ public static synchronized void useSystemTime() {
+ DateTimeUtils.setCurrentMillisSystem();
+ }
+
+ private TestTimeUtil() {
+ }
+}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 5a7b539..260b557 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -20,6 +20,7 @@
'//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
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 5c897e4..ec49f5c 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
@@ -489,16 +489,16 @@
/** Runnable function which can throw an exception. */
public static interface CommandRunnable {
- public void run() throws Exception;
+ void run() throws Exception;
}
/** Runnable function which can retrieve a project name related to the task */
public static interface ProjectCommandRunnable extends CommandRunnable {
// execute parser command before running, in order to be able to retrieve
// project name
- public void executeParseCommand() throws Exception;
+ void executeParseCommand() throws Exception;
- public Project.NameKey getProjectName();
+ Project.NameKey getProjectName();
}
/** Thrown from {@link CommandRunnable#run()} with client message and code. */
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index f78aba4..88e1142 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
import com.google.gerrit.server.git.QueueProvider;
import com.google.gerrit.server.git.WorkQueue;
import com.google.inject.Inject;
@@ -31,11 +32,13 @@
private final WorkQueue.Executor batchExecutor;
@Inject
- public CommandExecutorQueueProvider(@GerritServerConfig final Config config,
- final WorkQueue queues) {
- final int cores = Runtime.getRuntime().availableProcessors();
- poolSize = config.getInt("sshd", "threads", 3 * cores / 2);
- batchThreads = config.getInt("sshd", "batchThreads", cores == 1 ? 1 : 2);
+ public CommandExecutorQueueProvider(
+ @GerritServerConfig Config config,
+ ThreadSettingsConfig threadsSettingsConfig,
+ WorkQueue queues) {
+ poolSize = threadsSettingsConfig.getSshdThreads();
+ batchThreads = config.getInt("sshd", "batchThreads",
+ threadsSettingsConfig.getSshdBatchTreads());
if (batchThreads > poolSize) {
poolSize += batchThreads;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 6bd9c4c..ee4984b79 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -217,7 +217,7 @@
private void log(final int rc) {
if (logged.compareAndSet(false, true)) {
- log.onExecute(cmd, rc);
+ log.onExecute(cmd, rc, ctx.getSession());
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index d6b3e5c..33347e7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -19,10 +19,14 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
@@ -128,6 +132,7 @@
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* SSH daemon to communicate with Gerrit.
@@ -172,7 +177,8 @@
final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
@GerritServerConfig final Config cfg, final SshLog sshLog,
@SshListenAddresses final List<SocketAddress> listen,
- @SshAdvertisedAddresses final List<String> advertised) {
+ @SshAdvertisedAddresses final List<String> advertised,
+ MetricMaker metricMaker) {
setPort(IANA_SSH_PORT /* never used */);
this.cfg = cfg;
@@ -247,10 +253,39 @@
setKeyPairProvider(hostKeyProvider);
setCommandFactory(commandFactory);
setShellFactory(noShell);
+
+ final AtomicInteger connected = new AtomicInteger();
+ metricMaker.newCallbackMetric(
+ "sshd/sessions/connected",
+ Integer.class,
+ new Description("Currently connected SSH sessions")
+ .setGauge()
+ .setUnit("sessions"),
+ new Supplier<Integer>() {
+ @Override
+ public Integer get() {
+ return connected.get();
+ }
+ });
+
+ final Counter0 sessionsCreated = metricMaker.newCounter(
+ "sshd/sessions/created",
+ new Description("Rate of new SSH sessions")
+ .setRate()
+ .setUnit("sessions"));
+
+ final Counter0 authFailures = metricMaker.newCounter(
+ "sshd/sessions/authentication_failures",
+ new Description("Rate of SSH authentication failures")
+ .setRate()
+ .setUnit("failures"));
+
setSessionFactory(new SessionFactory() {
@Override
protected AbstractSession createSession(final IoSession io)
throws Exception {
+ connected.incrementAndGet();
+ sessionsCreated.increment();
if (io instanceof MinaSession) {
if (((MinaSession) io).getSession()
.getConfig() instanceof SocketSessionConfig) {
@@ -271,7 +306,9 @@
s.addCloseSessionListener(new SshFutureListener<CloseFuture>() {
@Override
public void operationComplete(CloseFuture future) {
+ connected.decrementAndGet();
if (sd.isAuthenticationError()) {
+ authFailures.increment();
sshLog.onAuthFail(sd);
}
}
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 3a8a1f5..cec3249 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
@@ -47,6 +47,7 @@
private static final String P_WAIT = "queueWaitTime";
private static final String P_EXEC = "executionTime";
private static final String P_STATUS = "status";
+ private static final String P_AGENT = "agent";
private final Provider<SshSession> session;
private final Provider<Context> context;
@@ -115,7 +116,7 @@
audit(null, "FAIL", "AUTH");
}
- void onExecute(DispatchCommand dcmd, int exitValue) {
+ void onExecute(DispatchCommand dcmd, int exitValue, SshSession sshSession) {
final Context ctx = context.get();
ctx.finished = TimeUtil.nowMs();
@@ -144,6 +145,10 @@
break;
}
event.setProperty(P_STATUS, status);
+ String peerAgent = sshSession.getPeerAgent();
+ if (peerAgent != null) {
+ event.setProperty(P_AGENT, peerAgent);
+ }
if (async != null) {
async.append(event);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
index 2622fbd..541081e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
@@ -30,6 +30,7 @@
private static final String P_WAIT = "queueWaitTime";
private static final String P_EXEC = "executionTime";
private static final String P_STATUS = "status";
+ private static final String P_AGENT = "agent";
private final Calendar calendar;
private long lastTimeMillis;
@@ -63,6 +64,7 @@
opt(P_WAIT, buf, event);
opt(P_EXEC, buf, event);
opt(P_STATUS, buf, event);
+ opt(P_AGENT, buf, event);
buf.append('\n');
return buf.toString();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index ff160e0..6285b20 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -35,6 +35,7 @@
private volatile CurrentUser identity;
private volatile String username;
private volatile String authError;
+ private volatile String peerAgent;
SshSession(final int sessionId, SocketAddress peer) {
this.sessionId = sessionId;
@@ -72,6 +73,14 @@
return remoteAsString;
}
+ public String getPeerAgent() {
+ return peerAgent;
+ }
+
+ public void setPeerAgent(String agent) {
+ peerAgent = agent;
+ }
+
String getUsername() {
return username;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index abb788d..0b12aa6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -14,18 +14,15 @@
package com.google.gerrit.sshd.commands;
-import com.google.common.collect.Lists;
import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.ReceiveCommits;
-import com.google.gerrit.server.git.ReceivePackInitializer;
-import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.sshd.AbstractGitCommand;
import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshSession;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.TooLargeObjectInPackException;
@@ -33,8 +30,6 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.PostReceiveHook;
-import org.eclipse.jgit.transport.PostReceiveHookChain;
import org.eclipse.jgit.transport.ReceivePack;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -62,13 +57,7 @@
private IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject
- private TransferConfig config;
-
- @Inject
- private DynamicSet<ReceivePackInitializer> receivePackInitializers;
-
- @Inject
- private DynamicSet<PostReceiveHook> postReceiveHooks;
+ private SshSession session;
private final Set<Account.Id> reviewerId = new HashSet<>();
private final Set<Account.Id> ccId = new HashSet<>();
@@ -100,19 +89,13 @@
verifyProjectVisible("reviewer", reviewerId);
verifyProjectVisible("CC", ccId);
+ receive.init();
receive.addReviewers(reviewerId);
receive.addExtraCC(ccId);
-
- final ReceivePack rp = receive.getReceivePack();
- rp.setRefLogIdent(currentUser.newRefLogIdent());
- rp.setTimeout(config.getTimeout());
- rp.setMaxObjectSizeLimit(config.getEffectiveMaxObjectSizeLimit(
- projectControl.getProjectState()));
- init(rp);
- rp.setPostReceiveHook(PostReceiveHookChain.newChain(
- Lists.newArrayList(postReceiveHooks)));
+ ReceivePack rp = receive.getReceivePack();
try {
rp.receive(in, out, err);
+ session.setPeerAgent(rp.getPeerUserAgent());
} catch (UnpackException badStream) {
// In case this was caused by the user pushing an object whose size
// is larger than the receive.maxObjectSizeLimit gerrit.config parameter
@@ -177,12 +160,6 @@
}
}
- private void init(ReceivePack rp) {
- for (ReceivePackInitializer initializer : receivePackInitializers) {
- initializer.init(projectControl.getProject().getNameKey(), rp);
- }
- }
-
private void verifyProjectVisible(final String type, final Set<Account.Id> who)
throws UnloggedFailure {
for (final Account.Id id : who) {
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 e9043f7..db32b3f 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
@@ -18,9 +18,8 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.ChangesCollection;
@@ -28,14 +27,10 @@
import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.ReviewerResource;
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.query.change.InternalChangeQuery;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -47,7 +42,9 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
@CommandMetaData(name = "set-reviewers", description = "Add or remove reviewers on a change")
@@ -69,7 +66,7 @@
@Argument(index = 0, required = true, multiValued = true, metaVar = "COMMIT", usage = "changes to modify")
void addChange(String token) {
try {
- changes.addAll(parseChangeId(token));
+ addChangeImpl(token);
} catch (UnloggedFailure e) {
throw new IllegalArgumentException(e.getMessage(), e);
} catch (OrmException e) {
@@ -81,9 +78,6 @@
private ReviewDb db;
@Inject
- private Provider<InternalChangeQuery> queryProvider;
-
- @Inject
private ReviewerResource.Factory reviewerFactory;
@Inject
@@ -96,24 +90,25 @@
private Provider<CurrentUser> userProvider;
@Inject
- private ChangeControl.GenericFactory changeControlFactory;
-
- @Inject
private ChangesCollection changesCollection;
+ @Inject
+ private ChangeUtil changeUtil;
+
private Set<Account.Id> toRemove = new HashSet<>();
- private Set<Change.Id> changes = new HashSet<>();
+
+ private Map<Change.Id, ChangeResource> changes = new LinkedHashMap<>();
@Override
protected void run() throws UnloggedFailure {
boolean ok = true;
- for (Change.Id changeId : changes) {
+ for (ChangeResource rsrc : changes.values()) {
try {
- ok &= modifyOne(changeId);
+ ok &= modifyOne(rsrc);
} catch (Exception err) {
ok = false;
- log.error("Error updating reviewers on change " + changeId, err);
- writeError("fatal", "internal error while updating " + changeId);
+ log.error("Error updating reviewers on change " + rsrc.getId(), err);
+ writeError("fatal", "internal error while updating " + rsrc.getId());
}
}
@@ -122,8 +117,7 @@
}
}
- private boolean modifyOne(Change.Id changeId) throws Exception {
- ChangeResource changeRsrc = changesCollection.parse(changeId);
+ private boolean modifyOne(ChangeResource changeRsrc) throws Exception {
boolean ok = true;
// Remove reviewers
@@ -168,92 +162,28 @@
return ok;
}
- private Set<Change.Id> parseChangeId(String idstr)
- throws UnloggedFailure, OrmException {
- Set<Change.Id> matched = new HashSet<>(4);
- boolean isCommit = idstr.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
-
- // By newer style changeKey?
- //
- boolean changeKeyParses = idstr.matches("^I[0-9a-f]*$");
- if (changeKeyParses) {
- for (ChangeData cd : queryProvider.get().byKeyPrefix(idstr)) {
- matchChange(matched, cd.change());
+ private void addChangeImpl(String id) throws UnloggedFailure, OrmException {
+ List<ChangeControl> matched =
+ changeUtil.findChanges(id, userProvider.get());
+ List<ChangeControl> toAdd = new ArrayList<>(changes.size());
+ for (ChangeControl ctl : matched) {
+ Change c = ctl.getChange();
+ if (!changes.containsKey(c.getId()) && inProject(c)
+ && ctl.isVisible(db)) {
+ toAdd.add(ctl);
}
}
-
- // By commit?
- //
- if (isCommit) {
- RevId id = new RevId(idstr);
- ResultSet<PatchSet> patches;
- if (id.isComplete()) {
- patches = db.patchSets().byRevision(id);
- } else {
- patches = db.patchSets().byRevisionRange(id, id.max());
- }
-
- for (PatchSet ps : patches) {
- matchChange(matched, ps.getId().getParentKey());
- }
- }
-
- // By older style changeId?
- //
- boolean changeIdParses = false;
- if (idstr.matches("^[1-9][0-9]*$")) {
- Change.Id id;
- try {
- id = Change.Id.parse(idstr);
- changeIdParses = true;
- } catch (IllegalArgumentException e) {
- id = null;
- changeIdParses = false;
- }
-
- if (changeIdParses) {
- matchChange(matched, id);
- }
- }
-
- if (!changeKeyParses && !isCommit && !changeIdParses) {
- throw error("\"" + idstr + "\" is not a valid change");
- }
-
- switch (matched.size()) {
+ switch (toAdd.size()) {
case 0:
- throw error("\"" + idstr + "\" no such change");
+ throw error("\"" + id + "\" no such change");
case 1:
- return matched;
+ ChangeControl ctl = toAdd.get(0);
+ changes.put(ctl.getId(), changesCollection.parse(ctl));
+ break;
default:
- throw error("\"" + idstr + "\" matches multiple changes");
- }
- }
-
- private void matchChange(Set<Change.Id> matched, Change.Id changeId) {
- if (changeId != null && !matched.contains(changeId)) {
- try {
- matchChange(matched, db.changes().get(changeId));
- } catch (OrmException e) {
- log.warn("Error reading change " + changeId, e);
- }
- }
- }
-
- private void matchChange(Set<Change.Id> matched, Change change) {
- try {
- if (change != null
- && inProject(change)
- && changeControlFactory.controlFor(change,
- userProvider.get()).isVisible(db)) {
- matched.add(change.getId());
- }
- } catch (NoSuchChangeException e) {
- // Ignore this change.
- } catch (OrmException e) {
- log.warn("Error reading change " + change.getId(), e);
+ throw error("\"" + id + "\" matches multiple changes");
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 34f7107..d278f4b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.git.ChangeCache;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.UploadPackMetricsHook;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.validators.UploadValidationException;
import com.google.gerrit.server.git.validators.UploadValidators;
@@ -58,6 +59,9 @@
@Inject
private SshSession session;
+ @Inject
+ private UploadPackMetricsHook uploadMetrics;
+
@Override
protected void runImpl() throws IOException, Failure {
if (!projectControl.canRunUploadPack()) {
@@ -71,6 +75,7 @@
}
up.setPackConfig(config.getPackConfig());
up.setTimeout(config.getTimeout());
+ up.setPostUploadHook(uploadMetrics);
List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
allPreUploadHooks.add(uploadValidatorsFactory.create(project, repo,
@@ -78,6 +83,7 @@
up.setPreUploadHook(PreUploadHookChain.newChain(allPreUploadHooks));
try {
up.upload(in, out, err);
+ session.setPeerAgent(up.getPeerUserAgent());
} catch (UploadValidationException e) {
// UploadValidationException is used by the UploadValidators to
// stop the uploadPack. We do not want this exception to go beyond this
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index dab19a5..f0430db 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.12</version>
+ <version>2.13-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 5e36318..a2947af 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
@@ -23,9 +23,11 @@
import com.google.gerrit.httpd.auth.oauth.OAuthModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.gerrit.httpd.raw.StaticModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
@@ -260,6 +262,7 @@
});
}
modules.add(new DatabaseModule());
+ modules.add(new DropWizardMetricMaker.ApiModule());
return Guice.createInjector(PRODUCTION, modules);
}
@@ -288,6 +291,7 @@
private Injector createSysInjector() {
final List<Module> modules = new ArrayList<>();
+ modules.add(new DropWizardMetricMaker.RestModule());
modules.add(new WorkQueue.Module());
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
@@ -319,7 +323,8 @@
modules.add(new AbstractModule() {
@Override
protected void configure() {
- bind(GerritOptions.class).toInstance(new GerritOptions(false, false));
+ bind(GerritOptions.class)
+ .toInstance(new GerritOptions(config, false, false, false));
}
});
modules.add(new GarbageCollectionModule());
@@ -347,8 +352,11 @@
final List<Module> modules = new ArrayList<>();
modules.add(RequestContextFilter.module());
modules.add(AllRequestFilter.module());
+ modules.add(RequestMetricsFilter.module());
modules.add(sysInjector.getInstance(GitOverHttpModule.class));
modules.add(sysInjector.getInstance(WebModule.class));
+ modules.add(sysInjector.getInstance(StaticModule.class));
+ modules.add(sysInjector.getInstance(RequireSslFilter.Module.class));
if (sshInjector != null) {
modules.add(sshInjector.getInstance(WebSshGlueModule.class));
} else {
diff --git a/lib/BUCK b/lib/BUCK
index c635bbb..7d19f27 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -1,14 +1,12 @@
include_defs('//lib/maven.defs')
+define_license(name = 'antlr')
define_license(name = 'Apache1.1')
define_license(name = 'Apache2.0')
-define_license(name = 'CC-BY3.0')
-define_license(name = 'MPL1.1')
-define_license(name = 'PublicDomain')
-define_license(name = 'antlr')
define_license(name = 'args4j')
define_license(name = 'automaton')
define_license(name = 'bouncycastle')
+define_license(name = 'CC-BY3.0')
define_license(name = 'clippy')
define_license(name = 'codemirror')
define_license(name = 'diffy')
@@ -17,12 +15,18 @@
define_license(name = 'h2')
define_license(name = 'jgit')
define_license(name = 'jsch')
+define_license(name = 'MPL1.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 = 'slf4j')
define_license(name = 'xz')
+
define_license(name = 'DO_NOT_DISTRIBUTE')
maven_jar(
diff --git a/lib/LICENSE-page.js b/lib/LICENSE-page.js
new file mode 100644
index 0000000..78152a9
--- /dev/null
+++ b/lib/LICENSE-page.js
@@ -0,0 +1,20 @@
+(The MIT License)
+
+Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
+
+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/LICENSE-polymer b/lib/LICENSE-polymer
new file mode 100644
index 0000000..322c5a8
--- /dev/null
+++ b/lib/LICENSE-polymer
@@ -0,0 +1,27 @@
+Copyright (c) 2014 The Polymer Authors. 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.
diff --git a/lib/LICENSE-promise-polyfill b/lib/LICENSE-promise-polyfill
new file mode 100644
index 0000000..6f7c0123
--- /dev/null
+++ b/lib/LICENSE-promise-polyfill
@@ -0,0 +1,20 @@
+Copyright (c) 2014 Taylor Hakes
+Copyright (c) 2014 Forbes Lindesay
+
+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/dropwizard/BUCK b/lib/dropwizard/BUCK
new file mode 100644
index 0000000..de73e13
--- /dev/null
+++ b/lib/dropwizard/BUCK
@@ -0,0 +1,8 @@
+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/js.defs b/lib/js.defs
new file mode 100644
index 0000000..9508c30
--- /dev/null
+++ b/lib/js.defs
@@ -0,0 +1,169 @@
+# 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', '$SRCDIR', '$(location %s)' % 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',
+ )
diff --git a/lib/js/BUCK b/lib/js/BUCK
new file mode 100644
index 0000000..060788c
--- /dev/null
+++ b/lib/js/BUCK
@@ -0,0 +1,328 @@
+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.6.5',
+ sha1 = '59d457122a161e42cc1625bbab8179c214b7ac11',
+)
+
+npm_binary(
+ name = 'crisper',
+ version = '2.0.1',
+ sha1 = 'b3b8bacc1f6d119af26664b8620e6a978aa7f7d3',
+ repository = GERRIT,
+)
+
+npm_binary(
+ name = 'vulcanize',
+ version = '1.14.0',
+ sha1 = '91eac280d031b5bbcafb5f86bb6ed30515fa2564',
+ 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 = 'font-roboto',
+ package = 'polymerelements/font-roboto',
+ version = '1.0.1',
+ license = 'polymer',
+ sha1 = '735676217f67221903d6be10cc2fb1b336bed13f',
+)
+
+bower_component(
+ name = 'iron-a11y-keys-behavior',
+ package = 'polymerelements/iron-a11y-keys-behavior',
+ version = '1.1.0',
+ deps = [':polymer'],
+ license = 'polymer',
+ sha1 = '0b7962ed8409336652da4b4e83d052dbe53d4e1a',
+)
+
+bower_component(
+ name = 'iron-ajax',
+ package = 'polymerelements/iron-ajax',
+ version = '1.1.0',
+ deps = [
+ ':polymer',
+ ':promise-polyfill',
+ ],
+ license = 'polymer',
+ sha1 = 'f94a3a3d847842c49def41e27da42c7c94f8d7c7',
+)
+
+bower_component(
+ name = 'iron-autogrow-textarea',
+ package = 'polymerelements/iron-autogrow-textarea',
+ version = '1.0.10',
+ deps = [
+ ':iron-behaviors',
+ ':iron-flex-layout',
+ ':iron-form-element-behavior',
+ ':iron-validatable-behavior',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = 'd368240e60a4b02ffc731ad8f45f3c8bbf47e9bd',
+)
+
+bower_component(
+ name = 'iron-behaviors',
+ package = 'polymerelements/iron-behaviors',
+ version = '1.0.11',
+ deps = [
+ ':iron-a11y-keys-behavior',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = 'e0fcfcd8696381fc78ff62261ba333e5e133f39d',
+)
+
+bower_component(
+ name = 'iron-dropdown',
+ package = 'polymerelements/iron-dropdown',
+ version = '1.0.6',
+ deps = [
+ ':iron-a11y-keys-behavior',
+ ':iron-behaviors',
+ ':iron-overlay-behavior',
+ ':iron-resizable-behavior',
+ ':neon-animation',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = 'b54ff404ce5535919979bb4488e4b6ae9146fc5a',
+)
+
+bower_component(
+ name = 'iron-fit-behavior',
+ package = 'polymerelements/iron-fit-behavior',
+ version = '1.0.5',
+ deps = [':polymer'],
+ license = 'polymer',
+ sha1 = 'c0273d22531451a1e64f447971ad16b357a7f7e0',
+)
+
+bower_component(
+ name = 'iron-flex-layout',
+ package = 'polymerelements/iron-flex-layout',
+ version = '1.2.2',
+ deps = [':polymer'],
+ license = 'polymer',
+ sha1 = '3ca2fbbf3b56d95677663f78304262dee68753c3',
+)
+
+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.6',
+ deps = [
+ ':iron-validatable-behavior',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = '2d3eedf0a26046c0e828b1ce3d5b102ee1d0ab19',
+)
+
+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.1.1',
+ deps = [
+ ':iron-fit-behavior',
+ ':iron-resizable-behavior',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = '98d80ea1cbee2631553d4fbc98da6cbb25748a4f',
+)
+
+bower_component(
+ name = 'iron-resizable-behavior',
+ package = 'polymerelements/iron-resizable-behavior',
+ version = '1.0.2',
+ deps = [':polymer'],
+ license = 'polymer',
+ sha1 = '954e82c70b5412d20e7b4d65195a844bb6dc9a07',
+)
+
+bower_component(
+ name = 'iron-selector',
+ package = 'polymerelements/iron-selector',
+ version = '1.0.8',
+ deps = [':polymer'],
+ license = 'polymer',
+ sha1 = '7559560733882656bf479b620669a1d60c3bda21',
+)
+
+bower_component(
+ name = 'iron-test-helpers',
+ package = 'polymerelements/iron-test-helpers',
+ version = '1.0.6',
+ deps = [':polymer'],
+ license = 'DO_NOT_DISTRIBUTE',
+ sha1 = 'c0f7c7f010ca3c63fb08ae0d9462e400380cde2c',
+)
+
+bower_component(
+ name = 'iron-validatable-behavior',
+ package = 'polymerelements/iron-validatable-behavior',
+ version = '1.0.5',
+ deps = [
+ ':iron-meta',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = '5a68250d6d9abcd576f116dc4fc7312426323883',
+)
+
+bower_component(
+ name = 'neon-animation',
+ package = 'polymerelements/neon-animation',
+ version = '1.0.8',
+ deps = [
+ ':iron-meta',
+ ':iron-resizable-behavior',
+ ':iron-selector',
+ ':paper-styles',
+ ':polymer',
+ ':web-animations-js',
+ ],
+ license = 'polymer',
+ sha1 = 'c5f3700e9259554db14f9dfddb290a42c099d88a',
+)
+
+bower_component(
+ name = 'page',
+ package = 'visionmedia/page.js',
+ version = '1.6.4',
+ license = 'page.js',
+ sha1 = 'cc442386d4e392be26c85873f463db76fafbaeaf',
+)
+
+bower_component(
+ name = 'paper-styles',
+ package = 'polymerelements/paper-styles',
+ version = '1.0.13',
+ deps = [
+ ':font-roboto',
+ ':iron-flex-layout',
+ ':polymer',
+ ],
+ license = 'polymer',
+ sha1 = 'e0bfdadfe10e070f39c16aa784de16734eed25a6',
+)
+
+bower_component(
+ name = 'polymer',
+ package = 'polymer/polymer',
+ version = '1.2.2',
+ deps = [':webcomponentsjs'],
+ license = 'polymer',
+ sha1 = '7f4033438425584d8912a80614d1a4f754438e15',
+)
+
+bower_component(
+ name = 'promise-polyfill',
+ package = 'polymerlabs/promise-polyfill',
+ version = '1.0.0',
+ deps = [':polymer'],
+ license = 'promise-polyfill',
+ sha1 = 'a3b598c06cbd7f441402e666ff748326030905d6',
+)
+
+bower_component(
+ name = 'test-fixture',
+ package = 'polymerelements/test-fixture',
+ version = '1.0.3',
+ semver = '^1.0.0',
+ license = 'DO_NOT_DISTRIBUTE',
+ sha1 = '21192d554ff6ad7eea894ca751c73b6bc46867dc',
+)
+
+bower_component(
+ name = 'web-animations-js',
+ package = 'web-animations/web-animations-js',
+ version = '2.1.2',
+ license = 'Apache2.0',
+ sha1 = '3e2f4648b770183f577cb5171785cfedcb3a960b',
+)
+
+bower_component(
+ name = 'webcomponentsjs',
+ package = 'webcomponentsjs',
+ version = '0.7.17',
+ license = 'polymer',
+ sha1 = '36e29cfe21caa71322a0b5026d7d423c33c0426f',
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index c5107d5..7d0a16c 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@
include_defs('//lib/maven.defs')
-VERSION = '5.3.0'
+VERSION = '5.3.1'
# core and backward-codecs both provide
# META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
@@ -16,7 +16,7 @@
maven_jar(
name = 'core_jar',
id = 'org.apache.lucene:lucene-core:' + VERSION,
- sha1 = '9e12bb7c39e964a544e3a23b9c8ffa9599d38f10',
+ sha1 = '36860653d7e09790ada96aeb1970b4ca396ac5d7',
license = 'Apache2.0',
exclude = [
'META-INF/LICENSE.txt',
@@ -28,7 +28,7 @@
maven_jar(
name = 'analyzers-common',
id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
- sha1 = '1502beac94cf437baff848ffbbb8f76172befa6b',
+ sha1 = 'bd804dbc1b8f7941018926e940d20d1016b36c4c',
license = 'Apache2.0',
deps = [':core-and-backward-codecs'],
exclude = [
@@ -40,7 +40,7 @@
maven_jar(
name = 'backward-codecs_jar',
id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
- sha1 = 'f654901e55fe56bdbe4be202767296929c2f8d9e',
+ sha1 = '380603f537317a78f9d9b7421bc2ac87586cb9a1',
license = 'Apache2.0',
deps = [':core_jar'],
exclude = [
@@ -53,7 +53,7 @@
maven_jar(
name = 'misc',
id = 'org.apache.lucene:lucene-misc:' + VERSION,
- sha1 = 'd03ce6d1bb8ab3926b3acc717418c474a49ade69',
+ sha1 = '7891bbc18b372135c2a52b471075b0bdf5f110ec',
license = 'Apache2.0',
deps = [':core-and-backward-codecs'],
exclude = [
@@ -65,7 +65,7 @@
maven_jar(
name = 'queryparser',
id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
- sha1 = '2c5e08580316c90b56a52e3cb686e1cf69db3f9e',
+ sha1 = 'bef0e2ac5b196dbab9d0b7c8cc8196b7ef5dd056',
license = 'Apache2.0',
deps = [':core-and-backward-codecs'],
exclude = [
diff --git a/plugins/README b/plugins/README
deleted file mode 100644
index 00df3c5..0000000
--- a/plugins/README
+++ /dev/null
@@ -1,11 +0,0 @@
-If you are adding a directory here:
-
-- Search all pom.xml files for "CORE PLUGIN LIST".
-- Add the new plugin to that location.
-- (optional) Thank the Maven developers for making this easy.
-
-- Ensure the plugin's pom.xml <version> is the same as Gerrit's
- own pom.xml(s). Gerrit will only embed a plugin that has the
- same version as itself.
-
-- Register the plugin as a submodule with git submodule.
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 82eefc2..eea84e7 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 82eefc2048a4dd69ab589213190dd8403295fb7d
+Subproject commit eea84e7e07ecf6ebb70ea5a6b0cde67f5a5576af
diff --git a/plugins/replication b/plugins/replication
index 675d9dd..4ab29b7 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 675d9dd370948f0508dbe536dfc65cc6cd3bb00d
+Subproject commit 4ab29b755a147a69d88bf000e276a7a2eaa6403b
diff --git a/polygerrit-ui/.gitattributes b/polygerrit-ui/.gitattributes
new file mode 100644
index 0000000..2125666
--- /dev/null
+++ b/polygerrit-ui/.gitattributes
@@ -0,0 +1 @@
+* text=auto
\ No newline at end of file
diff --git a/polygerrit-ui/.gitignore b/polygerrit-ui/.gitignore
new file mode 100644
index 0000000..73b5221
--- /dev/null
+++ b/polygerrit-ui/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+npm-debug.log
+dist
+bower.json
+bower_components
+.tmp
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK
new file mode 100644
index 0000000..4c055f4
--- /dev/null
+++ b/polygerrit-ui/BUCK
@@ -0,0 +1,15 @@
+include_defs('//lib/js.defs')
+
+bower_components(
+ name = 'polygerrit_components',
+ deps = [
+ '//lib/js:iron-a11y-keys-behavior',
+ '//lib/js:iron-ajax',
+ '//lib/js:iron-autogrow-textarea',
+ '//lib/js:iron-dropdown',
+ '//lib/js:iron-input',
+ '//lib/js:iron-selector',
+ '//lib/js:page',
+ '//lib/js:polymer',
+ ],
+)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
new file mode 100644
index 0000000..cbc2660
--- /dev/null
+++ b/polygerrit-ui/README.md
@@ -0,0 +1,67 @@
+# PolyGerrit
+
+## Installing [Node.js](https://nodejs.org/en/download/)
+
+```sh
+# Debian/Ubuntu
+sudo apt-get install nodejs-legacy
+
+# OS X with Homebrew
+brew install node
+```
+
+All other platforms: [download from
+nodejs.org](https://nodejs.org/en/download/).
+
+## Local UI, Production Data
+
+To test the local UI against gerrit-review.googlesource.com:
+
+```sh
+./run-server.sh
+```
+
+Then visit http://localhost:8081
+
+## Local UI, Test Data
+
+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](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
+
+Run a test server:
+
+```sh
+buck build polygerrit && \
+java -jar buck-out/gen/polygerrit/polygerrit.war daemon --polygerrit-dev -d ../gerrit_testsite --console-log --show-stack-trace
+```
+
+## Running Tests
+
+One-time setup:
+
+```sh
+# Debian/Ubuntu
+sudo apt-get install npm
+
+# OS X with Homebrew
+brew install npm
+
+# All platforms (including those above)
+sudo npm install -g web-component-tester
+```
+
+Run all web tests:
+
+```sh
+buck test --include web
+```
+
+If you need to pass additional arguments to `wct`:
+
+```sh
+WCT_ARGS='-p --some-flag="foo bar"' buck test --no-results-cache --include web
+```
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
new file mode 100644
index 0000000..93cf614
--- /dev/null
+++ b/polygerrit-ui/app/BUCK
@@ -0,0 +1,77 @@
+include_defs('//lib/js.defs')
+
+WCT_TEST_PATTERNS = ['test/**']
+PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
+APP_SRCS = glob(
+ ['**'],
+ excludes = [
+ 'BUCK',
+ 'index.html',
+ ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
+
+WEBJS = 'bower_components/webcomponentsjs/webcomponents-lite.js'
+
+# 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 {elements,bower_components/webcomponentsjs}',
+ 'unzip -qd elements $(location :gr-app)',
+ 'cp -rp $SRCDIR/* .',
+ 'unzip -p $(location //polygerrit-ui:polygerrit_components) %s>%s' % (WEBJS, WEBJS),
+ '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',
+ ],
+)
+
+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/elements/gr-account-dropdown.html b/polygerrit-ui/app/elements/gr-account-dropdown.html
new file mode 100644
index 0000000..887cb0c
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-account-dropdown.html
@@ -0,0 +1,98 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
+
+<dom-module id="gr-account-dropdown">
+ <style>
+ :host {
+ display: inline-block;
+ }
+ .dropdown-trigger {
+ color: #00e;
+ cursor: pointer;
+ }
+ .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;
+ }
+ 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>
+ <template>
+ <button class="dropdown-trigger" id="trigger"
+ on-tap="_showDropdownTapHandler">[[account.name]]</button>
+ <iron-dropdown id="dropdown"
+ vertical-align="top"
+ vertical-offset="25"
+ 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="/switch-account">Switch account</a></li>
+ <li><a href="/logout">Logout</a></li>
+ </ul>
+ </div>
+ </iron-dropdown>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-account-dropdown',
+
+ properties: {
+ account: Object,
+ },
+
+ _showDropdownTapHandler: function(e) {
+ this.$.dropdown.open();
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-ajax.html b/polygerrit-ui/app/elements/gr-ajax.html
new file mode 100644
index 0000000..9671b37
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-ajax.html
@@ -0,0 +1,97 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
+
+<dom-module id="gr-ajax">
+ <template>
+ <iron-ajax id="xhr"
+ auto="[[auto]]"
+ url="[[url]]"
+ params="[[params]]"
+ json-prefix=")]}'"
+ last-response="{{lastResponse}}"
+ loading="{{loading}}"
+ on-response="_handleResponse"
+ on-error="_handleError"
+ debounce-duration="300"></iron-ajax>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-ajax',
+
+ /**
+ * Fired when a response is received.
+ * This event does not have a gr- prefix in order to maintain a similar
+ * API to iron-ajax.
+ *
+ * @event response
+ */
+
+ /**
+ * Fired when an error is received.
+ * This event does not have a gr- prefix in order to maintain a similar
+ * API to iron-ajax.
+ *
+ * @event error
+ */
+
+ hostAttributes: {
+ hidden: true
+ },
+
+ properties: {
+ auto: {
+ type: Boolean,
+ value: false,
+ },
+ url: String,
+ params: {
+ type: Object,
+ value: function() {
+ return {};
+ },
+ },
+ lastResponse: {
+ type: Object,
+ notify: true,
+ },
+ loading: {
+ type: Boolean,
+ notify: true,
+ },
+ },
+
+ generateRequest: function() {
+ return this.$.xhr.generateRequest();
+ },
+
+ _handleResponse: function(e, req) {
+ this.fire('response', req, {bubbles: false});
+ },
+
+ _handleError: function(e, req) {
+ this.fire('error', req, {bubbles: false});
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
new file mode 100644
index 0000000..14786d8
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -0,0 +1,194 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="gr-account-dropdown.html">
+<link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-change-list-view.html">
+<link rel="import" href="gr-change-view.html">
+<link rel="import" href="gr-dashboard-view.html">
+<link rel="import" href="gr-diff-view.html">
+<link rel="import" href="gr-search-bar.html">
+
+<script src="../bower_components/page/page.js"></script>
+<script src="../scripts/app.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/util.js"></script>
+
+<dom-module id="gr-app">
+ <template>
+ <style>
+ :host {
+ background-color: var(--secondary-color);
+ display: flex;
+ min-height: 100vh;
+ flex-direction: column;
+ }
+ :host([constrained]) main {
+ margin: 0 auto;
+ width: 100%;
+ max-width: var(--max-constrained-width);
+ }
+ header,
+ footer {
+ background-color: var(--primary-color);
+ color: var(--primary-text-color);
+ padding: .5rem 1.25rem;
+ }
+ header {
+ display: flex;
+ align-items: center;
+ }
+ main {
+ flex: 1;
+ }
+ .bigTitle {
+ color: var(--primary-text-color);
+ font-size: 1.75em;
+ text-decoration: none;
+ }
+ .bigTitle:hover {
+ text-decoration: underline;
+ }
+ .headerRightItems {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ gr-search-bar {
+ width: 500px;
+ }
+ .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton,
+ .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown,
+ .accountContainer.loggedIn .loginButton,
+ .accountContainer.loggedOut gr-account-dropdown {
+ display: none;
+ }
+ .accountContainer {
+ align-items: center;
+ display: flex;
+ margin-left: var(--default-horizontal-margin);
+ }
+ </style>
+ <gr-ajax auto url="/accounts/self/detail" last-response="{{account}}"></gr-ajax>
+ <gr-ajax auto url="/config/server/info" last-response="{{config}}"></gr-ajax>
+ <header role="banner">
+ <a href="/" class="bigTitle">PolyGerrit</a>
+ <div class="headerRightItems">
+ <gr-search-bar value="{{params.query}}" role="search"></gr-search-bar>
+ <div class="accountContainer" id="accountContainer">
+ <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
+ <gr-account-dropdown account="[[account]]"></gr-account-dropdown>
+ </div>
+ </div>
+ </header>
+ <main>
+ <template is="dom-if" if="{{_showChangeListView}}" restamp="true">
+ <gr-change-list-view params="[[params]]"></gr-change-list-view>
+ </template>
+ <template is="dom-if" if="{{_showDashboardView}}" restamp="true">
+ <gr-dashboard-view params="[[params]]"></gr-dashboard-view>
+ </template>
+ <template is="dom-if" if="{{_showChangeView}}" restamp="true">
+ <gr-change-view params="[[params]]"></gr-change-view>
+ </template>
+ <template is="dom-if" if="{{_showDiffView}}" restamp="true">
+ <gr-diff-view params="[[params]]"></gr-diff-view>
+ </template>
+ </main>
+ <footer role="contentinfo">Powered by PolyGerrit</footer>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-app',
+
+ properties: {
+ account: {
+ type: Object,
+ observer: '_accountChanged',
+ },
+ accountReady: {
+ type: Object,
+ readOnly: true,
+ notify: true,
+ value: function() {
+ return new Promise(function(resolve) {
+ this._resolveAccountReady = resolve;
+ }.bind(this));
+ },
+ },
+ config: {
+ type: Object,
+ observer: '_configChanged',
+ },
+ configReady: {
+ type: Object,
+ readOnly: true,
+ notify: true,
+ value: function() {
+ return new Promise(function(resolve) {
+ this._resolveConfigReady = resolve;
+ }.bind(this));
+ },
+ },
+ constrained: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ params: Object,
+ route: {
+ type: String,
+ observer: '_routeChanged',
+ },
+ },
+
+ get loggedIn() {
+ return !!(this.account && Object.keys(this.account).length > 0);
+ },
+
+ _accountChanged: function() {
+ this._resolveAccountReady();
+ this.$.accountContainer.classList.toggle('loggedIn', this.loggedIn);
+ this.$.accountContainer.classList.toggle('loggedOut', !this.loggedIn);
+ },
+
+ _configChanged: function(config) {
+ this._resolveConfigReady(config);
+ },
+
+ _routeChanged: function(route) {
+ this.set('_showChangeListView', route == 'gr-change-list-view');
+ this.set('_showDashboardView', route == 'gr-dashboard-view');
+ this.set('_showChangeView', route == 'gr-change-view');
+ this.set('_showDiffView', route == 'gr-diff-view');
+ this.constrained = route == 'gr-change-view';
+ },
+
+ _loginTapHandler: function(e) {
+ e.preventDefault();
+ page.show('/login/' + encodeURIComponent(
+ window.location.pathname + window.location.hash));
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
new file mode 100644
index 0000000..ef0f864
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list-item.html
@@ -0,0 +1,207 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../styles/gr-change-list-styles.html">
+<link rel="import" href="gr-date-formatter.html">
+
+<dom-module id="gr-change-list-item">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ }
+ :host([selected]) {
+ background-color: #d8EdF9;
+ }
+ .cell {
+ border-bottom: 1px solid #eee;
+ flex-shrink: 0;
+ padding: .3em .5em;
+ }
+ a {
+ color: var(--default-text-color);
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ .positionIndicator {
+ visibility: hidden;
+ }
+ :host([selected]) .positionIndicator {
+ visibility: visible;
+ }
+ .avatarImage {
+ border-radius: 50%;
+ height: 1.3em;
+ vertical-align: -.3em;
+ width: 1.3em;
+ }
+ .u-monospace {
+ font-family: 'Source Code Pro';
+ }
+ .u-green {
+ color: #388E3C;
+ }
+ .u-red {
+ color: #D32F2F;
+ }
+ </style>
+ <style include="gr-change-list-styles"></style>
+ <span class="cell keyboard">
+ <span class="positionIndicator">▶</span>
+ </span>
+ <a class="cell subject" href$="[[changeURL]]">[[change.subject]]</a>
+ <span class="cell status">[[_computeChangeStatusString(change)]]</span>
+ <span class="cell owner">
+ <template is="dom-if" if="[[showAvatar]]">
+ <img class="avatarImage" src$="[[_computeAvatarURL(change.owner)]]">
+ </template>
+ <a href$="[[_computeOwnerLink(change.owner.email)]]"
+ title$="[[_computeOwnerTitle(change.owner)]]">[[change.owner.name]]</a>
+ </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">
+ <span class="u-green"><span>+</span>[[change.insertions]]</span>,
+ <span class="u-red"><span>-</span>[[change.deletions]]</span>
+ </span>
+ <span title="Code-Review"
+ class$="[[_computeCodeReviewClass(change.labels.Code_Review)]]">[[_computeCodeReviewLabel(change.labels.Code_Review)]]</span>
+ <span class="cell verified u-green" title="Verified">[[_computeVerifiedLabel(change.labels.Verified)]]</span>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-list-item',
+
+ properties: {
+ selected: {
+ type: Boolean,
+ reflectToAttribute: true,
+ },
+ change: Object,
+ changeURL: {
+ type: String,
+ computed: '_computeChangeURL(change._number)',
+ },
+ showAvatar: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
+ ready: function() {
+ app.configReady.then(function(cfg) {
+ this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ }.bind(this));
+ },
+
+ _computeChangeURL: function(changeNum) {
+ if (!changeNum) { return ''; }
+ return '/c/' + changeNum + '/';
+ },
+
+ _computeChangeStatusString: function(change) {
+ if (change.mergeable != null && change.mergeable == false) {
+ return 'Merge Conflict';
+ }
+ if (change.status == Changes.Status.MERGED) {
+ return 'Merged';
+ }
+ if (change.status == Changes.Status.DRAFT) {
+ return 'Draft';
+ }
+ if (change.status == Changes.Status.ABANDONED) {
+ return 'Abandoned';
+ }
+ return '';
+ },
+
+ _computeCodeReviewClass: function(codeReview) {
+ // Mimic a Set.
+ var classes = {
+ 'cell': true,
+ 'codeReview': true,
+ };
+ if (codeReview) {
+ if (codeReview.approved) {
+ classes['u-green'] = true;
+ }
+ if (codeReview.value == 1) {
+ classes['u-monospace'] = true;
+ classes['u-green'] = true;
+ } else if (codeReview.value == -1) {
+ classes['u-monospace'] = true;
+ classes['u-red'] = true;
+ }
+ }
+ return Object.keys(classes).sort().join(' ');
+ },
+
+ _computeCodeReviewLabel: function(codeReview) {
+ if (!codeReview) { return ''; }
+ if (codeReview.approved) {
+ return '✓';
+ }
+ if (codeReview.value == 1) {
+ return '+1';
+ }
+ if (codeReview.value == -1) {
+ return '-1';
+ }
+ return '';
+ },
+
+ _computeVerifiedLabel: function(verified) {
+ if (verified && verified.approved) {
+ return '✓';
+ }
+ return ''
+ },
+
+ _computeAvatarURL: function(owner) {
+ if (!owner) { return ''; }
+ return '/accounts/' + owner.email + '/avatar?s=32'
+ },
+
+ _computeOwnerLink: function(email) {
+ if (!email) { return ''; }
+ return '/q/owner:' + encodeURIComponent(email) + '+status:open';
+ },
+
+ _computeOwnerTitle: function(owner) {
+ if (!owner) { return ''; }
+ // TODO: Is this safe from XSS attacks?
+ return owner.name + ' <' + owner.email + '>';
+ },
+
+ _computeProjectURL: function(project) {
+ return '/projects/' + project + ',dashboards/default';
+ },
+
+ _computeProjectBranchURL: function(project, branch) {
+ return '/q/status:open+project:' + project + '+branch:' + branch;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list-view.html b/polygerrit-ui/app/elements/gr-change-list-view.html
new file mode 100644
index 0000000..e7cb9ce
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list-view.html
@@ -0,0 +1,128 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-change-list.html">
+
+<dom-module id="gr-change-list-view">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ margin: 0 1.25rem;
+ }
+ gr-change-list {
+ margin-top: 1em;
+ width: 100%;
+ }
+ nav {
+ margin-bottom: 1em;
+ padding: .5em 0;
+ text-align: center;
+ }
+ nav a {
+ display: inline-block;
+ }
+ nav a:first-of-type {
+ margin-right: .5em;
+ }
+ [hidden] {
+ display: none !important;
+ }
+ </style>
+ <gr-ajax
+ auto
+ url="/changes/"
+ params="[[_computeQueryParams(query, offset)]]"
+ last-response="{{_changes}}"></gr-ajax>
+ <gr-change-list changes="{{_changes}}"></gr-change-list>
+ <nav>
+ <a href$="[[_computeNavLink(query, offset, -1)]]"
+ hidden$="[[_hidePrevArrow(offset)]]">← Prev</a>
+ <a href$="[[_computeNavLink(query, offset, 1)]]"
+ hidden$="[[_hideNextArrow(_changes.length)]]">Next →</a>
+ </nav>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var DEFAULT_NUM_CHANGES = 25;
+
+ Polymer({
+ is: 'gr-change-list-view',
+
+ properties: {
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+
+ /**
+ * Change objects loaded from the server.
+ */
+ _changes: Array,
+ },
+
+ _paramsChanged: function(value) {
+ this.query = value.query;
+ this.offset = value.offset || 0;
+ },
+
+ _computeQueryParams: function(query, offset) {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.LABELS,
+ Changes.ListChangesOption.DETAILED_ACCOUNTS
+ );
+ var obj = {
+ n: DEFAULT_NUM_CHANGES, // Number of results to return.
+ O: options,
+ S: offset || 0,
+ };
+ if (query && query.length > 0) {
+ obj.q = query;
+ }
+ return obj;
+ },
+
+ _computeNavLink: function(query, offset, direction) {
+ // Offset could be a string when passed from the router.
+ offset = +(offset || 0);
+ var newOffset = Math.max(0, offset + (25 * direction));
+ var href = '/q/' + query;
+ if (newOffset > 0) {
+ href += ',' + newOffset;
+ }
+ return href;
+ },
+
+ _hidePrevArrow: function(offset) {
+ return offset == 0;
+ },
+
+ _hideNextArrow: function(changesLen) {
+ return changesLen < DEFAULT_NUM_CHANGES;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-list.html b/polygerrit-ui/app/elements/gr-change-list.html
new file mode 100644
index 0000000..8c0b796
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-list.html
@@ -0,0 +1,196 @@
+<!--
+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.
+-->
+
+<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="../styles/gr-change-list-styles.html">
+<link rel="import" href="gr-change-list-item.html">
+
+<dom-module id="gr-change-list">
+ <template>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ .headerRow {
+ display: flex;
+ }
+ .topHeader,
+ .groupHeader {
+ border-bottom: 1px solid #eee;
+ font-weight: bold;
+ padding: .3em .5em;
+ }
+ .topHeader {
+ background-color: #ddd;
+ flex-shrink: 0;
+ }
+ </style>
+ <style include="gr-change-list-styles"></style>
+ <div class="headerRow">
+ <span class="topHeader keyboard"></span> <!-- keyboard position indicator -->
+ <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>
+ <span class="topHeader codeReview" title="Code-Review">CR</span>
+ <span class="topHeader verified" title="Verified">V</span>
+ </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-repeat" items="[[changeGroup]]" as="change">
+ <gr-change-list-item change="[[change]]"
+ selected="[[_isSelected(groupIndex, index)]]"></gr-change-list-item>
+ </template>
+ </template>
+ </template>
+
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-list',
+
+ behaviors: [
+ Polymer.IronA11yKeysBehavior
+ ],
+
+ hostAttributes: {
+ tabindex: 0,
+ },
+
+ properties: {
+ /**
+ * An array of ChangeInfo objects to render.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+ */
+ changes: {
+ type: Array,
+ observer: '_changesChanged',
+ },
+ /**
+ * ChangeInfo objects grouped into arrays. The groups and changes
+ * properties should not be used together.
+ */
+ groups: {
+ type: Array,
+ value: function() { return []; },
+ observer: '_groupsChanged',
+ },
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return document.body;
+ }
+ },
+ groupTitles: {
+ type: Array,
+ value: function() { return []; },
+ },
+ selectedIndex: {
+ type: Number,
+ value: 0,
+ observer: '_selectedIndexChanged',
+ },
+ },
+
+ keyBindings: {
+ 'j k o enter': '_handleKey',
+ },
+
+ _isSelected: function(groupIndex, index) {
+ return index == this.selectedIndex;
+ },
+
+ _changesChanged: function(changes) {
+ this.groups = [changes];
+ },
+
+ _groupsChanged: function(groups) {
+ for (var i = 0; i < groups.length; i++) {
+ for (var j = 0; j < groups[i].length; j++) {
+ var change = groups[i][j];
+ if (change.labels && change.labels.hasOwnProperty('Code-Review')) {
+ // Transform Code-Review to Code_Review so it is a JS identifier
+ // that can be used in computed properties. This is a hack, but
+ // it'll all have to change to support dynamic label sets anyway.
+ change.labels['Code_Review'] = change.labels['Code-Review'];
+ delete change.labels['Code-Review'];
+ }
+ }
+ }
+ },
+
+ _groupTitle: function(groupIndex) {
+ if (groupIndex > this.groupTitles.length - 1) { return null; }
+ return this.groupTitles[groupIndex];
+ },
+
+ _selectedIndexChanged: function(value) {
+ // Don't re-render the entire list.
+ var changeEls = this._getListItems();
+ for (var i = 0; i < changeEls.length; i++) {
+ changeEls[i].toggleAttribute('selected', i == value);
+ }
+ },
+
+ _handleKey: function(e) {
+ if (util.shouldSupressKeyboardShortcut(e)) { return; }
+
+ if (this.groups == null) { return; }
+ var len = 0;
+ this.groups.forEach(function(group) {
+ len += group.length;
+ });
+ switch(e.detail.combo) {
+ case 'j':
+ if (this.selectedIndex == len - 1) { return; }
+ this.selectedIndex += 1;
+ break;
+ case 'k':
+ if (this.selectedIndex == 0) { return; }
+ this.selectedIndex -= 1;
+ break;
+ case 'o':
+ case 'enter':
+ page.show(this._changeURLForIndex(this.selectedIndex));
+ break;
+ }
+ },
+
+ _changeURLForIndex: function(index) {
+ var changeEls = this._getListItems();
+ if (index < changeEls.length && changeEls[index]) {
+ return changeEls[index].changeURL;
+ }
+ return '';
+ },
+
+ _getListItems: function() {
+ return Polymer.dom(this.root).querySelectorAll('gr-change-list-item');
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
new file mode 100644
index 0000000..8bef101
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -0,0 +1,322 @@
+<!--
+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.
+-->
+
+<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="gr-ajax.html">
+<link rel="import" href="gr-date-formatter.html">
+<link rel="import" href="gr-file-list.html">
+<link rel="import" href="gr-messages-list.html">
+<link rel="import" href="gr-reply-dropdown.html">
+
+<dom-module id="gr-change-view">
+ <template>
+ <style>
+ .container {
+ margin: 1em 0;
+ }
+ .container:not(.loading) {
+ background-color: var(--view-background-color);
+ }
+ .container.loading {
+ color: #666;
+ }
+ .headerContainer {
+ height: 4.1em;
+ }
+ .header {
+ background-color: var(--view-background-color);
+ display: flex;
+ max-width: var(--max-constrained-width);
+ padding: 1em var(--default-horizontal-margin);
+ white-space: nowrap;
+ width: 100%;
+ z-index: 1000;
+ }
+ .header.pinned {
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ position: fixed;
+ top: 0;
+ transition: box-shadow 250ms linear;
+ }
+ .header h2 {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ section {
+ margin: 10px 0;
+ padding: 10px var(--default-horizontal-margin);
+ }
+ section:first-of-type {
+ margin-top: 0;
+ }
+ table {
+ border-collapse: collapse;
+ }
+ td {
+ padding: 2px 5px;
+ vertical-align: top;
+ }
+ .changeInfo-label {
+ font-weight: bold;
+ text-align: right;
+ }
+ .summary {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ }
+ gr-file-list {
+ padding: 0 var(--default-horizontal-margin) 10px;
+ }
+ </style>
+ <gr-ajax id="detailXHR"
+ url="[[_computeDetailPath(changeNum)]]"
+ params="[[_computeDetailQueryParams()]]"
+ last-response="{{change}}"
+ loading="{{_loading}}"></gr-ajax>
+ <gr-ajax id="commentsXHR"
+ url="[[_computeCommentsPath(changeNum)]]"
+ last-response="{{comments}}"></gr-ajax>
+ <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
+ <div class="container" hidden$="{{_loading}}">
+ <div class="headerContainer">
+ <div class="header">
+ <h2>
+ <a href$="[[_computeChangePath(change._number)]]">[[change._number]]</a><span>:</span>
+ <span>[[change.subject]]</span>
+ </h2>
+ <gr-reply-dropdown id="replyDropdown"
+ change-num="[[changeNum]]"
+ patch-num="[[_computePatchNum(change.current_revision)]]"
+ labels="[[change.labels]]"
+ permitted-labels="[[change.permitted_labels]]"
+ on-send="_handleReplySent"
+ hidden$="[[!_loggedIn]]">Reply</gr-reply-dropdown>
+ </div>
+ </div>
+ <section class="changeInfo">
+ <table>
+ <tr>
+ <td class="changeInfo-label">Owner</td>
+ <td>[[change.owner.name]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Reviewers</td>
+ <td>
+ <template is="dom-repeat"
+ items="[[_computeReviewers(change.labels, change.owner)]]"
+ as="reviewer">
+ <div>[[reviewer.name]]</div>
+ </template>
+ </td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Project</td>
+ <td>[[change.project]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Branch</td>
+ <td>[[change.branch]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Topic</td>
+ <td>[[change.topic]]</td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Strategy</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td class="changeInfo-label">Updated</td>
+ <td>
+ <gr-date-formatter
+ date-str="[[change.updated]]"></gr-date-formatter>
+ </td>
+ </tr>
+ </table>
+ </section>
+ <section class="summary">[[_computeCurrentRevisionMessage(change)]]</section>
+ <gr-file-list id="fileList"
+ change-num="[[changeNum]]"
+ patch-num="[[_computePatchNum(change.current_revision)]]"
+ revision="[[change.current_revision]]"
+ comments="[[comments]]"></gr-file-list>
+ <gr-messages-list
+ change-num="[[changeNum]]"
+ messages="[[change.messages]]"
+ comments="[[comments]]"></gr-messages-list>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-view',
+
+ behaviors: [
+ Polymer.IronA11yKeysBehavior
+ ],
+
+ properties: {
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return document.body;
+ },
+ },
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ changeNum: Number,
+ comments: Object,
+
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: Boolean,
+ _headerContainerEl: Object,
+ _headerEl: Object,
+ _scrollHandler: Function,
+ },
+
+ keyBindings: {
+ 'a u': '_handleKey',
+ },
+
+ ready: function() {
+ app.accountReady.then(function() {
+ this._loggedIn = app.loggedIn;
+ }.bind(this));
+ this._scrollHandler = this._handleBodyScroll.bind(this);
+ },
+
+ attached: function() {
+ window.addEventListener('scroll', this._scrollHandler);
+ },
+
+ detached: function() {
+ window.removeEventListener('scroll', this._scrollHandler);
+ },
+
+ _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; }
+
+ var el = this._headerEl || this.$$('.header');
+ this._headerEl = el;
+ el.classList.toggle('pinned', window.scrollY >= top);
+ },
+
+ _handleReplySent: function(e) {
+ this._reload();
+ },
+
+ _paramsChanged: function(value) {
+ this.changeNum = value.changeNum;
+ if (!this.changeNum) {
+ this.change = null;
+ this.comments = null;
+ return;
+ }
+ this._reload();
+ },
+
+ _computeChangePath: function(changeNum) {
+ return '/c/' + changeNum;
+ },
+
+ _computeDetailPath: function(changeNum) {
+ return '/changes/' + changeNum + '/detail';
+ },
+
+ _computeCommitInfoPath: function(changeNum, commitHash) {
+ return '/changes/' + changeNum + '/revisions/' + commitHash + '/commit';
+ },
+
+ _computeCommentsPath: function(changeNum) {
+ return '/changes/' + changeNum + '/comments';
+ },
+
+ _computePatchNum: function(revision) {
+ return this.change && this.change.revisions[revision]._number;
+ },
+
+ _computeDetailQueryParams: function() {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.CURRENT_REVISION,
+ Changes.ListChangesOption.CURRENT_COMMIT,
+ Changes.ListChangesOption.CHANGE_ACTIONS
+ );
+ return { O: options };
+ },
+
+ _computeCurrentRevisionMessage: function(change) {
+ return change &&
+ change.revisions[change.current_revision].commit.message;
+ },
+
+ _computeReviewers: function(labels, owner) {
+ var reviewers =
+ (labels['Code-Review'] && labels['Code-Review'].all) || [];
+ if (reviewers.length == 1) { return reviewers; }
+ return reviewers.filter(function(reviewer) {
+ return reviewer._account_id != owner._account_id;
+ });
+ },
+
+ _handleKey: function(e) {
+ if (util.shouldSupressKeyboardShortcut(e)) { return; }
+ e.preventDefault();
+ switch(e.detail.combo) {
+ case 'a':
+ this.$.replyDropdown.open();
+ break;
+ case 'u':
+ page.show('/');
+ break;
+ }
+ },
+
+ _reload: function() {
+ this.$.detailXHR.generateRequest();
+ this.$.commentsXHR.generateRequest();
+ this.$.fileList.reload();
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-comment-list.html b/polygerrit-ui/app/elements/gr-comment-list.html
new file mode 100644
index 0000000..3c172e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-comment-list.html
@@ -0,0 +1,117 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-comment-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .file {
+ border-top: 1px solid #ddd;
+ font-weight: bold;
+ margin: 10px 0 3px;
+ padding: 10px 0 5px;
+ }
+ .container {
+ display: flex;
+ margin: 5px 0;
+ }
+ .lineNum {
+ margin-right: .35em;
+ min-width: 7em;
+ }
+ .message {
+ flex: 1;
+ white-space: pre-wrap;
+ }
+ </style>
+ <template is="dom-repeat" items="{{_files}}" as="file">
+ <div class="file">
+ <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
+ </div>
+ <template is="dom-repeat"
+ items="[[_computeCommentsForFile(file)]]" as="comment">
+ <div class="container">
+ <a class="lineNum"
+ href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+ <span hidden$="[[!comment.line]]">
+ <span>[[_computePatchDisplayName(comment)]]</span>
+ Line <span>[[comment.line]]</span>:
+ </span>
+ <span hidden$="[[comment.line]]">
+ File comment:
+ </span>
+ </a>
+ <div class="message">[[comment.message]]</div>
+ </div>
+ </template>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-comment-list',
+
+ properties: {
+ changeNum: Number,
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ patchNum: Number,
+
+ _files: Array,
+ },
+
+ _commentsChanged: function(value) {
+ this._files = Object.keys(value || {}).sort();
+ },
+
+ _computeFileDiffURL: function(file, changeNum, patchNum) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + file;
+ },
+
+ _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
+ var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
+ if (comment.line) {
+ // TODO(andybons): This is not correct if the comment is on the base.
+ diffURL += '#' + comment.line;
+ }
+ return diffURL;
+ },
+
+ _computeCommentsForFile: function(file) {
+ return this.comments[file];
+ },
+
+ _computePatchDisplayName: function(comment) {
+ if (comment.side == 'PARENT') {
+ return 'Base, ';
+ }
+ if (comment.patch_set != this.patchNum) {
+ return 'PS' + comment.patch_set + ', ';
+ }
+ return '';
+ }
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-dashboard-view.html b/polygerrit-ui/app/elements/gr-dashboard-view.html
new file mode 100644
index 0000000..88997f6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-dashboard-view.html
@@ -0,0 +1,78 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-dashboard-view">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ margin: 0 1.25rem;
+ }
+ gr-change-list {
+ margin-top: 1em;
+ width: 100%;
+ }
+ </style>
+ <gr-ajax
+ auto
+ url="/changes/"
+ params="[[_computeQueryParams()]]"
+ last-response="{{_results}}"></gr-ajax>
+ <gr-change-list groups="{{_results}}"
+ group-titles="[[_groupTitles]]"></gr-change-list>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-dashboard-view',
+
+ properties: {
+ _results: Array,
+ _groupTitles: {
+ type: Array,
+ value: [
+ 'Outgoing reviews',
+ 'Incoming reviews',
+ 'Recently closed',
+ ],
+ },
+ },
+
+ _computeQueryParams: function() {
+ var options = Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.LABELS,
+ Changes.ListChangesOption.DETAILED_ACCOUNTS,
+ Changes.ListChangesOption.REVIEWED
+ );
+ return {
+ O: options,
+ q: [
+ 'is:open owner:self',
+ 'is:open reviewer:self -owner:self',
+ 'is:closed (owner:self OR reviewer:self) -age:4w limit:10',
+ ],
+ };
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-date-formatter.html b/polygerrit-ui/app/elements/gr-date-formatter.html
new file mode 100644
index 0000000..55782f6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-date-formatter.html
@@ -0,0 +1,90 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-date-formatter">
+ <template>
+ <style>
+ :host {
+ display: inline;
+ }
+ </style>
+ <span>[[_computeDateStr(dateStr)]]</span>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var Duration = {
+ HOUR: 1000 * 60 * 60,
+ DAY: 1000 * 60 * 60 * 24,
+ };
+
+ var ShortMonthNames = [
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
+ 'Nov', 'Dec'
+ ];
+
+ Polymer({
+ is: 'gr-date-formatter',
+
+ properties: {
+ dateStr: {
+ type: String,
+ value: null,
+ notify: true
+ }
+ },
+
+ _computeDateStr: function(dateStr) {
+ return this._dateStr(this._parseDateStr(dateStr), new Date());
+ },
+
+ _parseDateStr: function(dateStr) {
+ if (!dateStr) { return null; }
+ return util.parseDate(dateStr);
+ },
+
+ _dateStr: function(t, now) {
+ if (!t) { return ''; }
+ var diff = now.getTime() - t.getTime();
+ if (diff < Duration.DAY && t.getDay() == now.getDay()) {
+ // Within 24 hours and on the same day:
+ // '2:14 AM'
+ var pm = t.getHours() >= 12;
+ var hours = t.getHours() === 0 ? 12 :
+ pm ? t.getHours() - 12 : t.getHours();
+ var minutes = t.getMinutes() < 10 ? '0' + t.getMinutes() :
+ t.getMinutes();
+ return hours + ':' + minutes + (pm ? ' PM' : ' AM');
+ } else if ((t.getDay() != now.getDay() || diff >= Duration.DAY) &&
+ diff < 180 * Duration.DAY) {
+ // From one to six months:
+ // 'Aug 29'
+ return ShortMonthNames[t.getMonth()] + ' ' + t.getDate();
+ } else if (diff >= 180 * Duration.DAY) {
+ // More than six months:
+ // 'Aug 29, 1997'
+ return ShortMonthNames[t.getMonth()] + ' ' + t.getDate() + ', ' +
+ t.getFullYear();
+ }
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/gr-diff-comment-thread.html
new file mode 100644
index 0000000..5454914
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-comment-thread.html
@@ -0,0 +1,160 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-diff-comment.html">
+
+<dom-module id="gr-diff-comment-thread">
+ <template>
+ <style>
+ :host {
+ display: block;
+ max-width: 50em;
+ white-space: normal;
+ }
+ </style>
+ <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
+ <gr-diff-comment
+ comment="{{comment}}"
+ change-num="[[changeNum]]"
+ patch-num="[[patchNum]]"
+ draft="[[comment.__draft]]"
+ editing="[[!comment.message]]"></gr-diff-comment>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+
+ Polymer({
+ is: 'gr-diff-comment-thread',
+
+ /**
+ * Fired when the height of the thread changes.
+ *
+ * @event gr-diff-comment-thread-height-changed
+ */
+
+ /**
+ * Fired when the thread should be discarded.
+ *
+ * @event gr-diff-comment-thread-discard
+ */
+
+ properties: {
+ changeNum: String,
+ comments: {
+ type: Array,
+ value: function() { return []; },
+ observer: '_commentsChanged',
+ },
+ patchNum: String,
+
+ _orderedComments: Array,
+ },
+
+ ready: function() {
+ this.addEventListener('gr-diff-comment-height-changed',
+ this._handleCommentHeightChange.bind(this));
+ this.addEventListener('gr-diff-comment-reply',
+ this._handleCommentReply.bind(this));
+ this.addEventListener('gr-diff-comment-discard',
+ this._handleCommentDiscard.bind(this));
+ },
+
+ _commentsChanged: function(comments) {
+ this._orderedComments = this._sortedComments(comments);
+ },
+
+ _sortedComments: function(comments) {
+ comments.sort(function(c1, c2) {
+ return util.parseDate(c1.updated) - util.parseDate(c2.updated);
+ });
+
+ 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);
+ }
+ return results;
+ },
+
+ _visitComment: function(parent, commentIDToReplies, results) {
+ results.push(parent);
+
+ var replies = commentIDToReplies[parent.id];
+ if (!replies) { return; }
+ for (var i = 0; i < replies.length; i++) {
+ this._visitComment(replies[i], commentIDToReplies, results);
+ }
+ },
+
+ _handleCommentHeightChange: function(e) {
+ // TODO: This fires for each comment on initialization. Optimize to only
+ // fire the top level "thread height has changed" event once during
+ // initial DOM stamp.
+ this.fire('gr-diff-comment-thread-height-changed',
+ {height: this.offsetHeight});
+ },
+
+ _handleCommentReply: function(e) {
+ console.log('should add reply...')
+ },
+
+ _handleCommentDiscard: function(e) {
+ var diffCommentEl = e.target;
+ var idx = this._indexOf(diffCommentEl.comment, this.comments);
+ if (idx == -1) {
+ throw Error('Cannot find comment ' +
+ JSON.stringify(diffCommentEl.comment));
+ }
+ this.comments.splice(idx, 1);
+ this._commentsChanged(this.comments);
+ if (this.comments.length == 0 && this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
+ this.fire('gr-diff-comment-thread-height-changed',
+ {height: this.offsetHeight});
+ },
+
+ _indexOf: function(comment, arr) {
+ for (var i = 0; i < arr.length; i++) {
+ var c = arr[i];
+ if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
+ (c.id != null && c.id == comment.id)) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html
new file mode 100644
index 0000000..d8c2537
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-comment.html
@@ -0,0 +1,344 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="gr-date-formatter.html">
+<link rel="import" href="gr-request.html">
+
+<dom-module id="gr-diff-comment">
+ <template>
+ <style>
+ :host {
+ border: 1px solid #ddd;
+ display: block;
+ }
+ :host([disabled]) {
+ pointer-events: none;
+ }
+ :host([disabled]) .container {
+ opacity: .5;
+ }
+ .header,
+ .message,
+ .actions {
+ padding: .5em .7em;
+ }
+ .header {
+ background-color: #eee;
+ display: flex;
+ font-family: 'Open Sans', sans-serif;
+ }
+ .headerLeft {
+ flex: 1;
+ }
+ .authorName,
+ .draftLabel {
+ font-weight: bold;
+ }
+ .draftLabel {
+ color: #999;
+ display: none;
+ }
+ .date {
+ justify-content: flex-end;
+ margin-left: 5px;
+ }
+ a.date:link,
+ a.date:visited {
+ color: #666;
+ text-decoration: none;
+ }
+ a.date:hover {
+ text-decoration: underline;
+ }
+ .message {
+ white-space: pre-wrap;
+ }
+ .actions {
+ display: flex;
+ padding-top: 0;
+ }
+ .action {
+ margin-right: 1em;
+ }
+ .action[disabled] {
+ opacity: .5;
+ pointer-events: none;
+ }
+ .danger {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ .editMessage {
+ display: none;
+ margin: .5em .7em;
+ width: calc(100% - 1.4em - 2px);
+ }
+ .danger .action {
+ margin-right: 0;
+ }
+ .container:not(.draft) .actions :not(.reply):not(.done) {
+ display: none;
+ }
+ .draft .reply,
+ .draft .done {
+ display: none;
+ }
+ .draft .draftLabel {
+ display: inline;
+ }
+ .draft:not(.editing) .save,
+ .draft:not(.editing) .cancel {
+ display: none;
+ }
+ .editing .message,
+ .editing .reply,
+ .editing .done,
+ .editing .edit {
+ display: none;
+ }
+ .editing .editMessage {
+ display: block;
+ }
+ </style>
+ <div class="container" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <span class="authorName">[[comment.author.name]]</span>
+ <span class="draftLabel">DRAFT</span>
+ </div>
+ <a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
+ <gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
+ </a>
+ </div>
+ <iron-autogrow-textarea
+ id="editTextarea"
+ class="editMessage"
+ disabled="{{disabled}}"
+ rows="4"
+ max-rows="4"
+ bind-value="{{_editDraft}}"></iron-autogrow-textarea>
+ <div class="message">[[comment.message]]</div>
+ <div class="actions">
+ <a class="action reply" href="#" on-tap="_handleReply">Reply</a>
+ <a class="action done" href="#" on-tap="_handleDone">Done</a>
+ <a class="action edit" href="#" on-tap="_handleEdit">Edit</a>
+ <a class="action save" href="#"
+ disabled$="[[_computeSaveDisabled(_editDraft)]]"
+ on-tap="_handleSave">Save</a>
+ <a class="action cancel" href="#" on-tap="_handleCancel">Cancel</a>
+ <div class="danger">
+ <a class="action discard" href="#" on-tap="_handleDiscard">Discard</a>
+ </div>
+ </div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff-comment',
+
+ /**
+ * Fired when the height of the comment changes.
+ *
+ * @event gr-diff-comment-height-changed
+ */
+
+ /**
+ * Fired when the Reply action is triggered.
+ *
+ * @event gr-diff-comment-reply
+ */
+
+ /**
+ * Fired when the Done action is triggered.
+ *
+ * @event gr-diff-comment-done
+ */
+
+ /**
+ * Fired when this comment is discarded.
+ *
+ * @event gr-diff-comment-discard
+ */
+
+ properties: {
+ changeNum: String,
+ comment: {
+ type: Object,
+ notify: true,
+ },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ draft: {
+ type: Boolean,
+ value: false,
+ observer: '_draftChanged',
+ },
+ editing: {
+ type: Boolean,
+ value: false,
+ observer: '_editingChanged',
+ },
+ patchNum: String,
+
+ _xhrPromise: Object, // Used for testing.
+ _editDraft: String,
+ },
+
+ attached: function() {
+ this._heightChanged();
+ },
+
+ _heightChanged: function() {
+ this.async(function() {
+ this.fire('gr-diff-comment-height-changed',
+ {height: this.offsetHeight});
+ }.bind(this));
+ },
+
+ _draftChanged: function(draft) {
+ this.$.container.classList.toggle('draft', draft);
+ },
+
+ _editingChanged: function(editing) {
+ this.$.container.classList.toggle('editing', editing);
+ if (editing) {
+ this.async(function() {
+ this.$.editTextarea.textarea.focus();
+ }.bind(this));
+ }
+ this._heightChanged();
+ },
+
+ _computeLinkToComment: function(comment) {
+ return '#' + comment.line;
+ },
+
+ _computeSaveDisabled: function(draft) {
+ return draft == null || draft.trim() == '';
+ },
+
+ _handleLinkTap: function(e) {
+ e.preventDefault();
+ var hash = this._computeLinkToComment(this.comment);
+ // Don't add the hash to the window history if it's already there.
+ // Otherwise you mess up expected back button behavior.
+ if (window.location.hash == hash) { return; }
+ // Change the URL but don’t trigger a nav event. Otherwise it will
+ // reload the page.
+ page.show(window.location.pathname + hash, null, false);
+ },
+
+ _handleReply: function(e) {
+ e.preventDefault();
+ this.fire('gr-diff-comment-reply');
+ },
+
+ _handleDone: function(e) {
+ e.preventDefault();
+ this.fire('gr-diff-comment-done');
+ },
+
+ _handleEdit: function(e) {
+ e.preventDefault();
+ this._editDraft = this.comment.message;
+ this.editing = true;
+ },
+
+ _handleSave: function(e) {
+ e.preventDefault();
+ this.comment.message = this._editDraft;
+ this.disabled = true;
+ var endpoint = this._restEndpoint(this.comment.id);
+ this._send('PUT', endpoint).then(function(req) {
+ this.disabled = false;
+ var comment = req.response;
+ comment.__draft = true;
+ // Maintain the ephemeral draft ID for identification by other
+ // elements.
+ if (this.comment.__draftID) {
+ comment.__draftID = this.comment.__draftID;
+ }
+ this.comment = comment;
+ this.editing = false;
+ }.bind(this)).catch(function(err) {
+ alert('Your draft couldn’t be saved. Check the console and contact ' +
+ 'the PolyGerrit team for assistance.');
+ this.disabled = false;
+ }.bind(this));
+ },
+
+ _handleCancel: function(e) {
+ e.preventDefault();
+ if (this.comment.message == null || this.comment.message.length == 0) {
+ this.fire('gr-diff-comment-discard');
+ return;
+ }
+ this._editDraft = this.comment.message;
+ this.editing = false;
+ },
+
+ _handleDiscard: function(e) {
+ e.preventDefault();
+ if (!this.comment.__draft) {
+ throw Error('Cannot discard a non-draft comment.');
+ }
+ this.disabled = true;
+ var commentID = this.comment.id;
+ if (!commentID) {
+ this.fire('gr-diff-comment-discard');
+ return;
+ }
+ this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
+ this.fire('gr-diff-comment-discard', this.comment);
+ }.bind(this)).catch(function(err) {
+ alert('Your draft couldn’t be deleted. Check the console and ' +
+ 'contact the PolyGerrit team for assistance.');
+ this.disabled = false;
+ }.bind(this));
+ },
+
+ _send: function(method, url) {
+ var xhr = document.createElement('gr-request');
+ this._xhrPromise = xhr.send({
+ method: method,
+ url: url,
+ body: this.comment,
+ });
+ return this._xhrPromise;
+ },
+
+ _restEndpoint: function(id) {
+ var path = '/changes/' + this.changeNum + '/revisions/' +
+ this.patchNum + '/drafts';
+ if (id) {
+ path += '/' + id;
+ }
+ return path;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-side.html b/polygerrit-ui/app/elements/gr-diff-side.html
new file mode 100644
index 0000000..6e150d6
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-side.html
@@ -0,0 +1,317 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-diff-side">
+ <template>
+ <style>
+ :host,
+ .container {
+ display: flex;
+ }
+ .content {
+ width: 80ch;
+ }
+ .lineNum:before,
+ .code:before {
+ /* To ensure the height is non-zero in these elements, a
+ zero-width space is set as its content. The character
+ itself doesn't matter. Just that there is something
+ there. */
+ content: '\200B';
+ }
+ .lineNum {
+ background-color: #eee;
+ color: #666;
+ padding: 0 .75em;
+ text-align: right;
+ }
+ .canComment .lineNum {
+ cursor: pointer;
+ }
+ .canComment .lineNum:hover {
+ background-color: #ccc;
+ }
+ .code {
+ white-space: pre;
+ }
+ .lightHighlight {
+ background-color: var(--light-highlight-color);
+ }
+ hl,
+ .darkHighlight {
+ background-color: var(--dark-highlight-color);
+ }
+ .br:after {
+ /* Line feed */
+ content: '\A';
+ }
+ .filler {
+ background: #eee;
+ }
+ </style>
+ <div class$="[[_computeContainerClass(canComment)]]">
+ <div class="numbers" id="numbers"></div>
+ <div class="content" id="content"></div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var CharCode = {
+ LESS_THAN: '<'.charCodeAt(0),
+ GREATER_THAN: '>'.charCodeAt(0),
+ AMPERSAND: '&'.charCodeAt(0),
+ SEMICOLON: ';'.charCodeAt(0),
+ };
+
+ Polymer({
+ is: 'gr-diff-side',
+
+ properties: {
+ canComment: {
+ type: Boolean,
+ value: false,
+ },
+ content: {
+ type: Array,
+ notify: true,
+ observer: '_render',
+ },
+ width: {
+ type: Number,
+ observer: '_widthChanged',
+ },
+
+ _lineFeedHTML: {
+ type: String,
+ value: '<span class="style-scope gr-diff-side br"></span>',
+ readOnly: true,
+ },
+ _highlightStartTag: {
+ type: String,
+ value: '<hl class="style-scope gr-diff-side">',
+ readOnly: true,
+ },
+ _highlightEndTag: {
+ type: String,
+ value: '</hl>',
+ readOnly: true,
+ },
+ },
+
+ scrollToLine: function(lineNum) {
+ if (isNaN(lineNum) || lineNum < 1) { return; }
+
+ var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
+ if (!el) { return; }
+
+ // Calculate where the line is relative to the window.
+ var top = el.offsetTop;
+ for (var offsetParent = el.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+
+ // 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
+ // element appears to be below the center of the window even when it
+ // isn't.
+ window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
+ },
+
+ _widthChanged: function(width) {
+ this.$.content.style.width = width + 'ch';
+ },
+
+ _computeContainerClass: function(canComment) {
+ return 'container' + (canComment ? ' canComment' : '');
+ },
+
+ _clearChildren: function(el) {
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+ },
+
+ _render: function(diff) {
+ this._clearChildren(this.$.numbers);
+ this._clearChildren(this.$.content);
+ for (var i = 0; i < diff.length; i++) {
+ switch (diff[i].type) {
+ case 'CODE':
+ this._renderCode(diff[i]);
+ break;
+ case 'FILLER':
+ this._renderFiller(diff[i]);
+ break;
+ }
+ }
+ },
+
+ _renderFiller: function(filler) {
+ var lineFillerEl = this._createElement('div', 'filler');
+ var fillerEl = this._createElement('div', 'filler');
+ var numLines = filler.numLines || 1;
+
+ lineFillerEl.textContent = '\n'.repeat(numLines);
+ for (var i = 0; i < numLines; i++) {
+ var newlineEl = this._createElement('span', 'br');
+ fillerEl.appendChild(newlineEl);
+ }
+ this.$.numbers.appendChild(lineFillerEl);
+ this.$.content.appendChild(fillerEl);
+ },
+
+ _renderCode: function(code) {
+ var lineNumEl = this._createElement('div', 'lineNum');
+ lineNumEl.setAttribute('data-line-num', code.lineNum);
+ var numLines = code.numLines || 1;
+ lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
+
+ var contentEl = this._createElement('div', 'code');
+ contentEl.setAttribute('data-line-num', code.lineNum);
+
+ if (code.highlight) {
+ contentEl.classList.add(code.intraline.length > 0 ?
+ 'lightHighlight' : 'darkHighlight');
+ }
+
+ var html = util.escapeHTML(code.content);
+ if (code.highlight && code.intraline.length > 0) {
+ html = this._addIntralineHighlights(code.content, html,
+ code.intraline);
+ }
+ if (numLines > 1) {
+ html = this._addNewLines(code.content, html, numLines);
+ }
+
+ // If the html is equivalent to the text then it didn't get highlighted
+ // or escaped. Use textContent which is faster than innerHTML.
+ if (code.content == html) {
+ contentEl.textContent = code.content;
+ } else {
+ contentEl.innerHTML = html;
+ }
+
+ this.$.numbers.appendChild(lineNumEl);
+ this.$.content.appendChild(contentEl);
+ },
+
+ // Advance `index` by the appropriate number of characters that would
+ // represent one source code character and return that index. For
+ // example, for source code '<span>' the escaped html string is
+ // '<span>'. Advancing from index 0 on the prior html string would
+ // return 4, since < maps to one source code character ('<').
+ _advanceChar: function(html, index) {
+ // Any tags don't count as characters
+ while (index < html.length &&
+ html.charCodeAt(index) == CharCode.LESS_THAN) {
+ while (index < html.length &&
+ html.charCodeAt(index) != CharCode.GREATER_THAN) {
+ index++;
+ }
+ index++; // skip the ">" itself
+ }
+ // An HTML entity (e.g., <) counts as one character.
+ if (index < html.length &&
+ html.charCodeAt(index) == CharCode.AMPERSAND) {
+ while (index < html.length &&
+ html.charCodeAt(index) != CharCode.SEMICOLON) {
+ index++;
+ }
+ }
+ return index + 1;
+ },
+
+ _addIntralineHighlights: function(content, html, highlights) {
+ var startTag = this._highlightStartTag;
+ var endTag = this._highlightEndTag;
+
+ for (var i = 0; i < highlights.length; i++) {
+ var hl = highlights[i];
+
+ var htmlStartIndex = 0;
+ for (var j = 0; j < hl.startIndex; j++) {
+ htmlStartIndex = this._advanceChar(html, htmlStartIndex);
+ }
+
+ var htmlEndIndex = 0;
+ if (hl.endIndex != null) {
+ for (var j = 0; j < hl.endIndex; j++) {
+ htmlEndIndex = this._advanceChar(html, htmlEndIndex);
+ }
+ } else {
+ // If endIndex isn't present, continue to the end of the line.
+ htmlEndIndex = html.length;
+ }
+ // The start and end indices could be the same if a highlight is meant
+ // to start at the end of a line and continue onto the next one.
+ // Ignore it.
+ if (htmlStartIndex != htmlEndIndex) {
+ html = html.slice(0, htmlStartIndex) + startTag +
+ html.slice(htmlStartIndex, htmlEndIndex) + endTag +
+ html.slice(htmlEndIndex);
+ }
+ }
+ return html;
+ },
+
+ _addNewLines: function(content, html, numLines) {
+ var htmlIndex = 0;
+ var indices = [];
+ for (var i = 0; i < content.length; i++) {
+ if (i > 0 && i % this.width == 0) {
+ indices.push(htmlIndex);
+ }
+ htmlIndex = this._advanceChar(html, htmlIndex)
+ }
+ var result = html;
+ var linesLeft = numLines;
+ // Since the result string is being altered in place, start from the end
+ // of the string so that the insertion indices are not affected as the
+ // result string changes.
+ for (var i = indices.length - 1; i >= 0; i--) {
+ result = result.slice(0, indices[i]) + this._lineFeedHTML +
+ result.slice(indices[i]);
+ linesLeft--;
+ }
+ // numLines is the total number of lines this code block should take up.
+ // Fill in the remaining ones.
+ for (var i = 0; i < linesLeft; i++) {
+ result += this._lineFeedHTML;
+ }
+ return result;
+ },
+
+ _createElement: function(tagName, className) {
+ var el = document.createElement(tagName);
+ // When Shady DOM is being used, these classes are added to account for
+ // Polymer's polyfill behavior. In order to guarantee sufficient
+ // specificity within the CSS rules, these are added to every element.
+ // Since the Polymer DOM utility functions (which would do this
+ // automatically) are not being used for performance reasons, this is
+ // done manually.
+ el.classList.add('style-scope', 'gr-diff-side', className);
+ return el;
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
new file mode 100644
index 0000000..4945a67
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-view.html
@@ -0,0 +1,183 @@
+<!--
+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.
+-->
+
+<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="gr-ajax.html">
+<link rel="import" href="gr-diff.html">
+
+<dom-module id="gr-diff-view">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ }
+ h3 {
+ margin-top: 1em;
+ padding: .75em var(--default-horizontal-margin);
+ }
+ .mainContainer {
+ border-bottom: 1px solid #eee;
+ border-collapse: collapse;
+ border-top: 1px solid #eee;
+ width: 100%;
+ }
+ </style>
+ <gr-ajax id="changeDetailXHR"
+ auto
+ url="[[_computeChangeDetailPath(_changeNum)]]"
+ params="[[_computeChangeDetailQueryParams()]]"
+ last-response="{{_change}}"></gr-ajax>
+ <gr-ajax id="filesXHR"
+ auto
+ url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
+ on-response="_handleFilesResponse"></gr-ajax>
+ <h3>
+ <a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
+ <span>[[_change.subject]]</span> — <span>[[params.path]]</span>
+ </h3>
+ <gr-diff id="diff"
+ auto
+ change-num="[[_changeNum]]"
+ patch-range="[[_patchRange]]"
+ path="[[_path]]"
+ on-render="_handleDiffRender">
+ </gr-diff>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff-view',
+
+ behaviors: [
+ Polymer.IronA11yKeysBehavior
+ ],
+
+ properties: {
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return document.body;
+ },
+ },
+ /**
+ * URL params passed from the router.
+ */
+ params: {
+ type: Object,
+ observer: '_paramsChanged',
+ },
+ _patchRange: Object,
+ _change: Object,
+ _changeNum: String,
+ _diff: Object,
+ _fileList: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _path: String,
+ },
+
+ keyBindings: {
+ '[ ] u': '_handleKey',
+ },
+
+ _handleKey: function(e) {
+ if (util.shouldSupressKeyboardShortcut(e)) { return; }
+
+ switch(e.detail.combo) {
+ case '[':
+ this._navToFile(this._fileList, -1);
+ break;
+ case ']':
+ this._navToFile(this._fileList, 1);
+ break;
+ case 'u':
+ if (this._changeNum) {
+ page.show(this._computeChangePath(this._changeNum));
+ }
+ break;
+ }
+ },
+
+ _handleDiffRender: function() {
+ if (window.location.hash.length > 0) {
+ this.$.diff.scrollToLine(
+ parseInt(window.location.hash.substring(1), 10));
+ }
+ },
+
+ _navToFile: function(fileList, direction) {
+ if (fileList.length == 0) { return; }
+
+ var idx = fileList.indexOf(this._path) + direction;
+ if (idx < 0 || idx > fileList.length - 1) {
+ page.show(this._computeChangePath(this._changeNum));
+ return;
+ }
+ page.show(this._diffURL(this._changeNum,
+ this._patchRange.patchNum,
+ fileList[idx]));
+ },
+
+ _diffURL: function(changeNum, patchNum, path) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + path;
+ },
+
+ _paramsChanged: function(value) {
+ this._changeNum = value.changeNum;
+ this._patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || 'PARENT',
+ };
+ this._path = value.path;
+
+ // When navigating away from the page, there is a possibility that the
+ // patch number is no longer a part of the URL (say when navigating to
+ // the top-level change info view) and therefore undefined in `params`.
+ if (!this._patchRange.patchNum) {
+ return;
+ }
+ },
+
+ _computeChangePath: function(changeNum) {
+ return '/c/' + changeNum;
+ },
+
+ _computeChangeDetailPath: function(changeNum) {
+ return '/changes/' + changeNum + '/detail';
+ },
+
+ _computeChangeDetailQueryParams: function() {
+ return { O: Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.ALL_REVISIONS
+ )};
+ },
+
+ _computeFilesPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/files';
+ },
+
+ _handleFilesResponse: function(e, req) {
+ this._fileList = Object.keys(e.detail.response).sort();
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html
new file mode 100644
index 0000000..23d14b5
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff.html
@@ -0,0 +1,394 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-diff-side.html">
+<link rel="import" href="gr-request.html">
+
+<dom-module id="gr-diff">
+ <template>
+ <style>
+ :host {
+ border-bottom: 1px solid #eee;
+ border-top: 1px solid #eee;
+ font-family: 'Source Code Pro', monospace;
+ display: flex;
+ white-space: pre;
+ }
+ gr-diff-side:first-of-type {
+ --light-highlight-color: #ffecec;
+ --dark-highlight-color: #faa;
+ }
+ gr-diff-side:last-of-type {
+ --light-highlight-color: #eaffea;
+ --dark-highlight-color: #9f9;
+ border-right: 1px solid #ddd;
+ }
+ </style>
+ <gr-ajax id="diffXHR"
+ url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
+ params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
+ last-response="{{_diffResponse}}"></gr-ajax>
+ <gr-ajax id="baseCommentsXHR"
+ url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+ <gr-ajax id="commentsXHR"
+ url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+ <gr-ajax id="baseDraftsXHR"
+ url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+ <gr-diff-side id="leftDiff"
+ content="{{_diff.leftSide}}"
+ width="[[sideWidth]]"
+ can-comment="[[_loggedIn]]"></gr-diff-side>
+ <gr-diff-side id="rightDiff"
+ content="{{_diff.rightSide}}"
+ width="[[sideWidth]]"
+ can-comment="[[_loggedIn]]"></gr-diff-side>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff',
+
+ /**
+ * Fired when the diff is rendered.
+ *
+ * @event render
+ */
+
+ properties: {
+ auto: {
+ type: Boolean,
+ value: false,
+ },
+ changeNum: String,
+ /*
+ * A single object to encompass basePatchNum and patchNum is used
+ * so that both can be set at once without incremental observers
+ * firing after each property changes.
+ */
+ patchRange: Object,
+ path: String,
+ sideWidth: {
+ type: Number,
+ value: 80,
+ },
+ _comments: Array,
+ _baseComments: Array,
+ _drafts: Array,
+ _baseDrafts: Array,
+ _diffResponse: Object,
+ _diff: {
+ type: Object,
+ value: function() { return {}; },
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _diffRequestsPromise: Object, // Used for testing.
+ },
+
+ observers: [
+ '_diffOptionsChanged(changeNum, patchRange, path)'
+ ],
+
+ ready: function() {
+ app.accountReady.then(function() {
+ this._loggedIn = app.loggedIn;
+ }.bind(this));
+ },
+
+ scrollToLine: function(lineNum) {
+ // TODO(andybons): Should this always be the right side?
+ this.$.rightDiff.scrollToLine(lineNum);
+ },
+
+ _diffOptionsChanged: function(changeNum, patchRange, path) {
+ if (!this.auto) { return; }
+
+ var promises = [this.$.diffXHR.generateRequest().completes];
+
+ var basePatchNum = patchRange.basePatchNum;
+ var patchNum = patchRange.patchNum;
+
+ app.accountReady.then(function() {
+ promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
+ this._diffRequestsPromise = Promise.all(promises).then(function() {
+ this._allDataReceived();
+ }.bind(this)).catch(function(err) {
+ alert('Oops. Something went wrong. Check the console and bug the ' +
+ 'PolyGerrit team for assistance.');
+ throw err;
+ });
+ }.bind(this));
+ },
+
+ _allDataReceived: function() {
+ this._processContent();
+
+ // Allow for the initial rendering to complete before firing the event.
+ this.async(function() {
+ this.fire('render', {bubbles: false});
+ }.bind(this), 1);
+ },
+
+ _getCommentsAndDrafts: function(basePatchNum, loggedIn) {
+ var promises = [];
+
+ function onlyParent(c) { return c.side == 'PARENT'; }
+ function withoutParent(c) { return c.side != 'PARENT'; }
+
+ var promises = [];
+ var commentsPromise = this.$.commentsXHR.generateRequest().completes;
+ promises.push(commentsPromise.then(function(req) {
+ var comments = req.response[this.path] || [];
+ if (basePatchNum == 'PARENT') {
+ this._baseComments = comments.filter(onlyParent);
+ }
+ this._comments = comments.filter(withoutParent);
+ }.bind(this)));
+
+ if (basePatchNum != 'PARENT') {
+ commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
+ promises.push(commentsPromise.then(function(req) {
+ this._baseComments =
+ (req.response[this.path] || []).filter(withoutParent);
+ }.bind(this)));
+ }
+
+ if (!loggedIn) {
+ this._baseDrafts = [];
+ this._drafts = [];
+ return Promise.all(promises);
+ }
+
+ var draftsPromise = this.$.draftsXHR.generateRequest().completes;
+ promises.push(draftsPromise.then(function(req) {
+ var drafts = req.response[this.path] || [];
+ if (basePatchNum == 'PARENT') {
+ this._baseDrafts = drafts.filter(onlyParent);
+ }
+ this._drafts = drafts.filter(withoutParent);
+ }.bind(this)));
+
+ if (basePatchNum != 'PARENT') {
+ draftsPromise = this.$baseDraftsXHR.generateRequest().completes;
+ promises.push(draftsPromise.then(function(req) {
+ this._baseDrafts =
+ (req.response[this.path] || []).filter(withoutParent);
+ }.bind(this)));
+ }
+
+ return Promise.all(promises);
+ },
+
+ _computeDiffPath: function(changeNum, patchNum, path) {
+ return Changes.baseURL(changeNum, patchNum) + '/files/' +
+ encodeURIComponent(path) + '/diff';
+ },
+
+ _computeCommentsPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/comments';
+ },
+
+ _computeDraftsPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/drafts';
+ },
+
+ _computeDiffQueryParams: function(basePatchNum) {
+ var params = {
+ context: 'ALL',
+ intraline: null
+ };
+ if (basePatchNum != 'PARENT') {
+ params.base = basePatchNum;
+ }
+ return params;
+ },
+
+ _processContent: function() {
+ var leftSide = [];
+ var rightSide = [];
+ var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
+ var ctx = {
+ left: {
+ lineNum: initialLineNum,
+ },
+ right: {
+ lineNum: initialLineNum,
+ }
+ };
+ for (var i = 0; i < this._diffResponse.content.length; i++) {
+ this._addDiffChunk(ctx, this._diffResponse.content[i], leftSide,
+ rightSide);
+ }
+ this._diff = {
+ leftSide: leftSide,
+ rightSide: rightSide,
+ };
+ },
+
+ _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
+ if (chunk.ab) {
+ for (var i = 0; i < chunk.ab.length; i++) {
+ var numLines = Math.ceil(chunk.ab[i].length / this.sideWidth);
+ // Blank lines within a diff content array indicate a newline.
+ leftSide.push({
+ type: 'CODE',
+ content: chunk.ab[i] || '\n',
+ numLines: numLines,
+ lineNum: ++ctx.left.lineNum,
+ });
+ rightSide.push({
+ type: 'CODE',
+ content: chunk.ab[i] || '\n',
+ numLines: numLines,
+ lineNum: ++ctx.right.lineNum,
+ });
+ }
+ }
+
+ var leftHighlights = [];
+ if (chunk.edit_a) {
+ leftHighlights =
+ this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
+ }
+ var rightHighlights = [];
+ if (chunk.edit_b) {
+ rightHighlights =
+ this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
+ }
+
+ var aLen = (chunk.a && chunk.a.length) || 0;
+ var bLen = (chunk.b && chunk.b.length) || 0;
+ var maxLen = Math.max(aLen, bLen);
+ for (var i = 0; i < maxLen; i++) {
+ var hasLeftContent = chunk.a && i < chunk.a.length;
+ var hasRightContent = chunk.b && i < chunk.b.length;
+ var leftContent = hasLeftContent ? chunk.a[i] : '';
+ var rightContent = hasRightContent ? chunk.b[i] : '';
+ var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
+ if (hasLeftContent) {
+ leftSide.push({
+ type: 'CODE',
+ content: leftContent || '\n',
+ numLines: maxNumLines,
+ lineNum: ++ctx.left.lineNum,
+ highlight: true,
+ intraline: leftHighlights.filter(function(hl) {
+ return hl.contentIndex == i;
+ }),
+ });
+ } else {
+ leftSide.push({
+ type: 'FILLER',
+ numLines: maxNumLines,
+ });
+ }
+ if (hasRightContent) {
+ rightSide.push({
+ type: 'CODE',
+ content: rightContent || '\n',
+ numLines: maxNumLines,
+ lineNum: ++ctx.right.lineNum,
+ highlight: true,
+ intraline: rightHighlights.filter(function(hl) {
+ return hl.contentIndex == i;
+ }),
+ });
+ } else {
+ rightSide.push({
+ type: 'FILLER',
+ numLines: maxNumLines,
+ });
+ }
+ }
+ },
+
+
+ // The `highlights` array consists of a list of <skip length, mark length>
+ // pairs, where the skip length is the number of characters between the
+ // end of the previous edit and the start of this edit, and the mark
+ // length is the number of edited characters following the skip. The start
+ // of the edits is from the beginning of the related diff content lines.
+ //
+ // Note that the implied newline character at the end of each line is
+ // included in the length calculation, and thus it is possible for the
+ // edits to span newlines.
+ //
+ // A line highlight object consists of three fields:
+ // - contentIndex: The index of the diffChunk `content` field (the line
+ // being referred to).
+ // - startIndex: Where the highlight should begin.
+ // - endIndex: (optional) Where the highlight should end. If omitted, the
+ // highlight is meant to be a continuation onto the next line.
+ _normalizeIntralineHighlights: function(content, highlights) {
+ var contentIndex = 0;
+ var idx = 0;
+ var normalized = [];
+ for (var i = 0; i < highlights.length; i++) {
+ var line = content[contentIndex] + '\n';
+ var hl = highlights[i];
+ var j = 0;
+ while (j < hl[0]) {
+ if (idx == line.length) {
+ idx = 0;
+ line = content[++contentIndex] + '\n';
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ var lineHighlight = {
+ contentIndex: contentIndex,
+ startIndex: idx,
+ };
+
+ j = 0;
+ while (line && j < hl[1]) {
+ if (idx == line.length) {
+ idx = 0;
+ line = content[++contentIndex] + '\n';
+ normalized.push(lineHighlight);
+ lineHighlight = {
+ contentIndex: contentIndex,
+ startIndex: idx,
+ };
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ lineHighlight.endIndex = idx;
+ normalized.push(lineHighlight);
+ }
+ return normalized;
+ },
+
+ _maxLinesSpanned: function(left, right) {
+ return Math.max(Math.ceil(left.length / this.sideWidth),
+ Math.ceil(right.length / this.sideWidth));
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html
new file mode 100644
index 0000000..dcc96ac
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-file-list.html
@@ -0,0 +1,161 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-ajax.html">
+
+<dom-module id="gr-file-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .tableContainer {
+ overflow-x: auto;
+ }
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ table a {
+ display: block;
+ }
+ td {
+ padding: 2px 0;
+ white-space: nowrap;
+ }
+ th {
+ text-align: left;
+ }
+ .status {
+ width: 20px;
+ }
+ .drafts {
+ color: #C62828;
+ font-weight: bold;
+ }
+ </style>
+ <gr-ajax id="filesXHR"
+ url="[[_computeFilesURL(changeNum, revision)]]"
+ on-response="_handleResponse"></gr-ajax>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsURL(changeNum, revision)]]"
+ last-response="{{_drafts}}"></gr-ajax>
+ </gr-ajax>
+ <div class="tableContainer">
+ <table>
+ <tr>
+ <th></th>
+ <th></th>
+ <th>Path</th>
+ <th>Comments</th>
+ <th>Stats</th>
+ </tr>
+ <template is="dom-repeat" items="{{files}}" as="file">
+ <tr>
+ <td></td>
+ <td class="status">[[file.status]]</td>
+ <td class="path">
+ <a class="file"
+ href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">[[file.__path]]</a>
+ </td>
+ <td>
+ <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
+ <span class="comments">[[_computeCommentsString(comments, file.__path)]]</span>
+ </td>
+ <td>
+ +<span>[[file.lines_inserted]]</span> lines,
+ -<span>[[file.lines_deleted]]</span> lines
+ </td>
+ </tr>
+ </template>
+ </table>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-file-list',
+
+ properties: {
+ patchNum: Number,
+ changeNum: Number,
+ revision: String,
+ comments: Object,
+
+ _drafts: Object,
+ },
+
+ observers: [
+ '_changeNumOrRevisionChanged(changeNum, revision)',
+ ],
+
+ reload: function() {
+ if (!!this.changeNum && !!this.revision) {
+ this.$.filesXHR.generateRequest();
+ this.$.draftsXHR.generateRequest();
+ }
+ },
+
+ _changeNumOrRevisionChanged: function(changeNum, revision) {
+ this.reload();
+ },
+
+ _computeFilesURL: function(changeNum, revision) {
+ return Changes.baseURL(changeNum, revision) + '/files';
+ },
+
+ _computeCommentsString: function(comments, path) {
+ var num = (comments[path] || []).length;
+ if (num == 0) { return ''; }
+ if (num == 1) { return '1 comment'; }
+ if (num > 1) { return num + ' comments'; };
+ },
+
+ _computeDraftsURL: function(changeNum, revision) {
+ return Changes.baseURL(changeNum, revision) + '/drafts';
+ },
+
+ _computeDraftsString: function(drafts, path) {
+ var num = (drafts[path] || []).length;
+ if (num == 0) { return ''; }
+ if (num == 1) { return '1 draft'; }
+ if (num > 1) { return num + ' drafts'; };
+ },
+
+ _handleResponse: function(e, req) {
+ var result = e.detail.response;
+ var paths = Object.keys(result).sort();
+ var files = [];
+ for (var i = 0; i < paths.length; i++) {
+ var info = result[paths[i]];
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ files.push(info)
+ }
+ this.files = files;
+ },
+
+ _computeDiffURL: function(changeNum, patchNum, path) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + path;
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
new file mode 100644
index 0000000..8e0af05
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-message.html
@@ -0,0 +1,182 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-comment-list.html">
+<link rel="import" href="gr-date-formatter.html">
+
+<dom-module id="gr-message">
+ <template>
+ <style>
+ :host {
+ border-top: 1px solid #ddd;
+ display: block;
+ position: relative;
+ }
+ :host(:not([expanded])) {
+ cursor: pointer;
+ }
+ .avatar {
+ border-radius: 50%;
+ position: absolute;
+ left: var(--default-horizontal-margin);
+ }
+ :not(.hideAvatar):not(.showAvatar) .avatar,
+ .hideAvatar .avatar {
+ display: none;
+ }
+ .collapsed .contentContainer {
+ color: #777;
+ white-space: nowrap;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ }
+ .showAvatar.expanded .contentContainer {
+ margin-left: calc(var(--default-horizontal-margin) + 2.5em);
+ padding: 10px 0;
+ }
+ .showAvatar.collapsed .contentContainer {
+ margin-left: calc(var(--default-horizontal-margin) + 1.75em);
+ padding: 10px 75px 10px 0;
+ }
+ .hideAvatar.collapsed .contentContainer,
+ .hideAvatar.expanded .contentContainer {
+ margin-left: 0;
+ padding: 10px 75px 10px 0;
+ }
+ .collapsed .avatar {
+ top: 8px;
+ }
+ .expanded .avatar {
+ top: 12px;
+ }
+ .collapsed .avatar {
+ height: 1.75em;
+ width: 1.75em;
+ }
+ .expanded .avatar {
+ height: 2.5em;
+ width: 2.5em;
+ }
+ .name {
+ font-weight: bold;
+ }
+ .collapsed .name,
+ .collapsed .content,
+ .collapsed .message {
+ display: inline;
+ }
+ .collapsed gr-comment-list {
+ display: none;
+ }
+ .collapsed .name,
+ .collapsed gr-date-formatter {
+ color: var(--default-text-color);
+ }
+ .expanded .name {
+ cursor: pointer;
+ }
+ .expanded .message {
+ white-space: pre-wrap;
+ }
+ gr-date-formatter {
+ position: absolute;
+ right: var(--default-horizontal-margin);
+ top: 10px;
+ }
+ </style>
+ <div class$="[[_computeClass(expanded, showAvatar)]]">
+ <img class="avatar" src$="[[_computeAvatarURL(message.author, showAvatar)]]">
+ <div class="contentContainer">
+ <div class="name" id="name">[[message.author.name]]</div>
+ <div class="content">
+ <div class="message">[[message.message]]</div>
+ <gr-comment-list
+ comments="[[comments]]"
+ change-num="[[changeNum]]"
+ patch-num="[[message._revision_number]]"></gr-comment-list>
+ </div>
+ <gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
+ </div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-message',
+
+ listeners: {
+ 'tap': '_tapHandler',
+ 'name.tap': '_collapseHandler',
+ },
+
+ properties: {
+ changeNum: Number,
+ message: Object,
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ expanded: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
+ showAvatar: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
+ ready: function() {
+ app.configReady.then(function(cfg) {
+ this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+ }.bind(this));
+ },
+
+ _commentsChanged: function(value) {
+ this.expanded = Object.keys(value || {}).length > 0;
+ },
+
+ _tapHandler: function(e) {
+ if (this.expanded) { return; }
+ this.expanded = true;
+ },
+
+ _collapseHandler: function(e) {
+ if (!this.expanded) { return; }
+ e.stopPropagation();
+ this.expanded = false;
+ },
+
+ _computeClass: function(expanded, showAvatar) {
+ var classes = [];
+ classes.push(expanded ? 'expanded' : 'collapsed');
+ classes.push(showAvatar ? 'showAvatar' : 'hideAvatar');
+ return classes.join(' ');
+ },
+
+ _computeAvatarURL: function(author, showAvatar) {
+ if (!showAvatar || !author) { return '' }
+ return '/accounts/' + author.email + '/avatar?s=100';
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-messages-list.html b/polygerrit-ui/app/elements/gr-messages-list.html
new file mode 100644
index 0000000..9d6df66
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-messages-list.html
@@ -0,0 +1,85 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-message.html">
+
+<dom-module id="gr-messages-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ h3 {
+ margin-bottom: .35em;
+ }
+ h3,
+ gr-message {
+ padding: 0 var(--default-horizontal-margin);
+ }
+ </style>
+ <h3>Messages</h3>
+ <template is="dom-repeat" items="{{messages}}" as="message">
+ <gr-message change-num="[[changeNum]]"
+ message="[[message]]"
+ comments="[[_computeCommentsForMessage(comments, message, index)]]"></gr-message>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-messages-list',
+
+ properties: {
+ changeNum: Number,
+ messages: {
+ type: Array,
+ value: function() { return []; },
+ },
+ comments: Object,
+ },
+
+ _computeCommentsForMessage: function(comments, message, index) {
+ var comments = comments || {};
+ var messages = this.messages || [];
+ var msgComments = {};
+ var mDate = util.parseDate(message.date);
+ var nextMDate;
+ if (index < messages.length - 1) {
+ nextMDate = util.parseDate(messages[index + 1].date);
+ }
+ 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) {
+ continue;
+ }
+ msgComments[file] = msgComments[file] || [];
+ msgComments[file].push(fileComments[i]);
+ }
+ }
+ }
+ return msgComments;
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reply-dropdown.html b/polygerrit-ui/app/elements/gr-reply-dropdown.html
new file mode 100644
index 0000000..dd607af
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-reply-dropdown.html
@@ -0,0 +1,308 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
+<link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-request.html">
+
+<dom-module id="gr-reply-dropdown">
+ <style>
+ :host {
+ display: inline-block;
+ }
+ :host([disabled]) {
+ pointer-events: none;
+ }
+ :host([disabled]) .dropdown-content {
+ opacity: .5;
+ }
+ .dropdown-content {
+ background-color: #fff;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+ }
+ button {
+ font: inherit;
+ background-color: #f1f2f3;
+ border: 1px solid #aaa;
+ border-radius: 2px;
+ padding: .2em .5em;
+ }
+ section {
+ border-top: 1px solid #ddd;
+ padding: .5em .75em;
+ }
+ .textareaContainer {
+ position: relative;
+ }
+ .message {
+ border: none;
+ }
+ .label:first-of-type {
+ margin-bottom: .25em;
+ }
+ .labelName {
+ display: inline-block;
+ text-align: right;
+ width: 6em;
+ margin-right: .5em;
+ }
+ iron-selector {
+ display: inline-flex;
+ }
+ iron-selector > button {
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-left: none;
+ padding: .25em 0;
+ cursor: pointer;
+ width: 3em;
+ text-align: center;
+ }
+ iron-selector > button:first-of-type {
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+ border-left: 1px solid #ddd;
+ }
+ iron-selector > button:last-of-type {
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ }
+ iron-selector > button.iron-selected {
+ background-color: #ddd;
+ }
+ .draftsContainer h3 {
+ margin-top: .25em;
+ }
+ .actionsContainer {
+ display: flex;
+ }
+ .action:link,
+ .action:visited {
+ color: #00e;
+ }
+ .danger {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ </style>
+ <template>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsURL(changeNum)]]"
+ last-response="{{_drafts}}"></gr-ajax>
+ <button id="trigger" on-tap="_showPopupTapHandler">Reply</button>
+ <iron-dropdown id="dropdown"
+ vertical-align="top"
+ vertical-offset="25"
+ horizontal-align="right">
+ <div class="dropdown-content">
+ <section class="textareaContainer">
+ <iron-autogrow-textarea
+ id="textarea"
+ class="message"
+ placeholder="Say something..."
+ disabled="{{disabled}}"
+ rows="1"
+ bind-value="{{_draft}}"></iron-autogrow-textarea>
+ </section>
+ <section>
+ <template is="dom-repeat"
+ items="[[_computeLabelArray(permittedLabels)]]" as="label">
+ <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">
+ <button data-value$="[[value]]">[[value]]</button>
+ </template>
+ </iron-selector>
+ </template>
+ </section>
+ <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
+ <h3>[[_computeDraftsTitle(_drafts)]]</h3>
+ <gr-comment-list
+ comments="[[_drafts]]"
+ change-num="[[changeNum]]"
+ patch-num="[[patchNum]]"></gr-comment-list>
+ </section>
+ <section class="actionsContainer">
+ <a class="action send" href="#" on-tap="_sendTapHandler">Send</a>
+ <div class="danger">
+ <a class="action cancel" href="#" on-tap="_cancelTapHandler">Cancel</a>
+ </div>
+ </section>
+ </div>
+ </iron-dropdown>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-reply-dropdown',
+
+ /**
+ * Fired when a reply is successfully sent.
+ *
+ * @event send
+ */
+
+ properties: {
+ changeNum: String,
+ patchNum: String,
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ },
+ labels: Object,
+ permittedLabels: Object,
+
+ _account: Object,
+ _drafts: Object,
+ _xhrPromise: Object, // Used for testing.
+ _draft: String,
+ },
+
+ get opened() {
+ return this.$.dropdown.opened;
+ },
+
+ ready: function() {
+ app.accountReady.then(function() {
+ this._account = app.account;
+ }.bind(this));
+ },
+
+ open: function() {
+ this.$.draftsXHR.generateRequest();
+ this.$.dropdown.open();
+ this.async(function() {
+ this.$.textarea.textarea.focus();
+ }.bind(this));
+ },
+
+ close: function() {
+ this._drafts = null;
+ this.$.dropdown.close();
+ },
+
+ _computeDraftsURL: function(changeNum) {
+ return '/changes/' + changeNum + '/drafts';
+ },
+
+ _computeHideDraftList: function(drafts) {
+ return Object.keys(drafts || {}).length == 0;
+ },
+
+ _computeDraftsTitle: function(drafts) {
+ var total = 0;
+ for (var file in drafts) {
+ total += drafts[file].length;
+ }
+ if (total == 0) { return ''; }
+ if (total == 1) { return '1 Draft'; }
+ if (total > 1) { return total + ' Drafts'; };
+ },
+
+ _computeLabelArray: function(labelsObj) {
+ return Object.keys(labelsObj).sort();
+ },
+
+ _computeIndexOfLabelValue: function(
+ labels, permittedLabels, labelName, account) {
+ var labelValue = labels[labelName].default_value;
+
+ // Is there an existing vote for the current user? If so, use that.
+ 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;
+ }
+ }
+ }
+
+ for (var i = 0; i < permittedLabels[labelName].length; i++) {
+ var val = parseInt(permittedLabels[labelName][i], 10);
+ if (val == labelValue) {
+ return i;
+ }
+ }
+ return null;
+ },
+
+ _computePermittedLabelValues: function(permittedLabels, label) {
+ return permittedLabels[label];
+ },
+
+ _showPopupTapHandler: function(e) {
+ e.preventDefault();
+ this.open();
+ },
+
+ _cancelTapHandler: function(e) {
+ e.preventDefault();
+ this.$.dropdown.close();
+ },
+
+ _sendTapHandler: function(e) {
+ e.preventDefault();
+ var obj = {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {},
+ };
+ for (var label in this.permittedLabels) {
+ var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+ var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
+ selectedVal = parseInt(selectedVal, 10);
+ obj.labels[label] = selectedVal;
+ }
+ if (this._draft != null) {
+ obj.message = this._draft;
+ }
+ this.disabled = true;
+ this._send(obj).then(function(req) {
+ this.fire('send', {bubbles: false});
+ this._draft = '';
+ this.disabled = false;
+ this.$.dropdown.close();
+ }.bind(this)).catch(function(err) {
+ alert('Oops. Something went wrong. Check the console and bug the ' +
+ 'PolyGerrit team for assistance.');
+ throw err;
+ }.bind(this));
+ },
+
+ _send: function(payload) {
+ var xhr = document.createElement('gr-request');
+ this._xhrPromise = xhr.send({
+ method: 'POST',
+ url: Changes.baseURL(this.changeNum, this.patchNum) + '/review',
+ body: payload,
+ });
+
+ return this._xhrPromise;
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-request.html b/polygerrit-ui/app/elements/gr-request.html
new file mode 100644
index 0000000..4e161445a
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-request.html
@@ -0,0 +1,47 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-ajax/iron-request.html">
+
+<dom-module id="gr-request">
+ <template>
+ <iron-request id="xhr"></iron-request>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-request',
+
+ hostAttributes: {
+ hidden: true
+ },
+
+ send: function(options) {
+ options.headers = options.headers || {};
+ options.headers['content-type'] =
+ options.headers['content-type'] || 'application/json';
+ options.headers['x-gerrit-auth'] = options.headers['x-gerrit-auth'] ||
+ util.getCookie('XSRF_TOKEN');
+ options.jsonPrefix = options.jsonPrefix || ')]}\'';
+ return this.$.xhr.send(options);
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-search-bar.html b/polygerrit-ui/app/elements/gr-search-bar.html
new file mode 100644
index 0000000..d9ad1a3
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-search-bar.html
@@ -0,0 +1,93 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../bower_components/iron-input/iron-input.html">
+
+<dom-module id="gr-search-bar">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ form {
+ display: flex;
+ margin-left: 3em;
+ }
+ input,
+ button {
+ border: 1px solid #aaa;
+ font: inherit;
+ padding: .2em .5em;
+ }
+ input {
+ flex: 1;
+ border-radius: 2px 0 0 2px;
+ }
+ button {
+ background-color: #f1f2f3;
+ border-radius: 0 2px 2px 0;
+ border-left-width: 0;
+ }
+ </style>
+ <form>
+ <input is="iron-input" id="searchInput" bind-value="{{_inputVal}}">
+ <button type="submit" id="searchButton">Search</button>
+ </form>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-search-bar',
+
+ listeners: {
+ 'searchInput.keydown': '_inputKeyDownHandler',
+ 'searchButton.tap': '_preventDefaultAndNavigateToInputVal',
+ },
+
+ properties: {
+ value: {
+ type: String,
+ value: '',
+ notify: true,
+ observer: '_valueChanged',
+ },
+ _inputVal: String,
+ },
+
+ _valueChanged: function(value) {
+ this._inputVal = value;
+ },
+
+ _inputKeyDownHandler: function(e) {
+ if (e.keyCode == 13) {
+ // Enter was pressed.
+ this._preventDefaultAndNavigateToInputVal(e);
+ }
+ },
+
+ _preventDefaultAndNavigateToInputVal: function(e) {
+ e.preventDefault();
+ page.show('/q/' + this._inputVal);
+ },
+
+ });
+
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/favicon.ico b/polygerrit-ui/app/favicon.ico
new file mode 100644
index 0000000..155217b
--- /dev/null
+++ b/polygerrit-ui/app/favicon.ico
Binary files differ
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
new file mode 100644
index 0000000..0e4f281
--- /dev/null
+++ b/polygerrit-ui/app/index.html
@@ -0,0 +1,29 @@
+<!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.
+-->
+
+<html lang="en">
+<meta charset="utf-8">
+<meta name="description" content="Gerrit Code Review">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>PolyGerrit</title>
+
+<link rel="stylesheet" href="/styles/main.css">
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<link rel="import" href="/elements/gr-app.html">
+
+<body unresolved>
+<gr-app id="app"></gr-app>
diff --git a/polygerrit-ui/app/polygerrit_wct_tests.py b/polygerrit-ui/app/polygerrit_wct_tests.py
new file mode 100644
index 0000000..571dcb8
--- /dev/null
+++ b/polygerrit-ui/app/polygerrit_wct_tests.py
@@ -0,0 +1,105 @@
+# 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,
+ },
+ },
+ })
+
+ 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/robots.txt b/polygerrit-ui/app/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/polygerrit-ui/app/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/polygerrit-ui/app/scripts/app.js b/polygerrit-ui/app/scripts/app.js
new file mode 100644
index 0000000..3ceb90d
--- /dev/null
+++ b/polygerrit-ui/app/scripts/app.js
@@ -0,0 +1,91 @@
+// 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.
+
+'use strict';
+
+// 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');
+
+window.addEventListener('WebComponentsReady', function() {
+ // Middleware
+ page(function(ctx, next) {
+ document.body.scrollTop = 0;
+ next();
+ });
+
+ function loadUser(ctx, next) {
+ app.accountReady.then(function() {
+ next();
+ });
+ }
+
+ // Routes.
+ page('/', loadUser, function() {
+ if (app.loggedIn) {
+ page.redirect('/dashboard/self');
+ } else {
+ page.redirect('/q/status:open');
+ }
+ });
+
+ page('/dashboard/(.*)', loadUser, function(data) {
+ if (app.loggedIn) {
+ app.route = 'gr-dashboard-view';
+ app.params = data.params;
+ } else {
+ page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
+ }
+ });
+
+ function queryHandler(data) {
+ app.route = 'gr-change-list-view';
+ app.params = data.params;
+ }
+
+ page('/q/:query,:offset', queryHandler);
+ page('/q/:query', queryHandler);
+
+ page(/^\/(\d+)\/?/, function(ctx) {
+ page.redirect('/c/' + ctx.params[0]);
+ });
+
+ page('/c/:changeNum', function(data) {
+ app.route = 'gr-change-view';
+ app.params = data.params;
+ });
+
+ page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
+ app.route = 'gr-diff-view';
+ var params = {
+ changeNum: ctx.params[0],
+ basePatchNum: ctx.params[2],
+ patchNum: ctx.params[4],
+ path: ctx.params[5]
+ };
+ // Don't allow diffing the same patch number against itself because WHY?
+ if (params.basePatchNum == params.patchNum) {
+ page.redirect('/c/' + params.changeNum + '/' + params.patchNum + '/' +
+ params.path);
+ return;
+ }
+ if (!params.patchNum) {
+ params.patchNum = params.basePatchNum;
+ delete(params.basePatchNum);
+ }
+ app.params = params;
+ });
+
+ page.start();
+});
diff --git a/polygerrit-ui/app/scripts/changes.js b/polygerrit-ui/app/scripts/changes.js
new file mode 100644
index 0000000..17c5eb0
--- /dev/null
+++ b/polygerrit-ui/app/scripts/changes.js
@@ -0,0 +1,93 @@
+// 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.
+
+'use strict';
+
+var Changes = Changes || {};
+
+Changes.DiffType = {
+ ADDED: 'ADDED',
+ COPIED: 'COPIED',
+ DELETED: 'DELETED',
+ MODIFIED: 'MODIFIED',
+ RENAMED: 'RENAMED',
+ REWRITE: 'REWRITE',
+};
+
+Changes.Status = {
+ NEW: 'NEW',
+ MERGED: 'MERGED',
+ ABANDONED: 'ABANDONED',
+ DRAFT: 'DRAFT',
+};
+
+// Must be kept in sync with the ListChangesOption enum and protobuf.
+Changes.ListChangesOption = {
+ LABELS: 0,
+ DETAILED_LABELS: 8,
+
+ // Return information on the current patch set of the change.
+ CURRENT_REVISION: 1,
+ ALL_REVISIONS: 2,
+
+ // If revisions are included, parse the commit object.
+ CURRENT_COMMIT: 3,
+ ALL_COMMITS: 4,
+
+ // If a patch set is included, include the files of the patch set.
+ CURRENT_FILES: 5,
+ ALL_FILES: 6,
+
+ // If accounts are included, include detailed account info.
+ DETAILED_ACCOUNTS: 7,
+
+ // Include messages associated with the change.
+ MESSAGES: 9,
+
+ // Include allowed actions client could perform.
+ CURRENT_ACTIONS: 10,
+
+ // Set the reviewed boolean for the caller.
+ REVIEWED: 11,
+
+ // Include download commands for the caller.
+ DOWNLOAD_COMMANDS: 13,
+
+ // Include patch set weblinks.
+ WEB_LINKS: 14,
+
+ // Include consistency check results.
+ CHECK: 15,
+
+ // Include allowed change actions client could perform.
+ CHANGE_ACTIONS: 16,
+
+ // Include a copy of commit messages including review footers.
+ COMMIT_FOOTERS: 17,
+
+ // Include push certificate information along with any patch sets.
+ PUSH_CERTIFICATES: 18
+};
+
+Changes.listChangesOptionsToHex = function() {
+ var v = 0;
+ for (var i = 0; i < arguments.length; i++) {
+ v |= 1 << arguments[i];
+ }
+ return v.toString(16);
+};
+
+Changes.baseURL = function(changeNum, patchNum) {
+ return '/changes/' + changeNum + '/revisions/' + patchNum;
+};
diff --git a/polygerrit-ui/app/scripts/fake-app.js b/polygerrit-ui/app/scripts/fake-app.js
new file mode 100644
index 0000000..87e2d04
--- /dev/null
+++ b/polygerrit-ui/app/scripts/fake-app.js
@@ -0,0 +1,28 @@
+// 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.
+
+'use strict';
+
+/**
+ * A stub of the global gr-app element. Use this for testing.
+ */
+var app = {
+ accountReady: {
+ then: function(cb) { cb(); },
+ },
+ configReady: {
+ then: function(cb) { cb(); },
+ },
+ loggedIn: false,
+};
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
new file mode 100644
index 0000000..93a0349
--- /dev/null
+++ b/polygerrit-ui/app/scripts/util.js
@@ -0,0 +1,64 @@
+// 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.
+
+'use strict';
+
+var util = util || {};
+
+util.parseDate = function(dateStr) {
+ // Timestamps are given in UTC and have the format
+ // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
+ // nanoseconds.
+ // Munge the date into an ISO 8061 format and parse that.
+ return new Date(dateStr.replace(' ', 'T') + 'Z');
+};
+
+util.htmlEntityMap = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ '\'': ''',
+ '/': '/'
+};
+
+util.escapeHTML = function(str) {
+ return str.replace(/[&<>"'\/]/g, function(s) {
+ return util.htmlEntityMap[s];
+ });
+};
+
+util.shouldSupressKeyboardShortcut = function(e) {
+ var target = e.detail.keyboardEvent.target;
+ return target.tagName == 'INPUT' ||
+ target.tagName == 'TEXTAREA' ||
+ target.tagName == 'SELECT' ||
+ target.tagName == 'BUTTON' ||
+ target.tagName == 'A';
+};
+
+util.getCookie = function(name) {
+ var key = name + '=';
+ var cookies = document.cookie.split(';');
+ for(var i = 0; i < cookies.length; i++) {
+ var c = cookies[i];
+ while (c.charAt(0) == ' ') {
+ c = c.substring(1);
+ }
+ if (c.indexOf(key) == 0) {
+ return c.substring(key.length, c.length);
+ }
+ }
+ return '';
+};
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
new file mode 100644
index 0000000..b25682a
--- /dev/null
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -0,0 +1,27 @@
+<!--
+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.
+-->
+<style is="custom-style">
+:root {
+ --primary-color: #fff;
+ --primary-text-color: #000;
+ --search-border-color: #ddd;
+ --secondary-color: #f1f2f3;
+ --default-text-color: #000;
+ --view-background-color: #fff;
+ --default-horizontal-margin: 1.25rem;
+ --max-constrained-width: 980px;
+}
+</style>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
new file mode 100644
index 0000000..9b44d31
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -0,0 +1,63 @@
+<!--
+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.
+-->
+<dom-module id="gr-change-list-styles">
+ <template>
+ <style>
+ .keyboard {
+ width: 2em;
+ }
+ .subject {
+ flex-grow: 1;
+ flex-shrink: 1;
+ word-break: break-word;
+ }
+ .status {
+ width: 9em;
+ }
+ .owner {
+ width: 15em;
+ }
+ .project,
+ .branch {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ .project {
+ width: 10em;
+ }
+ .branch {
+ width: 7em;
+ }
+ .updated {
+ width: 6em;
+ text-align: right;
+ }
+ .size {
+ width: 9em;
+ text-align: right;
+ }
+ .codeReview {
+ width: 2.6em;
+ text-align: center;
+ }
+ .verified {
+ width: 2em;
+ text-align: center;
+ }
+ </style>
+ </template>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
new file mode 100644
index 0000000..961fbe9
--- /dev/null
+++ b/polygerrit-ui/app/styles/main.css
@@ -0,0 +1,50 @@
+/*
+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.
+*/
+
+@import "//fonts.googleapis.com/css?family=Open+Sans:400,700";
+@import "//fonts.googleapis.com/css?family=Source+Code+Pro";
+
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+html,
+body {
+ height: 100%;
+ transition: none; /* Override the default Polymer fade-in. */
+}
+body {
+ font: 11px 'Open Sans', sans-serif;
+}
+
+@media only screen and (min-width: 1240px) {
+ body {
+ font-size: 12px;
+ }
+}
+@media only screen and (min-width: 1340px) {
+ body {
+ font-size: 13px;
+ }
+}
+@media only screen and (min-width: 1450px) {
+ body {
+ font-size: 14px;
+ }
+}
diff --git a/polygerrit-ui/app/test/gr-account-dropdown-test.html b/polygerrit-ui/app/test/gr-account-dropdown-test.html
new file mode 100644
index 0000000..c7be9ba
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-account-dropdown-test.html
@@ -0,0 +1,48 @@
+<!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-account-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="../elements/gr-account-dropdown.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-account-dropdown></gr-account-dropdown>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-account-dropdown tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('tap on trigger opens menu', function() {
+ assert.isFalse(element.$.dropdown.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.$.dropdown.opened);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-list-item-test.html b/polygerrit-ui/app/test/gr-change-list-item-test.html
new file mode 100644
index 0000000..bca44a9
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-list-item-test.html
@@ -0,0 +1,98 @@
+<!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-change-list-item</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.js"></script>
+
+<link rel="import" href="../elements/gr-change-list-item.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list-item></gr-change-list-item>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list-item tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('computed fields', function() {
+ assert.equal(element._computeChangeStatusString({mergeable: true}), '');
+ assert.equal(element._computeChangeStatusString({mergeable: false}),
+ 'Merge Conflict');
+ assert.equal(element._computeChangeStatusString({status: 'NEW'}), '');
+ assert.equal(element._computeChangeStatusString({status: 'MERGED'}),
+ 'Merged');
+ assert.equal(element._computeChangeStatusString({status: 'ABANDONED'}),
+ 'Abandoned');
+ assert.equal(element._computeChangeStatusString({status: 'DRAFT'}),
+ 'Draft');
+
+ assert.equal(element._computeCodeReviewClass(), 'cell codeReview');
+ assert.equal(element._computeCodeReviewClass({}), 'cell codeReview');
+ assert.equal(element._computeCodeReviewClass({approved: true, value: 1}),
+ 'cell codeReview u-green u-monospace');
+ assert.equal(element._computeCodeReviewClass({value: 1}),
+ 'cell codeReview u-green u-monospace');
+ assert.equal(element._computeCodeReviewClass({value: -1}),
+ 'cell codeReview u-monospace u-red');
+
+ assert.equal(element._computeCodeReviewLabel(), '');
+ assert.equal(element._computeCodeReviewLabel({}), '');
+ assert.equal(element._computeCodeReviewLabel({approved: true, value: 1}),
+ '✓');
+ assert.equal(element._computeCodeReviewLabel({value: 1}), '+1');
+ assert.equal(element._computeCodeReviewLabel({value: -1}), '-1');
+
+ assert.equal(element._computeVerifiedLabel(), '');
+ assert.equal(element._computeVerifiedLabel({}), '');
+ assert.equal(element._computeVerifiedLabel({approved: true}), '✓');
+
+
+ assert.equal(element._computeOwnerLink('andybons+gerrit@gmail.com'),
+ '/q/owner:andybons%2Bgerrit%40gmail.com+status:open');
+
+ assert.equal(element._computeOwnerTitle(
+ {
+ name: 'Andrew Bonventre',
+ email: 'andybons+gerrit@gmail.com'
+ }),
+ 'Andrew Bonventre <andybons+gerrit@gmail.com>');
+
+ // TODO(andybons): _computeProjectURL once it's not a constant.
+
+ assert.equal(element._computeProjectBranchURL(
+ 'combustible-stuff', 'lemons'),
+ '/q/status:open+project:combustible-stuff+branch:lemons');
+
+ element.change = { _number: 42 };
+ assert.equal(element.changeURL, '/c/42/');
+ element.change = { _number: 43 };
+ assert.equal(element.changeURL, '/c/43/');
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-list-test.html b/polygerrit-ui/app/test/gr-change-list-test.html
new file mode 100644
index 0000000..d713368
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-list-test.html
@@ -0,0 +1,148 @@
+<!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-change-list</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/page/page.js"></script>
+<script src="../scripts/fake-app.js"></script>
+<script src="../scripts/changes.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="../elements/gr-change-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-list></gr-change-list>
+ </template>
+</test-fixture>
+
+<test-fixture id="grouped">
+ <template>
+ <gr-change-list></gr-change-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-list basic tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('keyboard shortcuts', function() {
+ element.changes = [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ];
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 3);
+ assert.equal(element.selectedIndex, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+
+ var showStub = sinon.stub(page, 'show');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/2/'),
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // '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'
+ assert.equal(element.selectedIndex, 0);
+
+ showStub.restore();
+ });
+
+ });
+
+ suite('gr-change-list groups', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('keyboard shortcuts', function() {
+ element.groups = [
+ [
+ {_number: 0},
+ {_number: 1},
+ {_number: 2},
+ ],
+ [
+ {_number: 3},
+ {_number: 4},
+ {_number: 5},
+ ],
+ [
+ {_number: 6},
+ {_number: 7},
+ {_number: 8},
+ ]
+ ];
+ element.groupTitles = ['Group 1', 'Group 2', 'Group 3'];
+ flushAsynchronousOperations();
+ var elementItems = Polymer.dom(element.root).querySelectorAll(
+ 'gr-change-list-item');
+ assert.equal(elementItems.length, 9);
+ assert.equal(element.selectedIndex, 0);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+
+ var showStub = sinon.stub(page, 'show');
+ assert.equal(element.selectedIndex, 2);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/2/'),
+ 'Should navigate to /c/2/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k'
+ assert.equal(element.selectedIndex, 1);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/1/'),
+ 'Should navigate to /c/1/');
+
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j'
+ assert.equal(element.selectedIndex, 4);
+ MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
+ assert(showStub.lastCall.calledWithExactly('/c/4/'),
+ 'Should navigate to /c/4/');
+ showStub.restore();
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-change-view-test.html b/polygerrit-ui/app/test/gr-change-view-test.html
new file mode 100644
index 0000000..4cf1d4c
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-change-view-test.html
@@ -0,0 +1,61 @@
+<!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-change-view</title>
+
+<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/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-change-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-view></gr-change-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-view tests', function() {
+ var element;
+
+ setup(function() {
+ 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 dropdownEl = element.$.replyDropdown;
+ assert.isTrue(dropdownEl.opened);
+ dropdownEl.close();
+ assert.isFalse(dropdownEl.opened);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-date-formatter-test.html b/polygerrit-ui/app/test/gr-date-formatter-test.html
new file mode 100644
index 0000000..dca3e9d
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-date-formatter-test.html
@@ -0,0 +1,81 @@
+<!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-date-formatter</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="../elements/gr-date-formatter.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-date-formatter tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('date is parsed correctly', function() {
+ assert.notOk((new Date('foo')).valueOf());
+ var d = element._parseDateStr(element.getAttribute('date-str'));
+ assert.isAbove(d.valueOf(), 0);
+ });
+
+ function normalizedDate(dateStr) {
+ var d = new Date(dateStr);
+ d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
+ return d;
+ }
+
+ function testDates(nowStr, dateStr, expected) {
+ var now = normalizedDate(nowStr);
+ var t = normalizedDate(dateStr);
+ assert.equal(element._dateStr(t, now), expected);
+ }
+
+ test('dates strings are correct', function() {
+ // Within 24 hours on same day.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-07-29T15:34:00.000Z',
+ '3:34 PM');
+
+ // Within 24 hours on different days.
+ testDates('2015-07-29T03:34:00.000Z',
+ '2015-07-28T20:25:00.000Z',
+ 'Jul 28');
+
+ // More than 24 hours. Less than six months.
+ testDates('2015-07-29T20:34:00.000Z',
+ '2015-06-15T03:25:00.000Z',
+ 'Jun 15');
+
+ // More than six months.
+ testDates('2015-09-15T20:34:00.000Z',
+ '2015-01-15T03:25:00.000Z',
+ 'Jan 15, 2015');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-comment-test.html b/polygerrit-ui/app/test/gr-diff-comment-test.html
new file mode 100644
index 0000000..2e7f9bf
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-comment-test.html
@@ -0,0 +1,253 @@
+<!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-diff-comment</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/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="../elements/gr-diff-comment.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-comment></gr-diff-comment>
+ </template>
+</test-fixture>
+
+<test-fixture id="draft">
+ <template>
+ <gr-diff-comment draft="true"></gr-diff-comment>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-comment tests', function() {
+ var element;
+ setup(function() {
+ element = fixture('basic');
+ element.comment = {
+ 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',
+ }
+ });
+
+ test('proper event fires on reply', function(done) {
+ element.addEventListener('gr-diff-comment-reply', function(e) {
+ done();
+ });
+ MockInteractions.tap(element.$$('.reply'));
+ });
+
+ test('proper event fires on done', function(done) {
+ element.addEventListener('gr-diff-comment-done', function(e) {
+ done();
+ });
+ MockInteractions.tap(element.$$('.done'));
+ });
+
+ test('clicking on date link does not trigger nav', function() {
+ var showStub = sinon.stub(page, 'show');
+ var dateEl = element.$$('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+ var dest = window.location.pathname + '#5';
+ assert(showStub.lastCall.calledWithExactly(dest, null, false),
+ 'Should navigate to ' + dest + ' without triggering nav');
+ showStub.restore();
+ });
+ });
+
+ suite('gr-diff-comment draft tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('draft');
+ element.changeNum = 42;
+ element.patchNum = 1;
+ element.comment = {
+ __draft: true,
+ __draftID: 'temp_draft_id',
+ path: '/path/to/file',
+ line: 5,
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'PUT',
+ '/changes/42/revisions/1/drafts',
+ [
+ 201,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "created!"' +
+ '}'
+ ]
+ );
+
+ server.respondWith(
+ 'PUT',
+ /\/changes\/42\/revisions\/1\/drafts\/.+/,
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n{' +
+ '"id": "baf0414d_40572e03",' +
+ '"path": "/path/to/file",' +
+ '"line": 5,' +
+ '"updated": "2015-12-08 21:52:36.177000000",' +
+ '"message": "saved!"' +
+ '}'
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') != 'none';
+ }
+
+ test('button visibility states', function() {
+ element.draft = true;
+ assert.isTrue(isVisible(element.$$('.edit')), 'edit is visible');
+ assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
+ assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+ assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
+ assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
+
+ element.editing = true;
+ assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible')
+ assert.isTrue(isVisible(element.$$('.discard')), 'discard is visible');
+ assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
+ assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
+ assert.isFalse(isVisible(element.$$('.reply')), 'reply is not visible');
+ assert.isFalse(isVisible(element.$$('.done')), 'done is not visible');
+
+ element.draft = false,
+ element.editing = false;
+ assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible')
+ assert.isFalse(isVisible(element.$$('.discard')),
+ 'discard is not visible');
+ assert.isFalse(isVisible(element.$$('.save')), 'save is not visible');
+ assert.isFalse(isVisible(element.$$('.cancel')), 'cancel is not visible');
+ assert.isTrue(isVisible(element.$$('.reply')), 'edit is visible');
+ assert.isTrue(isVisible(element.$$('.done')), 'edit is visible');
+
+ element.draft = true;
+ });
+
+ test('draft creation/cancelation', function(done) {
+ assert.isFalse(element.editing);
+ MockInteractions.tap(element.$$('.edit'));
+ assert.isTrue(element.editing);
+
+ element._editDraft = '';
+ // Save should be disabled on an empty message.
+ var disabled = element.$$('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+ element._editDraft == ' ';
+ disabled = element.$$('.save').hasAttribute('disabled');
+ assert.isTrue(disabled, 'save button should be disabled.');
+
+ var numDiscardEvents = 0;
+ element.addEventListener('gr-diff-comment-discard', function(e) {
+ numDiscardEvents++;
+ if (numDiscardEvents == 2) {
+ done();
+ }
+ });
+ MockInteractions.tap(element.$$('.cancel'));
+ MockInteractions.tap(element.$$('.discard'));
+ });
+
+ test('draft saving/editing', function(done) {
+ element.draft = true;
+ MockInteractions.tap(element.$$('.edit'));
+ element._editDraft = 'good news, everyone!';
+ MockInteractions.tap(element.$$('.save'));
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when creating draft.');
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done creating draft.');
+ assert.equal(req.status, 201);
+ assert.equal(req.url, '/changes/42/revisions/1/drafts');
+ assert.equal(req.response.message, 'created!');
+ assert.isFalse(element.editing);
+ }).then(function() {
+ MockInteractions.tap(element.$$('.edit'));
+ element._editDraft = 'You’ll be delivering a package to Chapek 9, a ' +
+ 'world where humans are killed on sight.';
+ MockInteractions.tap(element.$$('.save'));
+ assert.isTrue(element.disabled,
+ 'Element should be disabled when updating draft.');
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done updating draft.');
+ assert.equal(req.status, 200);
+ assert.equal(req.url,
+ '/changes/42/revisions/1/drafts/baf0414d_40572e03');
+ assert.equal(req.response.message, 'saved!');
+ assert.isFalse(element.editing);
+ done();
+ });
+ });
+ });
+
+ test('proper event fires on done', function(done) {
+ element.addEventListener('gr-diff-comment-done', function(e) {
+ done();
+ });
+ MockInteractions.tap(element.$$('.done'));
+ });
+
+ test('clicking on date link does not trigger nav', function() {
+ var showStub = sinon.stub(page, 'show');
+ var dateEl = element.$$('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+ var dest = window.location.pathname + '#5';
+ assert(showStub.lastCall.calledWithExactly(dest, null, false),
+ 'Should navigate to ' + dest + ' without triggering nav');
+ showStub.restore();
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-comment-thread-test.html b/polygerrit-ui/app/test/gr-diff-comment-thread-test.html
new file mode 100644
index 0000000..7032712
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-comment-thread-test.html
@@ -0,0 +1,116 @@
+<!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-diff-comment-thread</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="../elements/gr-diff-comment-thread.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-comment-thread></gr-diff-comment-thread>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-comment-thread tests', function() {
+ var element;
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('comments are sorted correctly', function() {
+ var comments = [
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ },
+ {
+ id: 'sallys_confession',
+ 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'
+ },
+ {
+ id: 'sallys_mission',
+ message: 'i have to find santa',
+ updated: '2015-12-24 21:00:20.396000000'
+ }
+ ];
+ var results = element._sortedComments(comments);
+ assert.deepEqual(results, [
+ {
+ 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'
+ },
+ {
+ 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: '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'
+ }
+ ]);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-side-test.html b/polygerrit-ui/app/test/gr-diff-side-test.html
new file mode 100644
index 0000000..b54461b
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-side-test.html
@@ -0,0 +1,144 @@
+<!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-diff-side</title>
+
+<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="../elements/gr-diff-side.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-side></gr-diff-side>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-side tests', function() {
+ var element;
+
+ function isVisibleInWindow(el) {
+ var rect = el.getBoundingClientRect();
+ return rect.top >= 0 && rect.left >= 0 &&
+ rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
+ }
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('comments', function() {
+ assert.isFalse(element.$$('.container').classList.contains('canComment'));
+ element.canComment = true;
+ assert.isTrue(element.$$('.container').classList.contains('canComment'));
+ // TODO(andybons): Check for comment creation events firing/not firing
+ // when implemented.
+ });
+
+ test('scroll to line', function() {
+ var content = [];
+ for (var i = 0; i < 300; i++) {
+ content.push({
+ type: 'CODE',
+ content: 'All work and no play makes Jack a dull boy',
+ numLines: 1,
+ lineNum: i + 1,
+ highlight: false,
+ intraline: [],
+ });
+ }
+ element._render(content);
+
+ window.scrollTo(0, 0);
+ element.scrollToLine(-12849);
+ assert.equal(window.scrollY, 0);
+ element.scrollToLine('sup');
+ assert.equal(window.scrollY, 0);
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]');
+ assert.ok(lineEl);
+ element.scrollToLine(150);
+ assert.isAbove(window.scrollY, 0);
+ assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
+ });
+
+ test('intraline highlights', function() {
+ var content = ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>';
+ var html = util.escapeHTML(content);
+ var highlights = [
+ { startIndex: 0, endIndex: 33, },
+ { startIndex: 75 },
+ ];
+ assert.equal(
+ content.slice(highlights[0].startIndex, highlights[0].endIndex),
+ ' <gr-linked-text content="');
+ assert.equal(content.slice(highlights[1].startIndex),
+ '"></gr-linked-text>');
+ var result = element._addIntralineHighlights(content, html, highlights);
+ var expected = element._highlightStartTag +
+ ' <gr-linked-text content="' +
+ element._highlightEndTag +
+ '[[_computeCurrentRevisionMessage(change)]]' +
+ element._highlightStartTag +
+ '"></gr-linked-text>' +
+ element._highlightEndTag;
+ assert.equal(result, expected);
+ });
+
+ test('newlines', function() {
+ element.width = 80;
+ var content = [{
+ type: 'CODE',
+ content: 'A'.repeat(50),
+ numLines: 1,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ element._render(content);
+
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ var contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML, 'A'.repeat(50));
+
+ content = [{
+ type: 'CODE',
+ content: 'A'.repeat(100),
+ numLines: 2,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ element._render(content);
+
+ lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML,
+ 'A'.repeat(80) + element._lineFeedHTML +
+ 'A'.repeat(20) + element._lineFeedHTML);
+
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-test.html b/polygerrit-ui/app/test/gr-diff-test.html
new file mode 100644
index 0000000..b3a56d8
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-test.html
@@ -0,0 +1,268 @@
+<!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-diff</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-diff.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff auto></gr-diff>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.path = 'sieve.go';
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ /\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/,
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ change_type: 'MODIFIED',
+ content: [
+ {ab: ['doin some codez and stuffs']},
+ ]
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/comments',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_5f2b8b82',
+ line: 1,
+ message: 'this isn’t quite right',
+ updated: '2015-12-10 02:50:21.627000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2015-12-10 00:08:42.255000000',
+ },
+ ],
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/comments',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f30f3048',
+ line: 17,
+ message: 'What on earth are you thinking, here?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f6b1b1c8',
+ in_reply_to: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: 'a0407443_30dfe8fb',
+ in_reply_to: '001a2067_f30f3048',
+ line: 17,
+ message: '¯\\_(ツ)_/¯',
+ updated: '2015-12-12 18:50:21.627000000',
+ },
+ ],
+ }),
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('comments with parent', function(done) {
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 1);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comments between two patches', function(done) {
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 2);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('intraline normalization', function() {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ var content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ var highlights = [
+ [31, 34], [42, 26]
+ ];
+ var results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ }
+ ]);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a possibility that the',
+ ' // patch number is no longer a part of the URL (say when navigating to',
+ ' // the top-level change info view) and therefore undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ }
+ ]);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-view-test.html b/polygerrit-ui/app/test/gr-diff-view-test.html
new file mode 100644
index 0000000..8af2d50
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-view-test.html
@@ -0,0 +1,115 @@
+<!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-diff-view</title>
+
+<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/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-diff-view.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-view></gr-diff-view>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-view tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ element.$.changeDetailXHR.auto = false;
+ element.$.filesXHR.auto = false;
+ element.$.diff.auto = false;
+ });
+
+ test('keyboard shortcuts', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '10',
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+
+ var showStub = sinon.stub(page, 'show');
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'u'
+ assert(showStub.lastCall.calledWithExactly('/c/42'),
+ 'Should navigate to /c/42');
+
+ pressAndReleaseKeyIdentifierOn(element, '\U+005D'); // ']'
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/wheatley.md'),
+ 'Should navigate to /c/42/10/wheatley.md');
+ element._path = 'wheatley.md';
+
+ pressAndReleaseKeyIdentifierOn(element, '\U+005B'); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/glados.txt'),
+ 'Should navigate to /c/42/10/glados.txt');
+ element._path = 'glados.txt';
+
+ pressAndReleaseKeyIdentifierOn(element, '\U+005B'); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42/10/chell.go'),
+ 'Should navigate to /c/42/10/chell.go');
+ element._path = 'chell.go';
+
+ pressAndReleaseKeyIdentifierOn(element, '\U+005B'); // '['
+ assert(showStub.lastCall.calledWithExactly('/c/42'),
+ 'Should navigate to /c/42');
+
+ showStub.restore();
+
+ // https://github.com/PolymerElements/iron-test-helpers/issues/33
+ function keyboardEventFor(type, keyIdentifier) {
+ var event = new CustomEvent(type, {
+ bubbles: true,
+ cancelable: true
+ });
+
+ event.keyIdentifier = keyIdentifier;
+
+ return event;
+ }
+
+ function keyEventOn(target, type, keyIdentifier) {
+ target.dispatchEvent(keyboardEventFor(type, keyIdentifier));
+ }
+
+ function keyDownOn(target, keyIdentifier) {
+ keyEventOn(target, 'keydown', keyIdentifier);
+ }
+
+ function keyUpOn(target, keyIdentifier) {
+ keyEventOn(target, 'keyup', keyIdentifier);
+ }
+
+ function pressAndReleaseKeyIdentifierOn(target, keyIdentifier) {
+ keyDownOn(target, keyIdentifier);
+ Polymer.Base.async(function() {
+ keyUpOn(target, keyIdentifier);
+ }, 1);
+ }
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-reply-dropdown-test.html b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
new file mode 100644
index 0000000..4752017
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-reply-dropdown</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-reply-dropdown.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-reply-dropdown></gr-reply-dropdown>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-reply-dropdown tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.patchNum = 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
+ }
+ };
+ element.permittedLabels = {
+ 'Code-Review': [
+ '-1',
+ ' 0',
+ '+1'
+ ],
+ Verified: [
+ '-1',
+ ' 0',
+ '+1'
+ ]
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'POST',
+ '/changes/42/revisions/1/review',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ '{' +
+ '"labels": {' +
+ '"Code-Review": -1,' +
+ '"Verified": -1' +
+ '}' +
+ '}'
+ ]
+ );
+
+ // Allow the elements created by dom-repeat to be stamped.
+ flushAsynchronousOperations();
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('open close', function() {
+ assert.isFalse(element.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.opened);
+ element.close();
+ assert.isFalse(element.opened);
+ element.open();
+ assert.isTrue(element.opened);
+ MockInteractions.tap(element.$$('.cancel'));
+ assert.isFalse(element.opened);
+ });
+
+ test('label picker', function(done) {
+ assert.isFalse(element.opened);
+ MockInteractions.tap(element.$.trigger);
+ assert.isTrue(element.opened);
+
+ // Without this, the test fails to register taps on the label buttons.
+ // TODO(andybons): Look into test flakiness with Polymer team to figure
+ // out why exactly this is happening.
+ element.async(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"] > button[data-value="-1"]'));
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Verified"] > button[data-value="-1"]'));
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ element.async(function() {
+ MockInteractions.tap(element.$$('.send'));
+ assert.isTrue(element.disabled);
+
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done sending reply.');
+ assert.isFalse(element.opened);
+ assert.equal(req.status, 200);
+ assert.equal(req.url, '/changes/42/revisions/1/review');
+ var reqObj = JSON.parse(req.xhr.requestBody);
+ assert.deepEqual(reqObj, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': -1,
+ 'Verified': -1
+ },
+ message: 'I wholeheartedly disapprove'
+ });
+ assert.equal(req.response.labels['Code-Review'], -1);
+ assert.equal(req.response.labels['Verified'], -1);
+ assert.isFalse(element.opened);
+ done();
+ });
+ }, 1);
+ }, 1);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-search-bar-test.html b/polygerrit-ui/app/test/gr-search-bar-test.html
new file mode 100644
index 0000000..2063d33
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-search-bar-test.html
@@ -0,0 +1,63 @@
+<!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-search-bar</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="../elements/gr-search-bar.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-search-bar></gr-search-bar>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-search-bar tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('value is propagated to _inputVal', function() {
+ element.value = 'foo';
+ assert.equal(element._inputVal, 'foo');
+ });
+
+ test('tap on search button triggers nav', function(done) {
+ sinon.stub(element, '_preventDefaultAndNavigateToInputVal', function() {
+ element._preventDefaultAndNavigateToInputVal.restore();
+ done();
+ });
+ MockInteractions.tap(element.$.searchButton);
+ });
+
+ test('enter in search input triggers nav', function(done) {
+ sinon.stub(element, '_preventDefaultAndNavigateToInputVal', function() {
+ element._preventDefaultAndNavigateToInputVal.restore();
+ done();
+ });
+ MockInteractions.pressAndReleaseKeyOn(element.$.searchInput, 13);
+ });
+
+ });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
new file mode 100644
index 0000000..42cd24f
--- /dev/null
+++ b/polygerrit-ui/app/test/index.html
@@ -0,0 +1,44 @@
+<!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>Elements Test Runner</title>
+<meta charset="utf-8">
+<script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script>
+ var testFiles = [];
+
+ [ 'gr-account-dropdown-test.html',
+ 'gr-change-list-item-test.html',
+ 'gr-change-list-test.html',
+ 'gr-change-view-test.html',
+ 'gr-date-formatter-test.html',
+ 'gr-diff-comment-test.html',
+ 'gr-diff-comment-thread-test.html',
+ 'gr-diff-side-test.html',
+ 'gr-diff-test.html',
+ 'gr-diff-view-test.html',
+ 'gr-reply-dropdown-test.html',
+ 'gr-search-bar-test.html',
+ ].forEach(function(file) {
+ testFiles.push(file);
+ testFiles.push(file + '?dom=shadow');
+ });
+
+ WCT.loadSuites(testFiles);
+</script>
diff --git a/polygerrit-ui/run-server.sh b/polygerrit-ui/run-server.sh
new file mode 100755
index 0000000..70ee3cb
--- /dev/null
+++ b/polygerrit-ui/run-server.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# 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.
+
+set -eu
+
+while [[ ! -f .buckconfig && "$PWD" != / ]]; do
+ cd ..
+done
+if [[ ! -f .buckconfig ]]; then
+ echo "$(basename "$0"): must be run from a gerrit checkout" 1>&2
+ exit 1
+fi
+
+cd polygerrit-ui
+rm -rf bower_components
+buck build //polygerrit-ui:polygerrit_components
+unzip -q ../buck-out/gen/polygerrit-ui/polygerrit_components/polygerrit_components.bower_components.zip
+exec go run server.go
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
new file mode 100644
index 0000000..5fe6e36
--- /dev/null
+++ b/polygerrit-ui/server.go
@@ -0,0 +1,155 @@
+// 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 main
+
+import (
+ "bufio"
+ "compress/gzip"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+)
+
+var (
+ 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")
+ loggedIn = flag.Bool("logged_in", false, "Return user info as if the user is logged in")
+)
+
+func main() {
+ flag.Parse()
+
+ if *prod {
+ http.Handle("/", http.FileServer(http.Dir("dist")))
+ } else {
+ http.Handle("/bower_components/",
+ http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components"))))
+ http.Handle("/", http.FileServer(http.Dir("app")))
+ }
+
+ http.HandleFunc("/changes/", handleRESTProxy)
+ http.HandleFunc("/accounts/", handleRESTProxy)
+ http.HandleFunc("/config/", handleRESTProxy)
+ http.HandleFunc("/accounts/self/detail", handleAccountDetail)
+ log.Println("Serving on port", *port)
+ log.Fatal(http.ListenAndServe(*port, &server{}))
+}
+
+func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ req := &http.Request{
+ Method: "GET",
+ URL: &url.URL{
+ Scheme: "https",
+ Host: *restHost,
+ Opaque: r.URL.EscapedPath(),
+ RawQuery: r.URL.RawQuery,
+ },
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer res.Body.Close()
+ w.WriteHeader(res.StatusCode)
+ if _, err := io.Copy(w, res.Body); err != nil {
+ log.Println("Error copying response to ResponseWriter:", err)
+ return
+ }
+}
+
+func handleAccountDetail(w http.ResponseWriter, r *http.Request) {
+ if !*loggedIn {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
+ fmt.Fprint(w, accountInfo)
+}
+
+const accountInfo = `)]}'
+{
+ "registered_on": "2015-08-31 21:24:17.614000000",
+ "_account_id": 1021482,
+ "name": "Andrew Bonventre",
+ "email": "andybons@chromium.org",
+ "avatars": [
+ {
+ "url": "https://lh4.googleusercontent.com/-1EovlES413I/AAAAAAAAAAI/AAAAAAAAAAA/GQ5-31ULE1Q/s26-p/photo.jpg",
+ "height": 26
+ }
+ ]
+}`
+
+type gzipResponseWriter struct {
+ io.WriteCloser
+ http.ResponseWriter
+}
+
+func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter {
+ gz := gzip.NewWriter(w)
+ return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w}
+}
+
+func (w gzipResponseWriter) Write(b []byte) (int, error) {
+ return w.WriteCloser.Write(b)
+}
+
+func (w gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ h, ok := w.ResponseWriter.(http.Hijacker)
+ if !ok {
+ return nil, nil, errors.New("gzipResponseWriter: ResponseWriter does not satisfy http.Hijacker interface")
+ }
+ return h.Hijack()
+}
+
+type server struct{}
+
+// Any path prefixes that should resolve to index.html.
+var (
+ fePaths = []string{"/q/", "/c/", "/dashboard/"}
+ issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
+)
+
+func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL)
+ for _, prefix := range fePaths {
+ if strings.HasPrefix(r.URL.Path, prefix) {
+ r.URL.Path = "/"
+ log.Println("Redirecting to /")
+ break
+ } else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil {
+ r.URL.Path = "/"
+ log.Println("Redirecting to /")
+ break
+ }
+ }
+ if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ http.DefaultServeMux.ServeHTTP(w, r)
+ return
+ }
+ w.Header().Set("Content-Encoding", "gzip")
+ gzw := newGzipResponseWriter(w)
+ defer gzw.Close()
+ http.DefaultServeMux.ServeHTTP(gzw, r)
+}
diff --git a/polygerrit-ui/wct.conf.js b/polygerrit-ui/wct.conf.js
new file mode 100644
index 0000000..b6a6251
--- /dev/null
+++ b/polygerrit-ui/wct.conf.js
@@ -0,0 +1,17 @@
+var path = require('path');
+
+var ret = {
+ suites: ['app/test'],
+ webserver: {
+ pathMappings: []
+ }
+};
+
+var mapping = {};
+var rootPath = (__dirname).split(path.sep).slice(-1)[0];
+
+mapping['/components/' + rootPath + '/app/bower_components'] = 'bower_components';
+
+ret.webserver.pathMappings.push(mapping);
+
+module.exports = ret;
diff --git a/tools/build.defs b/tools/build.defs
index 893abba..3ea506c 100644
--- a/tools/build.defs
+++ b/tools/build.defs
@@ -61,15 +61,20 @@
)
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 = [
+ context = ui_deps + context + [
'//gerrit-main:main_bin',
'//gerrit-war:webapp_assets',
- ] + (['//gerrit-gwtui:' + ui] if ui else []) +
- context,
+ ],
docs = docs,
visibility = visibility,
)
diff --git a/tools/default.defs b/tools/default.defs
index 90096b2..191dfe5 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -27,7 +27,6 @@
# Set defaults on java rules:
# - Add AutoValue annotation processing support.
# - Treat source files as UTF-8.
-# - std_out_log_level = info (the default is too spammy)
_buck_java_library = java_library
def java_library(*args, **kwargs):
@@ -37,9 +36,9 @@
_buck_java_test = java_test
def java_test(*args, **kwargs):
_munge_args(kwargs)
- _do_not_spam_std_out(kwargs)
_buck_java_test(*args, **kwargs)
+
# Munge kwargs to set Gerrit-specific defaults.
def _munge_args(kwargs):
_set_auto_value(kwargs)
@@ -57,11 +56,6 @@
extra_args.extend(['-encoding', 'UTF-8'])
-def _do_not_spam_std_out(kwargs):
- level = 'std_out_log_level'
- if level not in kwargs:
- kwargs[level] = 'INFO'
-
def _set_auto_value(kwargs):
apk = 'annotation_processors'
if apk not in kwargs:
@@ -79,6 +73,18 @@
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,
diff --git a/tools/download_file.py b/tools/download_file.py
index 97d982f..bd67b50 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -21,7 +21,7 @@
import shutil
from subprocess import check_call, CalledProcessError
from sys import stderr
-from util import resolve_url
+from util import hash_file, resolve_url
from zipfile import ZipFile, BadZipfile, LargeZipFile
GERRIT_HOME = path.expanduser('~/.gerritcodereview')
@@ -33,17 +33,6 @@
LOCAL_PROPERTIES = 'local.properties'
-def hashfile(p):
- d = sha1()
- with open(p, 'rb') as f:
- while True:
- b = f.read(8192)
- if not b:
- break
- d.update(b)
- return d.hexdigest()
-
-
def safe_mkdirs(d):
if path.isdir(d):
return
@@ -148,7 +137,7 @@
exit(1)
if args.v:
- have = hashfile(cache_ent)
+ have = hash_file(sha1(), cache_ent).hexdigest()
if args.v != have:
print((
'%s:\n' +
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 754c1c8..9fbede3 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -76,7 +76,7 @@
if path.exists(path.join(root, 'src', 'test', 'java')):
testpath = """
<classpathentry kind="src" path="src/test/java"\
- out="buck-out/eclipse/test"/>"""
+ out="eclipse-out/test"/>"""
else:
testpath = ""
print("""\
@@ -85,7 +85,7 @@
<classpathentry 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="buck-out/eclipse/classes"/>
+ <classpathentry kind="output" path="eclipse-out/classes"/>
</classpath>""" % {"testpath": testpath}, file=fd)
def gen_classpath():
@@ -141,12 +141,12 @@
out = None
if s.startswith('lib/'):
- out = 'buck-out/eclipse/lib'
+ out = 'eclipse-out/lib'
elif s.startswith('plugins/'):
if args.plugins:
plugins.add(s)
continue
- out = 'buck-out/eclipse/' + s
+ out = 'eclipse-out/' + s
p = path.join(s, 'java')
if path.exists(p):
@@ -158,7 +158,7 @@
if out:
o = out + '/' + env
elif env == 'test':
- o = 'buck-out/eclipse/test'
+ o = 'eclipse-out/test'
for srctype in ['java', 'resources']:
p = path.join(s, 'src', env, srctype)
@@ -179,10 +179,10 @@
for s in sorted(gwt_src):
p = path.join(ROOT, s, 'src', 'main', 'java')
if path.exists(p):
- classpathentry('lib', p, out='buck-out/eclipse/gwtsrc')
+ classpathentry('lib', p, out='eclipse-out/gwtsrc')
classpathentry('con', JRE)
- classpathentry('output', 'buck-out/eclipse/classes')
+ classpathentry('output', 'eclipse-out/classes')
p = path.join(ROOT, '.classpath')
with open(p, 'w') as fd:
diff --git a/tools/js/BUCK b/tools/js/BUCK
new file mode 100644
index 0000000..ba4f19c
--- /dev/null
+++ b/tools/js/BUCK
@@ -0,0 +1,20 @@
+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/bower2buck.py b/tools/js/bower2buck.py
new file mode 100755
index 0000000..31c6dfe
--- /dev/null
+++ b/tools/js/bower2buck.py
@@ -0,0 +1,214 @@
+#!/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['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/download_bower.py b/tools/js/download_bower.py
new file mode 100644
index 0000000..bcc417c
--- /dev/null
+++ b/tools/js/download_bower.py
@@ -0,0 +1,121 @@
+#!/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 hashlib
+import json
+import optparse
+import os
+import shutil
+import subprocess
+import sys
+
+from tools import util
+
+
+CACHE_DIR = os.path.expanduser(os.path.join(
+ '~', '.gerritcodereview', 'buck-cache', 'downloaded-artifacts'))
+
+
+def bower_cmd(bower, *args):
+ cmd = bower.split(' ')
+ cmd.extend(args)
+ return cmd
+
+
+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)
+ out, err = p.communicate()
+ if p.returncode:
+ sys.stderr.write(err)
+ raise OSError('Command failed: %s' % cmd)
+
+ try:
+ info = json.loads(out)
+ except ValueError:
+ raise ValueError('invalid JSON from %s:\n%s' % (cmd, out))
+ info_name = info.get('name')
+ if info_name != name:
+ raise ValueError('expected package name %s, got: %s' % (name, info_name))
+ return info
+
+
+def ignore_deps(info):
+ # Tell bower to ignore dependencies so we just download this component. This
+ # is just an optimization, since we only pick out the component we need, but
+ # it's important when downloading sizable dependency trees.
+ #
+ # As of 1.6.5 I don't think ignoredDependencies can be specified on the
+ # command line with --config, so we have to create .bowerrc.
+ deps = info.get('dependencies')
+ if deps:
+ with open(os.path.join('.bowerrc'), 'w') as f:
+ json.dump({'ignoredDependencies': deps.keys()}, f)
+
+
+def cache_entry(name, package, version, sha1):
+ if not sha1:
+ sha1 = hashlib.sha1('%s#%s' % (package, version)).hexdigest()
+ return os.path.join(CACHE_DIR, '%s-%s.zip-%s' % (name, version, sha1))
+
+
+def main(args):
+ opts = optparse.OptionParser()
+ opts.add_option('-n', help='short name of component')
+ opts.add_option('-b', help='bower command')
+ opts.add_option('-p', help='full package name of component')
+ 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()
+
+ cwd = os.getcwd()
+ outzip = os.path.join(cwd, opts.o)
+ cached = cache_entry(opts.n, opts.p, opts.v, opts.s)
+
+ if not os.path.exists(cached):
+ info = bower_info(opts.b, opts.n, opts.p, opts.v)
+ ignore_deps(info)
+ subprocess.check_call(
+ bower_cmd(opts.b, '--quiet', 'install', '%s#%s' % (opts.p, opts.v)))
+ bc = os.path.join(cwd, 'bower_components')
+ subprocess.check_call(
+ ['zip', '-q', '--exclude', '.bower.json', '-r', cached, opts.n],
+ cwd=bc)
+
+ if opts.s:
+ path = os.path.join(bc, opts.n)
+ sha1 = util.hash_bower_component(hashlib.sha1(), path).hexdigest()
+ if opts.s != sha1:
+ print((
+ '%s#%s:\n'
+ 'expected %s\n'
+ 'received %s\n') % (opts.p, opts.v, opts.s, sha1), file=sys.stderr)
+ try:
+ os.remove(cached)
+ except OSError as err:
+ if path.exists(cached):
+ print('error removing %s: %s' % (cached, err), file=sys.stderr)
+ return 1
+
+ shutil.copyfile(cached, outzip)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
new file mode 100755
index 0000000..9eb6e34
--- /dev/null
+++ b/tools/js/npm_pack.py
@@ -0,0 +1,74 @@
+#!/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 json
+import os
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+
+def is_bundled(tar):
+ # No entries for directories, so scan for a matching prefix.
+ for entry in tar.getmembers():
+ if entry.name.startswith('package/node_modules/'):
+ return True
+ return False
+
+
+def bundle_dependencies():
+ with open('package.json') as f:
+ package = json.load(f)
+ package['bundledDependencies'] = package['dependencies'].keys()
+ with open('package.json', 'w') as f:
+ json.dump(package, f)
+
+
+def main(args):
+ if len(args) != 2:
+ print('Usage: %s <package> <version>' % sys.argv[0], file=sys.stderr)
+ return 1
+
+ name, version = args
+ filename = '%s-%s.tgz' % (name, version)
+ url = 'http://registry.npmjs.org/%s/-/%s' % (name, filename)
+
+ tmpdir = tempfile.mkdtemp();
+ tgz = os.path.join(tmpdir, filename)
+ atexit.register(lambda: shutil.rmtree(tmpdir))
+
+ subprocess.check_call(['curl', '--proxy-anyauth', '-ksfo', tgz, url])
+ with tarfile.open(tgz, 'r:gz') as tar:
+ if is_bundled(tar):
+ print('%s already has bundled node_modules' % filename)
+ return 1
+ tar.extractall(path=tmpdir)
+
+ oldpwd = os.getcwd()
+ os.chdir(os.path.join(tmpdir, 'package'))
+ bundle_dependencies()
+ subprocess.check_call(['npm', 'install'])
+ subprocess.check_call(['npm', 'pack'])
+ shutil.copy(filename, os.path.join(oldpwd, filename))
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/tools/js/run_npm_binary.py b/tools/js/run_npm_binary.py
new file mode 100644
index 0000000..d76eff5
--- /dev/null
+++ b/tools/js/run_npm_binary.py
@@ -0,0 +1,91 @@
+#!/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
+from distutils import spawn
+import hashlib
+import os
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+from tools import util
+
+
+def extract(path, outdir, bin):
+ if os.path.exists(os.path.join(outdir, bin)):
+ return # Another process finished extracting, ignore.
+
+ # Use a temp directory adjacent to outdir so shutil.move can use the same
+ # device atomically.
+ tmpdir = tempfile.mkdtemp(dir=os.path.dirname(outdir))
+ def cleanup():
+ try:
+ shutil.rmtree(tmpdir)
+ except OSError:
+ pass # Too late now
+ atexit.register(cleanup)
+
+ def extract_one(mem):
+ dest = os.path.join(outdir, mem.name)
+ tar.extract(mem, path=tmpdir)
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except OSError:
+ pass # Either exists, or will fail on the next line.
+ shutil.move(os.path.join(tmpdir, mem.name), dest)
+
+ with tarfile.open(path, 'r:gz') as tar:
+ for mem in tar.getmembers():
+ if mem.name != bin:
+ extract_one(mem)
+ # Extract bin last so other processes only short circuit when extraction is
+ # 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()
+ outdir = '%s-%s' % (path[:-len(suffix)], sha1)
+ rel_bin = os.path.join('package', 'bin', name)
+ bin = os.path.join(outdir, rel_bin)
+ if not os.path.isfile(bin):
+ extract(path, outdir, rel_bin)
+
+ nodejs = spawn.find_executable('nodejs')
+ if nodejs:
+ # Debian installs Node.js as 'nodejs', due to a conflict with another
+ # package.
+ subprocess.check_call([nodejs, bin] + args[1:])
+ else:
+ subprocess.check_call([bin] + args[1:])
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/tools/util.py b/tools/util.py
index ec895dd..96f6047 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
from os import path
REPO_ROOTS = {
@@ -49,3 +50,53 @@
root = root.rstrip('/')
rest = rest.lstrip('/')
return '/'.join([root, rest])
+
+
+def hash_file(hash_obj, path):
+ """Hash the contents of a file.
+
+ Args:
+ hash_obj: an open hash object, e.g. hashlib.sha1().
+ path: path to the file to hash.
+
+ Returns:
+ The passed-in hash_obj.
+ """
+ with open(path, 'rb') as f:
+ while True:
+ b = f.read(8192)
+ if not b:
+ 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