Merge "Switch from pegdown to flexmark-java"
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index ac303e9..b652bda9 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -144,9 +144,12 @@
`gerrit.config` globally (link:config-gerrit.html#receive.maxObjectSizeLimit[
receive.maxObjectSizeLimit]).
+
-The project specific setting in `project.config` is only honored when it
-further reduces the global limit. The setting is not inherited from the
-parent project; it must be explicitly set per project.
+The project specific setting in `project.config` may not set a value higher
+than the global limit (if configured). In other words, it is only honored when
+it further reduces the global limit.
++
+The setting is not inherited from the parent project; it must be explicitly
+set per project.
+
Default is zero.
+
@@ -256,7 +259,8 @@
- 'rejectEmptyCommit': Defines whether empty commits should be rejected when a change is merged.
Changes might not seem empty at first but when attempting to merge, rebasing can lead to an empty
-commit. If this option is set to 'true' the merge would fail.
+commit. If this option is set to 'true' the merge would fail. An empty commit is still allowed as
+the initial commit on a branch.
Merge strategy
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index d35772e..2153751 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -16,7 +16,8 @@
with the database while Gerrit is offline, it's not easy to backup the data,
and it's not possible to set up H2 in a load balanced/hotswap configuration.
-If this option interests you, you might want to consider link:install-quick.html[the quick guide].
+If this option interests you, you might want to consider
+link:linux-quickstart.html[the quick guide].
[[createdb_derby]]
=== Apache Derby
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
deleted file mode 100644
index 2503449..0000000
--- a/Documentation/install-quick.txt
+++ /dev/null
@@ -1,234 +0,0 @@
-= Gerrit Code Review - Quick get started guide
-
-****
-This guide was made with the impatient in mind, ready to try out Gerrit on their
-own server but not prepared to make the full installation procedure yet.
-
-Explanation is sparse and you should not use a server installed this way in a
-live setup, this is made with proof of concept activities in mind.
-
-It is presumed you install it on a Unix based server such as any of the Linux
-flavors or BSD.
-
-It's also presumed that you have access to an OpenID enabled email address.
-Examples of OpenID enable email providers are Gmail, Yahoo! Mail and Hotmail.
-It's also possible to register a custom email address with OpenID, but that is
-outside the scope of this quick installation guide. For testing purposes one of
-the above providers should be fine. Please note that network access to the
-OpenID provider you choose is necessary for both you and your Gerrit instance.
-****
-
-
-[[requirements]]
-== Requirements
-
-Most distributions come with Java today. Do you already have Java installed?
-
-----
- $ java -version
- openjdk version "1.8.0_72"
- OpenJDK Runtime Environment (build 1.8.0_72-b15)
- OpenJDK 64-Bit Server VM (build 25.72-b15, mixed mode)
-----
-
-If Java isn't installed, get it:
-
-* JRE, minimum version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
-
-
-[[user]]
-== Create a user to host the Gerrit service
-
-We will run the service as a non-privileged user on your system.
-First create the user and then become the user:
-
-----
- $ sudo adduser gerrit
- $ sudo su gerrit
-----
-
-If you don't have root privileges you could skip this step and run Gerrit
-as your own user as well.
-
-
-[[download]]
-== Download Gerrit
-
-It's time to download the archive that contains the Gerrit web and ssh service.
-
-You can choose from different versions to download from here:
-
-* https://www.gerritcodereview.com/download/index.html[A list of releases available]
-
-This tutorial is based on version 2.2.2, and you can download that from this link
-
-* https://www.gerritcodereview.com/download/gerrit-2.2.2.war[Link to the 2.2.2 war archive]
-
-
-[[initialization]]
-== Initialize the Site
-
-It's time to run the initialization, and with the batch switch enabled, we don't have to answer any questions at all:
-
-----
- gerrit@host:~$ java -jar gerrit.war init --batch -d ~/gerrit_testsite
- Generating SSH host key ... rsa(simple)... done
- Initialized /home/gerrit/gerrit_testsite
- Executing /home/gerrit/gerrit_testsite/bin/gerrit.sh start
- Starting Gerrit Code Review: OK
- gerrit@host:~$
-----
-
-When the init is complete, you can review your settings in the
-file `'$site_path/etc/gerrit.config'`.
-
-Note that initialization also starts the server. If any settings changes are
-made, the server must be restarted before they will take effect.
-
-----
- gerrit@host:~$ ~/gerrit_testsite/bin/gerrit.sh restart
- Stopping Gerrit Code Review: OK
- Starting Gerrit Code Review: OK
- gerrit@host:~$
-----
-
-The server can be also stopped and started by passing the `stop` and `start`
-commands to gerrit.sh.
-
-----
- gerrit@host:~$ ~/gerrit_testsite/bin/gerrit.sh stop
- Stopping Gerrit Code Review: OK
- gerrit@host:~$
- gerrit@host:~$ ~/gerrit_testsite/bin/gerrit.sh start
- Starting Gerrit Code Review: OK
- gerrit@host:~$
-----
-
-include::config-login-register.txt[]
-
-== Project creation
-
-Your base Gerrit server is now running and you have a user that's ready
-to interact with it. You now have two options, either you create a new
-test project to work with or you already have a git with history that
-you would like to import into Gerrit and try out code review on.
-
-=== New project from scratch
-If you choose to create a new repository from scratch, it's easier for
-you to create a project with an initial commit in it. That way first
-time setup between client and server is easier.
-
-This is done via the SSH port:
-
-----
- user@host:~$ ssh -p 29418 user@localhost gerrit create-project demo-project --empty-commit
- user@host:~$
-----
-
-This will create a repository that you can clone to work with.
-
-=== Already existing project
-
-The other alternative is if you already have a git project that you
-want to try out Gerrit on.
-First you have to create the project. This is done via the SSH port:
-
-----
- user@host:~$ ssh -p 29418 user@localhost gerrit create-project demo-project
- user@host:~$
-----
-
-You need to make sure that at least initially your account is granted
-"Create Reference" privileges for the refs/heads/* reference.
-This is done via the web interface in the Admin/Projects/Access page
-that correspond to your project.
-
-After that it's time to upload the previous history to the server:
-
-----
- user@host:~/my-project$ git push ssh://user@localhost:29418/demo-project *:*
- Counting objects: 2011, done.
- Writing objects: 100% (2011/2011), 456293 bytes, done.
- Total 2011 (delta 0), reused 0 (delta 0)
- To ssh://user@localhost:29418/demo-project
- * [new branch] master -> master
- user@host:~/my-project$
-----
-
-This will create a repository that you can clone to work with.
-
-
-== My first change
-
-Download a local clone of the repository and move into it
-
-----
- user@host:~$ git clone ssh://user@localhost:29418/demo-project
- Cloning into demo-project...
- remote: Counting objects: 2, done
- remote: Finding sources: 100% (2/2)
- remote: Total 2 (delta 0), reused 0 (delta 0)
- user@host:~$ cd demo-project
- user@host:~/demo-project$
-----
-
-Install the link:user-changeid.html[Change-Id commitmsg hook]
-
-----
- scp -p -P 29418 user@localhost:hooks/commit-msg $(git rev-parse --git-dir)/hooks/
-----
-
-Then make a change to the repository and upload it as a reviewable change
-in Gerrit.
-
-----
- user@host:~/demo-project$ date > testfile.txt
- user@host:~/demo-project$ git add testfile.txt
- user@host:~/demo-project$ git commit -m "My pretty test commit"
- [master ff643a5] My pretty test commit
- 1 files changed, 1 insertions(+), 0 deletions(-)
- create mode 100644 testfile.txt
- user@host:~/demo-project$
-----
-
-Usually when you push to a remote git, you push to the reference
-`'/refs/heads/branch'`, but when working with Gerrit you have to push to a
-virtual branch representing "code review before submission to branch".
-This virtual name space is known as /refs/for/<branch>
-
-----
- user@host:~/demo-project$ git push origin HEAD:refs/for/master
- Counting objects: 4, done.
- Writing objects: 100% (3/3), 293 bytes, done.
- Total 3 (delta 0), reused 0 (delta 0)
- remote:
- remote: New Changes:
- remote: http://localhost:8080/1
- remote:
- To ssh://user@localhost:29418/demo-project
- * [new branch] HEAD -> refs/for/master
- user@host:~/demo-project$
-----
-
-You should now be able to access your change by browsing to the http URL
-suggested above, http://localhost:8080/1
-
-
-== Quick Installation Complete
-
-This covers the scope of getting Gerrit started and your first change uploaded.
-It doesn't give any clue as to how the review workflow works, please read
-link:http://source.android.com/source/life-of-a-patch[Default Workflow] to
-learn more about the workflow of Gerrit.
-
-To read more on the installation of Gerrit please see link:install.html[the detailed
-installation page].
-
-
-GERRIT
-------
-
-Part of link:index.html[Gerrit Code Review]
-
-SEARCHBOX
----------
diff --git a/Documentation/install.txt b/Documentation/install.txt
index cc19b3f..dbca368 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -5,7 +5,9 @@
To run the Gerrit service, the following requirement must be met on the host:
-* JRE, minimum version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
+* JRE, version 1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
++
+Gerrit is not yet compatible with Java 9 or newer at this time.
By default, Gerrit uses link:note-db.html[NoteDB] as the storage backend. (If
desired, you can _optionally_ use an external database such as MySQL or
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index 2464c3a..686576a 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -16,9 +16,11 @@
Be sure you have:
+<<<<<<< HEAD
. A Unix-based server, including any Linux flavor, MacOS, or Berkeley Software
Distribution (BSD).
-. Java SE Runtime Environment 1.8 (or higher).
+. Java SE Runtime Environment version 1.8. Gerrit is not compatible with Java
+ 9 or newer yet.
== Download Gerrit
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 5fd8be4..310ec7b 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1410,6 +1410,95 @@
Content-Disposition: attachment
----
+[[check]]
+=== Check project consistency
+
+Performs consistency checks on the project.
+
+Which consistency checks should be performed is controlled by the
+link:#check-project-input[CheckProjectInput] entity in the request
+body.
+
+The following consistency checks are supported:
+
+[[auto-closeable-changes-check]]
+--
+* AutoCloseableChangesCheck: Searches for open changes that can be
+ auto-closed because a patch set of the change is already contained in
+ the destination branch or because the destination branch contains a
+ commit with the same Change-Id. Normally Gerrit auto-closes such
+ changes when the corresponding commits are pushed directly to the
+ repository. However if a branch is updated behind Gerrit's back or if
+ auto-closing changes fails (and the push is still successful) change
+ states can get inconsistent (changes that are already part of the
+ destination branch are still open). This consistency check is
+ intended to detect and repair this situation.
+--
+
+To fix any problems that can be fixed automatically set the `fix` field
+in the inputs for the consistency checks to `true`.
+
+This REST endpoint requires the
+link:access-control.html#capability_administrateServer[Administrate Server]
+global capability.
+
+.Request
+----
+ POST /projects/MyProject/check HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "auto_closeable_changes_check": {
+ "fix": true,
+ "branch": "refs/heads/master",
+ "max_commits": 100
+ }
+ }
+----
+
+As response a link:#check-project-result-info[CheckProjectResultInfo]
+entity is returned that results for the consistency checks.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "auto_closeable_changes_check_result": {
+ "auto_closeable_changes": {
+ "refs/heads/master": [
+ {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "insertions": 34,
+ "deletions": 101,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ },
+ "problems": [
+ {
+ "message": "Patch set 1 (2f15e416237ed9b561199f24184f5f5d2708c584) is merged into destination ref refs/heads/master (2f15e416237ed9b561199f24184f5f5d2708c584), but change status is NEW",
+ "status": "FIXED",
+ "outcome": "Marked change as merged"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+----
+
[[branch-endpoints]]
== Branch Endpoints
@@ -2850,6 +2939,52 @@
check. This defaults to `read`. If given, it `ref` must be given too.
|=========================================
+[[auto_closeable_changes_check_input]]
+=== AutoCloseableChangesCheckInput
+The `AutoCloseableChangesCheckInput` entity contains options for running
+the link:#auto-closeable-changes-check[AutoCloseableChangesCheck].
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`fix` |optional|
+Whether auto-closeable changes should be closed automatically.
+|`branch` ||
+The branch for which the link:#auto-closeable-changes-check[
+AutoCloseableChangesCheck] should be performed. The 'refs/heads/'
+prefix for the branch name can be omitted.
+|`skip_commits` |optional|
+Number of commits that should be skipped when walking the commits of
+the branch.
+|`max_commits` |optional|
+Maximum number of commits to walk. If not specified this defaults to
+10,000 commits. 10,000 is also the maximum that can be set.
+Auto-closing changes is an expensive operation and the more commits
+are walked the slower it gets. This is why you should avoid walking too
+many commits.
+|=============================
+
+[[auto_closeable_changes_check_result]]
+=== AutoCloseableChangesCheckResult
+The `AutoCloseableChangesCheckResult` entity contains the results of
+running the link:#auto-closeable-changes-check[AutoCloseableChangesCheck]
+on a project.
+
+[options="header",cols="1,6"]
+|====================================
+|Field Name |Description
+|`auto_closeable_changes`|
+Changes that can be auto-closed as list of
+link:rest-api-changes.html#change-info[ChangeInfo] entities. For each
+returned link:rest-api-changes.html#change-info[ChangeInfo] entity the
+`problems` field is populated that includes details about the detected
+issues. If `fix` in the link:#auto_closeable_changes_check_input[
+AutoCloseableChangesCheckInput] was set to `true`, `status` and
+`outcome` in link:rest-api-changes.html#problem-info[ProblemInfo] are
+populated. If the status says `FIXED` Gerrit was able to auto-close the
+change now.
+|====================================
+
[[ban-input]]
=== BanInput
The `BanInput` entity contains information for banning commits in a
@@ -2907,6 +3042,36 @@
If not set, `HEAD` will be used as base revision.
|=======================
+[[check-project-input]]
+=== CheckProjectInput
+The `CheckProjectInput` entity contains information about which
+consistency checks should be run on a project.
+
+[options="header",cols="1,^2,4"]
+|===========================================
+|Field Name ||Description
+|`auto_closeable_changes_check`|optional|
+Parameters for the link:#auto-closeable-changes-check[
+AutoCloseableChangesCheck] as
+link:rest-api-changes.html#auto_closeable_changes_check_input[
+AutoCloseableChangesCheckInput] entity.
+|===========================================
+
+[[check-project-result-info]]
+=== CheckProjectResultInfo
+The `CheckProjectResultInfo` entity contains results for consistency
+checks that have been run on a project.
+
+[options="header",cols="1,^2,4"]
+|==================================================
+|Field Name ||Description
+|`auto_closeable_changes_check_result`|optional|
+Results for the link:#auto-closeable-changes-check[
+AutoCloseableChangesCheck] as
+link:rest-api-changes.html#auto_closeable_changes_check_result[
+AutoCloseableChangesCheckResult] entity.
+|==================================================
+
[[config-info]]
=== ConfigInfo
The `ConfigInfo` entity contains information about the effective project
@@ -3270,7 +3435,7 @@
|===============================
|Field Name ||Description
|`value` |optional|
-The effective value of the max object size limit as a formatted string. +
+The effective value in bytes of the max object size limit. +
Not set if there is no limit for the object size.
|`configured_value`|optional|
The max object size limit that is configured on the project as a
@@ -3278,7 +3443,8 @@
Not set if there is no limit for the object size configured on project
level.
|`inherited_value` |optional|
-The max object size limit that is inherited as a formatted string. +
+The max object size limit that is inherited from the global config as a
+formatted string. +
Not set if there is no global limit for the object size.
|===============================
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index baf388e..31a32ad 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -421,7 +421,6 @@
$ git push exp
----
-
[[push_replace]]
=== Replace Changes
diff --git a/WORKSPACE b/WORKSPACE
index 82a5d30..3e0fdbc 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -16,7 +16,7 @@
name = "io_bazel_rules_closure",
sha256 = "4dd84dd2bdd6c9f56cb5a475d504ea31d199c34309e202e9379501d01c3067e5",
strip_prefix = "rules_closure-3103a773820b59b76345f94c231cb213e0d404e2",
- url = "https://github.com/bazelbuild/rules_closure/archive/3103a773820b59b76345f94c231cb213e0d404e2.tar.gz",
+ urls = ["https://github.com/bazelbuild/rules_closure/archive/3103a773820b59b76345f94c231cb213e0d404e2.tar.gz"],
)
# File is specific to Polymer and copied from the Closure Github -- should be
@@ -1002,8 +1002,8 @@
maven_jar(
name = "postgresql",
- artifact = "org.postgresql:postgresql:9.4.1211",
- sha1 = "721e3017fab68db9f0b08537ec91b8d757973ca8",
+ artifact = "org.postgresql:postgresql:42.2.4",
+ sha1 = "dff98730c28a4b3a3263f0cf4abb9a3392f815a7",
)
maven_jar(
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 74fcdc2..38e1b60 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
@@ -126,7 +126,25 @@
if (size == 0) {
return Resources.C.notAvailable();
}
- int p = Math.abs(Math.round(delta * 100 / size));
+ int p = Math.abs(saturatedCast(delta * 100 / size));
return p + "%";
}
+
+ /**
+ * Returns the {@code int} nearest in value to {@code value}.
+ *
+ * @param value any {@code long} value
+ * @return the same value cast to {@code int} if it is in the range of the {@code int} type,
+ * {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if it is too
+ * small
+ */
+ private static int saturatedCast(long value) {
+ if (value > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ }
+ if (value < Integer.MIN_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ return (int) value;
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index 0c2f6fa..fe27e9c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -38,6 +38,8 @@
String globalMaxObjectSizeLimit(String globalMaxObjectSizeLimit);
+ String noMaxObjectSizeLimit();
+
String pluginProjectOptionsTitle(String pluginName);
String pluginProjectInheritedValue(String value);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index 6338920..f746365 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -5,8 +5,9 @@
deletedGroup = Deleted Group {0}
deletedReference = Reference {0} was deleted
deletedSection = Section {0} was deleted
-effectiveMaxObjectSizeLimit = effective: {0}
+effectiveMaxObjectSizeLimit = effective: {0} bytes
globalMaxObjectSizeLimit = The global max object size limit is set to {0}. The limit cannot be increased on project level.
+noMaxObjectSizeLimit = No max object size limit is set.
pluginProjectOptionsTitle = {0} Plugin Options
pluginProjectOptionsTitle = {0} Plugin
pluginProjectInheritedValue = inherited: {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 751e951..05a29ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -439,14 +439,15 @@
setSubmitType(result.defaultSubmitType());
setState(result.state());
maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
- if (result.maxObjectSizeLimit().inheritedValue() != null) {
- effectiveMaxObjectSizeLimit.setVisible(true);
+ if (result.maxObjectSizeLimit().value() != null) {
effectiveMaxObjectSizeLimit.setText(
AdminMessages.I.effectiveMaxObjectSizeLimit(result.maxObjectSizeLimit().value()));
- effectiveMaxObjectSizeLimit.setTitle(
- AdminMessages.I.globalMaxObjectSizeLimit(result.maxObjectSizeLimit().inheritedValue()));
+ if (result.maxObjectSizeLimit().inheritedValue() != null) {
+ effectiveMaxObjectSizeLimit.setTitle(
+ AdminMessages.I.globalMaxObjectSizeLimit(result.maxObjectSizeLimit().inheritedValue()));
+ }
} else {
- effectiveMaxObjectSizeLimit.setVisible(false);
+ effectiveMaxObjectSizeLimit.setText(AdminMessages.I.noMaxObjectSizeLimit());
}
saveProject.setEnabled(false);
@@ -512,6 +513,9 @@
textBox.setValue(param.value());
addWidget(g, textBox, param);
}
+ if (textBox.getValue().length() > textBox.getVisibleLength()) {
+ textBox.setVisibleLength(textBox.getValue().length());
+ }
saveEnabler.listenTo(textBox);
return textBox;
}
diff --git a/java/Main.java b/java/Main.java
index f26b6df..11d8234 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -14,6 +14,7 @@
public final class Main {
private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
+ private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
// We don't do any real work here because we need to import
// the archive lookup code and we cannot import a class in
@@ -42,6 +43,9 @@
}
private static void configureFloggerBackend() {
+ System.setProperty(
+ FLOGGER_LOGGING_CONTEXT, "com.google.gerrit.server.logging.LoggingContext#getInstance");
+
if (System.getProperty(FLOGGER_BACKEND_PROPERTY) != null) {
// Flogger backend is already configured
return;
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index f9f95b5..218ee18 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -137,6 +137,10 @@
return events;
}
+ public void assertNoRefUpdatedEvents(String project, String branch) throws Exception {
+ getRefUpdatedEvents(project, branch, 0);
+ }
+
public void assertRefUpdatedEvents(String project, String branch, String... expected)
throws Exception {
ImmutableList<RefUpdatedEvent> events =
diff --git a/java/com/google/gerrit/extensions/api/access/GerritPermission.java b/java/com/google/gerrit/extensions/api/access/GerritPermission.java
index 133de31..02afbdc 100644
--- a/java/com/google/gerrit/extensions/api/access/GerritPermission.java
+++ b/java/com/google/gerrit/extensions/api/access/GerritPermission.java
@@ -18,7 +18,13 @@
/** Gerrit permission for hosts, projects, refs, changes, labels and plugins. */
public interface GerritPermission {
- /** @return readable identifier of this permission for exception message. */
+ /**
+ * A description in the context of an exception message.
+ *
+ * <p>Should be grammatical when used in the construction "not permitted: [description] on
+ * [resource]", although individual {@code PermissionBackend} implementations may vary the
+ * wording.
+ */
String describeForException();
static String describeEnumValue(Enum<?> value) {
diff --git a/java/com/google/gerrit/extensions/api/projects/CheckProjectInput.java b/java/com/google/gerrit/extensions/api/projects/CheckProjectInput.java
new file mode 100644
index 0000000..145b200
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/CheckProjectInput.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+public class CheckProjectInput {
+ public AutoCloseableChangesCheckInput autoCloseableChangesCheck;
+
+ public static class AutoCloseableChangesCheckInput {
+ /** Whether auto-closeable changes should be fixed by setting their status to MERGED. */
+ public Boolean fix;
+
+ /** Branch that should be checked for auto-closeable changes. */
+ public String branch;
+
+ /** Number of commits to skip. */
+ public Integer skipCommits;
+
+ /** Maximum number of commits to walk. */
+ public Integer maxCommits;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/CheckProjectResultInfo.java b/java/com/google/gerrit/extensions/api/projects/CheckProjectResultInfo.java
new file mode 100644
index 0000000..e685122
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/CheckProjectResultInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+import java.util.List;
+
+public class CheckProjectResultInfo {
+ public AutoCloseableChangesCheckResult autoCloseableChangesCheckResult;
+
+ public static class AutoCloseableChangesCheckResult {
+ public List<ChangeInfo> autoCloseableChanges;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index b3dd1f1..b5aff67 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.client.SubmitType;
@@ -58,9 +59,14 @@
}
public static class MaxObjectSizeLimitInfo {
- public String value;
- public String configuredValue;
- public String inheritedValue;
+ /* The effective value. Null if not set. */
+ @Nullable public String value;
+
+ /* The value configured on the project. Null if not set. */
+ @Nullable public String configuredValue;
+
+ /* The value configured globally. Null if not set. */
+ @Nullable public String inheritedValue;
}
public static class ConfigParameterInfo {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 63d40f0..0139b52 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -43,6 +43,8 @@
AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException;
+ CheckProjectResultInfo check(CheckProjectInput in) throws RestApiException;
+
ConfigInfo config() throws RestApiException;
ConfigInfo config(ConfigInput in) throws RestApiException;
@@ -243,6 +245,11 @@
}
@Override
+ public CheckProjectResultInfo check(CheckProjectInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public ConfigInfo config() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
index 9030a1c..5fc8ba6 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
import com.google.common.truth.IterableSubject;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
@@ -81,4 +82,10 @@
ContentEntry contentEntry = actual();
return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
}
+
+ public IntegerSubject numberOfSkippedLines() {
+ isNotNull();
+ ContentEntry contentEntry = actual();
+ return Truth.assertThat(contentEntry.skip).named("number of skipped lines");
+ }
}
diff --git a/java/com/google/gerrit/extensions/events/PrivateStateChangedListener.java b/java/com/google/gerrit/extensions/events/PrivateStateChangedListener.java
index e46ceb8..2da6ec9 100644
--- a/java/com/google/gerrit/extensions/events/PrivateStateChangedListener.java
+++ b/java/com/google/gerrit/extensions/events/PrivateStateChangedListener.java
@@ -15,7 +15,7 @@
package com.google.gerrit.extensions.events;
public interface PrivateStateChangedListener {
- interface Event extends ChangeEvent {}
+ interface Event extends RevisionEvent {}
void onPrivateStateChanged(Event event);
}
diff --git a/java/com/google/gerrit/extensions/events/WorkInProgressStateChangedListener.java b/java/com/google/gerrit/extensions/events/WorkInProgressStateChangedListener.java
index e957421..d0e2bc1 100644
--- a/java/com/google/gerrit/extensions/events/WorkInProgressStateChangedListener.java
+++ b/java/com/google/gerrit/extensions/events/WorkInProgressStateChangedListener.java
@@ -15,7 +15,7 @@
package com.google.gerrit.extensions.events;
public interface WorkInProgressStateChangedListener {
- interface Event extends ChangeEvent {}
+ interface Event extends RevisionEvent {}
void onWorkInProgressStateChanged(Event event);
}
diff --git a/java/com/google/gerrit/extensions/restapi/AuthException.java b/java/com/google/gerrit/extensions/restapi/AuthException.java
index 0b4f459..fe1744b 100644
--- a/java/com/google/gerrit/extensions/restapi/AuthException.java
+++ b/java/com/google/gerrit/extensions/restapi/AuthException.java
@@ -14,10 +14,17 @@
package com.google.gerrit.extensions.restapi;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+import java.util.Optional;
+
/** Caller cannot perform the request operation (HTTP 403 Forbidden). */
public class AuthException extends RestApiException {
private static final long serialVersionUID = 1L;
+ private Optional<String> advice = Optional.empty();
+
/** @param msg message to return to the client. */
public AuthException(String msg) {
super(msg);
@@ -30,4 +37,19 @@
public AuthException(String msg, Throwable cause) {
super(msg, cause);
}
+
+ public void setAdvice(String advice) {
+ checkArgument(!Strings.isNullOrEmpty(advice));
+ this.advice = Optional.of(advice);
+ }
+
+ /**
+ * Advice that the user can follow to acquire authorization to perform the action.
+ *
+ * <p>This may be long-form text with newlines, and may be printed to a terminal, for example in
+ * the message stream in response to a push.
+ */
+ public Optional<String> getAdvice() {
+ return advice;
+ }
}
diff --git a/java/com/google/gerrit/git/testing/PushResultSubject.java b/java/com/google/gerrit/git/testing/PushResultSubject.java
index c5163d1..929e182 100644
--- a/java/com/google/gerrit/git/testing/PushResultSubject.java
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -54,6 +54,13 @@
Truth.assertThat(trimMessages()).isEqualTo(Arrays.stream(expectedLines).collect(joining("\n")));
}
+ public void containsMessages(String... expectedLines) {
+ checkArgument(expectedLines.length > 0, "use hasNoMessages()");
+ isNotNull();
+ Iterable<String> got = Splitter.on("\n").split(trimMessages());
+ Truth.assertThat(got).containsAllIn(expectedLines).inOrder();
+ }
+
private String trimMessages() {
return trimMessages(actual().getMessages());
}
diff --git a/java/com/google/gerrit/httpd/QueryDocumentationFilter.java b/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
index 8b82c00..c41a7b9 100644
--- a/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
+++ b/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
@@ -59,7 +59,7 @@
HttpServletResponse rsp = (HttpServletResponse) response;
try {
List<DocResult> result = searcher.doQuery(request.getParameter("q"));
- RestApiServlet.replyJson(req, rsp, ImmutableListMultimap.of(), result);
+ RestApiServlet.replyJson(req, rsp, false, ImmutableListMultimap.of(), result);
} catch (DocQueryException e) {
logger.atSevere().withCause(e).log("Doc search failed");
rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
diff --git a/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 2870cd0..172321d 100644
--- a/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -56,8 +56,10 @@
import org.kohsuke.args4j.CmdLineException;
public class ParameterParser {
+ public static final String TRACE_PARAMETER = "trace";
+
private static final ImmutableSet<String> RESERVED_KEYS =
- ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields");
+ ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields", TRACE_PARAMETER);
@AutoValue
public abstract static class QueryParams {
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index a5c5a53..31d2a82 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
@@ -48,6 +49,7 @@
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
@@ -107,10 +109,12 @@
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.LockFailureException;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.util.http.CacheHeaders;
import com.google.gerrit.util.http.RequestUtil;
import com.google.gson.ExclusionStrategy;
@@ -131,6 +135,7 @@
import com.google.inject.util.Providers;
import java.io.BufferedReader;
import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.FilterOutputStream;
import java.io.IOException;
@@ -177,6 +182,8 @@
private static final String FORM_TYPE = "application/x-www-form-urlencoded";
+ @VisibleForTesting public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
+
// HTTP 422 Unprocessable Entity.
// TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
private static final int SC_UNPROCESSABLE_ENTITY = 422;
@@ -280,332 +287,345 @@
RestResource rsrc = TopLevelResource.INSTANCE;
ViewData viewData = null;
- try (PerThreadCache ignored = PerThreadCache.create()) {
- if (isCorsPreflight(req)) {
- doCorsPreflight(req, res);
- return;
- }
+ try (TraceContext traceContext = enableTracing(req, res)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
+ logger.atFinest().log(
+ "Received REST request: %s %s (parameters: %s)",
+ req.getMethod(), req.getRequestURI(), getParameterNames(req));
+ logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
- qp = ParameterParser.getQueryParams(req);
- checkCors(req, res, qp.hasXdOverride());
- if (qp.hasXdOverride()) {
- req = applyXdOverrides(req, qp);
- }
- checkUserSession(req);
-
- List<IdString> path = splitPath(req);
- RestCollection<RestResource, RestResource> rc = members.get();
- globals
- .permissionBackend
- .currentUser()
- .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
-
- viewData = new ViewData(null, null);
-
- if (path.isEmpty()) {
- if (rc instanceof NeedsParams) {
- ((NeedsParams) rc).setParams(qp.params());
+ if (isCorsPreflight(req)) {
+ doCorsPreflight(req, res);
+ return;
}
- if (isRead(req)) {
- viewData = new ViewData(null, rc.list());
- } else if (isPost(req)) {
- RestView<RestResource> restCollectionView =
- rc.views().get("gerrit", "POST_ON_COLLECTION./");
- if (restCollectionView != null) {
- viewData = new ViewData(null, restCollectionView);
- } else {
- throw methodNotAllowed(req);
- }
- } else {
- // DELETE on root collections is not supported
- throw methodNotAllowed(req);
+ qp = ParameterParser.getQueryParams(req);
+ checkCors(req, res, qp.hasXdOverride());
+ if (qp.hasXdOverride()) {
+ req = applyXdOverrides(req, qp);
}
- } else {
- IdString id = path.remove(0);
- try {
- rsrc = rc.parse(rsrc, id);
- if (path.isEmpty()) {
- checkPreconditions(req);
- }
- } catch (ResourceNotFoundException e) {
- if (!path.isEmpty()) {
- throw e;
- }
+ checkUserSession(req);
- if (isPost(req) || isPut(req)) {
- RestView<RestResource> createView = rc.views().get("gerrit", "CREATE./");
- if (createView != null) {
- viewData = new ViewData(null, createView);
- status = SC_CREATED;
- path.add(id);
- } else {
- throw e;
- }
- } else if (isDelete(req)) {
- RestView<RestResource> deleteView = rc.views().get("gerrit", "DELETE_MISSING./");
- if (deleteView != null) {
- viewData = new ViewData(null, deleteView);
- status = SC_NO_CONTENT;
- path.add(id);
- } else {
- throw e;
- }
- } else {
- throw e;
- }
- }
- if (viewData.view == null) {
- viewData = view(rc, req.getMethod(), path);
- }
- }
- checkRequiresCapability(viewData);
+ List<IdString> path = splitPath(req);
+ RestCollection<RestResource, RestResource> rc = members.get();
+ globals
+ .permissionBackend
+ .currentUser()
+ .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
- while (viewData.view instanceof RestCollection<?, ?>) {
- @SuppressWarnings("unchecked")
- RestCollection<RestResource, RestResource> c =
- (RestCollection<RestResource, RestResource>) viewData.view;
+ viewData = new ViewData(null, null);
if (path.isEmpty()) {
+ if (rc instanceof NeedsParams) {
+ ((NeedsParams) rc).setParams(qp.params());
+ }
+
if (isRead(req)) {
- viewData = new ViewData(null, c.list());
+ viewData = new ViewData(null, rc.list());
} else if (isPost(req)) {
RestView<RestResource> restCollectionView =
- c.views().get(viewData.pluginName, "POST_ON_COLLECTION./");
- if (restCollectionView != null) {
- viewData = new ViewData(null, restCollectionView);
- } else {
- throw methodNotAllowed(req);
- }
- } else if (isDelete(req)) {
- RestView<RestResource> restCollectionView =
- c.views().get(viewData.pluginName, "DELETE_ON_COLLECTION./");
+ rc.views().get("gerrit", "POST_ON_COLLECTION./");
if (restCollectionView != null) {
viewData = new ViewData(null, restCollectionView);
} else {
throw methodNotAllowed(req);
}
} else {
+ // DELETE on root collections is not supported
throw methodNotAllowed(req);
}
- break;
- }
- IdString id = path.remove(0);
- try {
- rsrc = c.parse(rsrc, id);
- checkPreconditions(req);
- viewData = new ViewData(null, null);
- } catch (ResourceNotFoundException e) {
- if (!path.isEmpty()) {
- throw e;
- }
+ } else {
+ IdString id = path.remove(0);
+ try {
+ rsrc = rc.parse(rsrc, id);
+ if (path.isEmpty()) {
+ checkPreconditions(req);
+ }
+ } catch (ResourceNotFoundException e) {
+ if (!path.isEmpty()) {
+ throw e;
+ }
- if (isPost(req) || isPut(req)) {
- RestView<RestResource> createView = c.views().get("gerrit", "CREATE./");
- if (createView != null) {
- viewData = new ViewData(null, createView);
- status = SC_CREATED;
- path.add(id);
+ if (isPost(req) || isPut(req)) {
+ RestView<RestResource> createView = rc.views().get("gerrit", "CREATE./");
+ if (createView != null) {
+ viewData = new ViewData(null, createView);
+ status = SC_CREATED;
+ path.add(id);
+ } else {
+ throw e;
+ }
+ } else if (isDelete(req)) {
+ RestView<RestResource> deleteView = rc.views().get("gerrit", "DELETE_MISSING./");
+ if (deleteView != null) {
+ viewData = new ViewData(null, deleteView);
+ status = SC_NO_CONTENT;
+ path.add(id);
+ } else {
+ throw e;
+ }
} else {
throw e;
}
- } else if (isDelete(req)) {
- RestView<RestResource> deleteView = c.views().get("gerrit", "DELETE_MISSING./");
- if (deleteView != null) {
- viewData = new ViewData(null, deleteView);
- status = SC_NO_CONTENT;
- path.add(id);
- } else {
- throw e;
- }
- } else {
- throw e;
}
- }
- if (viewData.view == null) {
- viewData = view(c, req.getMethod(), path);
+ if (viewData.view == null) {
+ viewData = view(rc, req.getMethod(), path);
+ }
}
checkRequiresCapability(viewData);
- }
- if (notModified(req, rsrc, viewData.view)) {
- res.sendError(SC_NOT_MODIFIED);
- return;
- }
+ while (viewData.view instanceof RestCollection<?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestCollection<RestResource, RestResource> c =
+ (RestCollection<RestResource, RestResource>) viewData.view;
- if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) {
- return;
- }
-
- if (viewData.view instanceof RestReadView<?> && isRead(req)) {
- result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
- } else if (viewData.view instanceof RestModifyView<?, ?>) {
- @SuppressWarnings("unchecked")
- RestModifyView<RestResource, Object> m =
- (RestModifyView<RestResource, Object>) viewData.view;
-
- Type type = inputType(m);
- inputRequestBody = parseRequest(req, type);
- result = m.apply(rsrc, inputRequestBody);
- if (inputRequestBody instanceof RawInput) {
- try (InputStream is = req.getInputStream()) {
- ServletUtils.consumeRequestBody(is);
+ if (path.isEmpty()) {
+ if (isRead(req)) {
+ viewData = new ViewData(null, c.list());
+ } else if (isPost(req)) {
+ RestView<RestResource> restCollectionView =
+ c.views().get(viewData.pluginName, "POST_ON_COLLECTION./");
+ if (restCollectionView != null) {
+ viewData = new ViewData(null, restCollectionView);
+ } else {
+ throw methodNotAllowed(req);
+ }
+ } else if (isDelete(req)) {
+ RestView<RestResource> restCollectionView =
+ c.views().get(viewData.pluginName, "DELETE_ON_COLLECTION./");
+ if (restCollectionView != null) {
+ viewData = new ViewData(null, restCollectionView);
+ } else {
+ throw methodNotAllowed(req);
+ }
+ } else {
+ throw methodNotAllowed(req);
+ }
+ break;
}
- }
- } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
- @SuppressWarnings("unchecked")
- RestCollectionCreateView<RestResource, RestResource, Object> m =
- (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
+ IdString id = path.remove(0);
+ try {
+ rsrc = c.parse(rsrc, id);
+ checkPreconditions(req);
+ viewData = new ViewData(null, null);
+ } catch (ResourceNotFoundException e) {
+ if (!path.isEmpty()) {
+ throw e;
+ }
- Type type = inputType(m);
- inputRequestBody = parseRequest(req, type);
- result = m.apply(rsrc, path.get(0), inputRequestBody);
- if (inputRequestBody instanceof RawInput) {
- try (InputStream is = req.getInputStream()) {
- ServletUtils.consumeRequestBody(is);
+ if (isPost(req) || isPut(req)) {
+ RestView<RestResource> createView = c.views().get("gerrit", "CREATE./");
+ if (createView != null) {
+ viewData = new ViewData(null, createView);
+ status = SC_CREATED;
+ path.add(id);
+ } else {
+ throw e;
+ }
+ } else if (isDelete(req)) {
+ RestView<RestResource> deleteView = c.views().get("gerrit", "DELETE_MISSING./");
+ if (deleteView != null) {
+ viewData = new ViewData(null, deleteView);
+ status = SC_NO_CONTENT;
+ path.add(id);
+ } else {
+ throw e;
+ }
+ } else {
+ throw e;
+ }
}
- }
- } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
- @SuppressWarnings("unchecked")
- RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
- (RestCollectionDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
-
- Type type = inputType(m);
- inputRequestBody = parseRequest(req, type);
- result = m.apply(rsrc, path.get(0), inputRequestBody);
- if (inputRequestBody instanceof RawInput) {
- try (InputStream is = req.getInputStream()) {
- ServletUtils.consumeRequestBody(is);
+ if (viewData.view == null) {
+ viewData = view(c, req.getMethod(), path);
}
+ checkRequiresCapability(viewData);
}
- } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
- @SuppressWarnings("unchecked")
- RestCollectionModifyView<RestResource, RestResource, Object> m =
- (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
- Type type = inputType(m);
- inputRequestBody = parseRequest(req, type);
- result = m.apply(rsrc, inputRequestBody);
- if (inputRequestBody instanceof RawInput) {
- try (InputStream is = req.getInputStream()) {
- ServletUtils.consumeRequestBody(is);
+ if (notModified(req, rsrc, viewData.view)) {
+ res.sendError(SC_NOT_MODIFIED);
+ return;
+ }
+
+ if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) {
+ return;
+ }
+
+ if (viewData.view instanceof RestReadView<?> && isRead(req)) {
+ result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
+ } else if (viewData.view instanceof RestModifyView<?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestModifyView<RestResource, Object> m =
+ (RestModifyView<RestResource, Object>) viewData.view;
+
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
+ result = m.apply(rsrc, inputRequestBody);
+ if (inputRequestBody instanceof RawInput) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
}
- }
- } else {
- throw new ResourceNotFoundException();
- }
+ } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestCollectionCreateView<RestResource, RestResource, Object> m =
+ (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
- if (result instanceof Response) {
- @SuppressWarnings("rawtypes")
- Response<?> r = (Response) result;
- status = r.statusCode();
- configureCaching(req, res, rsrc, viewData.view, r.caching());
- } else if (result instanceof Response.Redirect) {
- CacheHeaders.setNotCacheable(res);
- res.sendRedirect(((Response.Redirect) result).location());
- return;
- } else if (result instanceof Response.Accepted) {
- CacheHeaders.setNotCacheable(res);
- res.setStatus(SC_ACCEPTED);
- res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) result).location());
- return;
- } else {
- CacheHeaders.setNotCacheable(res);
- }
- res.setStatus(status);
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
+ result = m.apply(rsrc, path.get(0), inputRequestBody);
+ if (inputRequestBody instanceof RawInput) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
+ }
+ } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
+ (RestCollectionDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
- if (result != Response.none()) {
- result = Response.unwrap(result);
- if (result instanceof BinaryResult) {
- responseBytes = replyBinaryResult(req, res, (BinaryResult) result);
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
+ result = m.apply(rsrc, path.get(0), inputRequestBody);
+ if (inputRequestBody instanceof RawInput) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
+ }
+ } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestCollectionModifyView<RestResource, RestResource, Object> m =
+ (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
+
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
+ result = m.apply(rsrc, inputRequestBody);
+ if (inputRequestBody instanceof RawInput) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
+ }
} else {
- responseBytes = replyJson(req, res, qp.config(), result);
+ throw new ResourceNotFoundException();
}
- }
- } catch (MalformedJsonException | JsonParseException e) {
- responseBytes =
- replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
- } catch (BadRequestException e) {
- responseBytes =
- replyError(
- req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
- } catch (AuthException e) {
- responseBytes =
- replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
- } catch (AmbiguousViewException e) {
- responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
- } catch (ResourceNotFoundException e) {
- responseBytes =
- replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
- } catch (MethodNotAllowedException e) {
- responseBytes =
- replyError(
- req,
- res,
- status = SC_METHOD_NOT_ALLOWED,
- messageOr(e, "Method Not Allowed"),
- e.caching(),
- e);
- } catch (ResourceConflictException e) {
- responseBytes =
- replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
- } catch (PreconditionFailedException e) {
- responseBytes =
- replyError(
- req,
- res,
- status = SC_PRECONDITION_FAILED,
- messageOr(e, "Precondition Failed"),
- e.caching(),
- e);
- } catch (UnprocessableEntityException e) {
- responseBytes =
- replyError(
- req,
- res,
- status = SC_UNPROCESSABLE_ENTITY,
- messageOr(e, "Unprocessable Entity"),
- e.caching(),
- e);
- } catch (NotImplementedException e) {
- responseBytes =
- replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
- } catch (UpdateException e) {
- Throwable t = e.getCause();
- if (t instanceof LockFailureException) {
+
+ if (result instanceof Response) {
+ @SuppressWarnings("rawtypes")
+ Response<?> r = (Response) result;
+ status = r.statusCode();
+ configureCaching(req, res, rsrc, viewData.view, r.caching());
+ } else if (result instanceof Response.Redirect) {
+ CacheHeaders.setNotCacheable(res);
+ String location = ((Response.Redirect) result).location();
+ res.sendRedirect(location);
+ logger.atFinest().log("REST call redirected to: %s", location);
+ return;
+ } else if (result instanceof Response.Accepted) {
+ CacheHeaders.setNotCacheable(res);
+ res.setStatus(SC_ACCEPTED);
+ res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) result).location());
+ logger.atFinest().log("REST call succeeded: %d", SC_ACCEPTED);
+ return;
+ } else {
+ CacheHeaders.setNotCacheable(res);
+ }
+ res.setStatus(status);
+ logger.atFinest().log("REST call succeeded: %d", status);
+
+ if (result != Response.none()) {
+ result = Response.unwrap(result);
+ if (result instanceof BinaryResult) {
+ responseBytes = replyBinaryResult(req, res, (BinaryResult) result);
+ } else {
+ responseBytes = replyJson(req, res, false, qp.config(), result);
+ }
+ }
+ } catch (MalformedJsonException | JsonParseException e) {
responseBytes =
- replyError(req, res, status = SC_SERVICE_UNAVAILABLE, messageOr(t, "Lock failure"), e);
- } else {
+ replyError(
+ req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
+ } catch (BadRequestException e) {
+ responseBytes =
+ replyError(
+ req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
+ } catch (AuthException e) {
+ responseBytes =
+ replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
+ } catch (AmbiguousViewException e) {
+ responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
+ } catch (ResourceNotFoundException e) {
+ responseBytes =
+ replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
+ } catch (MethodNotAllowedException e) {
+ responseBytes =
+ replyError(
+ req,
+ res,
+ status = SC_METHOD_NOT_ALLOWED,
+ messageOr(e, "Method Not Allowed"),
+ e.caching(),
+ e);
+ } catch (ResourceConflictException e) {
+ responseBytes =
+ replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
+ } catch (PreconditionFailedException e) {
+ responseBytes =
+ replyError(
+ req,
+ res,
+ status = SC_PRECONDITION_FAILED,
+ messageOr(e, "Precondition Failed"),
+ e.caching(),
+ e);
+ } catch (UnprocessableEntityException e) {
+ responseBytes =
+ replyError(
+ req,
+ res,
+ status = SC_UNPROCESSABLE_ENTITY,
+ messageOr(e, "Unprocessable Entity"),
+ e.caching(),
+ e);
+ } catch (NotImplementedException e) {
+ responseBytes =
+ replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
+ } catch (UpdateException e) {
+ Throwable t = e.getCause();
+ if (t instanceof LockFailureException) {
+ responseBytes =
+ replyError(
+ req, res, status = SC_SERVICE_UNAVAILABLE, messageOr(t, "Lock failure"), e);
+ } else {
+ status = SC_INTERNAL_SERVER_ERROR;
+ responseBytes = handleException(e, req, res);
+ }
+ } catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
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,
+ qp != null ? qp.params() : ImmutableListMultimap.of(),
+ inputRequestBody,
+ status,
+ result,
+ rsrc,
+ viewData == null ? null : viewData.view));
}
- } catch (Exception e) {
- status = SC_INTERNAL_SERVER_ERROR;
- 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,
- qp != null ? qp.params() : ImmutableListMultimap.of(),
- inputRequestBody,
- status,
- result,
- rsrc,
- viewData == null ? null : viewData.view));
}
}
@@ -968,9 +988,22 @@
throw new InstantiationException("Cannot make " + type);
}
+ /**
+ * Sets a JSON reply on the given HTTP servlet response.
+ *
+ * @param req the HTTP servlet request
+ * @param res the HTTP servlet response on which the reply should be set
+ * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
+ * set to {@code true} if the reply may contain sensitive data
+ * @param config config parameters for the JSON formatting
+ * @param result the object that should be formatted as JSON
+ * @return the length of the response
+ * @throws IOException
+ */
public static long replyJson(
@Nullable HttpServletRequest req,
HttpServletResponse res,
+ boolean allowTracing,
ListMultimap<String, String> config,
Object result)
throws IOException {
@@ -985,6 +1018,21 @@
}
w.write('\n');
w.flush();
+
+ if (allowTracing) {
+ logger.atFinest().log(
+ "JSON response body:\n%s",
+ lazy(
+ () -> {
+ try {
+ ByteArrayOutputStream debugOut = new ByteArrayOutputStream();
+ buf.writeTo(debugOut, null);
+ return debugOut.toString(UTF_8.name());
+ } catch (IOException e) {
+ return "<JSON formatting failed>";
+ }
+ }));
+ }
return replyBinaryResult(
req, res, asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8));
}
@@ -1265,6 +1313,22 @@
}
}
+ private List<String> getParameterNames(HttpServletRequest req) {
+ List<String> parameterNames = new ArrayList<>(req.getParameterMap().keySet());
+ Collections.sort(parameterNames);
+ return parameterNames;
+ }
+
+ private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
+ String v = req.getParameter(ParameterParser.TRACE_PARAMETER);
+ if (v != null && (v.isEmpty() || Boolean.parseBoolean(v))) {
+ RequestId traceId = new RequestId();
+ res.setHeader(X_GERRIT_TRACE, traceId.toString());
+ return TraceContext.open().addTag(RequestId.Type.TRACE_ID, traceId);
+ }
+ return TraceContext.DISABLED;
+ }
+
private boolean isDelete(HttpServletRequest req) {
return "DELETE".equals(req.getMethod());
}
@@ -1341,17 +1405,34 @@
configureCaching(req, res, null, null, c);
checkArgument(statusCode >= 400, "non-error status: %s", statusCode);
res.setStatus(statusCode);
- return replyText(req, res, msg);
+ logger.atFinest().log("REST call failed: %d", statusCode);
+ return replyText(req, res, true, msg);
}
- static long replyText(@Nullable HttpServletRequest req, HttpServletResponse res, String text)
+ /**
+ * Sets a text reply on the given HTTP servlet response.
+ *
+ * @param req the HTTP servlet request
+ * @param res the HTTP servlet response on which the reply should be set
+ * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
+ * set to {@code true} if the reply may contain sensitive data
+ * @param text the text reply
+ * @return the length of the response
+ * @throws IOException
+ */
+ static long replyText(
+ @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
throws IOException {
if ((req == null || isRead(req)) && isMaybeHTML(text)) {
- return replyJson(req, res, ImmutableListMultimap.of("pp", "0"), new JsonPrimitive(text));
+ return replyJson(
+ req, res, allowTracing, ImmutableListMultimap.of("pp", "0"), new JsonPrimitive(text));
}
if (!text.endsWith("\n")) {
text += "\n";
}
+ if (allowTracing) {
+ logger.atFinest().log("Text response body:\n%s", text);
+ }
return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
}
diff --git a/java/com/google/gerrit/index/project/ProjectData.java b/java/com/google/gerrit/index/project/ProjectData.java
index 7365660..fb029ac 100644
--- a/java/com/google/gerrit/index/project/ProjectData.java
+++ b/java/com/google/gerrit/index/project/ProjectData.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
+import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.reviewdb.client.Project;
import java.util.ArrayList;
@@ -53,4 +54,11 @@
public ImmutableList<String> getParentNames() {
return tree().stream().skip(1).map(p -> p.getProject().getName()).collect(toImmutableList());
}
+
+ @Override
+ public String toString() {
+ MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
+ h.addValue(project.getName());
+ return h.toString();
+ }
}
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index ca74a52..53c92c9 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -105,6 +105,18 @@
return getChildren().get(i);
}
+ /** Get the number of leaf terms in this predicate. */
+ public int getLeafCount() {
+ int leafCount = 0;
+ for (Predicate<?> childPredicate : getChildren()) {
+ if (childPredicate instanceof IndexPredicate) {
+ leafCount++;
+ }
+ leafCount += childPredicate.getLeafCount();
+ }
+ return leafCount;
+ }
+
/** Create a copy of this predicate, with new children. */
public abstract Predicate<T> copy(Collection<? extends Predicate<T>> children);
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 27ed72f..1a42ebd 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -22,11 +22,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexCollection;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.IndexRewriter;
+import com.google.gerrit.index.IndexedQuery;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.SchemaDefinitions;
import com.google.gerrit.metrics.Description;
@@ -52,6 +54,8 @@
* holding on to a single instance.
*/
public abstract class QueryProcessor<T> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected static class Metrics {
final Timer1<String> executionTime;
@@ -206,6 +210,7 @@
List<Integer> limits = new ArrayList<>(cnt);
List<Predicate<T>> predicates = new ArrayList<>(cnt);
List<DataSource<T>> sources = new ArrayList<>(cnt);
+ int queryCount = 0;
for (Predicate<T> q : queries) {
int limit = getEffectiveLimit(q);
limits.add(limit);
@@ -224,11 +229,17 @@
// max for this user. The only way to see if there are more entities is to
// ask for one more result from the query.
QueryOptions opts = createOptions(indexConfig, start, limit + 1, getRequestedFields());
+ logger.atFine().log("Query options: " + opts);
Predicate<T> pred = rewriter.rewrite(q, opts);
if (enforceVisibility) {
pred = enforceVisibility(pred);
}
predicates.add(pred);
+ logger.atFine().log(
+ "%s index query[%d]:\n%s",
+ schemaDef.getName(),
+ queryCount++,
+ pred instanceof IndexedQuery ? pred.getChild(0) : pred);
@SuppressWarnings("unchecked")
DataSource<T> s = (DataSource<T>) pred;
@@ -243,12 +254,14 @@
out = new ArrayList<>(cnt);
for (int i = 0; i < cnt; i++) {
+ List<T> matchesList = matches.get(i).toList();
+ logger.atFine().log("Matches[%d]:\n%s", i, matchesList);
out.add(
QueryResult.create(
queryStrings != null ? queryStrings.get(i) : null,
predicates.get(i),
limits.get(i),
- matches.get(i).toList()));
+ matchesList));
}
// Only measure successful queries that actually touched the index.
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 3871ced..12f88d5 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -39,6 +39,7 @@
import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import java.io.IOException;
@@ -131,6 +132,7 @@
new ScheduledThreadPoolExecutor(
1,
new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
.setNameFormat(index + " Commit-%d")
.setDaemon(true)
.build());
@@ -171,6 +173,7 @@
Executors.newFixedThreadPool(
1,
new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
.setNameFormat(index + " Write-%d")
.setDaemon(true)
.build()));
diff --git a/java/com/google/gerrit/pgm/util/SiteProgram.java b/java/com/google/gerrit/pgm/util/SiteProgram.java
index 057496f..1338efb 100644
--- a/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -64,6 +64,8 @@
import org.kohsuke.args4j.Option;
public abstract class SiteProgram extends AbstractProgram {
+ private static final String CONNECTION_ERROR = "Cannot connect to SQL database";
+
@Option(
name = "--site-path",
aliases = {"-d"},
@@ -106,14 +108,13 @@
/** @return provides database connectivity and site path. */
protected Injector createDbInjector(boolean enableMetrics, DataSourceProvider.Context context) {
- Path sitePath = getSitePath();
List<Module> modules = new ArrayList<>();
Module sitePathModule =
new AbstractModule() {
@Override
protected void configure() {
- bind(Path.class).annotatedWith(SitePath.class).toInstance(sitePath);
+ bind(Path.class).annotatedWith(SitePath.class).toInstance(getSitePath());
bind(String.class)
.annotatedWith(SecureStoreClassName.class)
.toProvider(Providers.of(getConfiguredSecureStoreClass()));
@@ -198,16 +199,16 @@
Throwable why = first.getCause();
if (why instanceof SQLException) {
- throw die("Cannot connect to SQL database", why);
+ throw die(CONNECTION_ERROR, why);
}
if (why instanceof OrmException
&& why.getCause() != null
&& "Unable to determine driver URL".equals(why.getMessage())) {
why = why.getCause();
if (isCannotCreatePoolException(why)) {
- throw die("Cannot connect to SQL database", why.getCause());
+ throw die(CONNECTION_ERROR, why.getCause());
}
- throw die("Cannot connect to SQL database", why);
+ throw die(CONNECTION_ERROR, why);
}
StringBuilder buf = new StringBuilder();
@@ -259,8 +260,9 @@
for (Binding<DataSourceType> binding : dsTypeBindings) {
Annotation annotation = binding.getKey().getAnnotation();
if (annotation instanceof Named) {
- if (((Named) annotation).value().toLowerCase().contains(dbProductName)) {
- return ((Named) annotation).value();
+ Named named = (Named) annotation;
+ if (named.value().toLowerCase().contains(dbProductName)) {
+ return named.value();
}
}
}
diff --git a/java/com/google/gerrit/proto/ProtoGen.java b/java/com/google/gerrit/proto/ProtoGen.java
index 1c55a05..4a1598b 100644
--- a/java/com/google/gerrit/proto/ProtoGen.java
+++ b/java/com/google/gerrit/proto/ProtoGen.java
@@ -34,12 +34,11 @@
public class ProtoGen {
@Option(
- name = "--output",
- aliases = {"-o"},
- required = true,
- metaVar = "FILE",
- usage = "File to write .proto into"
- )
+ name = "--output",
+ aliases = {"-o"},
+ required = true,
+ metaVar = "FILE",
+ usage = "File to write .proto into")
private File file;
public static void main(String[] argv) throws Exception {
diff --git a/java/com/google/gerrit/reviewdb/client/Change.java b/java/com/google/gerrit/reviewdb/client/Change.java
index 201315e..8d4de05 100644
--- a/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/java/com/google/gerrit/reviewdb/client/Change.java
@@ -253,7 +253,10 @@
}
}
- /** Globally unique identification of this change. */
+ /**
+ * Globally unique identification of this change. This generally takes the form of a string
+ * "Ixxxxxx...", and is stored in the Change-Id footer of a commit.
+ */
public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index fc71ef6..96fcd39 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -40,6 +40,7 @@
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/prettify:server",
"//java/com/google/gerrit/reviewdb:server",
+ "//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/util/git",
"//java/com/google/gerrit/util/cli",
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index ec8620d..fa6cd6c 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -376,6 +376,8 @@
}
public static StarRef readLabels(Repository repo, String refName) throws IOException {
+ logger.atFine().log("Read star labels from %s", refName);
+
Ref ref = repo.exactRef(refName);
if (ref == null) {
return StarRef.MISSING;
@@ -448,6 +450,7 @@
private void updateLabels(
Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
throws IOException, OrmException, InvalidLabelsException {
+ logger.atFine().log("Update star labels in %s (labels=%s)", refName, labels);
try (RevWalk rw = new RevWalk(repo)) {
RefUpdate u = repo.updateRef(refName);
u.setExpectedOldObjectId(oldObjectId);
@@ -485,6 +488,7 @@
return;
}
+ logger.atFine().log("Delete star labels in %s", refName);
RefUpdate u = repo.updateRef(refName);
u.setForceUpdate(true);
u.setExpectedOldObjectId(oldObjectId);
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 76bfcfd..c74f9d4 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -179,6 +179,7 @@
@Override
public Optional<AccountState> load(Account.Id who) throws Exception {
+ logger.atFine().log("Loading account %s", who);
return accounts.get(who);
}
}
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index e56ad72..1854dc1 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
@@ -315,4 +316,11 @@
}
return properties;
}
+
+ @Override
+ public String toString() {
+ MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
+ h.addValue(getAccount().getId());
+ return h.toString();
+ }
}
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index e7aae15..06b51a7 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -144,6 +144,7 @@
@Override
public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
+ logger.atFine().log("Loading group %s by ID", key);
return groupQueryProvider.get().byId(key);
}
}
@@ -158,6 +159,7 @@
@Override
public Optional<InternalGroup> load(String name) throws Exception {
+ logger.atFine().log("Loading group '%s' by name", name);
return groupQueryProvider.get().byName(new AccountGroup.NameKey(name));
}
}
@@ -172,6 +174,7 @@
@Override
public Optional<InternalGroup> load(String uuid) throws Exception {
+ logger.atFine().log("Loading group %s by UUID", uuid);
return groups.getGroup(new AccountGroup.UUID(uuid));
}
}
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index f262a79..5906a06 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -148,6 +148,7 @@
@Override
public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) throws OrmException {
+ logger.atFine().log("Loading groups with member %s", memberId);
return groupQueryProvider
.get()
.byMember(memberId)
@@ -168,6 +169,7 @@
@Override
public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) throws OrmException {
+ logger.atFine().log("Loading parent groups of %s", key);
return groupQueryProvider
.get()
.bySubgroup(key)
@@ -187,6 +189,7 @@
@Override
public ImmutableList<AccountGroup.UUID> load(String key) throws Exception {
+ logger.atFine().log("Loading all external groups");
return groups.getExternalGroups().collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 533b1c0..dc1a873 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -152,6 +152,7 @@
@Override
public AllExternalIds load(ObjectId notesRev) throws Exception {
+ logger.atFine().log("Loading external IDs (revision=%s)", notesRev);
Multimap<Account.Id, ExternalId> extIdsByAccount =
MultimapBuilder.hashKeys().arrayListValues().build();
for (ExternalId extId : externalIdReader.all(notesRev)) {
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 7cd1db0..b049c40 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -615,6 +615,8 @@
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
+ logger.atFine().log("Reading external IDs");
+
noteMap = revision != null ? NoteMap.read(reader, revision) : NoteMap.newEmptyMap();
if (afterReadRevision != null) {
@@ -701,6 +703,8 @@
return false;
}
+ logger.atFine().log("Updating external IDs");
+
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Update external IDs\n");
}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index a49f8c4..463c23e 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -24,6 +24,8 @@
import com.google.gerrit.extensions.api.config.AccessCheckInput;
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.CheckProjectInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
import com.google.gerrit.extensions.api.projects.ChildProjectApi;
import com.google.gerrit.extensions.api.projects.CommitApi;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
@@ -53,6 +55,7 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectJson;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.Check;
import com.google.gerrit.server.restapi.project.CheckAccess;
import com.google.gerrit.server.restapi.project.ChildProjectsCollection;
import com.google.gerrit.server.restapi.project.CommitsCollection;
@@ -115,6 +118,7 @@
private final CommitApiImpl.Factory commitApi;
private final DashboardApiImpl.Factory dashboardApi;
private final CheckAccess checkAccess;
+ private final Check check;
private final Provider<ListDashboards> listDashboards;
private final GetHead getHead;
private final SetHead setHead;
@@ -148,6 +152,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Check check,
Provider<ListDashboards> listDashboards,
GetHead getHead,
SetHead setHead,
@@ -181,6 +186,7 @@
commitApi,
dashboardApi,
checkAccess,
+ check,
listDashboards,
getHead,
setHead,
@@ -216,6 +222,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Check check,
Provider<ListDashboards> listDashboards,
GetHead getHead,
SetHead setHead,
@@ -249,6 +256,7 @@
commitApi,
dashboardApi,
checkAccess,
+ check,
listDashboards,
getHead,
setHead,
@@ -284,6 +292,7 @@
CommitApiImpl.Factory commitApi,
DashboardApiImpl.Factory dashboardApi,
CheckAccess checkAccess,
+ Check check,
Provider<ListDashboards> listDashboards,
GetHead getHead,
SetHead setHead,
@@ -316,6 +325,7 @@
this.createAccessChange = createAccessChange;
this.dashboardApi = dashboardApi;
this.checkAccess = checkAccess;
+ this.check = check;
this.listDashboards = listDashboards;
this.getHead = getHead;
this.setHead = setHead;
@@ -372,15 +382,6 @@
}
@Override
- public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
- try {
- return checkAccess.apply(checkExists(), in);
- } catch (Exception e) {
- throw asRestApiException("Cannot check access rights", e);
- }
- }
-
- @Override
public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
try {
return setAccess.apply(checkExists(), p);
@@ -399,6 +400,24 @@
}
@Override
+ public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
+ try {
+ return checkAccess.apply(checkExists(), in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check access rights", e);
+ }
+ }
+
+ @Override
+ public CheckProjectResultInfo check(CheckProjectInput in) throws RestApiException {
+ try {
+ return check.apply(checkExists(), in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot check project", e);
+ }
+ }
+
+ @Override
public void description(DescriptionInput in) throws RestApiException {
try {
putDescription.apply(checkExists(), in);
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 8d12d32..ede8050 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -351,6 +351,7 @@
@Override
public Optional<Account.Id> load(String username) throws Exception {
+ logger.atFine().log("Loading account for username %s", username);
return externalIds
.get(ExternalId.Key.create(SCHEME_GERRIT, username))
.map(ExternalId::accountId);
@@ -367,6 +368,7 @@
@Override
public Set<AccountGroup.UUID> load(String username) throws Exception {
+ logger.atFine().log("Loading group for member with username %s", username);
final DirContext ctx = helper.open();
try {
return helper.queryForGroups(ctx, username, null);
@@ -386,6 +388,7 @@
@Override
public Boolean load(String groupDn) throws Exception {
+ logger.atFine().log("Loading groupDn %s", groupDn);
final DirContext ctx = helper.open();
try {
Name compositeGroupName = new CompositeName().add(groupDn);
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 13a09a1..4c364c5 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -24,10 +24,10 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.IntKeyCacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.IntKeyCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index ca399e7..2878624 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -20,6 +20,7 @@
import com.google.common.cache.Weigher;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.cache.serialize.JavaCacheSerializer;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.Scopes;
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
index 0239ea2..5635f44 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
@@ -16,6 +16,7 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import java.time.Duration;
/** Configure a persistent cache declared within a {@link CacheModule} instance. */
@@ -30,6 +31,9 @@
PersistentCacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
@Override
+ PersistentCacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
+
+ @Override
PersistentCacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
PersistentCacheBinding<K, V> version(int version);
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheDef.java b/java/com/google/gerrit/server/cache/PersistentCacheDef.java
index 9bd120f..8de685c 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheDef.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheDef.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.cache;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+
public interface PersistentCacheDef<K, V> extends CacheDef<K, V> {
long diskLimit();
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
index 2db9e56..59d66e3 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
@@ -20,6 +20,8 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.JavaCacheSerializer;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
@@ -63,6 +65,11 @@
}
@Override
+ public PersistentCacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration) {
+ return (PersistentCacheBinding<K, V>) super.expireFromMemoryAfterAccess(duration);
+ }
+
+ @Override
public PersistentCacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz) {
return (PersistentCacheBinding<K, V>) super.weigher(clazz);
}
diff --git a/java/com/google/gerrit/server/cache/h2/BUILD b/java/com/google/gerrit/server/cache/h2/BUILD
index fc57a11..2ce756b 100644
--- a/java/com/google/gerrit/server/cache/h2/BUILD
+++ b/java/com/google/gerrit/server/cache/h2/BUILD
@@ -8,6 +8,7 @@
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/lifecycle",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/cache/serialize",
"//lib:guava",
"//lib:h2",
"//lib/flogger:api",
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index 78de67dd..48c0a5b 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -17,9 +17,9 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.PersistentCacheDef;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.inject.TypeLiteral;
import java.time.Duration;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 9abccbc..a7824ea 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -75,11 +76,16 @@
if (cacheDir != null) {
executor =
Executors.newFixedThreadPool(
- 1, new ThreadFactoryBuilder().setNameFormat("DiskCache-Store-%d").build());
+ 1,
+ new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
+ .setNameFormat("DiskCache-Store-%d")
+ .build());
cleanup =
Executors.newScheduledThreadPool(
1,
new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
.setNameFormat("DiskCache-Prune-%d")
.setDaemon(true)
.build());
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 6878e46..606fdf0 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -25,8 +25,8 @@
import com.google.common.hash.BloomFilter;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.PersistentCache;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.inject.TypeLiteral;
import java.io.IOException;
import java.io.InvalidClassException;
@@ -235,6 +235,8 @@
@Override
public ValueHolder<V> load(K key) throws Exception {
+ logger.atFine().log("Loading value for %s from cache", key);
+
if (store.mightContain(key)) {
ValueHolder<V> h = store.getIfPresent(key);
if (h != null) {
diff --git a/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java b/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
index 44e2bb2..591883e 100644
--- a/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
@@ -17,7 +17,7 @@
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import com.google.common.hash.PrimitiveSink;
-import com.google.gerrit.server.cache.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
diff --git a/java/com/google/gerrit/server/cache/serialize/BUILD b/java/com/google/gerrit/server/cache/serialize/BUILD
new file mode 100644
index 0000000..957a153
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/BUILD
@@ -0,0 +1,12 @@
+java_library(
+ name = "serialize",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//lib:guava",
+ "//lib:gwtorm",
+ "//lib:protobuf",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/cache/BooleanCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/BooleanCacheSerializer.java
similarity index 96%
rename from java/com/google/gerrit/server/cache/BooleanCacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/BooleanCacheSerializer.java
index 59fc946..28cd6eb 100644
--- a/java/com/google/gerrit/server/cache/BooleanCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/BooleanCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
diff --git a/java/com/google/gerrit/server/cache/CacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
similarity index 96%
rename from java/com/google/gerrit/server/cache/CacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
index 08deecd..2d41f2c 100644
--- a/java/com/google/gerrit/server/cache/CacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
/**
* Interface for serializing/deserializing a type to/from a persistent cache.
diff --git a/java/com/google/gerrit/server/cache/EnumCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/EnumCacheSerializer.java
similarity index 96%
rename from java/com/google/gerrit/server/cache/EnumCacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/EnumCacheSerializer.java
index c5be783..7856e55 100644
--- a/java/com/google/gerrit/server/cache/EnumCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/EnumCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
diff --git a/java/com/google/gerrit/server/cache/IntKeyCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
similarity index 95%
rename from java/com/google/gerrit/server/cache/IntKeyCacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
index a07c004..cff8682 100644
--- a/java/com/google/gerrit/server/cache/IntKeyCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.base.Preconditions.checkNotNull;
diff --git a/java/com/google/gerrit/server/cache/IntegerCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/IntegerCacheSerializer.java
similarity index 97%
rename from java/com/google/gerrit/server/cache/IntegerCacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/IntegerCacheSerializer.java
index 5eddb71..3195941 100644
--- a/java/com/google/gerrit/server/cache/IntegerCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/IntegerCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.base.Preconditions.checkNotNull;
diff --git a/java/com/google/gerrit/server/cache/JavaCacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/JavaCacheSerializer.java
similarity index 97%
rename from java/com/google/gerrit/server/cache/JavaCacheSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/JavaCacheSerializer.java
index 55358bc..ee71846 100644
--- a/java/com/google/gerrit/server/cache/JavaCacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/JavaCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import com.google.gerrit.common.Nullable;
import java.io.ByteArrayInputStream;
diff --git a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
similarity index 98%
rename from java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
rename to java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
index c6fc0b9..4e0b106 100644
--- a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
+++ b/java/com/google/gerrit/server/cache/serialize/ProtoCacheSerializers.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.base.Preconditions.checkArgument;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
diff --git a/java/com/google/gerrit/server/cache/StringSerializer.java b/java/com/google/gerrit/server/cache/serialize/StringCacheSerializer.java
similarity index 94%
rename from java/com/google/gerrit/server/cache/StringSerializer.java
rename to java/com/google/gerrit/server/cache/serialize/StringCacheSerializer.java
index 1e456c7..525b75b 100644
--- a/java/com/google/gerrit/server/cache/StringSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/StringCacheSerializer.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -23,7 +23,7 @@
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
-public enum StringSerializer implements CacheSerializer<String> {
+public enum StringCacheSerializer implements CacheSerializer<String> {
INSTANCE;
@Override
diff --git a/java/com/google/gerrit/server/cache/testing/BUILD b/java/com/google/gerrit/server/cache/testing/BUILD
index ed412af..9a9f1ef 100644
--- a/java/com/google/gerrit/server/cache/testing/BUILD
+++ b/java/com/google/gerrit/server/cache/testing/BUILD
@@ -5,6 +5,7 @@
srcs = glob(["*.java"]),
visibility = ["//visibility:public"],
deps = [
+ "//java/com/google/gerrit/server/cache/serialize",
"//lib:guava",
"//lib:protobuf",
"//lib/commons:lang3",
diff --git a/java/com/google/gerrit/server/cache/testing/CacheSerializerTestUtil.java b/java/com/google/gerrit/server/cache/testing/CacheSerializerTestUtil.java
index 5d41490..b339e24 100644
--- a/java/com/google/gerrit/server/cache/testing/CacheSerializerTestUtil.java
+++ b/java/com/google/gerrit/server/cache/testing/CacheSerializerTestUtil.java
@@ -18,12 +18,16 @@
/** Static utilities for testing cache serializers. */
public class CacheSerializerTestUtil {
- public static ByteString bytes(int... ints) {
+ public static ByteString byteString(int... ints) {
+ return ByteString.copyFrom(byteArray(ints));
+ }
+
+ public static byte[] byteArray(int... ints) {
byte[] bytes = new byte[ints.length];
for (int i = 0; i < ints.length; i++) {
bytes[i] = (byte) ints[i];
}
- return ByteString.copyFrom(bytes);
+ return bytes;
}
private CacheSerializerTestUtil() {}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 24685af..a6786d8 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -29,11 +29,11 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.EnumCacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index ba54361..2d00886 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -26,12 +26,12 @@
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.cache.BooleanCacheSerializer;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.MergeabilityKeyProto;
+import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.submit.SubmitDryRun;
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 75a9323..49def5f 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -125,7 +125,7 @@
@Override
public void postUpdate(Context ctx) {
- stateChanged.fire(change, ctx.getAccount(), ctx.getWhen());
+ stateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
if (workInProgress || notify.ordinal() < NotifyHandling.OWNER_REVIEWERS.ordinal()) {
return;
}
diff --git a/java/com/google/gerrit/server/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index 2e97c06..2e97a58 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -19,6 +19,7 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
@@ -89,7 +90,11 @@
10,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(poolSize),
- new ThreadFactoryBuilder().setNameFormat("ChangeUpdate-%d").setDaemon(true).build(),
+ new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
+ .setNameFormat("ChangeUpdate-%d")
+ .setDaemon(true)
+ .build(),
new ThreadPoolExecutor.CallerRunsPolicy())));
}
}
diff --git a/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java b/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
index af42b08..d03eda4 100644
--- a/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
+++ b/java/com/google/gerrit/server/events/PrivateStateChangedEvent.java
@@ -18,7 +18,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.data.AccountAttribute;
-public class PrivateStateChangedEvent extends ChangeEvent {
+public class PrivateStateChangedEvent extends PatchSetEvent {
static final String TYPE = "private-state-changed";
public Supplier<AccountAttribute> changer;
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 41cc701..367a38b 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -472,10 +472,12 @@
try {
ChangeNotes notes = getNotes(ev.getChange());
Change change = notes.getChange();
+ PatchSet patchSet = getPatchSet(notes, ev.getRevision());
WorkInProgressStateChangedEvent event = new WorkInProgressStateChangedEvent(change);
event.change = changeAttributeSupplier(change, notes);
event.changer = accountAttributeSupplier(ev.getWho());
+ event.patchSet = patchSetAttributeSupplier(change, patchSet);
dispatcher.get().postEvent(change, event);
} catch (OrmException | PermissionBackendException e) {
@@ -488,10 +490,12 @@
try {
ChangeNotes notes = getNotes(ev.getChange());
Change change = notes.getChange();
+ PatchSet patchSet = getPatchSet(notes, ev.getRevision());
PrivateStateChangedEvent event = new PrivateStateChangedEvent(change);
event.change = changeAttributeSupplier(change, notes);
event.changer = accountAttributeSupplier(ev.getWho());
+ event.patchSet = patchSetAttributeSupplier(change, patchSet);
dispatcher.get().postEvent(change, event);
} catch (OrmException | PermissionBackendException e) {
diff --git a/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java b/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
index ad32672..5e52c7b 100644
--- a/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
+++ b/java/com/google/gerrit/server/events/WorkInProgressStateChangedEvent.java
@@ -18,7 +18,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.data.AccountAttribute;
-public class WorkInProgressStateChangedEvent extends ChangeEvent {
+public class WorkInProgressStateChangedEvent extends PatchSetEvent {
static final String TYPE = "wip-state-changed";
public Supplier<AccountAttribute> changer;
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index acd275d..2df56aa 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -18,13 +18,19 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.PrivateStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.sql.Timestamp;
@Singleton
@@ -40,12 +46,17 @@
this.util = util;
}
- public void fire(Change change, AccountState account, Timestamp when) {
+ public void fire(Change change, PatchSet patchSet, AccountState account, Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- Event event = new Event(util.changeInfo(change), util.accountInfo(account), when);
+ Event event =
+ new Event(
+ util.changeInfo(change),
+ util.revisionInfo(change.getProject(), patchSet),
+ util.accountInfo(account),
+ when);
for (PrivateStateChangedListener l : listeners) {
try {
l.onPrivateStateChanged(event);
@@ -53,16 +64,20 @@
util.logEventListenerError(event, l, e);
}
}
- } catch (OrmException e) {
+ } catch (OrmException
+ | PatchListNotAvailableException
+ | GpgException
+ | IOException
+ | PermissionBackendException e) {
logger.atSevere().withCause(e).log("Couldn't fire event");
}
}
- private static class Event extends AbstractChangeEvent
+ private static class Event extends AbstractRevisionEvent
implements PrivateStateChangedListener.Event {
- protected Event(ChangeInfo change, AccountInfo who, Timestamp when) {
- super(change, who, when, NotifyHandling.ALL);
+ protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+ super(change, revision, who, when, NotifyHandling.ALL);
}
}
}
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index 3f9f35b..1c22561 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -18,13 +18,19 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.sql.Timestamp;
@Singleton
@@ -41,12 +47,17 @@
this.util = util;
}
- public void fire(Change change, AccountState account, Timestamp when) {
+ public void fire(Change change, PatchSet patchSet, AccountState account, Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- Event event = new Event(util.changeInfo(change), util.accountInfo(account), when);
+ Event event =
+ new Event(
+ util.changeInfo(change),
+ util.revisionInfo(change.getProject(), patchSet),
+ util.accountInfo(account),
+ when);
for (WorkInProgressStateChangedListener l : listeners) {
try {
l.onWorkInProgressStateChanged(event);
@@ -54,16 +65,20 @@
util.logEventListenerError(event, l, e);
}
}
- } catch (OrmException e) {
+ } catch (OrmException
+ | PatchListNotAvailableException
+ | GpgException
+ | IOException
+ | PermissionBackendException e) {
logger.atSevere().withCause(e).log("Couldn't fire event");
}
}
- private static class Event extends AbstractChangeEvent
+ private static class Event extends AbstractRevisionEvent
implements WorkInProgressStateChangedListener.Event {
- protected Event(ChangeInfo change, AccountInfo who, Timestamp when) {
- super(change, who, when, NotifyHandling.ALL);
+ protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+ super(change, revision, who, when, NotifyHandling.ALL);
}
}
}
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index ac69ff1..1c87a63 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -18,6 +18,7 @@
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.inject.Inject;
+/** Print a change description for use in git command-line progress. */
public class DefaultChangeReportFormatter implements ChangeReportFormatter {
private final String canonicalWebUrl;
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 1b83097..5f1d8c6 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -144,6 +144,7 @@
@Override
public List<CachedChange> load(Project.NameKey key) throws Exception {
+ logger.atFine().log("Loading changes of project %s", key);
try (ManualRequestContext ctx = requestContext.open()) {
List<ChangeData> cds =
queryProvider
diff --git a/java/com/google/gerrit/server/git/TagCache.java b/java/com/google/gerrit/server/git/TagCache.java
index 4d0e056..535644d 100644
--- a/java/com/google/gerrit/server/git/TagCache.java
+++ b/java/com/google/gerrit/server/git/TagCache.java
@@ -17,7 +17,7 @@
import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.StringSerializer;
+import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -35,7 +35,7 @@
protected void configure() {
persist(CACHE_NAME, String.class, TagSetHolder.class)
.version(1)
- .keySerializer(StringSerializer.INSTANCE)
+ .keySerializer(StringCacheSerializer.INSTANCE)
.valueSerializer(TagSetHolder.Serializer.INSTANCE);
bind(TagCache.class);
}
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index 916a64a..ce8814f 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -21,10 +21,10 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto;
import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.CachedRefProto;
import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto.TagSetProto.TagProto;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.util.BitSet;
diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java
index 0790a36..4c0c035 100644
--- a/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -18,9 +18,9 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
import com.google.gerrit.server.cache.proto.Cache.TagSetHolderProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
import java.util.Collection;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
diff --git a/java/com/google/gerrit/server/git/TransferConfig.java b/java/com/google/gerrit/server/git/TransferConfig.java
index 204a0d5..f85f24b 100644
--- a/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/java/com/google/gerrit/server/git/TransferConfig.java
@@ -16,7 +16,6 @@
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.concurrent.TimeUnit;
@@ -66,14 +65,4 @@
public String getFormattedMaxObjectSizeLimit() {
return maxObjectSizeLimitFormatted;
}
-
- public long getEffectiveMaxObjectSizeLimit(ProjectState p) {
- long global = getMaxObjectSizeLimit();
- long local = p.getMaxObjectSizeLimit();
- if (global > 0 && local > 0) {
- return Math.min(global, local);
- }
- // zero means "no limit", in this case the max is more limiting
- return Math.max(global, local);
- }
}
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 98a1823..a2c12df 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -24,6 +24,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ScheduleConfig.Schedule;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -165,11 +166,12 @@
if (threadPriority != Thread.NORM_PRIORITY) {
ThreadFactory parent = executor.getThreadFactory();
executor.setThreadFactory(
- task -> {
- Thread t = parent.newThread(task);
- t.setPriority(threadPriority);
- return t;
- });
+ new LoggingContextAwareThreadFactory(
+ task -> {
+ Thread t = parent.newThread(task);
+ t.setPriority(threadPriority);
+ return t;
+ }));
}
return executor;
@@ -251,18 +253,19 @@
Executor(int corePoolSize, final String queueName) {
super(
corePoolSize,
- new ThreadFactory() {
- private final ThreadFactory parent = Executors.defaultThreadFactory();
- private final AtomicInteger tid = new AtomicInteger(1);
+ new LoggingContextAwareThreadFactory(
+ new ThreadFactory() {
+ private final ThreadFactory parent = Executors.defaultThreadFactory();
+ private final AtomicInteger tid = new AtomicInteger(1);
- @Override
- public Thread newThread(Runnable task) {
- final Thread t = parent.newThread(task);
- t.setName(queueName + "-" + tid.getAndIncrement());
- t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
- return t;
- }
- });
+ @Override
+ public Thread newThread(Runnable task) {
+ final Thread t = parent.newThread(task);
+ t.setName(queueName + "-" + tid.getAndIncrement());
+ t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
+ return t;
+ }
+ }));
all =
new ConcurrentHashMap<>( //
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index f0cc558..7fe0c04 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -224,7 +224,7 @@
receivePack.setAllowNonFastForwards(true);
receivePack.setRefLogIdent(user.newRefLogIdent());
receivePack.setTimeout(transferConfig.getTimeout());
- receivePack.setMaxObjectSizeLimit(transferConfig.getEffectiveMaxObjectSizeLimit(projectState));
+ receivePack.setMaxObjectSizeLimit(projectState.getEffectiveMaxObjectSizeLimit());
receivePack.setCheckReceivedObjects(projectState.getConfig().getCheckReceivedObjects());
receivePack.setRefFilter(new ReceiveRefFilter());
receivePack.setAllowPushOptions(true);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 3c5585c..a1f0de5 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git.receive;
import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
@@ -40,6 +41,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
+import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
@@ -61,7 +63,6 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
@@ -100,7 +101,6 @@
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.SetHashtagsOp;
import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.ProjectConfigEntry;
@@ -123,6 +123,7 @@
import com.google.gerrit.server.git.validators.RefOperationValidators;
import com.google.gerrit.server.git.validators.ValidationMessage;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
@@ -132,6 +133,7 @@
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.PermissionDeniedException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.CreateRefControl;
@@ -248,6 +250,10 @@
}
}
+ private static final String CANNOT_DELETE_CHANGES = "Cannot delete from '" + REFS_CHANGES + "'";
+ private static final String CANNOT_DELETE_CONFIG =
+ "Cannot delete project configuration from '" + RefNames.REFS_CONFIG + "'";
+
interface Factory {
ReceiveCommits create(
ProjectState projectState,
@@ -347,7 +353,6 @@
private final SshInfo sshInfo;
private final SubmoduleOp.Factory subOpFactory;
private final TagCache tagCache;
- private final String canonicalWebUrl;
// Assisted injected fields.
private final AllRefsWatcher allRefsWatcher;
@@ -363,12 +368,13 @@
private final PermissionBackend.ForProject permissions;
private final Project project;
private final Repository repo;
- private final RequestId receiveId;
// Collections populated during processing.
private final List<UpdateGroupsRequest> updateGroups;
private final List<ValidationMessage> messages;
- private final ListMultimap<ReceiveError, String> errors;
+ /** Multimap of error text to refnames that produced that error. */
+ private final ListMultimap<String, String> errors;
+
private final ListMultimap<String, String> pushOptions;
private final Map<Change.Id, ReplaceRequest> replaceByChange;
@@ -381,15 +387,6 @@
private final Set<ValidCommitKey> validCommits;
- /**
- * Actual commands to be executed, as opposed to the mix of actual and magic commands that were
- * provided over the wire.
- *
- * <p>Excludes commands executed implicitly as part of other {@link BatchUpdateOp}s, such as
- * creating patch set refs.
- */
- private final List<ReceiveCommand> actualCommands;
-
// Collections lazily populated during processing.
private ListMultimap<Change.Id, Ref> refsByChange;
private ListMultimap<ObjectId, Ref> refsById;
@@ -400,12 +397,8 @@
private String setFullNameTo;
private boolean setChangeAsPrivate;
private Optional<NoteDbPushOption> noteDbPushOption;
+ private Optional<Boolean> tracePushOption;
- // Handles for outputting back over the wire to the end user.
- private Task newProgress;
- private Task replaceProgress;
- private Task closeProgress;
- private Task commandProgress;
private MessageSender messageSender;
@Inject
@@ -446,7 +439,6 @@
SshInfo sshInfo,
SubmoduleOp.Factory subOpFactory,
TagCache tagCache,
- @CanonicalWebUrl @Nullable String canonicalWebUrl,
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@Assisted ReceivePack rp,
@@ -504,12 +496,9 @@
project = projectState.getProject();
labelTypes = projectState.getLabelTypes();
permissions = permissionBackend.user(user).project(project.getNameKey());
- receiveId = RequestId.forProject(project.getNameKey());
rejectCommits = BanCommit.loadRejectCommitsMap(rp.getRepository(), rp.getRevWalk());
- this.canonicalWebUrl = canonicalWebUrl;
// Collections populated during processing.
- actualCommands = new ArrayList<>();
errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
messages = new ArrayList<>();
pushOptions = LinkedListMultimap.create();
@@ -558,53 +547,157 @@
}
void processCommands(Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
- newProgress = progress.beginSubTask("new", UNKNOWN);
- replaceProgress = progress.beginSubTask("updated", UNKNOWN);
- closeProgress = progress.beginSubTask("closed", UNKNOWN);
- commandProgress = progress.beginSubTask("refs", UNKNOWN);
-
- try {
- parsePushOptions();
- logDebug("Parsing %d commands", commands.size());
- for (ReceiveCommand cmd : commands) {
- if (!projectState.getProject().getState().permitsWrite()) {
- reject(cmd, "prohibited by Gerrit: project state does not permit write");
- break;
- }
- parseCommand(cmd);
+ Task commandProgress = progress.beginSubTask("refs", UNKNOWN);
+ commands = commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
+ processCommandsUnsafe(commands, progress);
+ for (ReceiveCommand cmd : commands) {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
}
- } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
- for (ReceiveCommand cmd : actualCommands) {
+ }
+ commandProgress.end();
+ progress.end();
+
+ // Update account info with details discovered during commit walking. The account update happens
+ // in a separate batch update, and failure doesn't cause the push itself to fail.
+ updateAccountInfo();
+ }
+
+ // Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
+ private void processCommandsUnsafe(
+ Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
+ parsePushOptions();
+ try (TraceContext traceContext =
+ TraceContext.open()
+ .addTag(RequestId.Type.RECEIVE_ID, RequestId.forProject(project.getNameKey()))) {
+ if (tracePushOption.orElse(false)) {
+ RequestId traceId = new RequestId();
+ traceContext.addTag(RequestId.Type.TRACE_ID, traceId);
+ addMessage(RequestId.Type.TRACE_ID.name() + ": " + traceId);
+ }
+ try {
+ if (!projectState.getProject().getState().permitsWrite()) {
+ for (ReceiveCommand cmd : commands) {
+ reject(cmd, "prohibited by Gerrit: project state does not permit write");
+ }
+ return;
+ }
+
+ logger.atFine().log("Parsing %d commands", commands.size());
+
+ List<ReceiveCommand> magicCommands = new ArrayList<>();
+ List<ReceiveCommand> directPatchSetPushCommands = new ArrayList<>();
+ List<ReceiveCommand> regularCommands = new ArrayList<>();
+
+ for (ReceiveCommand cmd : commands) {
+ if (MagicBranch.isMagicBranch(cmd.getRefName())) {
+ magicCommands.add(cmd);
+ } else if (isDirectChangesPush(cmd.getRefName())) {
+ directPatchSetPushCommands.add(cmd);
+ } else {
+ regularCommands.add(cmd);
+ }
+ }
+
+ int commandTypes =
+ (magicCommands.isEmpty() ? 0 : 1)
+ + (directPatchSetPushCommands.isEmpty() ? 0 : 1)
+ + (regularCommands.isEmpty() ? 0 : 1);
+
+ if (commandTypes > 1) {
+ for (ReceiveCommand cmd : commands) {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ cmd.setResult(REJECTED_OTHER_REASON, "cannot combine normal pushes and magic pushes");
+ }
+ }
+ return;
+ }
+
+ if (!regularCommands.isEmpty()) {
+ handleRegularCommands(regularCommands, progress);
+ }
+
+ for (ReceiveCommand cmd : directPatchSetPushCommands) {
+ parseDirectChangesPush(cmd);
+ }
+
+ boolean first = true;
+ for (ReceiveCommand cmd : magicCommands) {
+ if (first) {
+ parseMagicBranch(cmd);
+ first = false;
+ } else {
+ reject(cmd, "duplicate request");
+ }
+ }
+ } catch (PermissionBackendException | NoSuchProjectException | IOException err) {
+ logger.atSevere().withCause(err).log("Failed to process refs in %s", project.getName());
+ return;
+ }
+
+ Task newProgress = progress.beginSubTask("new", UNKNOWN);
+ Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
+
+ List<CreateRequest> newChanges = Collections.emptyList();
+ if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
+ newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
+ }
+ preparePatchSetsForReplace(newChanges);
+ insertChangesAndPatchSets(newChanges, replaceProgress);
+ newProgress.end();
+ replaceProgress.end();
+
+ if (!errors.isEmpty()) {
+ logger.atFine().log("Handling error conditions: %s", errors.keySet());
+ for (String error : errors.keySet()) {
+ receivePack.sendMessage("error: " + buildError(error, errors.get(error)));
+ }
+ receivePack.sendMessage(String.format("User: %s", user.getLoggableName()));
+ receivePack.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+ }
+
+ reportMessages(newChanges);
+ }
+ }
+
+ private void handleRegularCommands(List<ReceiveCommand> cmds, MultiProgressMonitor progress)
+ throws PermissionBackendException, IOException, NoSuchProjectException {
+ for (ReceiveCommand cmd : cmds) {
+ parseRegularCommand(cmd);
+ }
+
+ try (BatchUpdate bu =
+ batchUpdateFactory.create(
+ db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+ ObjectInserter ins = repo.newObjectInserter();
+ ObjectReader reader = ins.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ bu.setRepository(repo, rw, ins).updateChangesInParallel();
+ bu.setRefLogMessage("push");
+
+ int added = 0;
+ for (ReceiveCommand cmd : cmds) {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ bu.addRepoOnlyOp(new UpdateOneRefOp(cmd));
+ added++;
+ }
+ }
+ logger.atFine().log("Added %d additional ref updates", added);
+ bu.execute();
+ } catch (UpdateException | RestApiException e) {
+ for (ReceiveCommand cmd : cmds) {
if (cmd.getResult() == NOT_ATTEMPTED) {
cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
}
}
- logError(String.format("Failed to process refs in %s", project.getName()), err);
- }
-
- List<CreateRequest> newChanges = Collections.emptyList();
- if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
- newChanges = selectNewAndReplacedChangesFromMagicBranch();
- }
- preparePatchSetsForReplace(newChanges);
- insertChangesAndPatchSets(newChanges);
- newProgress.end();
- replaceProgress.end();
-
- if (!errors.isEmpty()) {
- logDebug("Handling error conditions: %s", errors.keySet());
- for (ReceiveError error : errors.keySet()) {
- receivePack.sendMessage(buildError(error, errors.get(error)));
- }
- receivePack.sendMessage(String.format("User: %s", user.getLoggableName()));
- receivePack.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+ logger.atFine().withCause(e).log("update failed:");
}
Set<Branch.NameKey> branches = new HashSet<>();
- for (ReceiveCommand c : actualCommands) {
+ for (ReceiveCommand c : cmds) {
// Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
- // should happen in this loop are things that can't happen within one BatchUpdate because they
- // involve kicking off an additional BatchUpdate.
+ // should happen in this loop are things that can't happen within one BatchUpdate because
+ // they involve kicking off an additional BatchUpdate.
if (c.getResult() != OK) {
continue;
}
@@ -613,7 +706,9 @@
case CREATE:
case UPDATE:
case UPDATE_NONFASTFORWARD:
- autoCloseChanges(c);
+ Task closeProgress = progress.beginSubTask("closed", UNKNOWN);
+ autoCloseChanges(c, closeProgress);
+ closeProgress.end();
branches.add(new Branch.NameKey(project.getNameKey(), c.getRefName()));
break;
@@ -626,21 +721,13 @@
// Update superproject gitlinks if required.
if (!branches.isEmpty()) {
try (MergeOpRepoManager orm = ormProvider.get()) {
- orm.setContext(db, TimeUtil.nowTs(), user, receiveId);
+ orm.setContext(db, TimeUtil.nowTs(), user);
SubmoduleOp op = subOpFactory.create(branches, orm);
op.updateSuperProjects();
} catch (SubmoduleException e) {
- logError("Can't update the superprojects", e);
+ logger.atSevere().withCause(e).log("Can't update the superprojects");
}
}
-
- // Update account info with details discovered during commit walking.
- updateAccountInfo();
-
- closeProgress.end();
- commandProgress.end();
- progress.end();
- reportMessages(newChanges);
}
private void reportMessages(List<CreateRequest> newChanges) {
@@ -661,7 +748,7 @@
replaceByChange
.values()
.stream()
- .filter(r -> !r.skip && r.inputCommand.getResult() == OK)
+ .filter(r -> r.inputCommand.getResult() == OK)
.sorted(comparingInt(r -> r.notes.getChangeId().get()))
.collect(toList());
if (!updated.isEmpty()) {
@@ -689,7 +776,7 @@
subject = receivePack.getRevWalk().parseCommit(u.newCommitId).getShortMessage();
} catch (IOException e) {
// Log and fall back to original change subject
- logWarn("failed to get subject for edit patch set", e);
+ logger.atWarning().withCause(e).log("failed to get subject for edit patch set");
subject = u.notes.getChange().getSubject();
}
} else {
@@ -722,15 +809,14 @@
}
}
- private void insertChangesAndPatchSets(List<CreateRequest> newChanges) {
+ private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
- logWarn(
- String.format(
- "Skipping change updates on %s because ref update failed: %s %s",
- project.getName(),
- magicBranchCmd.getResult(),
- Strings.nullToEmpty(magicBranchCmd.getMessage())));
+ logger.atWarning().log(
+ "Skipping change updates on %s because ref update failed: %s %s",
+ project.getName(),
+ magicBranchCmd.getResult(),
+ Strings.nullToEmpty(magicBranchCmd.getMessage()));
return;
}
@@ -741,26 +827,22 @@
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo, rw, ins).updateChangesInParallel();
- bu.setRequestId(receiveId);
bu.setRefLogMessage("push");
- logDebug("Adding %d replace requests", newChanges.size());
+ logger.atFine().log("Adding %d replace requests", newChanges.size());
for (ReplaceRequest replace : replaceByChange.values()) {
replace.addOps(bu, replaceProgress);
}
- logDebug("Adding %d create requests", newChanges.size());
+ logger.atFine().log("Adding %d create requests", newChanges.size());
for (CreateRequest create : newChanges) {
create.addOps(bu);
}
- logDebug("Adding %d group update requests", newChanges.size());
+ logger.atFine().log("Adding %d group update requests", newChanges.size());
updateGroups.forEach(r -> r.addOps(bu));
- logDebug("Adding %d additional ref updates", actualCommands.size());
- actualCommands.forEach(c -> bu.addRepoOnlyOp(new UpdateOneRefOp(c)));
-
- logDebug("Executing batch");
+ logger.atFine().log("Executing batch");
try {
bu.execute();
} catch (UpdateException e) {
@@ -777,16 +859,17 @@
replace.inputCommand.setResult(OK);
}
} else {
- logDebug("Rejecting due to message from ReplaceOp");
+ logger.atFine().log("Rejecting due to message from ReplaceOp");
reject(replace.inputCommand, rejectMessage);
}
}
} catch (ResourceConflictException e) {
- addMessage(e.getMessage());
+ addError(e.getMessage());
reject(magicBranchCmd, "conflict");
} catch (RestApiException | IOException err) {
- logError("Can't insert change/patch set for " + project.getName(), err);
+ logger.atSevere().withCause(err).log(
+ "Can't insert change/patch set for %s", project.getName());
reject(magicBranchCmd, "internal server error: " + err.getMessage());
}
@@ -794,7 +877,7 @@
try {
submit(newChanges, replaceByChange.values());
} catch (ResourceConflictException e) {
- addMessage(e.getMessage());
+ addError(e.getMessage());
reject(magicBranchCmd, "conflict");
} catch (RestApiException
| OrmException
@@ -802,26 +885,21 @@
| IOException
| ConfigInvalidException
| PermissionBackendException e) {
- logError("Error submitting changes to " + project.getName(), e);
+ logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
reject(magicBranchCmd, "error during submit");
}
}
}
- private String buildError(ReceiveError error, List<String> branches) {
+ private String buildError(String error, List<String> branches) {
StringBuilder sb = new StringBuilder();
if (branches.size() == 1) {
- sb.append("Branch ").append(branches.get(0)).append(":\n");
- sb.append(error.get());
+ sb.append("branch ").append(branches.get(0)).append(":\n");
+ sb.append(error);
return sb.toString();
}
- sb.append("Branches");
- String delim = " ";
- for (String branch : branches) {
- sb.append(delim).append(branch);
- delim = ", ";
- }
- return sb.append(":\n").append(error.get()).toString();
+ sb.append("branches ").append(Joiner.on(", ").join(branches));
+ return sb.append(":\n").append(error).toString();
}
/** Parses push options specified as "git push -o OPTION" */
@@ -850,13 +928,69 @@
} else {
noteDbPushOption = Optional.of(NoteDbPushOption.DISALLOW);
}
+
+ List<String> traceValues = pushOptions.get("trace");
+ if (!traceValues.isEmpty()) {
+ String value = traceValues.get(traceValues.size() - 1);
+ tracePushOption = Optional.of(value.isEmpty() || Boolean.parseBoolean(value));
+ } else {
+ tracePushOption = Optional.empty();
+ }
}
- private void parseCommand(ReceiveCommand cmd)
+ private static boolean isDirectChangesPush(String refname) {
+ Matcher m = NEW_PATCHSET_PATTERN.matcher(refname);
+ return m.matches();
+ }
+
+ private void parseDirectChangesPush(ReceiveCommand cmd) {
+ Matcher m = NEW_PATCHSET_PATTERN.matcher(cmd.getRefName());
+ checkArgument(m.matches());
+
+ if (allowPushToRefsChanges) {
+ // The referenced change must exist and must still be open.
+ Change.Id changeId = Change.Id.parse(m.group(1));
+ parseReplaceCommand(cmd, changeId);
+ } else {
+ reject(cmd, "upload to refs/changes not allowed");
+ }
+ }
+
+ // Wrap ReceiveCommand so the progress counter works automatically.
+ private ReceiveCommand wrapReceiveCommand(ReceiveCommand cmd, Task progress) {
+ String refname = cmd.getRefName();
+
+ if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
+ refname = RefNames.refsUsers(user.getAccountId());
+ logger.atFine().log("Swapping out command for %s to %s", RefNames.REFS_USERS_SELF, refname);
+ }
+
+ // We must also update the original, because callers may inspect it afterwards to decide if
+ // the command went through or not.
+ return new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), refname, cmd.getType()) {
+ @Override
+ public void setResult(Result s, String m) {
+ if (getResult() == NOT_ATTEMPTED) { // Only report the progress update once.
+ progress.update(1);
+ }
+ // Counter intuitively, we don't check that results == NOT_ATTEMPTED here.
+ // This is so submit-on-push can still reject the update if the change is created
+ // successfully
+ // (status OK) but the submit failed (merge failed: REJECTED_OTHER_REASON).
+ super.setResult(s, m);
+ cmd.setResult(s, m);
+ }
+ };
+ }
+
+ /*
+ * Interpret a normal push.
+ */
+ private void parseRegularCommand(ReceiveCommand cmd)
throws PermissionBackendException, NoSuchProjectException, IOException {
if (cmd.getResult() != NOT_ATTEMPTED) {
// Already rejected by the core receive process.
- logDebug("Already processed by core: %s %s", cmd.getResult(), cmd);
+ logger.atFine().log("Already processed by core: %s %s", cmd.getResult(), cmd);
return;
}
@@ -864,45 +998,12 @@
reject(cmd, "not valid ref");
return;
}
-
- if (MagicBranch.isMagicBranch(cmd.getRefName())) {
- parseMagicBranch(cmd);
- return;
- }
-
- if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(cmd.getRefName())) {
- String newName = RefNames.refsUsers(user.getAccountId());
- logDebug("Swapping out command for %s to %s", RefNames.REFS_USERS_SELF, newName);
- final ReceiveCommand orgCmd = cmd;
- cmd =
- new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), newName, cmd.getType()) {
- @Override
- public void setResult(Result s, String m) {
- super.setResult(s, m);
- orgCmd.setResult(s, m);
- }
- };
- }
-
- Matcher m = NEW_PATCHSET_PATTERN.matcher(cmd.getRefName());
- if (m.matches()) {
- if (allowPushToRefsChanges) {
- // The referenced change must exist and must still be open.
- //
- Change.Id changeId = Change.Id.parse(m.group(1));
- parseReplaceCommand(cmd, changeId);
- } else {
- reject(cmd, "upload to refs/changes not allowed");
- }
- return;
- }
-
if (RefNames.isNoteDbMetaRef(cmd.getRefName())) {
// Reject pushes to NoteDb refs without a special option and permission. Note that this
// prohibition doesn't depend on NoteDb being enabled in any way, since all sites will
// migrate to NoteDb eventually, and we don't want garbage data waiting there when the
// migration finishes.
- logDebug(
+ logger.atFine().log(
"%s NoteDb ref %s with %s=%s",
cmd.getType(), cmd.getRefName(), NoteDbPushOption.OPTION_NAME, noteDbPushOption);
if (!Optional.of(NoteDbPushOption.ALLOW).equals(noteDbPushOption)) {
@@ -953,7 +1054,7 @@
}
if (isConfig(cmd)) {
- logDebug("Processing %s command", cmd.getRefName());
+ logger.atFine().log("Processing %s command", cmd.getRefName());
try {
permissions.check(ProjectPermission.WRITE_CONFIG);
} catch (AuthException e) {
@@ -978,13 +1079,9 @@
addError(" " + err.getMessage());
}
reject(cmd, "invalid project configuration");
- logError(
- "User "
- + user.getLoggableName()
- + " tried to push invalid project configuration "
- + cmd.getNewId().name()
- + " for "
- + project.getName());
+ logger.atSevere().log(
+ "User %s tried to push invalid project configuration %s for %s",
+ user.getLoggableName(), cmd.getNewId().name(), project.getName());
return;
}
Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
@@ -1054,14 +1151,9 @@
}
} catch (Exception e) {
reject(cmd, "invalid project configuration");
- logError(
- "User "
- + user.getLoggableName()
- + " tried to push invalid project configuration "
- + cmd.getNewId().name()
- + " for "
- + project.getName(),
- e);
+ logger.atSevere().withCause(e).log(
+ "User %s tried to push invalid project configuration %s for %s",
+ user.getLoggableName(), cmd.getNewId().name(), project.getName());
return;
}
break;
@@ -1085,13 +1177,12 @@
try {
obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
} catch (IOException err) {
- logError(
- "Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " creation",
- err);
+ logger.atSevere().withCause(err).log(
+ "Invalid object %s for %s creation", cmd.getNewId().name(), cmd.getRefName());
reject(cmd, "invalid object");
return;
}
- logDebug("Creating %s", cmd);
+ logger.atFine().log("Creating %s", cmd);
if (isHead(cmd) && !isCommit(cmd)) {
return;
@@ -1102,45 +1193,32 @@
// Must pass explicit user instead of injecting a provider into CreateRefControl, since
// Provider<CurrentUser> within ReceiveCommits will always return anonymous.
createRefControl.checkCreateRef(Providers.of(user), receivePack.getRepository(), branch, obj);
- } catch (AuthException | ResourceConflictException denied) {
+ } catch (AuthException denied) {
+ rejectProhibited(cmd, denied);
+ return;
+ } catch (ResourceConflictException denied) {
reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
return;
}
- if (!validRefOperation(cmd)) {
- // validRefOperation sets messages, so no need to provide more feedback.
- return;
+ if (validRefOperation(cmd)) {
+ validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
}
-
- validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
- actualCommands.add(cmd);
}
private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException {
- logDebug("Updating %s", cmd);
- boolean ok;
- try {
- permissions.ref(cmd.getRefName()).check(RefPermission.UPDATE);
- ok = true;
- } catch (AuthException err) {
- ok = false;
- }
- if (ok) {
+ logger.atFine().log("Updating %s", cmd);
+ Optional<AuthException> err = checkRefPermission(cmd, RefPermission.UPDATE);
+ if (!err.isPresent()) {
if (isHead(cmd) && !isCommit(cmd)) {
+ reject(cmd, "head must point to commit");
return;
}
- if (!validRefOperation(cmd)) {
- return;
+ if (validRefOperation(cmd)) {
+ validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
}
- validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
- actualCommands.add(cmd);
} else {
- if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) {
- errors.put(ReceiveError.CONFIG_UPDATE, RefNames.REFS_CONFIG);
- } else {
- errors.put(ReceiveError.UPDATE, cmd.getRefName());
- }
- reject(cmd, "prohibited by Gerrit: ref update access denied");
+ rejectProhibited(cmd, err.get());
}
}
@@ -1149,7 +1227,8 @@
try {
obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
} catch (IOException err) {
- logError("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName(), err);
+ logger.atSevere().withCause(err).log(
+ "Invalid object %s for %s", cmd.getNewId().name(), cmd.getRefName());
reject(cmd, "invalid object");
return false;
}
@@ -1162,34 +1241,21 @@
}
private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
- logDebug("Deleting %s", cmd);
+ logger.atFine().log("Deleting %s", cmd);
if (cmd.getRefName().startsWith(REFS_CHANGES)) {
- errors.put(ReceiveError.DELETE_CHANGES, cmd.getRefName());
+ errors.put(CANNOT_DELETE_CHANGES, cmd.getRefName());
reject(cmd, "cannot delete changes");
- } else if (canDelete(cmd)) {
- if (!validRefOperation(cmd)) {
- return;
- }
- actualCommands.add(cmd);
- } else if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) {
+ } else if (isConfigRef(cmd.getRefName())) {
+ errors.put(CANNOT_DELETE_CONFIG, cmd.getRefName());
reject(cmd, "cannot delete project configuration");
+ }
+
+ Optional<AuthException> err = checkRefPermission(cmd, RefPermission.DELETE);
+ if (!err.isPresent()) {
+ validRefOperation(cmd);
+
} else {
- errors.put(ReceiveError.DELETE, cmd.getRefName());
- reject(cmd, "cannot delete references");
- }
- }
-
- private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException {
- if (isConfigRef(cmd.getRefName())) {
- // Never allow to delete the meta config branch.
- return false;
- }
-
- try {
- permissions.ref(cmd.getRefName()).check(RefPermission.DELETE);
- return projectState.statePermitsWrite();
- } catch (AuthException e) {
- return false;
+ rejectProhibited(cmd, err.get());
}
}
@@ -1200,13 +1266,12 @@
} catch (IncorrectObjectTypeException notCommit) {
newObject = null;
} catch (IOException err) {
- logError(
- "Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " forced update",
- err);
+ logger.atSevere().withCause(err).log(
+ "Invalid object %s for %s forced update", cmd.getNewId().name(), cmd.getRefName());
reject(cmd, "invalid object");
return;
}
- logDebug("Rewinding %s", cmd);
+ logger.atFine().log("Rewinding %s", cmd);
if (newObject != null) {
validateNewCommits(new Branch.NameKey(project.getNameKey(), cmd.getRefName()), cmd);
@@ -1215,23 +1280,48 @@
}
}
- boolean ok;
- try {
- permissions.ref(cmd.getRefName()).check(RefPermission.FORCE_UPDATE);
- ok = true;
- } catch (AuthException err) {
- ok = false;
- }
- if (ok) {
- if (!validRefOperation(cmd)) {
- return;
- }
- actualCommands.add(cmd);
+ Optional<AuthException> err = checkRefPermission(cmd, RefPermission.FORCE_UPDATE);
+ if (!err.isPresent()) {
+ validRefOperation(cmd);
} else {
- cmd.setResult(REJECTED_OTHER_REASON, "need '" + PermissionRule.FORCE_PUSH + "' privilege.");
+ rejectProhibited(cmd, err.get());
}
}
+ private Optional<AuthException> checkRefPermission(ReceiveCommand cmd, RefPermission perm)
+ throws PermissionBackendException {
+ return checkRefPermission(permissions.ref(cmd.getRefName()), perm);
+ }
+
+ private Optional<AuthException> checkRefPermission(
+ PermissionBackend.ForRef forRef, RefPermission perm) throws PermissionBackendException {
+ try {
+ forRef.check(perm);
+ return Optional.empty();
+ } catch (AuthException e) {
+ return Optional.of(e);
+ }
+ }
+
+ private void rejectProhibited(ReceiveCommand cmd, AuthException err) {
+ err.getAdvice().ifPresent(a -> errors.put(a, cmd.getRefName()));
+ reject(cmd, prohibited(err, cmd.getRefName()));
+ }
+
+ private static String prohibited(AuthException e, String alreadyDisplayedResource) {
+ String msg = e.getMessage();
+ if (e instanceof PermissionDeniedException) {
+ PermissionDeniedException pde = (PermissionDeniedException) e;
+ if (pde.getResource().isPresent()
+ && pde.getResource().get().equals(alreadyDisplayedResource)) {
+ // Avoid repeating resource name if exactly the given name was already displayed by the
+ // generic git push machinery.
+ msg = PermissionDeniedException.MESSAGE_PREFIX + pde.describePermission();
+ }
+ }
+ return "prohibited by Gerrit: " + msg;
+ }
+
static class MagicBranchInput {
private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
@@ -1249,6 +1339,9 @@
CmdLineParser cmdLineParser;
Set<String> hashtags = new HashSet<>();
+ @Option(name = "--trace", metaVar = "NAME", usage = "enable tracing")
+ boolean trace;
+
@Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
List<ObjectId> base;
@@ -1501,14 +1594,8 @@
* <p>Assumes we are handling a magic branch here.
*/
private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
- // Permit exactly one new change request per push.
- if (magicBranch != null) {
- reject(cmd, "duplicate request");
- return;
- }
-
- logDebug("Found magic branch %s", cmd.getRefName());
- magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
+ logger.atFine().log("Found magic branch %s", cmd.getRefName());
+ MagicBranchInput magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
magicBranch.reviewer.addAll(extraReviewers.get(ReviewerStateInternal.REVIEWER));
magicBranch.cc.addAll(extraReviewers.get(ReviewerStateInternal.CC));
@@ -1519,7 +1606,7 @@
ref = magicBranch.parse(repo, receivePack.getAdvertisedRefs().keySet(), pushOptions);
} catch (CmdLineException e) {
if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
- logDebug("Invalid branch syntax");
+ logger.atFine().log("Invalid branch syntax");
reject(cmd, e.getMessage());
return;
}
@@ -1540,13 +1627,13 @@
return;
}
if (projectState.isAllUsers() && RefNames.REFS_USERS_SELF.equals(ref)) {
- logDebug("Handling %s", RefNames.REFS_USERS_SELF);
+ logger.atFine().log("Handling %s", RefNames.REFS_USERS_SELF);
ref = RefNames.refsUsers(user.getAccountId());
}
if (!receivePack.getAdvertisedRefs().containsKey(ref)
&& !ref.equals(readHEAD(repo))
&& !ref.equals(RefNames.REFS_CONFIG)) {
- logDebug("Ref %s not found", ref);
+ logger.atFine().log("Ref %s not found", ref);
if (ref.startsWith(Constants.R_HEADS)) {
String n = ref.substring(Constants.R_HEADS.length());
reject(cmd, "branch " + n + " not found");
@@ -1559,22 +1646,16 @@
magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
magicBranch.perm = permissions.ref(ref);
- try {
- magicBranch.perm.check(RefPermission.CREATE_CHANGE);
- } catch (AuthException denied) {
- errors.put(ReceiveError.CODE_REVIEW, ref);
- reject(cmd, denied.getMessage());
- return;
- }
- if (!projectState.statePermitsWrite()) {
- reject(cmd, "project state does not permit write");
+ Optional<AuthException> err = checkRefPermission(magicBranch.perm, RefPermission.CREATE_CHANGE);
+ if (err.isPresent()) {
+ rejectProhibited(cmd, err.get());
return;
}
// TODO(davido): Remove legacy support for drafts magic branch option
// after repo-tool supports private and work-in-progress changes.
if (magicBranch.draft && !receiveConfig.allowDrafts) {
- errors.put(ReceiveError.CODE_REVIEW, ref);
+ errors.put(ReceiveError.CODE_REVIEW.get(), ref);
reject(cmd, "draft workflow is disabled");
return;
}
@@ -1607,10 +1688,9 @@
}
if (magicBranch.submit) {
- try {
- permissions.ref(ref).check(RefPermission.UPDATE_BY_SUBMIT);
- } catch (AuthException e) {
- reject(cmd, e.getMessage());
+ err = checkRefPermission(magicBranch.perm, RefPermission.UPDATE_BY_SUBMIT);
+ if (err.isPresent()) {
+ rejectProhibited(cmd, err.get());
return;
}
}
@@ -1619,10 +1699,10 @@
RevCommit tip;
try {
tip = walk.parseCommit(magicBranch.cmd.getNewId());
- logDebug("Tip of push: %s", tip.name());
+ logger.atFine().log("Tip of push: %s", tip.name());
} catch (IOException ex) {
magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
- logError("Invalid pack upload; one or more objects weren't sent", ex);
+ logger.atSevere().withCause(ex).log("Invalid pack upload; one or more objects weren't sent");
return;
}
@@ -1649,12 +1729,12 @@
|| magicBranch.base != null
|| magicBranch.merged
|| tip.getParentCount() == 0) {
- logDebug("Forcing newChangeForAllNotInTarget = false");
+ logger.atFine().log("Forcing newChangeForAllNotInTarget = false");
newChangeForAllNotInTarget = false;
}
if (magicBranch.base != null) {
- logDebug("Handling %base: %s", magicBranch.base);
+ logger.atFine().log("Handling %%base: %s", magicBranch.base);
magicBranch.baseCommit = Lists.newArrayListWithCapacity(magicBranch.base.size());
for (ObjectId id : magicBranch.base) {
try {
@@ -1666,7 +1746,8 @@
reject(cmd, "base not found");
return;
} catch (IOException e) {
- logWarn(String.format("Project %s cannot read %s", project.getName(), id.name()), e);
+ logger.atWarning().withCause(e).log(
+ "Project %s cannot read %s", project.getName(), id.name());
reject(cmd, "internal server error");
return;
}
@@ -1677,31 +1758,40 @@
return; // readBranchTip already rejected cmd.
}
magicBranch.baseCommit = Collections.singletonList(branchTip);
- logDebug("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
+ logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
}
} catch (IOException ex) {
- logWarn(
- String.format("Error walking to %s in project %s", destBranch, project.getName()), ex);
+ logger.atWarning().withCause(ex).log(
+ "Error walking to %s in project %s", destBranch, project.getName());
reject(cmd, "internal server error");
return;
}
- // Validate that the new commits are connected with the target
- // branch. If they aren't, we want to abort. We do this check by
- // looking to see if we can compute a merge base between the new
- // commits and the target branch head.
- //
+ if (validateConnected(magicBranch.dest, tip)) {
+ this.magicBranch = magicBranch;
+ }
+ }
+
+ // Validate that the new commits are connected with the target
+ // branch. If they aren't, we want to abort. We do this check by
+ // looking to see if we can compute a merge base between the new
+ // commits and the target branch head.
+ private boolean validateConnected(Branch.NameKey dest, RevCommit tip) {
+ RevWalk walk = receivePack.getRevWalk();
try {
- Ref targetRef = receivePack.getAdvertisedRefs().get(magicBranch.dest.get());
+ Ref targetRef = receivePack.getAdvertisedRefs().get(dest.get());
if (targetRef == null || targetRef.getObjectId() == null) {
// The destination branch does not yet exist. Assume the
// history being sent for review will start it and thus
// is "connected" to the branch.
- logDebug("Branch is unborn");
- return;
+ logger.atFine().log("Branch is unborn");
+
+ // This is not an error condition.
+ return true;
}
+
RevCommit h = walk.parseCommit(targetRef.getObjectId());
- logDebug("Current branch tip: %s", h.name());
+ logger.atFine().log("Current branch tip: %s", h.name());
RevFilter oldRevFilter = walk.getRevFilter();
try {
walk.reset();
@@ -1710,6 +1800,7 @@
walk.markStart(h);
if (walk.next() == null) {
reject(magicBranch.cmd, "no common ancestry");
+ return false;
}
} finally {
walk.reset();
@@ -1717,8 +1808,10 @@
}
} catch (IOException e) {
magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
- logError("Invalid pack upload; one or more objects weren't sent", e);
+ logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
+ return false;
}
+ return true;
}
private static String readHEAD(Repository repo) {
@@ -1741,7 +1834,7 @@
// Handle an upload to refs/changes/XX/CHANGED-NUMBER.
private void parseReplaceCommand(ReceiveCommand cmd, Change.Id changeId) {
- logDebug("Parsing replace command");
+ logger.atFine().log("Parsing replace command");
if (cmd.getType() != ReceiveCommand.Type.CREATE) {
reject(cmd, "invalid usage");
return;
@@ -1750,9 +1843,9 @@
RevCommit newCommit;
try {
newCommit = receivePack.getRevWalk().parseCommit(cmd.getNewId());
- logDebug("Replacing with %s", newCommit);
+ logger.atFine().log("Replacing with %s", newCommit);
} catch (IOException e) {
- logError("Cannot parse " + cmd.getNewId().name() + " as commit", e);
+ logger.atSevere().withCause(e).log("Cannot parse %s as commit", cmd.getNewId().name());
reject(cmd, "invalid commit");
return;
}
@@ -1761,11 +1854,11 @@
try {
changeEnt = notesFactory.createChecked(db, project.getNameKey(), changeId).getChange();
} catch (NoSuchChangeException e) {
- logError("Change not found " + changeId, e);
+ logger.atSevere().withCause(e).log("Change not found %s", changeId);
reject(cmd, "change " + changeId + " not found");
return;
} catch (OrmException e) {
- logError("Cannot lookup existing change " + changeId, e);
+ logger.atSevere().withCause(e).log("Cannot lookup existing change %s", changeId);
reject(cmd, "database error");
return;
}
@@ -1774,7 +1867,7 @@
return;
}
- logDebug("Replacing change %s", changeEnt.getId());
+ logger.atFine().log("Replacing change %s", changeEnt.getId());
requestReplace(cmd, true, changeEnt, newCommit);
}
@@ -1797,8 +1890,8 @@
return true;
}
- private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch() {
- logDebug("Finding new and replaced changes");
+ private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) {
+ logger.atFine().log("Finding new and replaced changes");
List<CreateRequest> newChanges = new ArrayList<>();
ListMultimap<ObjectId, Ref> existing = changeRefsById();
@@ -1874,13 +1967,15 @@
List<String> idList = c.getFooterLines(CHANGE_ID);
if (!idList.isEmpty()) {
- pending.put(c, new ChangeLookup(c, new Change.Key(idList.get(idList.size() - 1).trim())));
+ pending.put(
+ c, lookupByChangeKey(c, new Change.Key(idList.get(idList.size() - 1).trim())));
} else {
- pending.put(c, new ChangeLookup(c));
+ pending.put(c, lookupByCommit(c));
}
+
int n = pending.size() + newChanges.size();
if (maxBatchChanges != 0 && n > maxBatchChanges) {
- logDebug("%d changes exceeds limit of %d", n, maxBatchChanges);
+ logger.atFine().log("%d changes exceeds limit of %d", n, maxBatchChanges);
reject(
magicBranch.cmd,
"the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
@@ -1899,12 +1994,12 @@
continue;
}
- logDebug("Creating new change for %s even though it is already tracked", name);
+ logger.atFine().log("Creating new change for %s even though it is already tracked", name);
}
if (!validCommit(receivePack.getRevWalk(), magicBranch.dest, magicBranch.cmd, c, null)) {
// Not a change the user can propose? Abort as early as possible.
- logDebug("Aborting early due to invalid commit");
+ logger.atFine().log("Aborting early due to invalid commit");
return Collections.emptyList();
}
@@ -1914,16 +2009,16 @@
magicBranch.cmd,
"Pushing merges in commit chains with 'all not in target' is not allowed,\n"
+ "to override please set the base manually");
- logDebug("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
+ logger.atFine().log("Rejecting merge commit %s with newChangeForAllNotInTarget", name);
// TODO(dborowitz): Should we early return here?
}
if (idList.isEmpty()) {
- newChanges.add(new CreateRequest(c, magicBranch.dest.get()));
+ newChanges.add(new CreateRequest(c, magicBranch.dest.get(), newProgress));
continue;
}
}
- logDebug(
+ logger.atFine().log(
"Finished initial RevWalk with %d commits total: %d already"
+ " tracked, %d new changes with no Change-Id, and %d deferred"
+ " lookups",
@@ -1940,14 +2035,14 @@
}
if (newChangeIds.contains(p.changeKey)) {
- logDebug("Multiple commits with Change-Id %s", p.changeKey);
+ logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
return Collections.emptyList();
}
List<ChangeData> changes = p.destChanges;
if (changes.size() > 1) {
- logDebug(
+ logger.atFine().log(
"Multiple changes in branch %s with Change-Id %s: %s",
magicBranch.dest,
p.changeKey,
@@ -2003,9 +2098,9 @@
}
newChangeIds.add(p.changeKey);
}
- newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get()));
+ newChanges.add(new CreateRequest(p.commit, magicBranch.dest.get(), newProgress));
}
- logDebug(
+ logger.atFine().log(
"Finished deferred lookups with %d updates and %d new changes",
replaceByChange.size(), newChanges.size());
} catch (IOException e) {
@@ -2013,10 +2108,10 @@
// identified the missing object earlier before we got control.
//
magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
- logError("Invalid pack upload; one or more objects weren't sent", e);
+ logger.atSevere().withCause(e).log("Invalid pack upload; one or more objects weren't sent");
return Collections.emptyList();
} catch (OrmException e) {
- logError("Cannot query database to locate prior changes", e);
+ logger.atSevere().withCause(e).log("Cannot query database to locate prior changes");
reject(magicBranch.cmd, "database error");
return Collections.emptyList();
}
@@ -2044,9 +2139,9 @@
for (UpdateGroupsRequest update : updateGroups) {
update.groups = ImmutableList.copyOf((groups.get(update.commit)));
}
- logDebug("Finished updating groups from GroupCollector");
+ logger.atFine().log("Finished updating groups from GroupCollector");
} catch (OrmException e) {
- logError("Error collecting groups for changes", e);
+ logger.atSevere().withCause(e).log("Error collecting groups for changes");
reject(magicBranch.cmd, "internal server error");
}
return newChanges;
@@ -2058,7 +2153,7 @@
notesFactory.create(db, project.getNameKey(), Change.Id.fromRef(ref.getName()));
Change change = notes.getChange();
if (change.getDest().equals(magicBranch.dest)) {
- logDebug("Found change %s from existing refs.", change.getKey());
+ logger.atFine().log("Found change %s from existing refs.", change.getKey());
// Reindex the change asynchronously, ignoring errors.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError = indexer.indexAsync(project.getNameKey(), change.getId());
@@ -2079,7 +2174,7 @@
if (magicBranch.baseCommit != null) {
markExplicitBasesUninteresting();
} else if (magicBranch.merged) {
- logDebug("Marking parents of merged commit %s uninteresting", start.name());
+ logger.atFine().log("Marking parents of merged commit %s uninteresting", start.name());
for (RevCommit c : start.getParents()) {
rw.markUninteresting(c);
}
@@ -2090,13 +2185,13 @@
}
private void markExplicitBasesUninteresting() throws IOException {
- logDebug("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
+ logger.atFine().log("Marking %d base commits uninteresting", magicBranch.baseCommit.size());
for (RevCommit c : magicBranch.baseCommit) {
receivePack.getRevWalk().markUninteresting(c);
}
Ref targetRef = allRefs().get(magicBranch.dest.get());
if (targetRef != null) {
- logDebug(
+ logger.atFine().log(
"Marking target ref %s (%s) uninteresting",
magicBranch.dest.get(), targetRef.getObjectId().name());
receivePack
@@ -2150,37 +2245,44 @@
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
i++;
} catch (IOException e) {
- logWarn(String.format("Invalid ref %s in %s", ref.getName(), project.getName()), e);
+ logger.atWarning().withCause(e).log(
+ "Invalid ref %s in %s", ref.getName(), project.getName());
}
}
}
- logDebug("Marked %d heads as uninteresting", i);
+ logger.atFine().log("Marked %d heads as uninteresting", i);
}
private static boolean isValidChangeId(String idStr) {
return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
}
- private class ChangeLookup {
+ private static class ChangeLookup {
final RevCommit commit;
- final Change.Key changeKey;
+
+ @Nullable final Change.Key changeKey;
final List<ChangeData> destChanges;
- ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
- commit = c;
- changeKey = key;
- destChanges = queryProvider.get().byBranchKey(magicBranch.dest, key);
- }
-
- ChangeLookup(RevCommit c) throws OrmException {
- commit = c;
- destChanges = queryProvider.get().byBranchCommit(magicBranch.dest, c.getName());
- changeKey = null;
+ ChangeLookup(RevCommit c, @Nullable Change.Key key, final List<ChangeData> destChanges) {
+ this.commit = c;
+ this.changeKey = key;
+ this.destChanges = destChanges;
}
}
+ ChangeLookup lookupByChangeKey(RevCommit c, Change.Key key) throws OrmException {
+ return new ChangeLookup(c, key, queryProvider.get().byBranchKey(magicBranch.dest, key));
+ }
+
+ ChangeLookup lookupByCommit(RevCommit c) throws OrmException {
+ return new ChangeLookup(
+ c, null, queryProvider.get().byBranchCommit(magicBranch.dest, c.getName()));
+ }
+
+ /** Represents a commit for which a Change should be created. */
private class CreateRequest {
final RevCommit commit;
+ final Task progress;
private final String refName;
Change.Id changeId;
@@ -2190,9 +2292,10 @@
Change change;
- CreateRequest(RevCommit commit, String refName) {
+ CreateRequest(RevCommit commit, String refName, Task progress) {
this.commit = commit;
this.refName = refName;
+ this.progress = progress;
}
private void setChangeId(int id) {
@@ -2287,7 +2390,7 @@
return false;
}
});
- bu.addOp(changeId, new ChangeProgressOp(newProgress));
+ bu.addOp(changeId, new ChangeProgressOp(progress));
} catch (Exception e) {
throw INSERT_EXCEPTION.apply(e);
}
@@ -2308,7 +2411,7 @@
Change tipChange = bySha.get(magicBranch.cmd.getNewId());
checkNotNull(
tipChange, "tip of push does not correspond to a change; found these changes: %s", bySha);
- logDebug(
+ logger.atFine().log(
"Processing submit with tip change %s (%s)", tipChange.getId(), magicBranch.cmd.getNewId());
try (MergeOp op = mergeOpProvider.get()) {
op.merge(db, tipChange, user, false, new SubmitInput(), false);
@@ -2321,34 +2424,27 @@
for (Iterator<ReplaceRequest> itr = replaceByChange.values().iterator(); itr.hasNext(); ) {
ReplaceRequest req = itr.next();
if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
- req.validate(false);
- if (req.skip && req.cmd == null) {
- itr.remove();
- }
+ req.validateNewPatchSet();
}
}
} catch (OrmException err) {
- logError(
- String.format(
- "Cannot read database before replacement for project %s", project.getName()),
- err);
+ logger.atSevere().withCause(err).log(
+ "Cannot read database before replacement for project %s", project.getName());
for (ReplaceRequest req : replaceByChange.values()) {
if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
}
}
} catch (IOException | PermissionBackendException err) {
- logError(
- String.format(
- "Cannot read repository before replacement for project %s", project.getName()),
- err);
+ logger.atSevere().withCause(err).log(
+ "Cannot read repository before replacement for project %s", project.getName());
for (ReplaceRequest req : replaceByChange.values()) {
if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
}
}
}
- logDebug("Read %d changes to replace", replaceByChange.size());
+ logger.atFine().log("Read %d changes to replace", replaceByChange.size());
if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
// Cancel creations tied to refs/for/ or refs/drafts/ command.
@@ -2372,6 +2468,7 @@
}
}
+ /** Represents a commit that should be stored in a new patchset of an existing change. */
private class ReplaceRequest {
final Change.Id ontoChange;
final ObjectId newCommitId;
@@ -2383,7 +2480,6 @@
ReceiveCommand prev;
ReceiveCommand cmd;
PatchSetInfo info;
- boolean skip;
private PatchSet.Id priorPatchSet;
List<String> groups = ImmutableList.of();
private ReplaceOp replaceOp;
@@ -2402,10 +2498,8 @@
receivePack.getRevWalk().parseCommit(ref.getObjectId()),
PatchSet.Id.fromRef(ref.getName()));
} catch (IOException err) {
- logWarn(
- String.format(
- "Project %s contains invalid change ref %s", project.getName(), ref.getName()),
- err);
+ logger.atWarning().withCause(err).log(
+ "Project %s contains invalid change ref %s", project.getName(), ref.getName());
}
}
}
@@ -2421,18 +2515,46 @@
* <li>May reset {@code receivePack.getRevWalk()}; do not call in the middle of a walk.
* </ul>
*
- * @param autoClose whether the caller intends to auto-close the change after adding a new patch
- * set.
* @return whether the new commit is valid
* @throws IOException
* @throws OrmException
* @throws PermissionBackendException
*/
- boolean validate(boolean autoClose)
- throws IOException, OrmException, PermissionBackendException {
- if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
+ boolean validateNewPatchSet() throws IOException, OrmException, PermissionBackendException {
+ if (!validateNewPatchSetCommit()) {
return false;
- } else if (notes == null) {
+ }
+ sameTreeWarning();
+
+ if (magicBranch != null) {
+ validateMagicBranchWipStatusChange();
+ if (inputCommand.getResult() != NOT_ATTEMPTED) {
+ return false;
+ }
+
+ if (magicBranch.edit || magicBranch.draft) {
+ return newEdit();
+ }
+ }
+
+ newPatchSet();
+ return true;
+ }
+
+ boolean validateNewPatchSetForAutoClose()
+ throws IOException, OrmException, PermissionBackendException {
+ if (!validateNewPatchSetCommit()) {
+ return false;
+ }
+
+ newPatchSet();
+ return true;
+ }
+
+ /** Validates the new PS against permissions and notedb status. */
+ private boolean validateNewPatchSetCommit()
+ throws IOException, OrmException, PermissionBackendException {
+ if (notes == null) {
reject(inputCommand, "change " + ontoChange + " not found");
return false;
}
@@ -2445,7 +2567,6 @@
}
RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
- RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
// Not allowed to create a new patch set if the current patch set is locked.
if (psUtil.isPatchSetLocked(notes)) {
@@ -2460,10 +2581,6 @@
return false;
}
- if (!projectState.statePermitsWrite()) {
- reject(inputCommand, "cannot add patch set to " + ontoChange + ".");
- return false;
- }
if (change.getStatus().isClosed()) {
reject(inputCommand, "change " + ontoChange + " closed");
return false;
@@ -2493,44 +2610,13 @@
receivePack.getRevWalk(), change.getDest(), inputCommand, newCommit, change)) {
return false;
}
- receivePack.getRevWalk().parseBody(priorCommit);
+ return true;
+ }
- // Don't allow the same tree if the commit message is unmodified
- // or no parents were updated (rebase), else warn that only part
- // of the commit was modified.
- if (newCommit.getTree().equals(priorCommit.getTree())) {
- boolean messageEq =
- Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
- boolean parentsEq = parentsEqual(newCommit, priorCommit);
- boolean authorEq = authorEqual(newCommit, priorCommit);
- ObjectReader reader = receivePack.getRevWalk().getObjectReader();
-
- if (messageEq && parentsEq && authorEq && !autoClose) {
- addMessage(
- String.format(
- "(W) No changes between prior commit %s and new commit %s",
- reader.abbreviate(priorCommit).name(), reader.abbreviate(newCommit).name()));
- } else {
- StringBuilder msg = new StringBuilder();
- msg.append("(I) ");
- msg.append(reader.abbreviate(newCommit).name());
- msg.append(":");
- msg.append(" no files changed");
- if (!authorEq) {
- msg.append(", author changed");
- }
- if (!messageEq) {
- msg.append(", message updated");
- }
- if (!parentsEq) {
- msg.append(", was rebased");
- }
- addMessage(msg.toString());
- }
- }
-
- if (magicBranch != null
- && (magicBranch.workInProgress || magicBranch.ready)
+ /** Validates whether the WIP change is allowed. Rejects inputCommand if not. */
+ private void validateMagicBranchWipStatusChange() throws PermissionBackendException {
+ Change change = notes.getChange();
+ if ((magicBranch.workInProgress || magicBranch.ready)
&& magicBranch.workInProgress != change.isWorkInProgress()
&& !user.getAccountId().equals(change.getOwner())) {
boolean hasWriteConfigPermission = false;
@@ -2546,19 +2632,51 @@
permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
} catch (AuthException e1) {
reject(inputCommand, ONLY_CHANGE_OWNER_OR_PROJECT_OWNER_CAN_MODIFY_WIP);
- return false;
}
}
}
-
- if (magicBranch != null && (magicBranch.edit || magicBranch.draft)) {
- return newEdit();
- }
-
- newPatchSet();
- return true;
}
+ /** prints a warning if the new PS has the same tree as the previous commit. */
+ private void sameTreeWarning() throws IOException {
+ RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+ RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+
+ if (newCommit.getTree().equals(priorCommit.getTree())) {
+ boolean messageEq =
+ Objects.equals(newCommit.getFullMessage(), priorCommit.getFullMessage());
+ boolean parentsEq = parentsEqual(newCommit, priorCommit);
+ boolean authorEq = authorEqual(newCommit, priorCommit);
+ ObjectReader reader = receivePack.getRevWalk().getObjectReader();
+
+ if (messageEq && parentsEq && authorEq) {
+ addMessage(
+ String.format(
+ "warning: no changes between prior commit %s and new commit %s",
+ reader.abbreviate(priorCommit).name(), reader.abbreviate(newCommit).name()));
+ } else {
+ StringBuilder msg = new StringBuilder();
+ msg.append("warning: ").append(reader.abbreviate(newCommit).name());
+ msg.append(":");
+ msg.append(" no files changed");
+ if (!authorEq) {
+ msg.append(", author changed");
+ }
+ if (!messageEq) {
+ msg.append(", message updated");
+ }
+ if (!parentsEq) {
+ msg.append(", was rebased");
+ }
+ addMessage(msg.toString());
+ }
+ }
+ }
+
+ /**
+ * Sets cmd and prev to the ReceiveCommands for change edits. Returns false if there was a
+ * failure.
+ */
private boolean newEdit() {
psId = notes.getChange().currentPatchSetId();
Optional<ChangeEdit> edit = null;
@@ -2566,7 +2684,7 @@
try {
edit = editUtil.byChange(notes, user);
} catch (AuthException | IOException e) {
- logError("Cannot retrieve edit", e);
+ logger.atSevere().withCause(e).log("Cannot retrieve edit");
return false;
}
@@ -2589,8 +2707,8 @@
return true;
}
+ /** Creates a ReceiveCommand for a new edit. */
private void createEditCommand() {
- // create new edit
cmd =
new ReceiveCommand(
ObjectId.zeroId(),
@@ -2598,6 +2716,7 @@
RefNames.refsEdit(user.getAccountId(), notes.getChangeId(), psId));
}
+ /** Updates 'this' to add a new patchset. */
private void newPatchSet() throws IOException, OrmException {
RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
psId =
@@ -2699,11 +2818,11 @@
public void postUpdate(Context ctx) {
String refName = cmd.getRefName();
if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
- logDebug("Updating tag cache on fast-forward of %s", cmd.getRefName());
+ logger.atFine().log("Updating tag cache on fast-forward of %s", cmd.getRefName());
tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId());
}
if (isConfig(cmd)) {
- logDebug("Reloading project in cache");
+ logger.atFine().log("Reloading project in cache");
try {
projectCache.evict(project);
} catch (IOException e) {
@@ -2712,7 +2831,7 @@
}
ProjectState ps = projectCache.get(project.getNameKey());
try {
- logDebug("Updating project description");
+ logger.atFine().log("Updating project description");
repo.setGitwebDescription(ps.getProject().getDescription());
} catch (IOException e) {
logger.atWarning().withCause(e).log("cannot update description of %s", project.getName());
@@ -2797,6 +2916,8 @@
&& Objects.equals(aAuthor.getEmailAddress(), bAuthor.getEmailAddress());
}
+ // Run RefValidators on the command. If any validator fails, the command status is set to
+ // REJECTED, and the return value is 'false'
private boolean validRefOperation(ReceiveCommand cmd) {
RefOperationValidators refValidators = refValidatorsFactory.create(getProject(), user, cmd);
@@ -2813,25 +2934,25 @@
private void validateNewCommits(Branch.NameKey branch, ReceiveCommand cmd)
throws PermissionBackendException {
- PermissionBackend.ForRef perm = permissions.ref(branch.get());
if (!RefNames.REFS_CONFIG.equals(cmd.getRefName())
&& !(MagicBranch.isMagicBranch(cmd.getRefName())
|| NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
&& pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION)) {
- try {
- if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
- throw new AuthException(
- "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
- }
-
- perm.check(RefPermission.SKIP_VALIDATION);
- if (!Iterables.isEmpty(rejectCommits)) {
- throw new AuthException("reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
- }
- logDebug("Short-circuiting new commit validation");
- } catch (AuthException denied) {
- reject(cmd, denied.getMessage());
+ if (projectState.is(BooleanProjectConfig.USE_SIGNED_OFF_BY)) {
+ reject(cmd, "requireSignedOffBy prevents option " + PUSH_OPTION_SKIP_VALIDATION);
+ return;
}
+
+ Optional<AuthException> err =
+ checkRefPermission(permissions.ref(branch.get()), RefPermission.SKIP_VALIDATION);
+ if (err.isPresent()) {
+ rejectProhibited(cmd, err.get());
+ return;
+ }
+ if (!Iterables.isEmpty(rejectCommits)) {
+ reject(cmd, "reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
+ }
+ logger.atFine().log("Short-circuiting new commit validation");
return;
}
@@ -2851,13 +2972,11 @@
int n = 0;
for (RevCommit c; (c = walk.next()) != null; ) {
if (++n > limit) {
- logDebug("Number of new commits exceeds limit of %d", limit);
- addMessage(
+ logger.atFine().log("Number of new commits exceeds limit of %d", limit);
+ reject(
+ cmd,
String.format(
- "Cannot push more than %d commits to %s without %s option "
- + "(see %sDocumentation/user-upload.html#skip_validation for details)",
- limit, branch.get(), PUSH_OPTION_SKIP_VALIDATION, canonicalWebUrl));
- reject(cmd, "too many commits");
+ "more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
return;
}
if (existing.keySet().contains(c)) {
@@ -2867,15 +2986,15 @@
}
if (missingFullName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
- logDebug("Will update full name of caller");
+ logger.atFine().log("Will update full name of caller");
setFullNameTo = c.getCommitterIdent().getName();
missingFullName = false;
}
}
- logDebug("Validated %d new commits", n);
+ logger.atFine().log("Validated %d new commits", n);
} catch (IOException err) {
cmd.setResult(REJECTED_MISSING_OBJECT);
- logError("Invalid pack upload; one or more objects weren't sent", err);
+ logger.atSevere().withCause(err).log("Invalid pack upload; one or more objects weren't sent");
}
}
@@ -2906,7 +3025,7 @@
perm, branch, user.asIdentifiedUser(), sshInfo, repo, rw, change);
messages.addAll(validators.validate(receiveEvent));
} catch (CommitValidationException e) {
- logDebug("Commit validation failed on %s", c.name());
+ logger.atFine().log("Commit validation failed on %s", c.name());
messages.addAll(e.getMessages());
reject(cmd, e.getMessage());
return false;
@@ -2915,15 +3034,16 @@
return true;
}
- private void autoCloseChanges(ReceiveCommand cmd) {
- logDebug("Starting auto-closing of changes");
+ private void autoCloseChanges(ReceiveCommand cmd, Task progress) {
+ logger.atFine().log("Starting auto-closing of changes");
String refName = cmd.getRefName();
checkState(
!MagicBranch.isMagicBranch(refName),
"shouldn't be auto-closing changes on magic branch %s",
refName);
+
// TODO(dborowitz): Combine this BatchUpdate with the main one in
- // insertChangesAndPatchSets.
+ // handleRegularCommands
try {
retryHelper.execute(
updateFactory -> {
@@ -2933,7 +3053,6 @@
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo, rw, ins).updateChangesInParallel();
- bu.setRequestId(receiveId);
// TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
RevCommit newTip = rw.parseCommit(cmd.getNewId());
@@ -2987,8 +3106,8 @@
for (ReplaceRequest req : replaceAndClose) {
Change.Id id = req.notes.getChangeId();
- if (!req.validate(true)) {
- logDebug("Not closing %s because validation failed", id);
+ if (!req.validateNewPatchSetForAutoClose()) {
+ logger.atFine().log("Not closing %s because validation failed", id);
continue;
}
req.addOps(bu, null);
@@ -2997,15 +3116,15 @@
mergedByPushOpFactory
.create(requestScopePropagator, req.psId, refName)
.setPatchSetProvider(req.replaceOp::getPatchSet));
- bu.addOp(id, new ChangeProgressOp(closeProgress));
+ bu.addOp(id, new ChangeProgressOp(progress));
}
- logDebug(
+ logger.atFine().log(
"Auto-closing %s changes with existing patch sets and %s with new patch sets",
existingPatchSets, newPatchSets);
bu.execute();
} catch (IOException | OrmException | PermissionBackendException e) {
- logError("Failed to auto-close changes", e);
+ logger.atSevere().withCause(e).log("Failed to auto-close changes");
}
return null;
},
@@ -3015,9 +3134,9 @@
.timeout(retryHelper.getDefaultTimeout(ActionType.CHANGE_UPDATE).multipliedBy(5))
.build());
} catch (RestApiException e) {
- logError("Can't insert patchset", e);
+ logger.atSevere().withCause(e).log("Can't insert patchset");
} catch (UpdateException e) {
- logError("Failed to auto-close changes", e);
+ logger.atSevere().withCause(e).log("Failed to auto-close changes");
}
}
@@ -3043,7 +3162,7 @@
if (setFullNameTo == null) {
return;
}
- logDebug("Updating full name of caller");
+ logger.atFine().log("Updating full name of caller");
try {
Optional<AccountState> accountState =
accountsUpdateProvider
@@ -3060,7 +3179,7 @@
.map(AccountState::getAccount)
.ifPresent(a -> user.getAccount().setFullName(a.getFullName()));
} catch (OrmException | IOException | ConfigInvalidException e) {
- logWarn("Failed to update full name of caller", e);
+ logger.atWarning().withCause(e).log("Failed to update full name of caller");
}
}
@@ -3084,11 +3203,8 @@
return allRefsWatcher.getAllRefs();
}
- private void reject(@Nullable ReceiveCommand cmd, String why) {
- if (cmd != null) {
- cmd.setResult(REJECTED_OTHER_REASON, why);
- commandProgress.update(1);
- }
+ private void reject(ReceiveCommand cmd, String why) {
+ cmd.setResult(REJECTED_OTHER_REASON, why);
}
private static boolean isHead(ReceiveCommand cmd) {
@@ -3098,46 +3214,4 @@
private static boolean isConfig(ReceiveCommand cmd) {
return cmd.getRefName().equals(RefNames.REFS_CONFIG);
}
-
- private void logDebug(String msg) {
- logger.atFine().log(receiveId + msg);
- }
-
- private void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(receiveId + msg, arg);
- }
-
- private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
- logger.atFine().log(receiveId + msg, arg1, arg2);
- }
-
- private void logDebug(
- String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
- logger.atFine().log(receiveId + msg, arg1, arg2, arg3);
- }
-
- private void logDebug(
- String msg,
- @Nullable Object arg1,
- @Nullable Object arg2,
- @Nullable Object arg3,
- @Nullable Object arg4) {
- logger.atFine().log(receiveId + msg, arg1, arg2, arg3, arg4);
- }
-
- private void logWarn(String msg, Throwable t) {
- logger.atWarning().withCause(t).log("%s%s", receiveId, msg);
- }
-
- private void logWarn(String msg) {
- logWarn(msg, null);
- }
-
- private void logError(String msg, Throwable t) {
- logger.atSevere().withCause(t).log("%s%s", receiveId, msg);
- }
-
- private void logError(String msg) {
- logError(msg, null);
- }
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index b71f01e..03a1b33 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -24,8 +24,7 @@
"only change owner or project owner can modify Work-in-Progress";
static final String COMMAND_REJECTION_MESSAGE_FOOTER =
- "Please read the documentation and contact an administrator\n"
- + "if you feel the configuration is incorrect";
+ "Contact an administrator to fix the permissions";
static final String SAME_CHANGE_ID_IN_MULTIPLE_CHANGES =
"same Change-Id in multiple changes.\n"
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index 1b74241..80d462f 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -25,6 +25,7 @@
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multiset;
+import com.google.common.flogger.FluentLogger;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupReference;
@@ -87,6 +88,8 @@
* </ul>
*/
public class GroupNameNotes extends VersionedMetaData {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private static final String SECTION_NAME = "group";
private static final String UUID_PARAM = "uuid";
private static final String NAME_PARAM = "name";
@@ -323,6 +326,8 @@
protected void onLoad() throws IOException, ConfigInvalidException {
nameConflicting = false;
+ logger.atFine().log("Reading group notes");
+
if (revision != null) {
NoteMap noteMap = NoteMap.read(reader, revision);
if (newGroupName.isPresent()) {
@@ -365,6 +370,8 @@
return false;
}
+ logger.atFine().log("Updating group notes");
+
NoteMap noteMap = revision == null ? NoteMap.newEmptyMap() : NoteMap.read(reader, revision);
if (oldGroupName.isPresent()) {
removeNote(noteMap, oldGroupName.get(), inserter);
diff --git a/java/com/google/gerrit/server/logging/LoggingContext.java b/java/com/google/gerrit/server/logging/LoggingContext.java
new file mode 100644
index 0000000..04a23e9
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContext.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.flogger.backend.Tags;
+import java.util.logging.Level;
+
+/**
+ * Logging context for Flogger.
+ *
+ * <p>To configure this logging context for Flogger set the following system property (also see
+ * {@link com.google.common.flogger.backend.system.DefaultPlatform}):
+ *
+ * <ul>
+ * <li>{@code
+ * flogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance}.
+ * </ul>
+ */
+public class LoggingContext extends com.google.common.flogger.backend.system.LoggingContext {
+ private static final LoggingContext INSTANCE = new LoggingContext();
+
+ private static final ThreadLocal<MutableTags> tags = new ThreadLocal<>();
+
+ private LoggingContext() {}
+
+ /** This method is expected to be called via reflection (and might otherwise be unused). */
+ public static LoggingContext getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean shouldForceLogging(String loggerName, Level level, boolean isEnabled) {
+ return false;
+ }
+
+ @Override
+ public Tags getTags() {
+ MutableTags mutableTags = tags.get();
+ return mutableTags != null ? mutableTags.getTags() : Tags.empty();
+ }
+
+ public ImmutableSetMultimap<String, String> getTagsAsMap() {
+ MutableTags mutableTags = tags.get();
+ return mutableTags != null ? mutableTags.asMap() : ImmutableSetMultimap.of();
+ }
+
+ boolean addTag(String name, String value) {
+ return getMutableTags().add(name, value);
+ }
+
+ void removeTag(String name, String value) {
+ MutableTags mutableTags = getMutableTags();
+ mutableTags.remove(name, value);
+ if (mutableTags.isEmpty()) {
+ tags.remove();
+ }
+ }
+
+ void setTags(ImmutableSetMultimap<String, String> newTags) {
+ if (newTags.isEmpty()) {
+ tags.remove();
+ return;
+ }
+ getMutableTags().set(newTags);
+ }
+
+ void clearTags() {
+ tags.remove();
+ }
+
+ private MutableTags getMutableTags() {
+ MutableTags mutableTags = tags.get();
+ if (mutableTags == null) {
+ mutableTags = new MutableTags();
+ tags.set(mutableTags);
+ }
+ return mutableTags;
+ }
+}
diff --git a/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java b/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java
new file mode 100644
index 0000000..16d24ac
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/LoggingContextAwareThreadFactory.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * ThreadFactory that copies the logging context of the current thread to any new thread that is
+ * created by this ThreadFactory.
+ */
+public class LoggingContextAwareThreadFactory implements ThreadFactory {
+ private final ThreadFactory parentThreadFactory;
+
+ public LoggingContextAwareThreadFactory() {
+ this.parentThreadFactory = Executors.defaultThreadFactory();
+ }
+
+ public LoggingContextAwareThreadFactory(ThreadFactory parentThreadFactory) {
+ this.parentThreadFactory = parentThreadFactory;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread callingThread = Thread.currentThread();
+ ImmutableSetMultimap<String, String> tags = LoggingContext.getInstance().getTagsAsMap();
+ return parentThreadFactory.newThread(
+ () -> {
+ if (callingThread.equals(Thread.currentThread())) {
+ // propagation of logging context is not needed
+ r.run();
+ return;
+ }
+
+ // propagate logging context
+ LoggingContext.getInstance().setTags(tags);
+ try {
+ r.run();
+ } finally {
+ LoggingContext.getInstance().clearTags();
+ }
+ });
+ }
+}
diff --git a/java/com/google/gerrit/server/logging/MutableTags.java b/java/com/google/gerrit/server/logging/MutableTags.java
new file mode 100644
index 0000000..a936a43
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/MutableTags.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.backend.Tags;
+
+public class MutableTags {
+ private final SetMultimap<String, String> tagMap =
+ MultimapBuilder.hashKeys().hashSetValues().build();
+ private Tags tags = Tags.empty();
+
+ public Tags getTags() {
+ return tags;
+ }
+
+ /**
+ * Adds a tag if a tag with the same name and value doesn't exist yet.
+ *
+ * @param name the name of the tag
+ * @param value the value of the tag
+ * @return {@code true} if the tag was added, {@code false} if the tag was not added because it
+ * already exists
+ */
+ public boolean add(String name, String value) {
+ checkNotNull(name, "tag name is required");
+ checkNotNull(value, "tag value is required");
+ boolean ret = tagMap.put(name, value);
+ if (ret) {
+ buildTags();
+ }
+ return ret;
+ }
+
+ /**
+ * Removes the tag with the given name and value.
+ *
+ * @param name the name of the tag
+ * @param value the value of the tag
+ */
+ public void remove(String name, String value) {
+ checkNotNull(name, "tag name is required");
+ checkNotNull(value, "tag value is required");
+ if (tagMap.remove(name, value)) {
+ buildTags();
+ }
+ }
+
+ /**
+ * Checks if the contained tag map is empty.
+ *
+ * @return {@code true} if there are no tags, otherwise {@code false}
+ */
+ public boolean isEmpty() {
+ return tagMap.isEmpty();
+ }
+
+ /** Clears all tags. */
+ public void clear() {
+ tagMap.clear();
+ tags = Tags.empty();
+ }
+
+ /**
+ * Returns the tags as Multimap.
+ *
+ * @return the tags as Multimap
+ */
+ public ImmutableSetMultimap<String, String> asMap() {
+ return ImmutableSetMultimap.copyOf(tagMap);
+ }
+
+ /**
+ * Replaces the existing tags with the provided tags.
+ *
+ * @param tags the tags that should be set.
+ */
+ void set(ImmutableSetMultimap<String, String> tags) {
+ tagMap.clear();
+ tags.forEach(tagMap::put);
+ buildTags();
+ }
+
+ private void buildTags() {
+ if (tagMap.isEmpty()) {
+ if (tags.isEmpty()) {
+ return;
+ }
+ tags = Tags.empty();
+ return;
+ }
+
+ Tags.Builder tagsBuilder = Tags.builder();
+ tagMap.forEach(tagsBuilder::addTag);
+ tags = tagsBuilder.build();
+ }
+}
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
new file mode 100644
index 0000000..cb479cc
--- /dev/null
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.google.gerrit.server.util.RequestId;
+
+public class TraceContext implements AutoCloseable {
+ public static final TraceContext DISABLED = new TraceContext();
+
+ public static TraceContext open() {
+ return new TraceContext();
+ }
+
+ // Table<TAG_NAME, TAG_VALUE, REMOVE_ON_CLOSE>
+ private final Table<String, String, Boolean> tags = HashBasedTable.create();
+
+ private TraceContext() {}
+
+ public TraceContext addTag(RequestId.Type requestId, Object tagValue) {
+ return addTag(checkNotNull(requestId, "request ID is required").name(), tagValue);
+ }
+
+ public TraceContext addTag(String tagName, Object tagValue) {
+ String name = checkNotNull(tagName, "tag name is required");
+ String value = checkNotNull(tagValue, "tag value is required").toString();
+ tags.put(name, value, LoggingContext.getInstance().addTag(name, value));
+ return this;
+ }
+
+ @Override
+ public void close() {
+ for (Table.Cell<String, String, Boolean> cell : tags.cellSet()) {
+ if (cell.getValue()) {
+ LoggingContext.getInstance().removeTag(cell.getRowKey(), cell.getColumnKey());
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index a083a71..8f4ad74 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.reviewdb.client.Change;
@@ -42,6 +43,8 @@
/** View of contents at a single ref related to some change. * */
public abstract class AbstractChangeNotes<T> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
@VisibleForTesting
@Singleton
public static class Args {
@@ -145,6 +148,11 @@
if (loaded) {
return self();
}
+
+ logger.atFine().log(
+ "Load %s for change %s of project %s from %s (%s)",
+ getClass().getSimpleName(), getChangeId(), getProjectName(), getRefName(), primaryStorage);
+
boolean read = args.migration.readChanges();
if (!read && primaryStorage == PrimaryStorage.NOTE_DB) {
throw new OrmException("NoteDb is required to read change " + changeId);
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 010c5c0..e0cc771 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -41,6 +42,8 @@
/** A single delta related to a specific patch-set of a change. */
public abstract class AbstractChangeUpdate {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected final NotesMigration migration;
protected final ChangeNoteUtil noteUtil;
protected final Account.Id accountId;
@@ -218,6 +221,11 @@
checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
checkNotReadOnly();
+
+ logger.atFinest().log(
+ "%s for change %s of project %s in %s (NoteDb)",
+ getClass().getSimpleName(), getId(), getProjectName(), getRefName());
+
ObjectId z = ObjectId.zeroId();
CommitBuilder cb = applyImpl(rw, ins, curr);
if (cb == null) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index f0187ed..41f4ed2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -25,10 +25,10 @@
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.inject.Inject;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 11d6880..c51aec3 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -23,7 +23,7 @@
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.ProtoCacheSerializers.toByteString;
+import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -53,14 +53,14 @@
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gson.Gson;
diff --git a/java/com/google/gerrit/server/patch/DiffExecutorModule.java b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
index 5359479..f3776e0 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutorModule.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.patch;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
@@ -32,6 +33,10 @@
@DiffExecutor
public ExecutorService createDiffExecutor() {
return Executors.newCachedThreadPool(
- new ThreadFactoryBuilder().setNameFormat("Diff-%d").setDaemon(true).build());
+ new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
+ .setNameFormat("Diff-%d")
+ .setDaemon(true)
+ .build());
}
}
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index b4f7251..a3d9048 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -285,6 +285,13 @@
int aSize = a.src.size();
int bSize = b.src.size();
+ if (edits.isEmpty() && (aSize == 0 || bSize == 0)) {
+ // The diff was requested for a file which was either added or deleted but which JGit doesn't
+ // consider a file addition/deletion (e.g. requesting a diff for the old file name of a
+ // renamed file looks like a deletion).
+ return;
+ }
+
Optional<Edit> lastEdit = getLast(edits);
if (isNewlineAtEndDeleted()) {
Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
diff --git a/java/com/google/gerrit/server/permissions/PermissionDeniedException.java b/java/com/google/gerrit/server/permissions/PermissionDeniedException.java
new file mode 100644
index 0000000..6018263
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/PermissionDeniedException.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.extensions.api.access.GerritPermission;
+import com.google.gerrit.extensions.restapi.AuthException;
+import java.util.Optional;
+
+/**
+ * This signals that some permission check failed. The message is short so it can print on a
+ * single-line in the Git output.
+ */
+public class PermissionDeniedException extends AuthException {
+ private static final long serialVersionUID = 1L;
+
+ public static final String MESSAGE_PREFIX = "not permitted: ";
+
+ private final GerritPermission permission;
+ private final Optional<String> resource;
+
+ public PermissionDeniedException(GerritPermission permission) {
+ super(MESSAGE_PREFIX + checkNotNull(permission).describeForException());
+ this.permission = permission;
+ this.resource = Optional.empty();
+ }
+
+ public PermissionDeniedException(GerritPermission permission, String resource) {
+ super(
+ MESSAGE_PREFIX
+ + checkNotNull(permission).describeForException()
+ + " on "
+ + checkNotNull(resource));
+ this.permission = permission;
+ this.resource = Optional.of(resource);
+ }
+
+ public String describePermission() {
+ return permission.describeForException();
+ }
+
+ public Optional<String> getResource() {
+ return resource;
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 3bd2817..762dfbe 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
@@ -41,6 +42,8 @@
/** Manages access control for Git references (aka branches, tags). */
class RefControl {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final ProjectControl projectControl;
private final String refName;
@@ -392,15 +395,37 @@
/** True if the user has this permission. */
private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
if (isBlocked(permissionName, isChangeOwner, withForce)) {
+ logger.atFine().log(
+ "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'"
+ + " because this permission is blocked",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName);
return false;
}
for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ logger.atFine().log(
+ "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName);
return true;
}
}
+ logger.atFine().log(
+ "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s'",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName);
return false;
}
@@ -449,7 +474,88 @@
@Override
public void check(RefPermission perm) throws AuthException, PermissionBackendException {
if (!can(perm)) {
- throw new AuthException(perm.describeForException() + " not permitted for " + refName);
+ PermissionDeniedException pde = new PermissionDeniedException(perm, refName);
+ switch (perm) {
+ case UPDATE:
+ if (refName.equals(RefNames.REFS_CONFIG)) {
+ pde.setAdvice(
+ "Configuration changes can only be pushed by project owners\n"
+ + "who also have 'Push' rights on "
+ + RefNames.REFS_CONFIG);
+ } else {
+ pde.setAdvice("To push into this reference you need 'Push' rights.");
+ }
+ break;
+ case DELETE:
+ pde.setAdvice(
+ "You need 'Delete Reference' rights or 'Push' rights with the \n"
+ + "'Force Push' flag set to delete references.");
+ break;
+ case CREATE_CHANGE:
+ // This is misleading in the default permission backend, since "create change" on a
+ // branch is encoded as "push" on refs/for/DESTINATION.
+ pde.setAdvice(
+ "You need 'Create Change' rights to upload code review requests.\n"
+ + "Verify that you are pushing to the right branch.");
+ break;
+ case CREATE:
+ pde.setAdvice("You need 'Create' rights to create new references.");
+ break;
+ case CREATE_SIGNED_TAG:
+ pde.setAdvice("You need 'Create Signed Tag' rights to push a signed tag.");
+ break;
+ case CREATE_TAG:
+ pde.setAdvice("You need 'Create Tag' rights to push a normal tag.");
+ break;
+ case FORCE_UPDATE:
+ pde.setAdvice(
+ "You need 'Push' rights with 'Force' flag set to do a non-fastforward push.");
+ break;
+ case FORGE_AUTHOR:
+ pde.setAdvice(
+ "You need 'Forge Author' rights to push commits with another user as author.");
+ break;
+ case FORGE_COMMITTER:
+ pde.setAdvice(
+ "You need 'Forge Committer' rights to push commits with another user as committer.");
+ break;
+ case FORGE_SERVER:
+ pde.setAdvice(
+ "You need 'Forge Server' rights to push merge commits authored by the server.");
+ break;
+ case MERGE:
+ pde.setAdvice(
+ "You need 'Push Merge' in addition to 'Push' rights to push merge commits.");
+ break;
+
+ case READ:
+ pde.setAdvice("You need 'Read' rights to fetch or clone this ref.");
+ break;
+
+ case READ_CONFIG:
+ pde.setAdvice("You need 'Read' rights on refs/meta/config to see the configuration.");
+ break;
+ case READ_PRIVATE_CHANGES:
+ pde.setAdvice("You need 'Read Private Changes' to see private changes.");
+ break;
+ case SET_HEAD:
+ pde.setAdvice("You need 'Set HEAD' rights to set the default branch.");
+ break;
+ case SKIP_VALIDATION:
+ pde.setAdvice(
+ "You need 'Forge Author', 'Forge Server', 'Forge Committer'\n"
+ + "and 'Push Merge' rights to skip validation.");
+ break;
+ case UPDATE_BY_SUBMIT:
+ pde.setAdvice(
+ "You need 'Submit' rights on refs/for/ to submit changes during change upload.");
+ break;
+
+ case WRITE_CONFIG:
+ pde.setAdvice("You need 'Write' rights on refs/meta/config.");
+ break;
+ }
+ throw pde;
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheClock.java b/java/com/google/gerrit/server/project/ProjectCacheClock.java
index 5d208f3..188ee08 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheClock.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheClock.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.concurrent.Executors;
@@ -56,6 +57,7 @@
Executors.newScheduledThreadPool(
1,
new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
.setNameFormat("ProjectCacheClock-%d")
.setDaemon(true)
.setPriority(Thread.MIN_PRIORITY)
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index df80e35..090c4f5 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -269,6 +269,7 @@
@Override
public ProjectState load(String projectName) throws Exception {
+ logger.atFine().log("Loading project %s", projectName);
long now = clock.read();
Project.NameKey key = new Project.NameKey(projectName);
try (Repository git = mgr.openRepository(key)) {
@@ -298,6 +299,7 @@
@Override
public ImmutableSortedSet<Project.NameKey> load(ListKey key) throws Exception {
+ logger.atFine().log("Loading project list");
return ImmutableSortedSet.copyOf(mgr.list());
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 7ebbc51..adfaf62 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -46,7 +47,10 @@
ThreadPoolExecutor pool =
new ScheduledThreadPoolExecutor(
config.getInt("cache", "projects", "loadThreads", cpus),
- new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build());
+ new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
+ .setNameFormat("ProjectCacheLoader-%d")
+ .build());
Thread scheduler =
new Thread(
() -> {
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 726e513..dafe639 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -46,6 +46,7 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.BranchOrderSection;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -87,6 +88,7 @@
private final ProjectConfig config;
private final Map<String, ProjectLevelConfig> configs;
private final Set<AccountGroup.UUID> localOwners;
+ private final long globalMaxObjectSizeLimit;
/** Last system time the configuration's revision was examined. */
private volatile long lastCheckGeneration;
@@ -107,14 +109,15 @@
@Inject
public ProjectState(
- final SitePaths sitePaths,
- final ProjectCache projectCache,
- final AllProjectsName allProjectsName,
- final AllUsersName allUsersName,
- final GitRepositoryManager gitMgr,
- final List<CommentLinkInfo> commentLinks,
- final CapabilityCollection.Factory limitsFactory,
- @Assisted final ProjectConfig config) {
+ SitePaths sitePaths,
+ ProjectCache projectCache,
+ AllProjectsName allProjectsName,
+ AllUsersName allUsersName,
+ GitRepositoryManager gitMgr,
+ List<CommentLinkInfo> commentLinks,
+ CapabilityCollection.Factory limitsFactory,
+ TransferConfig transferConfig,
+ @Assisted ProjectConfig config) {
this.sitePaths = sitePaths;
this.projectCache = projectCache;
this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
@@ -128,6 +131,7 @@
isAllProjects
? limitsFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
: null;
+ this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit();
if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
localOwners = Collections.emptySet();
@@ -260,6 +264,15 @@
}
}
+ public long getEffectiveMaxObjectSizeLimit() {
+ long local = getMaxObjectSizeLimit();
+ if (globalMaxObjectSizeLimit > 0 && local > 0) {
+ return Math.min(globalMaxObjectSizeLimit, local);
+ }
+ // zero means "no limit", in this case the max is more limiting
+ return Math.max(globalMaxObjectSizeLimit, local);
+ }
+
/** Get the sections that pertain only to this project. */
List<SectionMatcher> getLocalAccessSections() {
List<SectionMatcher> sm = localAccessSections;
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
new file mode 100644
index 0000000..e61c5df
--- /dev/null
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -0,0 +1,333 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.index.query.Predicate.and;
+import static com.google.gerrit.index.query.Predicate.or;
+import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
+import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo.AutoCloseableChangesCheckResult;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeIdPredicate;
+import com.google.gerrit.server.query.change.CommitPredicate;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.query.change.ProjectPredicate;
+import com.google.gerrit.server.query.change.RefPredicate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+@Singleton
+public class ProjectsConsistencyChecker {
+ @VisibleForTesting public static final int AUTO_CLOSE_MAX_COMMITS_LIMIT = 10000;
+
+ private final GitRepositoryManager repoManager;
+ private final RetryHelper retryHelper;
+ private final Provider<InternalChangeQuery> changeQueryProvider;
+ private final ChangeJson.Factory changeJsonFactory;
+ private final IndexConfig indexConfig;
+
+ @Inject
+ ProjectsConsistencyChecker(
+ GitRepositoryManager repoManager,
+ RetryHelper retryHelper,
+ Provider<InternalChangeQuery> changeQueryProvider,
+ ChangeJson.Factory changeJsonFactory,
+ IndexConfig indexConfig) {
+ this.repoManager = repoManager;
+ this.retryHelper = retryHelper;
+ this.changeQueryProvider = changeQueryProvider;
+ this.changeJsonFactory = changeJsonFactory;
+ this.indexConfig = indexConfig;
+ }
+
+ public CheckProjectResultInfo check(Project.NameKey projectName, CheckProjectInput input)
+ throws IOException, OrmException, RestApiException {
+ CheckProjectResultInfo r = new CheckProjectResultInfo();
+ if (input.autoCloseableChangesCheck != null) {
+ r.autoCloseableChangesCheckResult =
+ checkForAutoCloseableChanges(projectName, input.autoCloseableChangesCheck);
+ }
+ return r;
+ }
+
+ private AutoCloseableChangesCheckResult checkForAutoCloseableChanges(
+ Project.NameKey projectName, AutoCloseableChangesCheckInput input)
+ throws IOException, OrmException, RestApiException {
+ AutoCloseableChangesCheckResult r = new AutoCloseableChangesCheckResult();
+ if (Strings.isNullOrEmpty(input.branch)) {
+ throw new BadRequestException("branch is required");
+ }
+
+ boolean fix = input.fix != null ? input.fix : false;
+
+ if (input.maxCommits != null && input.maxCommits > AUTO_CLOSE_MAX_COMMITS_LIMIT) {
+ throw new BadRequestException(
+ "max commits can at most be set to " + AUTO_CLOSE_MAX_COMMITS_LIMIT);
+ }
+ int maxCommits = input.maxCommits != null ? input.maxCommits : AUTO_CLOSE_MAX_COMMITS_LIMIT;
+
+ // Result that we want to return to the client.
+ List<ChangeInfo> autoCloseableChanges = new ArrayList<>();
+
+ // Remember the change IDs of all changes that we already included into the result, so that we
+ // can avoid including the same change twice.
+ Set<Change.Id> seenChanges = new HashSet<>();
+
+ try (Repository repo = repoManager.openRepository(projectName);
+ RevWalk rw = new RevWalk(repo)) {
+ String branch = RefNames.fullName(input.branch);
+ Ref ref = repo.exactRef(branch);
+ if (ref == null) {
+ throw new UnprocessableEntityException(
+ String.format("branch '%s' not found", input.branch));
+ }
+
+ rw.reset();
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ rw.sort(RevSort.TOPO);
+ rw.sort(RevSort.REVERSE);
+
+ // Cache the SHA1's of all merged commits. We need this for knowing which commit merged the
+ // change when auto-closing changes by commit.
+ List<ObjectId> mergedSha1s = new ArrayList<>();
+
+ // Cache the Change-Id to commit SHA1 mapping for all Change-Id's that we find in merged
+ // commits. We need this for knowing which commit merged the change when auto-closing
+ // changes by Change-Id.
+ Map<Change.Key, ObjectId> changeIdToMergedSha1 = new HashMap<>();
+
+ // Base predicate which is fixed for every change query.
+ Predicate<ChangeData> basePredicate =
+ and(new ProjectPredicate(projectName.get()), new RefPredicate(branch), open());
+
+ int maxLeafPredicates = indexConfig.maxTerms() - basePredicate.getLeafCount();
+
+ // List of predicates by which we want to find open changes for the branch. These predicates
+ // will be combined with the 'or' operator.
+ List<Predicate<ChangeData>> predicates = new ArrayList<>(maxLeafPredicates);
+
+ RevCommit commit;
+ int skippedCommits = 0;
+ int walkedCommits = 0;
+ while ((commit = rw.next()) != null) {
+ if (input.skipCommits != null && skippedCommits < input.skipCommits) {
+ skippedCommits++;
+ continue;
+ }
+
+ if (walkedCommits >= maxCommits) {
+ break;
+ }
+ walkedCommits++;
+
+ ObjectId commitId = commit.copy();
+ mergedSha1s.add(commitId);
+
+ // Consider all Change-Id lines since this is what ReceiveCommits#autoCloseChanges does.
+ List<String> changeIds = commit.getFooterLines(CHANGE_ID);
+
+ // Number of predicates that we need to add for this commit, 1 per Change-Id plus one for
+ // the commit.
+ int newPredicatesCount = changeIds.size() + 1;
+
+ // We accumulated the max number of query terms that can be used in one query, execute
+ // the query and start a new one.
+ if (predicates.size() + newPredicatesCount > maxLeafPredicates) {
+ autoCloseableChanges.addAll(
+ executeQueryAndAutoCloseChanges(
+ basePredicate, seenChanges, predicates, fix, changeIdToMergedSha1, mergedSha1s));
+ mergedSha1s.clear();
+ changeIdToMergedSha1.clear();
+ predicates.clear();
+
+ if (newPredicatesCount > maxLeafPredicates) {
+ // Whee, a single commit generates more than maxLeafPredicates predicates. Give up.
+ throw new ResourceConflictException(
+ String.format(
+ "commit %s contains more Change-Ids than we can handle", commit.name()));
+ }
+ }
+
+ changeIds.forEach(
+ changeId -> {
+ // It can happen that there are multiple merged commits with the same Change-Id
+ // footer (e.g. if a change was cherry-picked to a stable branch stable branch which
+ // then got merged back into master, or just by directly pushing several commits
+ // with the same Change-Id). In this case it is hard to say which of the commits
+ // should be used to auto-close an open change with the same Change-Id (and branch).
+ // Possible approaches are:
+ // 1. use the oldest commit with that Change-Id to auto-close the change
+ // 2. use the newest commit with that Change-Id to auto-close the change
+ // Possibility 1. has the disadvantage that the commit may have been merged before
+ // the change was created in which case it is strange how it could auto-close the
+ // change. Also this strategy would require to walk all commits since otherwise we
+ // cannot be sure that we have seen the oldest commit with that Change-Id.
+ // Possibility 2 has the disadvantage that it doesn't produce the same result as if
+ // auto-closing on push would have worked, since on direct push the first commit with
+ // a Change-Id of an open change would have closed that change. Also for this we
+ // would need to consider all commits that are skipped.
+ // Since both possibilities are not perfect and require extra effort we choose the
+ // easiest approach, which is use the newest commit with that Change-Id that we have
+ // seen (this means we ignore skipped commits). This should be okay since the
+ // important thing for callers is that auto-closable changes are closed. Which of the
+ // commits is used to auto-close a change if there are several candidates is of minor
+ // importance and hence can be non-deterministic.
+ Change.Key changeKey = new Change.Key(changeId);
+ if (!changeIdToMergedSha1.containsKey(changeKey)) {
+ changeIdToMergedSha1.put(changeKey, commitId);
+ }
+
+ // Find changes that have a matching Change-Id.
+ predicates.add(new ChangeIdPredicate(changeId));
+ });
+
+ // Find changes that have a matching commit.
+ predicates.add(new CommitPredicate(commit.name()));
+ }
+
+ if (predicates.size() > 0) {
+ // Execute the query with the remaining predicates that were collected.
+ autoCloseableChanges.addAll(
+ executeQueryAndAutoCloseChanges(
+ basePredicate, seenChanges, predicates, fix, changeIdToMergedSha1, mergedSha1s));
+ }
+ }
+
+ r.autoCloseableChanges = autoCloseableChanges;
+ return r;
+ }
+
+ private List<ChangeInfo> executeQueryAndAutoCloseChanges(
+ Predicate<ChangeData> basePredicate,
+ Set<Change.Id> seenChanges,
+ List<Predicate<ChangeData>> predicates,
+ boolean fix,
+ Map<Change.Key, ObjectId> changeIdToMergedSha1,
+ List<ObjectId> mergedSha1s)
+ throws OrmException {
+ if (predicates.isEmpty()) {
+ return ImmutableList.of();
+ }
+
+ try {
+ List<ChangeData> queryResult =
+ retryHelper.execute(
+ ActionType.INDEX_QUERY,
+ () -> {
+ // Execute the query.
+ return changeQueryProvider
+ .get()
+ .setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET)
+ .query(and(basePredicate, or(predicates)));
+ },
+ OrmException.class::isInstance);
+
+ // Result for this query that we want to return to the client.
+ List<ChangeInfo> autoCloseableChangesByBranch = new ArrayList<>();
+
+ for (ChangeData autoCloseableChange : queryResult) {
+ // Skip changes that we have already processed, either by this query or by
+ // earlier queries.
+ if (seenChanges.add(autoCloseableChange.getId())) {
+ retryHelper.execute(
+ ActionType.CHANGE_UPDATE,
+ () -> {
+ // Auto-close by change
+ if (changeIdToMergedSha1.containsKey(autoCloseableChange.change().getKey())) {
+ autoCloseableChangesByBranch.add(
+ changeJson(
+ fix, changeIdToMergedSha1.get(autoCloseableChange.change().getKey()))
+ .format(autoCloseableChange));
+ return null;
+ }
+
+ // Auto-close by commit
+ for (ObjectId patchSetSha1 :
+ autoCloseableChange
+ .patchSets()
+ .stream()
+ .map(ps -> ObjectId.fromString(ps.getRevision().get()))
+ .collect(toSet())) {
+ if (mergedSha1s.contains(patchSetSha1)) {
+ autoCloseableChangesByBranch.add(
+ changeJson(fix, patchSetSha1).format(autoCloseableChange));
+ break;
+ }
+ }
+ return null;
+ },
+ OrmException.class::isInstance);
+ }
+ }
+
+ return autoCloseableChangesByBranch;
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, OrmException.class);
+ throw new OrmException(e);
+ }
+ }
+
+ private ChangeJson changeJson(Boolean fix, ObjectId mergedAs) {
+ ChangeJson changeJson = changeJsonFactory.create(ListChangesOption.CHECK);
+ if (fix != null && fix.booleanValue()) {
+ FixInput fixInput = new FixInput();
+ fixInput.expectMergedAs = mergedAs.name();
+ changeJson.fix(fixInput);
+ }
+ return changeJson;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
index cc9fc0d..bd7b7fe 100644
--- a/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.account;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.index.query.IsVisibleToPredicate;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountState;
@@ -21,6 +22,8 @@
import com.google.gwtorm.server.OrmException;
public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected final AccountControl accountControl;
public AccountIsVisibleToPredicate(AccountControl accountControl) {
@@ -30,7 +33,11 @@
@Override
public boolean match(AccountState accountState) throws OrmException {
- return accountControl.canSee(accountState);
+ boolean canSee = accountControl.canSee(accountState);
+ if (!canSee) {
+ logger.atFine().log("Filter out non-visisble account: %s", accountState);
+ }
+ return canSee;
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 27baef1..f81ea15 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -74,10 +74,11 @@
try {
ProjectState projectState = projectCache.checkedGet(cd.project());
if (projectState == null) {
- logger.atInfo().log("No such project: %s", cd.project());
+ logger.atFine().log("Filter out change %s of non-existing project %s", cd, cd.project());
return false;
}
if (!projectState.statePermitsRead()) {
+ logger.atFine().log("Filter out change %s of non-reabable project %s", cd, cd.project());
return false;
}
} catch (IOException e) {
@@ -94,11 +95,13 @@
Throwable cause = e.getCause();
if (cause instanceof RepositoryNotFoundException) {
logger.atWarning().withCause(e).log(
- "Skipping change %s because the corresponding repository was not found", cd.getId());
+ "Filter out change %s because the corresponding repository %s was not found",
+ cd, cd.project());
return false;
}
throw new OrmException("unable to check permissions on change " + cd.getId(), e);
} catch (AuthException e) {
+ logger.atFine().log("Filter out non-visisble change: %s", cd);
return false;
}
diff --git a/java/com/google/gerrit/server/query/change/ConflictKey.java b/java/com/google/gerrit/server/query/change/ConflictKey.java
index 52904f7..42f5b13 100644
--- a/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -20,10 +20,10 @@
import com.google.common.base.Enums;
import com.google.common.collect.Ordering;
import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.server.cache.CacheSerializer;
-import com.google.gerrit.server.cache.ProtoCacheSerializers;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ConflictKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
diff --git a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
index 0b8c5ee..426c5d6 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
@@ -15,8 +15,8 @@
package com.google.gerrit.server.query.change;
import com.google.common.cache.Cache;
-import com.google.gerrit.server.cache.BooleanCacheSerializer;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 3c62848..495d27c 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -138,7 +138,21 @@
}
public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key) throws OrmException {
- return query(and(ref(branch), project(branch.getParentKey()), change(key)));
+ return query(byBranchKeyPred(branch, key));
+ }
+
+ public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key)
+ throws OrmException {
+ return query(and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open()));
+ }
+
+ public static Predicate<ChangeData> byBranchKeyOpenPred(
+ Project.NameKey project, String branch, Change.Key key) {
+ return and(byBranchKeyPred(new Branch.NameKey(project, branch), key), open());
+ }
+
+ private static Predicate<ChangeData> byBranchKeyPred(Branch.NameKey branch, Change.Key key) {
+ return and(ref(branch), project(branch.getParentKey()), change(key));
}
public List<ChangeData> byProject(Project.NameKey project) throws OrmException {
@@ -264,13 +278,28 @@
public List<ChangeData> byBranchCommit(String project, String branch, String hash)
throws OrmException {
- return query(and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash)));
+ return query(byBranchCommitPred(project, branch, hash));
}
public List<ChangeData> byBranchCommit(Branch.NameKey branch, String hash) throws OrmException {
return byBranchCommit(branch.getParentKey().get(), branch.get(), hash);
}
+ public List<ChangeData> byBranchCommitOpen(String project, String branch, String hash)
+ throws OrmException {
+ return query(and(byBranchCommitPred(project, branch, hash), open()));
+ }
+
+ public static Predicate<ChangeData> byBranchCommitOpenPred(
+ Project.NameKey project, String branch, String hash) {
+ return and(byBranchCommitPred(project.get(), branch, hash), open());
+ }
+
+ private static Predicate<ChangeData> byBranchCommitPred(
+ String project, String branch, String hash) {
+ return and(new ProjectPredicate(project), new RefPredicate(branch), commit(hash));
+ }
+
public List<ChangeData> bySubmissionId(String cs) throws OrmException {
if (Strings.isNullOrEmpty(cs)) {
return Collections.emptyList();
diff --git a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
index ffa59c2..144a81c 100644
--- a/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.group;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.index.query.IsVisibleToPredicate;
import com.google.gerrit.server.CurrentUser;
@@ -24,6 +25,8 @@
import com.google.gwtorm.server.OrmException;
public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<InternalGroup> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected final GroupControl.GenericFactory groupControlFactory;
protected final CurrentUser user;
@@ -37,7 +40,11 @@
@Override
public boolean match(InternalGroup group) throws OrmException {
try {
- return groupControlFactory.controlFor(user, group.getGroupUUID()).isVisible();
+ boolean canSee = groupControlFactory.controlFor(user, group.getGroupUUID()).isVisible();
+ if (!canSee) {
+ logger.atFine().log("Filter out non-visisble group: %s", group.getGroupUUID());
+ }
+ return canSee;
} catch (NoSuchGroupException e) {
// Ignored
return false;
diff --git a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
index dc567ea..b1c5af0 100644
--- a/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/project/ProjectIsVisibleToPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.project;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.index.project.ProjectData;
import com.google.gerrit.index.query.IsVisibleToPredicate;
import com.google.gerrit.server.CurrentUser;
@@ -24,6 +25,8 @@
import com.google.gwtorm.server.OrmException;
public class ProjectIsVisibleToPredicate extends IsVisibleToPredicate<ProjectData> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected final PermissionBackend permissionBackend;
protected final CurrentUser user;
@@ -36,13 +39,19 @@
@Override
public boolean match(ProjectData pd) throws OrmException {
if (!pd.getProject().getState().permitsRead()) {
+ logger.atFine().log("Filter out non-readable project: %s", pd);
return false;
}
- return permissionBackend
- .user(user)
- .project(pd.getProject().getNameKey())
- .testOrFalse(ProjectPermission.ACCESS);
+ boolean canSee =
+ permissionBackend
+ .user(user)
+ .project(pd.getProject().getNameKey())
+ .testOrFalse(ProjectPermission.ACCESS);
+ if (!canSee) {
+ logger.atFine().log("Filter out non-visible project: %s", pd);
+ }
+ return canSee;
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 47c6970..6b7a708 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -107,8 +107,13 @@
ProjectState projectState,
List<Account.Id> candidateList)
throws OrmException, IOException, ConfigInvalidException {
+ logger.atFine().log("Candidates %s", candidateList);
+
String query = suggestReviewers.getQuery();
+ logger.atFine().log("query: %s", query);
+
double baseWeight = config.getInt("addReviewer", "baseWeight", 1);
+ logger.atFine().log("base weight: %s", baseWeight);
Map<Account.Id, MutableDouble> reviewerScores;
if (Strings.isNullOrEmpty(query)) {
@@ -116,6 +121,7 @@
} else {
reviewerScores = baseRankingForCandidateList(candidateList, projectState, baseWeight);
}
+ logger.atFine().log("Base reviewer scores: %s", reviewerScores);
// Send the query along with a candidate list to all plugins and merge the
// results. Plugins don't necessarily need to use the candidates list, they
@@ -163,6 +169,7 @@
}
}
}
+ logger.atFine().log("Reviewer scores: %s", reviewerScores);
} catch (ExecutionException | InterruptedException e) {
logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
return ImmutableList.of();
@@ -170,12 +177,20 @@
if (changeNotes != null) {
// Remove change owner
- reviewerScores.remove(changeNotes.getChange().getOwner());
+ if (reviewerScores.remove(changeNotes.getChange().getOwner()) != null) {
+ logger.atFine().log("Remove change owner %s", changeNotes.getChange().getOwner());
+ }
// Remove existing reviewers
- reviewerScores
- .keySet()
- .removeAll(approvalsUtil.getReviewers(dbProvider.get(), changeNotes).byState(REVIEWER));
+ approvalsUtil
+ .getReviewers(dbProvider.get(), changeNotes)
+ .byState(REVIEWER)
+ .forEach(
+ r -> {
+ if (reviewerScores.remove(r) != null) {
+ logger.atFine().log("Remove existing reviewer %s", r);
+ }
+ });
}
// Sort results
@@ -185,6 +200,7 @@
.stream()
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
List<Account.Id> sortedSuggestions = sorted.map(Map.Entry::getKey).collect(toList());
+ logger.atFine().log("Sorted suggestions: %s", sortedSuggestions);
return sortedSuggestions;
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 57ff0a3..3becf24 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.index.account.AccountField;
@@ -228,24 +229,30 @@
// For performance reasons we don't use AccountQueryProvider as it would always load the
// complete account from the cache (or worse, from NoteDb) even though we only need the ID
// which we can directly get from the returned results.
+ Predicate<AccountState> pred =
+ Predicate.and(
+ AccountPredicates.isActive(),
+ accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
+ logger.atFine().log("accounts index query: %s", pred);
ResultSet<FieldBundle> result =
accountIndexes
.getSearchIndex()
.getSource(
- Predicate.and(
- AccountPredicates.isActive(),
- accountQueryBuilder.defaultQuery(suggestReviewers.getQuery())),
+ pred,
QueryOptions.create(
indexConfig,
0,
suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER,
ImmutableSet.of(AccountField.ID.getName())))
.readRaw();
- return result
- .toList()
- .stream()
- .map(f -> new Account.Id(f.getValue(AccountField.ID).intValue()))
- .collect(toList());
+ List<Account.Id> matches =
+ result
+ .toList()
+ .stream()
+ .map(f -> new Account.Id(f.getValue(AccountField.ID).intValue()))
+ .collect(toList());
+ logger.atFine().log("Matches: %s", matches);
+ return matches;
} catch (QueryParseException e) {
return ImmutableList.of();
}
@@ -374,8 +381,10 @@
GroupAsReviewer result = new GroupAsReviewer();
int maxAllowed = suggestReviewers.getMaxAllowed();
int maxAllowedWithoutConfirmation = suggestReviewers.getMaxAllowedWithoutConfirmation();
+ logger.atFine().log("maxAllowedWithoutConfirmation: " + maxAllowedWithoutConfirmation);
if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+ logger.atFine().log("Ignore group %s that is not legal as reviewer", group.getUUID());
return result;
}
@@ -383,6 +392,7 @@
Set<Account> members = groupMembers.listAccounts(group.getUUID(), project.getNameKey());
if (members.isEmpty()) {
+ logger.atFine().log("Ignore group %s since it has no members", group.getUUID());
return result;
}
@@ -392,6 +402,11 @@
}
boolean needsConfirmation = result.size > maxAllowedWithoutConfirmation;
+ if (needsConfirmation) {
+ logger.atFine().log(
+ "group %s needs confirmation to be added as reviewer, it has %d members",
+ group.getUUID(), result.size);
+ }
// require that at least one member in the group can see the change
for (Account account : members) {
@@ -401,9 +416,12 @@
} else {
result.allowed = true;
}
+ logger.atFine().log("Suggest group %s", group.getUUID());
return result;
}
}
+ logger.atFine().log(
+ "Ignore group %s since none of its members can see the change", group.getUUID());
} catch (NoSuchProjectException e) {
return result;
}
diff --git a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
index 1b50834..8aac92c 100644
--- a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
@@ -18,8 +18,11 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.PrivateStateChanged;
+import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -44,19 +47,23 @@
}
private final ChangeMessagesUtil cmUtil;
+ private final PatchSetUtil psUtil;
private final boolean isPrivate;
private final Input input;
private final PrivateStateChanged privateStateChanged;
private Change change;
+ private PatchSet ps;
@Inject
SetPrivateOp(
PrivateStateChanged privateStateChanged,
+ PatchSetUtil psUtil,
@Assisted ChangeMessagesUtil cmUtil,
@Assisted boolean isPrivate,
@Assisted Input input) {
this.cmUtil = cmUtil;
+ this.psUtil = psUtil;
this.isPrivate = isPrivate;
this.input = input;
this.privateStateChanged = privateStateChanged;
@@ -65,6 +72,8 @@
@Override
public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException {
change = ctx.getChange();
+ ChangeNotes notes = ctx.getNotes();
+ ps = psUtil.get(ctx.getDb(), notes, change.currentPatchSetId());
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
change.setPrivate(isPrivate);
change.setLastUpdatedOn(ctx.getWhen());
@@ -75,7 +84,7 @@
@Override
public void postUpdate(Context ctx) {
- privateStateChanged.fire(change, ctx.getAccount(), ctx.getWhen());
+ privateStateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
}
private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index 8b39e8e..b1d49f2 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.config.GerritConfigListenerHelper.acceptIfChanged;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.ConfigKey;
@@ -27,6 +28,8 @@
import org.kohsuke.args4j.Option;
public class SuggestReviewers {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private static final int DEFAULT_MAX_SUGGESTED = 10;
protected final Provider<ReviewDb> dbProvider;
@@ -101,6 +104,8 @@
"addreviewer",
"maxWithoutConfirmation",
PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+
+ logger.atFine().log("AccountVisibility: %s", av.name());
}
public static GerritConfigListener configListener() {
diff --git a/java/com/google/gerrit/server/restapi/project/Check.java b/java/com/google/gerrit/server/restapi/project/Check.java
new file mode 100644
index 0000000..a6fd764
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/Check.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.extensions.api.projects.CheckProjectInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectsConsistencyChecker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class Check implements RestModifyView<ProjectResource, CheckProjectInput> {
+ private final PermissionBackend permissionBackend;
+ private final ProjectsConsistencyChecker projectsConsistencyChecker;
+
+ @Inject
+ Check(
+ PermissionBackend permissionBackend, ProjectsConsistencyChecker projectsConsistencyChecker) {
+ this.permissionBackend = permissionBackend;
+ this.projectsConsistencyChecker = projectsConsistencyChecker;
+ }
+
+ @Override
+ public CheckProjectResultInfo apply(ProjectResource rsrc, CheckProjectInput input)
+ throws AuthException, BadRequestException, ResourceConflictException, Exception {
+ permissionBackend.user(rsrc.getUser()).check(GlobalPermission.ADMINISTRATE_SERVER);
+ return projectsConsistencyChecker.check(rsrc.getNameKey(), input);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
index 0d52090..076bf78 100644
--- a/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ConfigInfoImpl.java
@@ -48,7 +48,7 @@
boolean serverEnableSignedPush,
ProjectState projectState,
CurrentUser user,
- TransferConfig config,
+ TransferConfig transferConfig,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
PluginConfigFactory cfgFactory,
AllProjectsName allProjects,
@@ -72,14 +72,7 @@
this.requireSignedPush = null;
}
- MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
- maxObjectSizeLimit.value =
- config.getEffectiveMaxObjectSizeLimit(projectState) == config.getMaxObjectSizeLimit()
- ? config.getFormattedMaxObjectSizeLimit()
- : p.getMaxObjectSizeLimit();
- maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
- maxObjectSizeLimit.inheritedValue = config.getFormattedMaxObjectSizeLimit();
- this.maxObjectSizeLimit = maxObjectSizeLimit;
+ this.maxObjectSizeLimit = getMaxObjectSizeLimit(projectState, transferConfig, p);
this.defaultSubmitType = new SubmitTypeInfo();
this.defaultSubmitType.value = projectState.getSubmitType();
@@ -114,6 +107,16 @@
this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections();
}
+ private MaxObjectSizeLimitInfo getMaxObjectSizeLimit(
+ ProjectState projectState, TransferConfig transferConfig, Project p) {
+ MaxObjectSizeLimitInfo info = new MaxObjectSizeLimitInfo();
+ long value = projectState.getEffectiveMaxObjectSizeLimit();
+ info.value = value == 0 ? null : String.valueOf(value);
+ info.configuredValue = p.getMaxObjectSizeLimit();
+ info.inheritedValue = transferConfig.getFormattedMaxObjectSizeLimit();
+ return info;
+ }
+
private Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
ProjectState project,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
diff --git a/java/com/google/gerrit/server/restapi/project/GetConfig.java b/java/com/google/gerrit/server/restapi/project/GetConfig.java
index aafff9e..7fedc8f 100644
--- a/java/com/google/gerrit/server/restapi/project/GetConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/GetConfig.java
@@ -31,7 +31,7 @@
@Singleton
public class GetConfig implements RestReadView<ProjectResource> {
private final boolean serverEnableSignedPush;
- private final TransferConfig config;
+ private final TransferConfig transferConfig;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final PluginConfigFactory cfgFactory;
private final AllProjectsName allProjects;
@@ -41,14 +41,14 @@
@Inject
public GetConfig(
@EnableSignedPush boolean serverEnableSignedPush,
- TransferConfig config,
+ TransferConfig transferConfig,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
PluginConfigFactory cfgFactory,
AllProjectsName allProjects,
UiActions uiActions,
DynamicMap<RestView<ProjectResource>> views) {
this.serverEnableSignedPush = serverEnableSignedPush;
- this.config = config;
+ this.transferConfig = transferConfig;
this.pluginConfigEntries = pluginConfigEntries;
this.allProjects = allProjects;
this.cfgFactory = cfgFactory;
@@ -62,7 +62,7 @@
serverEnableSignedPush,
resource.getProjectState(),
resource.getUser(),
- config,
+ transferConfig,
pluginConfigEntries,
cfgFactory,
allProjects,
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index a57438e..0497787 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -54,6 +54,8 @@
post(PROJECT_KIND, "check.access").to(CheckAccess.class);
get(PROJECT_KIND, "check.access").to(CheckAccessReadView.class);
+ post(PROJECT_KIND, "check").to(Check.class);
+
get(PROJECT_KIND, "parent").to(GetParent.class);
put(PROJECT_KIND, "parent").to(SetParent.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index db596e6..f4eb781 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -71,7 +71,7 @@
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
- private final TransferConfig config;
+ private final TransferConfig transferConfig;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
private final PluginConfigFactory cfgFactory;
private final AllProjectsName allProjects;
@@ -86,7 +86,7 @@
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory,
- TransferConfig config,
+ TransferConfig transferConfig,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
PluginConfigFactory cfgFactory,
AllProjectsName allProjects,
@@ -98,7 +98,7 @@
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
- this.config = config;
+ this.transferConfig = transferConfig;
this.pluginConfigEntries = pluginConfigEntries;
this.cfgFactory = cfgFactory;
this.allProjects = allProjects;
@@ -168,12 +168,12 @@
throw new ResourceConflictException("Cannot update " + projectName);
}
- ProjectState state = projectStateFactory.create(projectConfig);
+ ProjectState state = projectStateFactory.create(ProjectConfig.read(md));
return new ConfigInfoImpl(
serverEnableSignedPush,
state,
user.get(),
- config,
+ transferConfig,
pluginConfigEntries,
cfgFactory,
allProjects,
diff --git a/java/com/google/gerrit/server/schema/DataSourceProvider.java b/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 8021a54..d4cfaa6 100644
--- a/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -44,6 +44,8 @@
/** Provides access to the DataSource. */
@Singleton
public class DataSourceProvider implements Provider<DataSource>, LifecycleListener {
+ private static final String DATABASE_KEY = "database";
+
private final Config cfg;
private final MetricMaker metrics;
private final Context ctx;
@@ -93,7 +95,7 @@
}
private DataSource open(Config cfg, Context context, DataSourceType dst) {
- ConfigSection dbs = new ConfigSection(cfg, "database");
+ ConfigSection dbs = new ConfigSection(cfg, DATABASE_KEY);
String driver = dbs.optional("driver");
if (Strings.isNullOrEmpty(driver)) {
driver = dst.getDriver();
@@ -112,41 +114,41 @@
if (context == Context.SINGLE_USER) {
usePool = false;
} else {
- usePool = cfg.getBoolean("database", "connectionpool", dst.usePool());
+ usePool = cfg.getBoolean(DATABASE_KEY, "connectionpool", dst.usePool());
}
if (usePool) {
- final BasicDataSource ds = new BasicDataSource();
- ds.setDriverClassName(driver);
- ds.setUrl(url);
+ final BasicDataSource lds = new BasicDataSource();
+ lds.setDriverClassName(driver);
+ lds.setUrl(url);
if (username != null && !username.isEmpty()) {
- ds.setUsername(username);
+ lds.setUsername(username);
}
if (password != null && !password.isEmpty()) {
- ds.setPassword(password);
+ lds.setPassword(password);
}
int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
- ds.setMaxActive(poolLimit);
- ds.setMinIdle(cfg.getInt("database", "poolminidle", 4));
- ds.setMaxIdle(cfg.getInt("database", "poolmaxidle", Math.min(poolLimit, 16)));
- ds.setMaxWait(
+ lds.setMaxActive(poolLimit);
+ lds.setMinIdle(cfg.getInt(DATABASE_KEY, "poolminidle", 4));
+ lds.setMaxIdle(cfg.getInt(DATABASE_KEY, "poolmaxidle", Math.min(poolLimit, 16)));
+ lds.setMaxWait(
ConfigUtil.getTimeUnit(
cfg,
- "database",
+ DATABASE_KEY,
null,
"poolmaxwait",
MILLISECONDS.convert(30, SECONDS),
MILLISECONDS));
- ds.setInitialSize(ds.getMinIdle());
+ lds.setInitialSize(lds.getMinIdle());
long evictIdleTimeMs = 1000L * 60;
- ds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
- ds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
- ds.setTestOnBorrow(true);
- ds.setTestOnReturn(true);
- ds.setValidationQuery(dst.getValidationQuery());
- ds.setValidationQueryTimeout(5);
- exportPoolMetrics(ds);
- return intercept(interceptor, ds);
+ lds.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
+ lds.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
+ lds.setTestOnBorrow(true);
+ lds.setTestOnReturn(true);
+ lds.setValidationQuery(dst.getValidationQuery());
+ lds.setValidationQueryTimeout(5);
+ exportPoolMetrics(lds);
+ return intercept(interceptor, lds);
}
// Don't use the connection pool.
//
diff --git a/java/com/google/gerrit/server/submit/FastForwardOp.java b/java/com/google/gerrit/server/submit/FastForwardOp.java
index f1749f4..08f5abb 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOp.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOp.java
@@ -28,6 +28,7 @@
@Override
protected void updateRepoImpl(RepoContext ctx) throws IntegrationException {
if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
+ && toMerge.getParentCount() > 0
&& toMerge.getTree().equals(toMerge.getParent(0).getTree())) {
toMerge.setStatusCode(EMPTY_COMMIT);
return;
diff --git a/java/com/google/gerrit/server/submit/GitModules.java b/java/com/google/gerrit/server/submit/GitModules.java
index fd9a6fa..1fccbdd 100644
--- a/java/com/google/gerrit/server/submit/GitModules.java
+++ b/java/com/google/gerrit/server/submit/GitModules.java
@@ -22,7 +22,6 @@
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
-import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.server.util.git.SubmoduleSectionParser;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -51,7 +50,6 @@
private static final String GIT_MODULES = ".gitmodules";
- private final RequestId submissionId;
private final Branch.NameKey branch;
Set<SubmoduleSubscription> subscriptions;
@@ -62,9 +60,8 @@
@Assisted MergeOpRepoManager orm)
throws IOException {
this.branch = branch;
- this.submissionId = orm.getSubmissionId();
Project.NameKey project = branch.getParentKey();
- logDebug("Loading .gitmodules of %s for project %s", branch, project);
+ logger.atFine().log("Loading .gitmodules of %s for project %s", branch, project);
try {
OpenRepo or = orm.getRepo(project);
ObjectId id = or.repo.resolve(branch.get());
@@ -76,7 +73,7 @@
try (TreeWalk tw = TreeWalk.forPath(or.repo, GIT_MODULES, commit.getTree())) {
if (tw == null || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
subscriptions = Collections.emptySet();
- logDebug("The .gitmodules file doesn't exist in %s", branch);
+ logger.atFine().log("The .gitmodules file doesn't exist in %s", branch);
return;
}
}
@@ -94,22 +91,14 @@
}
public Collection<SubmoduleSubscription> subscribedTo(Branch.NameKey src) {
- logDebug("Checking for a subscription of %s for %s", src, branch);
+ logger.atFine().log("Checking for a subscription of %s for %s", src, branch);
Collection<SubmoduleSubscription> ret = new ArrayList<>();
for (SubmoduleSubscription s : subscriptions) {
if (s.getSubmodule().equals(src)) {
- logDebug("Found %s", s);
+ logger.atFine().log("Found %s", s);
ret.add(s);
}
}
return ret;
}
-
- private void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(submissionId + msg, arg);
- }
-
- private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
- logger.atFine().log(submissionId + msg, arg1, arg2);
- }
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 9cfa272..0538f97 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -63,6 +63,7 @@
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.validators.MergeValidationException;
import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -455,78 +456,83 @@
this.dryrun = dryrun;
this.caller = caller;
this.ts = TimeUtil.nowTs();
- submissionId = RequestId.forChange(change);
this.db = db;
- openRepoManager();
+ this.submissionId = RequestId.forChange(change);
- logDebug("Beginning integration of %s", change);
- try {
- ChangeSet indexBackedChangeSet =
- mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
- checkState(
- indexBackedChangeSet.ids().contains(change.getId()),
- "change %s missing from %s",
- change.getId(),
- indexBackedChangeSet);
- if (indexBackedChangeSet.furtherHiddenChanges()) {
- throw new AuthException(
- "A change to be submitted with " + change.getId() + " is not visible");
+ try (TraceContext traceContext =
+ TraceContext.open().addTag(RequestId.Type.SUBMISSION_ID, submissionId)) {
+ openRepoManager();
+
+ logger.atFine().log("Beginning integration of %s", change);
+ try {
+ ChangeSet indexBackedChangeSet =
+ mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
+ checkState(
+ indexBackedChangeSet.ids().contains(change.getId()),
+ "change %s missing from %s",
+ change.getId(),
+ indexBackedChangeSet);
+ if (indexBackedChangeSet.furtherHiddenChanges()) {
+ throw new AuthException(
+ "A change to be submitted with " + change.getId() + " is not visible");
+ }
+ logger.atFine().log("Calculated to merge %s", indexBackedChangeSet);
+
+ // Reload ChangeSet so that we don't rely on (potentially) stale index data for merging
+ ChangeSet cs = reloadChanges(indexBackedChangeSet);
+
+ // Count cross-project submissions outside of the retry loop. The chance of a single project
+ // failing increases with the number of projects, so the failure count would be inflated if
+ // this metric were incremented inside of integrateIntoHistory.
+ int projects = cs.projects().size();
+ if (projects > 1) {
+ topicMetrics.topicSubmissions.increment();
+ }
+
+ RetryTracker retryTracker = new RetryTracker();
+ retryHelper.execute(
+ updateFactory -> {
+ long attempt = retryTracker.lastAttemptNumber + 1;
+ boolean isRetry = attempt > 1;
+ if (isRetry) {
+ logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt);
+ this.ts = TimeUtil.nowTs();
+ openRepoManager();
+ }
+ this.commitStatus = new CommitStatus(cs, isRetry);
+ if (checkSubmitRules) {
+ logger.atFine().log("Checking submit rules and state");
+ checkSubmitRulesAndState(cs, isRetry);
+ } else {
+ logger.atFine().log("Bypassing submit rules");
+ bypassSubmitRules(cs, isRetry);
+ }
+ try {
+ integrateIntoHistory(cs);
+ } catch (IntegrationException e) {
+ logger.atSevere().withCause(e).log("Error from integrateIntoHistory");
+ throw new ResourceConflictException(e.getMessage(), e);
+ }
+ return null;
+ },
+ RetryHelper.options()
+ .listener(retryTracker)
+ // Up to the entire submit operation is retried, including possibly many projects.
+ // Multiply the timeout by the number of projects we're actually attempting to
+ // submit.
+ .timeout(
+ retryHelper
+ .getDefaultTimeout(ActionType.CHANGE_UPDATE)
+ .multipliedBy(cs.projects().size()))
+ .build());
+
+ if (projects > 1) {
+ topicMetrics.topicSubmissionsCompleted.increment();
+ }
+ } catch (IOException e) {
+ // Anything before the merge attempt is an error
+ throw new OrmException(e);
}
- logDebug("Calculated to merge %s", indexBackedChangeSet);
-
- // Reload ChangeSet so that we don't rely on (potentially) stale index data for merging
- ChangeSet cs = reloadChanges(indexBackedChangeSet);
-
- // Count cross-project submissions outside of the retry loop. The chance of a single project
- // failing increases with the number of projects, so the failure count would be inflated if
- // this metric were incremented inside of integrateIntoHistory.
- int projects = cs.projects().size();
- if (projects > 1) {
- topicMetrics.topicSubmissions.increment();
- }
-
- RetryTracker retryTracker = new RetryTracker();
- retryHelper.execute(
- updateFactory -> {
- long attempt = retryTracker.lastAttemptNumber + 1;
- boolean isRetry = attempt > 1;
- if (isRetry) {
- logDebug("Retrying, attempt #%d; skipping merged changes", attempt);
- this.ts = TimeUtil.nowTs();
- openRepoManager();
- }
- this.commitStatus = new CommitStatus(cs, isRetry);
- if (checkSubmitRules) {
- logDebug("Checking submit rules and state");
- checkSubmitRulesAndState(cs, isRetry);
- } else {
- logDebug("Bypassing submit rules");
- bypassSubmitRules(cs, isRetry);
- }
- try {
- integrateIntoHistory(cs);
- } catch (IntegrationException e) {
- logError("Error from integrateIntoHistory", e);
- throw new ResourceConflictException(e.getMessage(), e);
- }
- return null;
- },
- RetryHelper.options()
- .listener(retryTracker)
- // Up to the entire submit operation is retried, including possibly many projects.
- // Multiply the timeout by the number of projects we're actually attempting to submit.
- .timeout(
- retryHelper
- .getDefaultTimeout(ActionType.CHANGE_UPDATE)
- .multipliedBy(cs.projects().size()))
- .build());
-
- if (projects > 1) {
- topicMetrics.topicSubmissionsCompleted.increment();
- }
- } catch (IOException e) {
- // Anything before the merge attempt is an error
- throw new OrmException(e);
}
}
@@ -535,7 +541,7 @@
orm.close();
}
orm = ormProvider.get();
- orm.setContext(db, ts, caller, submissionId);
+ orm.setContext(db, ts, caller);
}
private ChangeSet reloadChanges(ChangeSet changeSet) {
@@ -581,7 +587,7 @@
private void integrateIntoHistory(ChangeSet cs)
throws IntegrationException, RestApiException, UpdateException {
checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
- logDebug("Beginning merge attempt on %s", cs);
+ logger.atFine().log("Beginning merge attempt on %s", cs);
Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
ListMultimap<Branch.NameKey, ChangeData> cbb;
@@ -609,7 +615,6 @@
batchUpdateFactory.execute(
orm.batchUpdates(allProjects),
new SubmitStrategyListener(submitInput, strategies, commitStatus),
- submissionId,
dryrun);
} catch (NoSuchProjectException e) {
throw new ResourceNotFoundException(e.getMessage());
@@ -723,7 +728,7 @@
throw new IntegrationException("Failed to determine already accepted commits.", e);
}
- logDebug("Found %d existing heads", alreadyAccepted.size());
+ logger.atFine().log("Found %d existing heads", alreadyAccepted.size());
return alreadyAccepted;
}
@@ -737,7 +742,7 @@
private BranchBatch validateChangeList(OpenRepo or, Collection<ChangeData> submitted)
throws IntegrationException {
- logDebug("Validating %d changes", submitted.size());
+ logger.atFine().log("Validating %d changes", submitted.size());
Set<CodeReviewCommit> toSubmit = new LinkedHashSet<>(submitted.size());
SetMultimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted);
@@ -775,7 +780,7 @@
}
if (chg.currentPatchSetId() == null) {
String msg = "Missing current patch set on change";
- logError(msg + " " + changeId);
+ logger.atSevere().log("%s %s", msg, changeId);
commitStatus.problem(changeId, msg);
continue;
}
@@ -856,7 +861,7 @@
commit.add(or.canMergeFlag);
toSubmit.add(commit);
}
- logDebug("Submitting on this run: %s", toSubmit);
+ logger.atFine().log("Submitting on this run: %s", toSubmit);
return new AutoValue_MergeOp_BranchBatch(submitType, toSubmit);
}
@@ -894,7 +899,7 @@
try {
return orm.getRepo(project);
} catch (NoSuchProjectException e) {
- logWarn("Project " + project + " no longer exists, abandoning open changes.");
+ logger.atWarning().log("Project %s no longer exists, abandoning open changes.", project);
abandonAllOpenChangeForDeletedProject(project);
} catch (IOException e) {
throw new IntegrationException("Error opening project " + project, e);
@@ -907,7 +912,6 @@
for (ChangeData cd : queryProvider.get().byProjectOpen(destProject)) {
try (BatchUpdate bu =
batchUpdateFactory.create(db, destProject, internalUserFactory.create(), ts)) {
- bu.setRequestId(submissionId);
bu.addOp(
cd.getId(),
new BatchUpdateOp() {
@@ -936,12 +940,14 @@
try {
bu.execute();
} catch (UpdateException | RestApiException e) {
- logWarn("Cannot abandon changes for deleted project " + destProject, e);
+ logger.atWarning().withCause(e).log(
+ "Cannot abandon changes for deleted project %s", destProject);
}
}
}
} catch (OrmException e) {
- logWarn("Cannot abandon changes for deleted project " + destProject, e);
+ logger.atWarning().withCause(e).log(
+ "Cannot abandon changes for deleted project %s", destProject);
}
}
@@ -965,28 +971,4 @@
+ " projects involved; some projects may have submitted successfully, but others may have"
+ " failed";
}
-
- private void logDebug(String msg) {
- logger.atFine().log(submissionId + msg);
- }
-
- private void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(submissionId + msg, arg);
- }
-
- private void logWarn(String msg, Throwable t) {
- logger.atWarning().withCause(t).log("%s%s", submissionId, msg);
- }
-
- private void logWarn(String msg) {
- logger.atWarning().log("%s%s", submissionId, msg);
- }
-
- private void logError(String msg, Throwable t) {
- logger.atSevere().withCause(t).log("%s%s", submissionId, msg);
- }
-
- private void logError(String msg) {
- logError(msg, null);
- }
}
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index 67059e6..3f07ed7 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -31,7 +31,6 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.util.RequestId;
import com.google.inject.Inject;
import java.io.IOException;
import java.sql.Timestamp;
@@ -112,7 +111,6 @@
batchUpdateFactory
.create(db, getProjectName(), caller, ts)
.setRepository(repo, rw, ins)
- .setRequestId(submissionId)
.setOnSubmitValidators(onSubmitValidatorsFactory.create());
}
return update;
@@ -162,7 +160,6 @@
private ReviewDb db;
private Timestamp ts;
private IdentifiedUser caller;
- private RequestId submissionId;
@Inject
MergeOpRepoManager(
@@ -178,15 +175,10 @@
openRepos = new HashMap<>();
}
- public void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller, RequestId submissionId) {
+ public void setContext(ReviewDb db, Timestamp ts, IdentifiedUser caller) {
this.db = db;
this.ts = ts;
this.caller = caller;
- this.submissionId = submissionId;
- }
-
- public RequestId getSubmissionId() {
- return submissionId;
}
public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index abe3632..290e917 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -22,7 +22,6 @@
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -103,7 +102,8 @@
@Override
public final void updateRepo(RepoContext ctx) throws Exception {
- logDebug("%s#updateRepo for change %s", getClass().getSimpleName(), toMerge.change().getId());
+ logger.atFine().log(
+ "%s#updateRepo for change %s", getClass().getSimpleName(), toMerge.change().getId());
checkState(
ctx.getRevWalk() == args.rw,
"SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same"
@@ -117,18 +117,18 @@
if (alreadyMergedCommit == null) {
updateRepoImpl(ctx);
} else {
- logDebug("Already merged as %s", alreadyMergedCommit.name());
+ logger.atFine().log("Already merged as %s", alreadyMergedCommit.name());
}
CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip();
if (Objects.equals(tipBefore, tipAfter)) {
- logDebug("Did not move tip", getClass().getSimpleName());
+ logger.atFine().log("Did not move tip");
return;
} else if (tipAfter == null) {
- logDebug("No merge tip, no update to perform");
+ logger.atFine().log("No merge tip, no update to perform");
return;
}
- logDebug("Moved tip from %s to %s", tipBefore, tipAfter);
+ logger.atFine().log("Moved tip from %s to %s", tipBefore, tipAfter);
checkProjectConfig(ctx, tipAfter);
@@ -144,7 +144,7 @@
throws IntegrationException {
String refName = getDest().get();
if (RefNames.REFS_CONFIG.equals(refName)) {
- logDebug("Loading new configuration from %s", RefNames.REFS_CONFIG);
+ logger.atFine().log("Loading new configuration from %s", RefNames.REFS_CONFIG);
try {
ProjectConfig cfg = new ProjectConfig(getProject());
cfg.load(ctx.getRevWalk(), commit);
@@ -216,7 +216,8 @@
@Override
public final boolean updateChange(ChangeContext ctx) throws Exception {
- logDebug("%s#updateChange for change %s", getClass().getSimpleName(), toMerge.change().getId());
+ logger.atFine().log(
+ "%s#updateChange for change %s", getClass().getSimpleName(), toMerge.change().getId());
toMerge.setNotes(ctx.getNotes()); // Update change and notes from ctx.
PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId());
PatchSet.Id newPsId;
@@ -225,12 +226,12 @@
// Either another thread won a race, or we are retrying a whole topic submission after one
// repo failed with lock failure.
if (alreadyMergedCommit == null) {
- logDebug(
+ logger.atFine().log(
"Change is already merged according to its status, but we were unable to find it"
+ " merged into the current tip (%s)",
args.mergeTip.getCurrentTip().name());
} else {
- logDebug("Change is already merged");
+ logger.atFine().log("Change is already merged");
}
changeAlreadyMerged = true;
return false;
@@ -276,7 +277,7 @@
checkNotNull(commit, "missing commit for change " + id);
CommitMergeStatus s = commit.getStatusCode();
checkNotNull(s, "status not set for change " + id + " expected to previously fail fast");
- logDebug("Status of change %s (%s) on %s: %s", id, commit.name(), c.getDest(), s);
+ logger.atFine().log("Status of change %s (%s) on %s: %s", id, commit.name(), c.getDest(), s);
setApproval(ctx, args.caller);
mergeResultRev =
@@ -302,7 +303,7 @@
private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx)
throws IOException, OrmException {
PatchSet.Id psId = alreadyMergedCommit.getPatchsetId();
- logDebug("Fixing up already-merged patch set %s", psId);
+ logger.atFine().log("Fixing up already-merged patch set %s", psId);
PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes());
ctx.getRevWalk().parseBody(alreadyMergedCommit);
ctx.getChange()
@@ -310,7 +311,7 @@
psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject());
PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
if (existing != null) {
- logDebug("Patch set row exists, only updating change");
+ logger.atFine().log("Patch set row exists, only updating change");
return existing;
}
// No patch set for the already merged commit, although we know it came form
@@ -336,7 +337,7 @@
PatchSet.Id oldPsId = toMerge.getPatchsetId();
PatchSet.Id newPsId = ctx.getChange().currentPatchSetId();
- logDebug("Add approval for %s", id);
+ logger.atFine().log("Add approval for %s", id);
ChangeUpdate origPsUpdate = ctx.getUpdate(oldPsId);
origPsUpdate.putReviewer(user.getAccountId(), REVIEWER);
LabelNormalizer.Result normalized = approve(ctx, origPsUpdate);
@@ -397,7 +398,7 @@
// change happened.
for (PatchSetApproval psa : normalized.unchanged()) {
if (includeUnchanged || psa.isLegacySubmit()) {
- logDebug("Adding submit label %s", psa);
+ logger.atFine().log("Adding submit label %s", psa);
update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
}
}
@@ -488,7 +489,7 @@
private void setMerged(ChangeContext ctx, ChangeMessage msg) throws OrmException {
Change c = ctx.getChange();
ReviewDb db = ctx.getDb();
- logDebug("Setting change %s merged", c.getId());
+ logger.atFine().log("Setting change %s merged", c.getId());
c.setStatus(Change.Status.MERGED);
c.setSubmissionId(args.submissionId.toStringForStorage());
@@ -511,7 +512,7 @@
// If we naively execute postUpdate even if the change is already merged when updateChange
// being, then we are subject to a race where postUpdate steps are run twice if two submit
// processes run at the same time.
- logDebug("Skipping post-update steps for change %s", getId());
+ logger.atFine().log("Skipping post-update steps for change %s", getId());
return;
}
postUpdateImpl(ctx);
@@ -595,37 +596,4 @@
"cannot update gitlink for the commit at branch: " + args.destBranch);
}
}
-
- protected final void logDebug(String msg) {
- logger.atFine().log(this.args.submissionId + msg);
- }
-
- protected final void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(this.args.submissionId + msg, arg);
- }
-
- protected final void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
- logger.atFine().log(this.args.submissionId + msg, arg1, arg2);
- }
-
- protected final void logDebug(
- String msg,
- @Nullable Object arg1,
- @Nullable Object arg2,
- @Nullable Object arg3,
- @Nullable Object arg4) {
- logger.atFine().log(this.args.submissionId + msg, arg1, arg2, arg3, arg4);
- }
-
- protected final void logWarn(String msg, Throwable t) {
- logger.atWarning().withCause(t).log("%s%s", args.submissionId, msg);
- }
-
- protected void logError(String msg, Throwable t) {
- logger.atSevere().withCause(t).log("%s%s", args.submissionId, msg);
- }
-
- protected void logError(String msg) {
- logError(msg, null);
- }
}
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index 319e2e1..50be62a 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -15,12 +15,12 @@
package com.google.gerrit.server.submit;
import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.SetMultimap;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubscribeSection;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Branch;
@@ -28,6 +28,7 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.UsedAt;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
import com.google.gerrit.server.git.CodeReviewCommit;
@@ -140,17 +141,31 @@
private final MergeOpRepoManager orm;
private final Map<Branch.NameKey, GitModules> branchGitModules;
- // always update-to-current branch tips during submit process
+ /** Branches updated as part of the enclosing submit or push batch. */
+ private final ImmutableSet<Branch.NameKey> updatedBranches;
+
+ /**
+ * Current branch tips, taking into account commits created during the submit process as well as
+ * submodule updates produced by this class.
+ */
private final Map<Branch.NameKey, CodeReviewCommit> branchTips;
- // branches for all the submitting changes
- private final Set<Branch.NameKey> updatedBranches;
- // branches which in either a submodule or a superproject
+
+ /**
+ * Branches in a superproject that contain submodule subscriptions, plus branches in submodules
+ * which are subscribed to by some superproject.
+ */
private final Set<Branch.NameKey> affectedBranches;
- // sorted version of affectedBranches
+
+ /** Copy of {@link #affectedBranches}, sorted by submodule traversal order. */
private final ImmutableSet<Branch.NameKey> sortedBranches;
- // map of superproject branch and its submodule subscriptions
+
+ /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
private final SetMultimap<Branch.NameKey, SubmoduleSubscription> targets;
- // map of superproject and its branches which has submodule subscriptions
+
+ /**
+ * Multimap of superproject name to all branch names within that superproject which have submodule
+ * subscriptions.
+ */
private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject;
private SubmoduleOp(
@@ -174,29 +189,57 @@
cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
this.orm = orm;
- this.updatedBranches = updatedBranches;
+ this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
this.affectedBranches = new HashSet<>();
this.branchTips = new HashMap<>();
this.branchGitModules = new HashMap<>();
this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
- this.sortedBranches = calculateSubscriptionMap();
+ this.sortedBranches = calculateSubscriptionMaps();
}
- private ImmutableSet<Branch.NameKey> calculateSubscriptionMap() throws SubmoduleException {
+ /**
+ * Calculate the internal maps used by the operation.
+ *
+ * <p>In addition to the return value, the following fields are populated as a side effect:
+ *
+ * <ul>
+ * <li>{@link #affectedBranches}
+ * <li>{@link #targets}
+ * <li>{@link #branchesByProject}
+ * </ul>
+ *
+ * @return the ordered set to be stored in {@link #sortedBranches}.
+ * @throws SubmoduleException if an error occurred walking projects.
+ */
+ // TODO(dborowitz): This setup process is hard to follow, in large part due to the accumulation of
+ // mutable maps, which makes this whole class difficult to understand.
+ //
+ // A cleaner architecture for this process might be:
+ // 1. Separate out the code to parse submodule subscriptions and build up an in-memory data
+ // structure representing the subscription graph, using a separate class with a properly-
+ // documented interface.
+ // 2. Walk the graph to produce a work plan. This would be a list of items indicating: create a
+ // commit in project X reading branch tips for submodules S1..Sn and updating gitlinks in X.
+ // 3. Execute the work plan, i.e. convert the items into BatchUpdate.Ops and add them to the
+ // relevant updates.
+ //
+ // In addition to improving readability, this approach has the advantage of making (1) and (2)
+ // testable using small tests.
+ private ImmutableSet<Branch.NameKey> calculateSubscriptionMaps() throws SubmoduleException {
if (!enableSuperProjectSubscriptions) {
- logDebug("Updating superprojects disabled");
+ logger.atFine().log("Updating superprojects disabled");
return null;
}
- logDebug("Calculating superprojects - submodules map");
+ logger.atFine().log("Calculating superprojects - submodules map");
LinkedHashSet<Branch.NameKey> allVisited = new LinkedHashSet<>();
for (Branch.NameKey updatedBranch : updatedBranches) {
if (allVisited.contains(updatedBranch)) {
continue;
}
- searchForSuperprojects(updatedBranch, new LinkedHashSet<Branch.NameKey>(), allVisited);
+ searchForSuperprojects(updatedBranch, new LinkedHashSet<>(), allVisited);
}
// Since the searchForSuperprojects will add all branches (related or
@@ -213,7 +256,7 @@
LinkedHashSet<Branch.NameKey> currentVisited,
LinkedHashSet<Branch.NameKey> allVisited)
throws SubmoduleException {
- logDebug("Now processing %s", current);
+ logger.atFine().log("Now processing %s", current);
if (currentVisited.contains(current)) {
throw new SubmoduleException(
@@ -275,9 +318,9 @@
private Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src, SubscribeSection s)
throws IOException {
Collection<Branch.NameKey> ret = new HashSet<>();
- logDebug("Inspecting SubscribeSection %s", s);
+ logger.atFine().log("Inspecting SubscribeSection %s", s);
for (RefSpec r : s.getMatchingRefSpecs()) {
- logDebug("Inspecting [matching] ref %s", r);
+ logger.atFine().log("Inspecting [matching] ref %s", r);
if (!r.matchSource(src.get())) {
continue;
}
@@ -295,7 +338,7 @@
}
for (RefSpec r : s.getMultiMatchRefSpecs()) {
- logDebug("Inspecting [all] ref %s", r);
+ logger.atFine().log("Inspecting [all] ref %s", r);
if (!r.matchSource(src.get())) {
continue;
}
@@ -319,17 +362,18 @@
}
}
}
- logDebug("Returning possible branches: %s for project %s", ret, s.getProject());
+ logger.atFine().log("Returning possible branches: %s for project %s", ret, s.getProject());
return ret;
}
+ @UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
public Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
Branch.NameKey srcBranch) throws IOException {
- logDebug("Calculating possible superprojects for %s", srcBranch);
+ logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
Collection<SubmoduleSubscription> ret = new ArrayList<>();
Project.NameKey srcProject = srcBranch.getParentKey();
for (SubscribeSection s : projectCache.get(srcProject).getSubscribeSections(srcBranch)) {
- logDebug("Checking subscribe section %s", s);
+ logger.atFine().log("Checking subscribe section %s", s);
Collection<Branch.NameKey> branches = getDestinationBranches(srcBranch, s);
for (Branch.NameKey targetBranch : branches) {
Project.NameKey targetProject = targetBranch.getParentKey();
@@ -337,11 +381,11 @@
OpenRepo or = orm.getRepo(targetProject);
ObjectId id = or.repo.resolve(targetBranch.get());
if (id == null) {
- logDebug("The branch %s doesn't exist.", targetBranch);
+ logger.atFine().log("The branch %s doesn't exist.", targetBranch);
continue;
}
} catch (NoSuchProjectException e) {
- logDebug("The project %s doesn't exist", targetProject);
+ logger.atFine().log("The project %s doesn't exist", targetProject);
continue;
}
@@ -353,7 +397,7 @@
ret.addAll(m.subscribedTo(srcBranch));
}
}
- logDebug("Calculated superprojects for %s are %s", srcBranch, ret);
+ logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
return ret;
}
@@ -376,15 +420,14 @@
}
}
}
- batchUpdateFactory.execute(
- orm.batchUpdates(superProjects), BatchUpdateListener.NONE, orm.getSubmissionId(), false);
+ batchUpdateFactory.execute(orm.batchUpdates(superProjects), BatchUpdateListener.NONE, false);
} catch (RestApiException | UpdateException | IOException | NoSuchProjectException e) {
throw new SubmoduleException("Cannot update gitlinks", e);
}
}
/** Create a separate gitlink commit */
- public CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber)
+ private CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber)
throws IOException, SubmoduleException {
OpenRepo or;
try {
@@ -406,14 +449,18 @@
addBranchTip(subscriber, currentCommit);
}
- StringBuilder msgbuf = new StringBuilder("");
+ StringBuilder msgbuf = new StringBuilder();
PersonIdent author = null;
DirCache dc = readTree(or.rw, currentCommit);
DirCacheEditor ed = dc.editor();
int count = 0;
- List<SubmoduleSubscription> subscriptions = new ArrayList<>(targets.get(subscriber));
- Collections.sort(subscriptions, comparing(SubmoduleSubscription::getPath));
+ List<SubmoduleSubscription> subscriptions =
+ targets
+ .get(subscriber)
+ .stream()
+ .sorted(comparing(SubmoduleSubscription::getPath))
+ .collect(toList());
for (SubmoduleSubscription s : subscriptions) {
if (count > 0) {
msgbuf.append("\n\n");
@@ -452,8 +499,7 @@
}
/** Amend an existing commit with gitlink updates */
- public CodeReviewCommit composeGitlinksCommit(
- Branch.NameKey subscriber, CodeReviewCommit currentCommit)
+ CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber, CodeReviewCommit currentCommit)
throws IOException, SubmoduleException {
OpenRepo or;
try {
@@ -462,7 +508,7 @@
throw new SubmoduleException("Cannot access superproject", e);
}
- StringBuilder msgbuf = new StringBuilder("");
+ StringBuilder msgbuf = new StringBuilder();
DirCache dc = readTree(or.rw, currentCommit);
DirCacheEditor ed = dc.editor();
for (SubmoduleSubscription s : targets.get(subscriber)) {
@@ -496,6 +542,7 @@
private RevCommit updateSubmodule(
DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
throws SubmoduleException, IOException {
+ logger.atFine().log("Updating gitlink for %s", s);
OpenRepo subOr;
try {
subOr = orm.getRepo(s.getSubmodule().getParentKey());
@@ -523,7 +570,14 @@
// check that the old gitlink is a commit that actually exists. If not, then there is an
// inconsistency between the superproject and subproject state, and we don't want to risk
// making things worse by updating the gitlink to something else.
- oldCommit = subOr.rw.parseCommit(dce.getObjectId());
+ try {
+ oldCommit = subOr.rw.parseCommit(dce.getObjectId());
+ } catch (IOException e) {
+ // Broken gitlink; sanity check failed. Warn and continue so the submit operation can
+ // proceed, it will just skip this gitlink update.
+ logger.atSevere().withCause(e).log("Failed to read commit %s", dce.getObjectId().name());
+ return null;
+ }
}
final CodeReviewCommit newCommit;
@@ -577,7 +631,8 @@
msgbuf.append(" from branch '");
msgbuf.append(s.getSubmodule().getShortName());
msgbuf.append("'");
- msgbuf.append("\n to " + newCommit.getName());
+ msgbuf.append("\n to ");
+ msgbuf.append(newCommit.getName());
// newly created submodule gitlink, do not append whole history
if (oldCommit == null) {
@@ -628,7 +683,7 @@
return dc;
}
- public ImmutableSet<Project.NameKey> getProjectsInOrder() throws SubmoduleException {
+ ImmutableSet<Project.NameKey> getProjectsInOrder() throws SubmoduleException {
LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
for (Project.NameKey project : branchesByProject.keySet()) {
addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects);
@@ -671,7 +726,7 @@
projects.add(project);
}
- public ImmutableSet<Branch.NameKey> getBranchesInOrder() {
+ ImmutableSet<Branch.NameKey> getBranchesInOrder() {
LinkedHashSet<Branch.NameKey> branches = new LinkedHashSet<>();
if (sortedBranches != null) {
branches.addAll(sortedBranches);
@@ -680,27 +735,15 @@
return ImmutableSet.copyOf(branches);
}
- public boolean hasSubscription(Branch.NameKey branch) {
+ boolean hasSubscription(Branch.NameKey branch) {
return targets.containsKey(branch);
}
- public void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
+ void addBranchTip(Branch.NameKey branch, CodeReviewCommit tip) {
branchTips.put(branch, tip);
}
- public void addOp(BatchUpdate bu, Branch.NameKey branch) {
+ void addOp(BatchUpdate bu, Branch.NameKey branch) {
bu.addRepoOnlyOp(new GitlinkOp(branch));
}
-
- private void logDebug(String msg) {
- logger.atFine().log(orm.getSubmissionId() + " " + msg);
- }
-
- private void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(orm.getSubmissionId() + " " + msg, arg);
- }
-
- private void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
- logger.atFine().log(orm.getSubmissionId() + " " + msg, arg1, arg2);
- }
}
diff --git a/java/com/google/gerrit/server/submit/TestHelperOp.java b/java/com/google/gerrit/server/submit/TestHelperOp.java
index 2f0a3f6..bbb198a 100644
--- a/java/com/google/gerrit/server/submit/TestHelperOp.java
+++ b/java/com/google/gerrit/server/submit/TestHelperOp.java
@@ -15,12 +15,10 @@
package com.google.gerrit.server.submit;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.RepoContext;
-import com.google.gerrit.server.util.RequestId;
import java.io.IOException;
import java.util.Queue;
import org.eclipse.jgit.lib.ObjectId;
@@ -30,27 +28,22 @@
private final Change.Id changeId;
private final TestSubmitInput input;
- private final RequestId submissionId;
TestHelperOp(Change.Id changeId, SubmitStrategy.Arguments args) {
this.changeId = changeId;
this.input = (TestSubmitInput) args.submitInput;
- this.submissionId = args.submissionId;
}
@Override
public void updateRepo(RepoContext ctx) throws IOException {
Queue<Boolean> q = input.generateLockFailures;
if (q != null && !q.isEmpty() && q.remove()) {
- logDebug("Adding bogus ref update to trigger lock failure, via change %s", changeId);
+ logger.atFine().log(
+ "Adding bogus ref update to trigger lock failure, via change %s", changeId);
ctx.addRefUpdate(
ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
ObjectId.zeroId(),
"refs/test/" + getClass().getSimpleName());
}
}
-
- private void logDebug(String msg, @Nullable Object arg) {
- logger.atFine().log(submissionId + msg, arg);
- }
}
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index dd3cc73..f72512f 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -126,10 +126,7 @@
@SuppressWarnings({"rawtypes", "unchecked"})
public void execute(
- Collection<BatchUpdate> updates,
- BatchUpdateListener listener,
- @Nullable RequestId requestId,
- boolean dryRun)
+ Collection<BatchUpdate> updates, BatchUpdateListener listener, boolean dryRun)
throws UpdateException, RestApiException {
checkNotNull(listener);
checkDifferentProject(updates);
@@ -141,11 +138,11 @@
if (migration.disableChangeReviewDb()) {
ImmutableList<NoteDbBatchUpdate> noteDbUpdates =
(ImmutableList) ImmutableList.copyOf(updates);
- NoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun);
+ NoteDbBatchUpdate.execute(noteDbUpdates, listener, dryRun);
} else {
ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
(ImmutableList) ImmutableList.copyOf(updates);
- ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun);
+ ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, dryRun);
}
}
@@ -159,20 +156,6 @@
}
}
- static void setRequestIds(
- Collection<? extends BatchUpdate> updates, @Nullable RequestId requestId) {
- if (requestId != null) {
- for (BatchUpdate u : updates) {
- checkArgument(
- u.requestId == null || u.requestId == requestId,
- "refusing to overwrite RequestId %s in update with %s",
- u.requestId,
- requestId);
- u.setRequestId(requestId);
- }
- }
- }
-
static Order getOrder(Collection<? extends BatchUpdate> updates, BatchUpdateListener listener) {
Order o = null;
for (BatchUpdate u : updates) {
@@ -248,7 +231,6 @@
protected BatchRefUpdate batchRefUpdate;
protected Order order;
protected OnSubmitValidators onSubmitValidators;
- protected RequestId requestId;
protected PushCertificate pushCert;
protected String refLogMessage;
@@ -284,11 +266,6 @@
protected abstract Context newContext();
- public BatchUpdate setRequestId(RequestId requestId) {
- this.requestId = requestId;
- return this;
- }
-
public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
checkState(this.repoView == null, "repo already set");
repoView = new RepoView(repo, revWalk, inserter);
@@ -384,39 +361,39 @@
return this;
}
- protected void logDebug(String msg, Throwable t) {
+ protected static void logDebug(String msg, Throwable t) {
// Only log if there is a requestId assigned, since those are the
// expensive/complicated requests like MergeOp. Doing it every time would be
// noisy.
- if (requestId != null) {
- logger.atFine().withCause(t).log(requestId + "%s", msg);
+ if (RequestId.isSet()) {
+ logger.atFine().withCause(t).log("%s", msg);
}
}
- protected void logDebug(String msg) {
+ protected static void logDebug(String msg) {
// Only log if there is a requestId assigned, since those are the
// expensive/complicated requests like MergeOp. Doing it every time would be
// noisy.
- if (requestId != null) {
- logger.atFine().log(requestId + msg);
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg);
}
}
- protected void logDebug(String msg, @Nullable Object arg) {
+ protected static void logDebug(String msg, @Nullable Object arg) {
// Only log if there is a requestId assigned, since those are the
// expensive/complicated requests like MergeOp. Doing it every time would be
// noisy.
- if (requestId != null) {
- logger.atFine().log(requestId + msg, arg);
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg, arg);
}
}
- protected void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ protected static void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
// Only log if there is a requestId assigned, since those are the
// expensive/complicated requests like MergeOp. Doing it every time would be
// noisy.
- if (requestId != null) {
- logger.atFine().log(requestId + msg, arg1, arg2);
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg, arg1, arg2);
}
}
}
diff --git a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
index 8612fac..abe865c 100644
--- a/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
@@ -21,7 +21,6 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -35,7 +34,6 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.util.RequestId;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -68,15 +66,11 @@
}
static void execute(
- ImmutableList<NoteDbBatchUpdate> updates,
- BatchUpdateListener listener,
- @Nullable RequestId requestId,
- boolean dryrun)
+ ImmutableList<NoteDbBatchUpdate> updates, BatchUpdateListener listener, boolean dryrun)
throws UpdateException, RestApiException {
if (updates.isEmpty()) {
return;
}
- setRequestIds(updates, requestId);
try {
@SuppressWarnings("deprecation")
@@ -293,7 +287,7 @@
@Override
public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
- execute(ImmutableList.of(this), listener, requestId, false);
+ execute(ImmutableList.of(this), listener, false);
}
@Override
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 9bf4bb2..df50bd5 100644
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -28,7 +28,6 @@
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.metrics.Description;
@@ -51,6 +50,7 @@
import com.google.gerrit.server.git.InsertedObject;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NoteDbChangeState;
@@ -58,7 +58,6 @@
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException;
import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.util.RequestId;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -240,15 +239,11 @@
}
static void execute(
- ImmutableList<ReviewDbBatchUpdate> updates,
- BatchUpdateListener listener,
- @Nullable RequestId requestId,
- boolean dryrun)
+ ImmutableList<ReviewDbBatchUpdate> updates, BatchUpdateListener listener, boolean dryrun)
throws UpdateException, RestApiException {
if (updates.isEmpty()) {
return;
}
- setRequestIds(updates, requestId);
try {
Order order = getOrder(updates, listener);
boolean updateChangesInParallel = getUpdateChangesInParallel(updates);
@@ -358,7 +353,7 @@
@Override
public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
- execute(ImmutableList.of(this), listener, requestId, false);
+ execute(ImmutableList.of(this), listener, false);
}
@Override
@@ -616,7 +611,6 @@
NoteDbUpdateManager.StagedResult noteDbResult;
boolean dirty;
boolean deleted;
- private String taskId;
private ChangeTask(
Change.Id id, Collection<BatchUpdateOp> changeOps, Thread mainThread, boolean dryrun) {
@@ -628,27 +622,30 @@
@Override
public Void call() throws Exception {
- taskId = id.toString() + "-" + Thread.currentThread().getId();
- if (Thread.currentThread() == mainThread) {
- initRepository();
- Repository repo = repoView.getRepository();
- try (RevWalk rw = new RevWalk(repo)) {
- call(ReviewDbBatchUpdate.this.db, repo, rw);
+ try (TraceContext traceContext =
+ TraceContext.open()
+ .addTag("TASK_ID", id.toString() + "-" + Thread.currentThread().getId())) {
+ if (Thread.currentThread() == mainThread) {
+ initRepository();
+ Repository repo = repoView.getRepository();
+ try (RevWalk rw = new RevWalk(repo)) {
+ call(ReviewDbBatchUpdate.this.db, repo, rw);
+ }
+ } else {
+ // Possible optimization: allow Ops to declare whether they need to
+ // access the repo from updateChange, and don't open in this thread
+ // unless we need it. However, as of this writing the only operations
+ // that are executed in parallel are during ReceiveCommits, and they
+ // all need the repo open anyway. (The non-parallel case above does not
+ // reopen the repo.)
+ try (ReviewDb threadLocalDb = schemaFactory.open();
+ Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo)) {
+ call(threadLocalDb, repo, rw);
+ }
}
- } else {
- // Possible optimization: allow Ops to declare whether they need to
- // access the repo from updateChange, and don't open in this thread
- // unless we need it. However, as of this writing the only operations
- // that are executed in parallel are during ReceiveCommits, and they
- // all need the repo open anyway. (The non-parallel case above does not
- // reopen the repo.)
- try (ReviewDb threadLocalDb = schemaFactory.open();
- Repository repo = repoManager.openRepository(project);
- RevWalk rw = new RevWalk(repo)) {
- call(threadLocalDb, repo, rw);
- }
+ return null;
}
- return null;
}
private void call(ReviewDb db, Repository repo, RevWalk rw) throws Exception {
@@ -822,18 +819,6 @@
private boolean isNewChange(Change.Id id) {
return newChanges.containsKey(id);
}
-
- private void logDebug(String msg, Throwable t) {
- ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg, t);
- }
-
- private void logDebug(String msg) {
- ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg);
- }
-
- private void logDebug(String msg, @Nullable Object arg) {
- ReviewDbBatchUpdate.this.logDebug("[" + taskId + "] " + msg, arg);
- }
}
private static Iterable<Change> changesToUpdate(ChangeContextImpl ctx) {
diff --git a/java/com/google/gerrit/server/util/RequestId.java b/java/com/google/gerrit/server/util/RequestId.java
index 8e8db12..78f68aa 100644
--- a/java/com/google/gerrit/server/util/RequestId.java
+++ b/java/com/google/gerrit/server/util/RequestId.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.util;
+import com.google.common.base.Enums;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.logging.LoggingContext;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -36,6 +39,20 @@
MACHINE_ID = id;
}
+ public enum Type {
+ RECEIVE_ID,
+ SUBMISSION_ID,
+ TRACE_ID;
+
+ static boolean isId(String id) {
+ return id != null && Enums.getIfPresent(Type.class, id).isPresent();
+ }
+ }
+
+ public static boolean isSet() {
+ return LoggingContext.getInstance().getTagsAsMap().keySet().stream().anyMatch(Type::isId);
+ }
+
public static RequestId forChange(Change c) {
return new RequestId(c.getId().toString());
}
@@ -46,17 +63,18 @@
private final String str;
- private RequestId(String resourceId) {
+ public RequestId() {
+ this(null);
+ }
+
+ private RequestId(@Nullable String resourceId) {
Hasher h = Hashing.murmur3_128().newHasher();
h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
str =
- "["
- + resourceId
- + "-"
+ (resourceId != null ? resourceId + "-" : "")
+ TimeUtil.nowTs().getTime()
+ "-"
- + h.hash().toString().substring(0, 8)
- + "]";
+ + h.hash().toString().substring(0, 8);
}
@Override
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 3eef4d6..68ea7bb 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -22,6 +22,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.logging.LoggingContextAwareThreadFactory;
import com.google.gerrit.sshd.SshScope.Context;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -79,6 +80,7 @@
destroyExecutor =
Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder()
+ .setThreadFactory(new LoggingContextAwareThreadFactory())
.setNameFormat("SshCommandDestroy-%s")
.setDaemon(true)
.build());
diff --git a/java/com/google/gerrit/sshd/SshCommand.java b/java/com/google/gerrit/sshd/SshCommand.java
index 3e42ebe..300a602 100644
--- a/java/com/google/gerrit/sshd/SshCommand.java
+++ b/java/com/google/gerrit/sshd/SshCommand.java
@@ -14,11 +14,17 @@
package com.google.gerrit.sshd;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.util.RequestId;
import java.io.IOException;
import java.io.PrintWriter;
import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Option;
public abstract class SshCommand extends BaseCommand {
+ @Option(name = "--trace", usage = "enable request tracing")
+ private boolean trace;
+
protected PrintWriter stdout;
protected PrintWriter stderr;
@@ -31,7 +37,7 @@
parseCommandLine();
stdout = toPrintWriter(out);
stderr = toPrintWriter(err);
- try {
+ try (TraceContext traceContext = enableTracing()) {
SshCommand.this.run();
} finally {
stdout.flush();
@@ -42,4 +48,13 @@
}
protected abstract void run() throws UnloggedFailure, Failure, Exception;
+
+ private TraceContext enableTracing() {
+ if (trace) {
+ RequestId traceId = new RequestId();
+ stderr.println(String.format("%s: %s", RequestId.Type.TRACE_ID, traceId));
+ return TraceContext.open().addTag(RequestId.Type.TRACE_ID, traceId);
+ }
+ return TraceContext.DISABLED;
+ }
}
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index b573062..3cd1a0c 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -102,6 +102,8 @@
@Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
+ logger.atFine().log("Loading SSH keys for account with username %s", username);
+
Optional<ExternalId> user = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
if (!user.isPresent()) {
return NO_SUCH_USER;
diff --git a/java/com/google/gerrit/testing/ConfigSuite.java b/java/com/google/gerrit/testing/ConfigSuite.java
index b0229c3..ff87fd8 100644
--- a/java/com/google/gerrit/testing/ConfigSuite.java
+++ b/java/com/google/gerrit/testing/ConfigSuite.java
@@ -24,6 +24,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.gerrit.server.logging.LoggingContext;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -105,11 +106,13 @@
*/
public class ConfigSuite extends Suite {
private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
+ private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
static {
System.setProperty(
FLOGGER_BACKEND_PROPERTY,
"com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
+ System.setProperty(FLOGGER_LOGGING_CONTEXT, LoggingContext.class.getName() + "#getInstance");
}
public static final String DEFAULT = "default";
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 0a42b1e..6fd9545 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -214,16 +214,6 @@
}
@Test
- public void reflog() throws Exception {
- // Tests are using DfsRepository which does not implement getReflogReader,
- // so this will always fail.
- // TODO: change this if/when DfsRepository#getReflogReader is implemented.
- exception.expect(MethodNotAllowedException.class);
- exception.expectMessage("reflog not supported");
- gApi.projects().name(project.get()).branch("master").reflog();
- }
-
- @Test
public void get() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
new file mode 100644
index 0000000..6c6ad3d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckProjectIT.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.CheckProjectInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectInput.AutoCloseableChangesCheckInput;
+import com.google.gerrit.extensions.api.projects.CheckProjectResultInfo;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.project.ProjectsConsistencyChecker;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckProjectIT extends AbstractDaemonTest {
+ private TestRepository<InMemoryRepository> serverSideTestRepo;
+
+ @Before
+ public void setUp() throws Exception {
+ serverSideTestRepo =
+ new TestRepository<>((InMemoryRepository) repoManager.openRepository(project));
+ }
+
+ @Test
+ public void noProblem() throws Exception {
+ PushOneCommit.Result r = createChange("refs/for/master");
+ String branch = r.getChange().change().getDest().get();
+
+ ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectResultInfo checkResult =
+ gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
+ assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+ }
+
+ @Test
+ public void detectAutoCloseableChangeByCommit() throws Exception {
+ RevCommit commit = pushCommitWithoutChangeIdForReview();
+ ChangeInfo change =
+ Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
+
+ String branch = "refs/heads/master";
+ serverSideTestRepo.branch(branch).update(testRepo.getRevWalk().parseCommit(commit));
+
+ ChangeInfo info = gApi.changes().id(change._number).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectResultInfo checkResult =
+ gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toList()))
+ .containsExactly(change._number);
+
+ info = gApi.changes().id(change._number).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+ }
+
+ @Test
+ public void fixAutoCloseableChangeByCommit() throws Exception {
+ RevCommit commit = pushCommitWithoutChangeIdForReview();
+ ChangeInfo change =
+ Iterables.getOnlyElement(gApi.changes().query("commit:" + commit.name()).get());
+
+ String branch = "refs/heads/master";
+ serverSideTestRepo.branch(branch).update(commit);
+
+ ChangeInfo info = gApi.changes().id(change._number).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
+ input.autoCloseableChangesCheck.fix = true;
+ CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toSet()))
+ .containsExactly(change._number);
+
+ info = gApi.changes().id(change._number).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
+ public void detectAutoCloseableChangeByChangeId() throws Exception {
+ PushOneCommit.Result r = createChange("refs/for/master");
+ String branch = r.getChange().change().getDest().get();
+
+ RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
+ serverSideTestRepo.branch(branch).update(amendedCommit);
+
+ ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectResultInfo checkResult =
+ gApi.projects().name(project.get()).check(checkProjectInputForAutoCloseableCheck(branch));
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toSet()))
+ .containsExactly(r.getChange().getId().get());
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+ }
+
+ @Test
+ public void fixAutoCloseableChangeByChangeId() throws Exception {
+ PushOneCommit.Result r = createChange("refs/for/master");
+ String branch = r.getChange().change().getDest().get();
+
+ RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
+ serverSideTestRepo.branch(branch).update(amendedCommit);
+
+ ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
+ input.autoCloseableChangesCheck.fix = true;
+ CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toSet()))
+ .containsExactly(r.getChange().getId().get());
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
+ public void maxCommits() throws Exception {
+ PushOneCommit.Result r = createChange("refs/for/master");
+ String branch = r.getChange().change().getDest().get();
+
+ RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
+ serverSideTestRepo.branch(branch).update(amendedCommit);
+
+ serverSideTestRepo.commit(amendedCommit);
+
+ ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
+ input.autoCloseableChangesCheck.fix = true;
+ input.autoCloseableChangesCheck.maxCommits = 1;
+ CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ input.autoCloseableChangesCheck.maxCommits = 2;
+ checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toSet()))
+ .containsExactly(r.getChange().getId().get());
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
+ public void skipCommits() throws Exception {
+ PushOneCommit.Result r = createChange("refs/for/master");
+ String branch = r.getChange().change().getDest().get();
+
+ RevCommit amendedCommit = serverSideTestRepo.amend(r.getCommit()).create();
+ serverSideTestRepo.branch(branch).update(amendedCommit);
+
+ serverSideTestRepo.commit(amendedCommit);
+
+ ChangeInfo info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck(branch);
+ input.autoCloseableChangesCheck.fix = true;
+ input.autoCloseableChangesCheck.maxCommits = 1;
+ CheckProjectResultInfo checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(checkResult.autoCloseableChangesCheckResult.autoCloseableChanges).isEmpty();
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+ input.autoCloseableChangesCheck.skipCommits = 1;
+ checkResult = gApi.projects().name(project.get()).check(input);
+ assertThat(
+ checkResult
+ .autoCloseableChangesCheckResult
+ .autoCloseableChanges
+ .stream()
+ .map(i -> i._number)
+ .collect(toSet()))
+ .containsExactly(r.getChange().getId().get());
+
+ info = gApi.changes().id(r.getChange().getId().get()).info();
+ assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
+ public void noBranch() throws Exception {
+ CheckProjectInput input = new CheckProjectInput();
+ input.autoCloseableChangesCheck = new AutoCloseableChangesCheckInput();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("branch is required");
+ gApi.projects().name(project.get()).check(input);
+ }
+
+ @Test
+ public void nonExistingBranch() throws Exception {
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck("non-existing");
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("branch 'non-existing' not found");
+ gApi.projects().name(project.get()).check(input);
+ }
+
+ @Test
+ public void branchPrefixCanBeOmitted() throws Exception {
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck("master");
+ gApi.projects().name(project.get()).check(input);
+ }
+
+ @Test
+ public void setLimitForMaxCommits() throws Exception {
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck("refs/heads/master");
+ input.autoCloseableChangesCheck.maxCommits =
+ ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT;
+ gApi.projects().name(project.get()).check(input);
+ }
+
+ @Test
+ public void tooLargeMaxCommits() throws Exception {
+ CheckProjectInput input = checkProjectInputForAutoCloseableCheck("refs/heads/master");
+ input.autoCloseableChangesCheck.maxCommits =
+ ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT + 1;
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage(
+ "max commits can at most be set to "
+ + ProjectsConsistencyChecker.AUTO_CLOSE_MAX_COMMITS_LIMIT);
+ gApi.projects().name(project.get()).check(input);
+ }
+
+ private RevCommit pushCommitWithoutChangeIdForReview() throws Exception {
+ setRequireChangeId(InheritableBoolean.FALSE);
+ RevCommit commit =
+ testRepo
+ .branch("HEAD")
+ .commit()
+ .message("A change")
+ .author(admin.getIdent())
+ .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()))
+ .create();
+ pushHead(testRepo, "refs/for/master");
+ return commit;
+ }
+
+ private static CheckProjectInput checkProjectInputForAutoCloseableCheck(String branch) {
+ CheckProjectInput input = new CheckProjectInput();
+ input.autoCloseableChangesCheck = new AutoCloseableChangesCheckInput();
+ input.autoCloseableChangesCheck.branch = branch;
+ return input;
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 8479dd1..5be8dfd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -23,6 +23,7 @@
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -368,7 +369,7 @@
gApi.projects().name(project.get()).branch("test").create(new BranchInput());
setApiUser(user);
exception.expect(AuthException.class);
- exception.expectMessage("set HEAD not permitted for refs/heads/test");
+ exception.expectMessage("not permitted: set HEAD on refs/heads/test");
gApi.projects().name(project.get()).head("test");
}
@@ -410,6 +411,94 @@
ImmutableMap.of(project.get(), 1L, middle.get(), 1L, leave.get(), 1L));
}
+ @Test
+ public void maxObjectSizeIsNotSetByDefault() throws Exception {
+ ConfigInfo info = getConfig();
+ assertThat(info.maxObjectSizeLimit.value).isNull();
+ assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+ }
+
+ @Test
+ public void maxObjectSizeCanBeSetAndCleared() throws Exception {
+ // Set a value
+ ConfigInfo info = setMaxObjectSize("100k");
+ assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+ assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+
+ // Clear the value
+ info = setMaxObjectSize("0");
+ assertThat(info.maxObjectSizeLimit.value).isNull();
+ assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+ }
+
+ @Test
+ public void maxObjectSizeIsNotInheritedFromParentProject() throws Exception {
+ Project.NameKey child = createProject(name("child"), project);
+
+ ConfigInfo info = setMaxObjectSize("100k");
+ assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+ assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+
+ info = getConfig(child);
+ assertThat(info.maxObjectSizeLimit.value).isNull();
+ assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isNull();
+ }
+
+ @Test
+ @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
+ public void maxObjectSizeIsInheritedFromGlobalConfig() throws Exception {
+ ConfigInfo info = getConfig();
+ assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+ assertThat(info.maxObjectSizeLimit.configuredValue).isNull();
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+ }
+
+ @Test
+ @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
+ public void maxObjectSizeOverridesGlobalConfigWhenLower() throws Exception {
+ ConfigInfo info = setMaxObjectSize("100k");
+ assertThat(info.maxObjectSizeLimit.value).isEqualTo("102400");
+ assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("100k");
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+ }
+
+ @Test
+ @GerritConfig(name = "receive.maxObjectSizeLimit", value = "200k")
+ public void maxObjectSizeDoesNotOverrideGlobalConfigWhenHigher() throws Exception {
+ ConfigInfo info = setMaxObjectSize("300k");
+ assertThat(info.maxObjectSizeLimit.value).isEqualTo("204800");
+ assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo("300k");
+ assertThat(info.maxObjectSizeLimit.inheritedValue).isEqualTo("200k");
+ }
+
+ @Test
+ public void invalidMaxObjectSizeIsRejected() throws Exception {
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("100 foo");
+ setMaxObjectSize("100 foo");
+ }
+
+ private ConfigInfo setConfig(Project.NameKey name, ConfigInput input) throws Exception {
+ return gApi.projects().name(name.get()).config(input);
+ }
+
+ private ConfigInfo setConfig(ConfigInput input) throws Exception {
+ return setConfig(project, input);
+ }
+
+ private ConfigInfo getConfig(Project.NameKey name) throws Exception {
+ return gApi.projects().name(name.get()).config();
+ }
+
+ private ConfigInfo getConfig() throws Exception {
+ return getConfig(project);
+ }
+
private ConfigInput createTestConfigInput() {
ConfigInput input = new ConfigInput();
input.description = "some description";
@@ -427,6 +516,12 @@
return input;
}
+ private ConfigInfo setMaxObjectSize(String value) throws Exception {
+ ConfigInput input = new ConfigInput();
+ input.maxObjectSizeLimit = value;
+ return setConfig(input);
+ }
+
private static class ProjectIndexedCounter implements ProjectIndexedListener {
private final AtomicLongMap<String> countsByProject = AtomicLongMap.create();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 53cc5ad..057f837 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -32,6 +32,9 @@
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.changes.FileApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
@@ -2343,6 +2346,126 @@
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
+ @Test
+ public void diffOfUnmodifiedFileWithWholeFileContextReturnsFileContents() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(
+ changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
+ .get();
+ // We don't list the full file contents here as that is not the focus of this test.
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .commonLines()
+ .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+ .inOrder();
+ }
+
+ @Test
+ public void diffOfUnmodifiedFileWithCommentAndWholeFileContextReturnsFileContents()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
+ gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
+ addModifiedPatchSet(
+ changeId, FILE_NAME2, content -> content.replace("2nd line\n", "Second line\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
+ .get();
+ // We don't list the full file contents here as that is not the focus of this test.
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .commonLines()
+ .containsAllOf("Line 1", "Line two", "Line 3", "Line 4", "Line 5")
+ .inOrder();
+ }
+
+ @Test
+ public void diffOfNonExistentFileIsAnEmptyDiffResult() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, "a_non-existent_file.txt")
+ .withBase(initialPatchSetId)
+ .withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
+ .get();
+ assertThat(diffInfo).content().isEmpty();
+ }
+
+ @Test
+ public void requestingDiffForOldFileNameOfRenamedFileYieldsReasonableResult() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ String newFilePath = "a_new_file.txt";
+ gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
+ .get();
+ // This behavior has been present in Gerrit for quite some time. It differs from the results
+ // returned for other cases (e.g. requesting the diff with whole file context for an unmodified
+ // file; requesting the diff with whole file context for a non-existent file). However, it's not
+ // completely clear what should be returned. The closest would be the result of a file deletion
+ // but that might also be misleading for users as actually a file rename occurred. In fact,
+ // requesting the diff result for the old file name of a renamed file is not a reasonable use
+ // case at all. We at least guarantee that we don't run into an internal error.
+ assertThat(diffInfo).content().element(0).commonLines().isNull();
+ assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0);
+ }
+
+ @Test
+ public void requestingDiffForOldFileNameOfRenamedFileWithCommentOnOldFileYieldsReasonableResult()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, content -> content.replace("Line 2\n", "Line two\n"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ CommentInput comment = createCommentInput(2, 0, 3, 0, "Should be 'Line 2'.");
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(comment));
+ gApi.changes().id(changeId).revision(previousPatchSetId).review(reviewInput);
+ String newFilePath = "a_new_file.txt";
+ gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withContext(DiffPreferencesInfo.WHOLE_FILE_CONTEXT)
+ .get();
+ // See comment for requestingDiffForOldFileNameOfRenamedFileYieldsReasonableResult().
+ // This test should additionally ensure that we also don't run into an internal error when
+ // a comment is present.
+ assertThat(diffInfo).content().element(0).commonLines().isNull();
+ assertThat(diffInfo).content().element(0).numberOfSkippedLines().isGreaterThan(0);
+ }
+
+ private static CommentInput createCommentInput(
+ int startLine, int startCharacter, int endLine, int endCharacter, String message) {
+ CommentInput comment = new CommentInput();
+ comment.range = new Comment.Range();
+ comment.range.startLine = startLine;
+ comment.range.startCharacter = startCharacter;
+ comment.range.endLine = endLine;
+ comment.range.endCharacter = endCharacter;
+ comment.message = message;
+ return comment;
+ }
+
private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff =
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index b4ae8a2..1bdaf9d 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -1228,7 +1228,7 @@
// BatchUpdate implementations differ in how they hook into progress monitors. We mostly just
// care that there is a new change.
- assertThat(pr.getMessages()).containsMatch("changes: new: 1,( refs: 1)? done");
+ assertThat(pr.getMessages()).containsMatch("changes: .*new: 1.*done");
assertTwoChangesWithSameRevision(r);
}
@@ -2037,7 +2037,8 @@
assertPushOk(pushHead(testRepo, master), master);
commits.addAll(initChanges(3));
- assertPushRejected(pushHead(testRepo, master), master, "too many commits");
+ assertPushRejected(
+ pushHead(testRepo, master), master, "more than 2 commits, and skip-validation not set");
grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS);
PushResult r =
@@ -2079,7 +2080,7 @@
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
pr = pushOne(testRepo, c.name(), ref, false, false, opts);
- assertPushRejected(pr, ref, "prohibited by Gerrit: create not permitted for " + ref);
+ assertPushRejected(pr, ref, "prohibited by Gerrit: not permitted: create");
grant(project, "refs/changes/*", Permission.CREATE);
grant(project, "refs/changes/*", Permission.PUSH);
@@ -2101,11 +2102,11 @@
.push()
.setRefSpecs(
new RefSpec(noteDbCommit.name() + ":" + ref),
- new RefSpec(changeCommit.name() + ":refs/for/master"))
+ new RefSpec(changeCommit.name() + ":refs/heads/permitted"))
.call());
assertPushRejected(pr, ref, "NoteDb update requires -o notedb=allow");
- assertPushOk(pr, "refs/for/master");
+ assertPushOk(pr, "refs/heads/permitted");
}
private DraftInput newDraft(String path, int line, String message) {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 252ec88..943b052 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -29,11 +29,22 @@
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.StreamSupport;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.FileMode;
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.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTree;
@@ -451,4 +462,47 @@
RevCommit c = rw.parseCommit(commitId);
return c.getAuthorIdent();
}
+
+ protected void directUpdateSubmodule(String project, String refName, String path, AnyObjectId id)
+ throws Exception {
+ path = name(path);
+ try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)));
+ ObjectInserter ins = serverRepo.newObjectInserter();
+ RevWalk rw = new RevWalk(serverRepo)) {
+ Ref ref = serverRepo.exactRef(refName);
+ assertThat(ref).named(refName).isNotNull();
+ ObjectId oldCommitId = ref.getObjectId();
+
+ DirCache dc = DirCache.newInCore();
+ DirCacheBuilder b = dc.builder();
+ b.addTree(
+ new byte[0], DirCacheEntry.STAGE_0, rw.getObjectReader(), rw.parseTree(oldCommitId));
+ b.finish();
+ DirCacheEditor e = dc.editor();
+ e.add(
+ new PathEdit(path) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.GITLINK);
+ ent.setObjectId(id);
+ }
+ });
+ e.finish();
+
+ CommitBuilder cb = new CommitBuilder();
+ cb.addParentId(oldCommitId);
+ cb.setTreeId(dc.writeTree(ins));
+ PersonIdent ident = serverIdent.get();
+ cb.setAuthor(ident);
+ cb.setCommitter(ident);
+ cb.setMessage("Direct update submodule " + path);
+ ObjectId newCommitId = ins.insert(cb);
+ ins.flush();
+
+ RefUpdate ru = serverRepo.updateRef(refName);
+ ru.setExpectedOldObjectId(oldCommitId);
+ ru.setNewObjectId(newCommitId);
+ assertThat(ru.update()).isEqualTo(RefUpdate.Result.FAST_FORWARD);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index 87ac022..d80faa8 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -51,7 +51,7 @@
pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
push2.setForce(true);
PushOneCommit.Result r2 = push2.to("refs/heads/master");
- r2.assertErrorStatus("need 'Force Push' privilege.");
+ r2.assertErrorStatus("not permitted: force update");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
index 80430c4..907ad7f 100644
--- a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -46,6 +46,7 @@
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.TrackingRefUpdate;
import org.junit.Before;
import org.junit.Test;
@@ -78,20 +79,40 @@
}
@Test
+ public void mixingMagicAndRegularPush() throws Exception {
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/for/master");
+
+ String msg = "cannot combine normal pushes and magic pushes";
+ assertThat(r.getRemoteUpdate("refs/heads/master")).isNotEqualTo(Status.OK);
+ assertThat(r.getRemoteUpdate("refs/for/master")).isNotEqualTo(Status.OK);
+ assertThat(r.getRemoteUpdate("refs/for/master").getMessage()).isEqualTo(msg);
+ }
+
+ @Test
+ public void mixingDirectChangesAndRegularPush() throws Exception {
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/changes/01/101");
+
+ String msg = "cannot combine normal pushes and magic pushes";
+ assertThat(r.getRemoteUpdate("refs/heads/master")).isNotEqualTo(Status.OK);
+ assertThat(r.getRemoteUpdate("refs/changes/01/101")).isNotEqualTo(Status.OK);
+ assertThat(r.getRemoteUpdate("refs/heads/master").getMessage()).isEqualTo(msg);
+ }
+
+ @Test
public void fastForwardUpdateDenied() throws Exception {
testRepo.branch("HEAD").commit().create();
PushResult r = push("HEAD:refs/heads/master");
assertThat(r)
.onlyRef("refs/heads/master")
- .isRejected("prohibited by Gerrit: ref update access denied");
+ .isRejected("prohibited by Gerrit: not permitted: update");
assertThat(r)
.hasMessages(
- "Branch refs/heads/master:",
- "You are not allowed to perform this operation.",
+ "error: branch refs/heads/master:",
"To push into this reference you need 'Push' rights.",
"User: admin",
- "Please read the documentation and contact an administrator",
- "if you feel the configuration is incorrect");
+ "Contact an administrator to fix the permissions");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@@ -99,24 +120,25 @@
public void nonFastForwardUpdateDenied() throws Exception {
ObjectId commit = testRepo.commit().create();
PushResult r = push("+" + commit.name() + ":refs/heads/master");
- assertThat(r).onlyRef("refs/heads/master").isRejected("need 'Force Push' privilege.");
- assertThat(r).hasNoMessages();
- // TODO(dborowitz): Why does this not mention refs?
- assertThat(r).hasProcessed(ImmutableMap.of());
+ assertThat(r)
+ .onlyRef("refs/heads/master")
+ .isRejected("prohibited by Gerrit: not permitted: force update");
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@Test
public void deleteDenied() throws Exception {
PushResult r = push(":refs/heads/master");
- assertThat(r).onlyRef("refs/heads/master").isRejected("cannot delete references");
+ assertThat(r)
+ .onlyRef("refs/heads/master")
+ .isRejected("prohibited by Gerrit: not permitted: delete");
assertThat(r)
.hasMessages(
- "Branch refs/heads/master:",
+ "error: branch refs/heads/master:",
"You need 'Delete Reference' rights or 'Push' rights with the ",
"'Force Push' flag set to delete references.",
"User: admin",
- "Please read the documentation and contact an administrator",
- "if you feel the configuration is incorrect");
+ "Contact an administrator to fix the permissions");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@@ -126,8 +148,8 @@
PushResult r = push("HEAD:refs/heads/newbranch");
assertThat(r)
.onlyRef("refs/heads/newbranch")
- .isRejected("prohibited by Gerrit: create not permitted for refs/heads/newbranch");
- assertThat(r).hasNoMessages();
+ .isRejected("prohibited by Gerrit: not permitted: create");
+ assertThat(r).containsMessages("You need 'Create' rights to create new references.");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@@ -141,22 +163,20 @@
testRepo.branch("HEAD").commit().create();
PushResult r = push(":refs/heads/foo", ":refs/heads/bar", "HEAD:refs/heads/master");
- assertThat(r).ref("refs/heads/foo").isRejected("cannot delete references");
- assertThat(r).ref("refs/heads/bar").isRejected("cannot delete references");
+ assertThat(r).ref("refs/heads/foo").isRejected("prohibited by Gerrit: not permitted: delete");
+ assertThat(r).ref("refs/heads/bar").isRejected("prohibited by Gerrit: not permitted: delete");
assertThat(r)
.ref("refs/heads/master")
- .isRejected("prohibited by Gerrit: ref update access denied");
+ .isRejected("prohibited by Gerrit: not permitted: update");
assertThat(r)
.hasMessages(
- "Branches refs/heads/foo, refs/heads/bar:",
+ "error: branches refs/heads/foo, refs/heads/bar:",
"You need 'Delete Reference' rights or 'Push' rights with the ",
"'Force Push' flag set to delete references.",
- "Branch refs/heads/master:",
- "You are not allowed to perform this operation.",
+ "error: branch refs/heads/master:",
"To push into this reference you need 'Push' rights.",
"User: admin",
- "Please read the documentation and contact an administrator",
- "if you feel the configuration is incorrect");
+ "Contact an administrator to fix the permissions");
}
@Test
@@ -188,16 +208,14 @@
// ReceiveCommits theoretically has a different message when a WRITE_CONFIG check fails, but
// it never gets there, since DefaultPermissionBackend special-cases refs/meta/config and
// denies UPDATE if the user is not a project owner.
- .isRejected("prohibited by Gerrit: ref update access denied");
+ .isRejected("prohibited by Gerrit: not permitted: update");
assertThat(r)
.hasMessages(
- "Branch refs/meta/config:",
- "You are not allowed to perform this operation.",
+ "error: branch refs/meta/config:",
"Configuration changes can only be pushed by project owners",
"who also have 'Push' rights on refs/meta/config",
"User: admin",
- "Please read the documentation and contact an administrator",
- "if you feel the configuration is incorrect");
+ "Contact an administrator to fix the permissions");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
@@ -216,15 +234,12 @@
PushResult r = push("HEAD:refs/for/master");
assertThat(r)
.onlyRef("refs/for/master")
- .isRejected("create change not permitted for refs/heads/master");
+ .isRejected("prohibited by Gerrit: not permitted: create change on refs/heads/master");
assertThat(r)
- .hasMessages(
- "Branch refs/heads/master:",
- "You need 'Push' rights to upload code review requests.",
- "Verify that you are pushing to the right branch.",
- "User: admin",
- "Please read the documentation and contact an administrator",
- "if you feel the configuration is incorrect");
+ .containsMessages(
+ "error: branch refs/for/master:",
+ "You need 'Create Change' rights to upload code review requests.",
+ "Verify that you are pushing to the right branch.");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@@ -239,8 +254,10 @@
PushResult r = push("HEAD:refs/for/master%submit");
assertThat(r)
.onlyRef("refs/for/master%submit")
- .isRejected("update by submit not permitted for refs/heads/master");
- assertThat(r).hasNoMessages();
+ .isRejected("prohibited by Gerrit: not permitted: update by submit on refs/heads/master");
+ assertThat(r)
+ .containsMessages(
+ "You need 'Submit' rights on refs/for/ to submit changes during change upload.");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
@@ -274,8 +291,11 @@
push(c -> c.setPushOptions(ImmutableList.of("skip-validation")), "HEAD:refs/heads/master");
assertThat(r)
.onlyRef("refs/heads/master")
- .isRejected("skip validation not permitted for refs/heads/master");
- assertThat(r).hasNoMessages();
+ .isRejected("prohibited by Gerrit: not permitted: skip validation");
+ assertThat(r)
+ .containsMessages(
+ "You need 'Forge Author', 'Forge Server', 'Forge Committer'",
+ "and 'Push Merge' rights to skip validation.");
assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
}
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 0705dca..700b18b 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -37,11 +37,8 @@
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -62,38 +59,6 @@
}
@Test
- public void submitOnPushWithTag() throws Exception {
- grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
- grant(project, "refs/tags/*", Permission.CREATE);
- grant(project, "refs/tags/*", Permission.PUSH);
- PushOneCommit.Tag tag = new PushOneCommit.Tag("v1.0");
- PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
- push.setTag(tag);
- PushOneCommit.Result r = push.to("refs/for/master%submit");
- r.assertOkStatus();
- r.assertChange(Change.Status.MERGED, null, admin);
- assertSubmitApproval(r.getPatchSetId());
- assertCommit(project, "refs/heads/master");
- assertTag(project, "refs/heads/master", tag);
- }
-
- @Test
- public void submitOnPushWithAnnotatedTag() throws Exception {
- grant(project, "refs/for/refs/heads/master", Permission.SUBMIT);
- grant(project, "refs/tags/*", Permission.PUSH);
- PushOneCommit.AnnotatedTag tag =
- new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent());
- PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
- push.setTag(tag);
- PushOneCommit.Result r = push.to("refs/for/master%submit");
- r.assertOkStatus();
- r.assertChange(Change.Status.MERGED, null, admin);
- assertSubmitApproval(r.getPatchSetId());
- assertCommit(project, "refs/heads/master");
- assertTag(project, "refs/heads/master", tag);
- }
-
- @Test
public void submitOnPushToRefsMetaConfig() throws Exception {
grant(project, "refs/for/refs/meta/config", Permission.SUBMIT);
@@ -158,7 +123,7 @@
@Test
public void submitOnPushNotAllowed_Error() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master%submit");
- r.assertErrorStatus("update by submit not permitted");
+ r.assertErrorStatus("not permitted: update by submit");
}
@Test
@@ -170,7 +135,7 @@
push(
"refs/for/master%submit",
PushOneCommit.SUBJECT, "a.txt", "other content", r.getChangeId());
- r.assertErrorStatus("update by submit not permitted");
+ r.assertErrorStatus("not permitted: update by submit ");
}
@Test
@@ -385,31 +350,6 @@
}
}
- private void assertTag(Project.NameKey project, String branch, PushOneCommit.Tag tag)
- throws Exception {
- try (Repository repo = repoManager.openRepository(project)) {
- Ref tagRef = repo.findRef(tag.name);
- assertThat(tagRef).isNotNull();
- ObjectId taggedCommit = null;
- if (tag instanceof PushOneCommit.AnnotatedTag) {
- PushOneCommit.AnnotatedTag annotatedTag = (PushOneCommit.AnnotatedTag) tag;
- try (RevWalk rw = new RevWalk(repo)) {
- RevObject object = rw.parseAny(tagRef.getObjectId());
- assertThat(object).isInstanceOf(RevTag.class);
- RevTag tagObject = (RevTag) object;
- assertThat(tagObject.getFullMessage()).isEqualTo(annotatedTag.message);
- assertThat(tagObject.getTaggerIdent()).isEqualTo(annotatedTag.tagger);
- taggedCommit = tagObject.getObject();
- }
- } else {
- taggedCommit = tagRef.getObjectId();
- }
- ObjectId headCommit = repo.exactRef(branch).getObjectId();
- assertThat(taggedCommit).isNotNull();
- assertThat(taggedCommit).isEqualTo(headCommit);
- }
- }
-
private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
throws Exception {
PushOneCommit push =
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 847004f..98e3cae 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -29,6 +29,7 @@
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
@@ -655,6 +656,65 @@
}
}
+ @Test
+ public void updateOnlyRelevantSubmodules() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> subRepo1 = createProjectWithPush("subscribed-to-project-1");
+ TestRepository<?> subRepo2 = createProjectWithPush("subscribed-to-project-2");
+
+ allowMatchingSubmoduleSubscription(
+ "subscribed-to-project-1", "refs/heads/master", "super-project", "refs/heads/master");
+ allowMatchingSubmoduleSubscription(
+ "subscribed-to-project-2", "refs/heads/master", "super-project", "refs/heads/master");
+
+ Config config = new Config();
+ prepareSubmoduleConfigEntry(config, "subscribed-to-project-1", "master");
+ prepareSubmoduleConfigEntry(config, "subscribed-to-project-2", "master");
+ pushSubmoduleConfig(superRepo, "master", config);
+
+ // Push once to initialize submodules.
+ ObjectId subTip2 = pushChangeTo(subRepo2, "master");
+ ObjectId subTip1 = pushChangeTo(subRepo1, "master");
+
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-1", subTip1);
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-2", subTip2);
+
+ directUpdateRef("subscribed-to-project-2", "refs/heads/master");
+ subTip1 = pushChangeTo(subRepo1, "master");
+
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-1", subTip1);
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project-2", subTip2);
+ }
+
+ @Test
+ public void skipUpdatingBrokenGitlinkPointer() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+ allowMatchingSubmoduleSubscription(
+ "subscribed-to-project", "refs/heads/master", "super-project", "refs/heads/master");
+ createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+ // Push once to initialize submodule.
+ ObjectId subTip = pushChangeTo(subRepo, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", subTip);
+
+ // Write an invalid SHA-1 directly to the gitlink.
+ ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ directUpdateSubmodule("super-project", "refs/heads/master", "subscribed-to-project", badId);
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+
+ // Push succeeds, but gitlink update is skipped.
+ pushChangeTo(subRepo, "master");
+ expectToHaveSubmoduleState(superRepo, "master", "subscribed-to-project", badId);
+ }
+
+ private ObjectId directUpdateRef(String project, String ref) throws Exception {
+ try (Repository serverRepo = repoManager.openRepository(new Project.NameKey(name(project)))) {
+ return new TestRepository<>(serverRepo).branch(ref).commit().create().copy();
+ }
+ }
+
private void testSubmoduleSubjectCommitMessageAndExpectTruncation() throws Exception {
TestRepository<?> superRepo = createProjectWithPush("super-project");
TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 2812c86..eef3295 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -856,4 +856,44 @@
.that(superRepo.getRepository().resolve("origin/master^"))
.isEqualTo(superPreviousId);
}
+
+ @Test
+ public void skipUpdatingBrokenGitlinkPointer() throws Exception {
+ TestRepository<?> superRepo = createProjectWithPush("super-project");
+ TestRepository<?> sub1 = createProjectWithPush("sub1");
+ TestRepository<?> sub2 = createProjectWithPush("sub2");
+
+ allowMatchingSubmoduleSubscription(
+ "sub1", "refs/heads/master", "super-project", "refs/heads/master");
+ allowMatchingSubmoduleSubscription(
+ "sub2", "refs/heads/master", "super-project", "refs/heads/master");
+
+ Config config = new Config();
+ prepareSubmoduleConfigEntry(config, "sub1", "master");
+ prepareSubmoduleConfigEntry(config, "sub2", "master");
+ pushSubmoduleConfig(superRepo, "master", config);
+
+ // Write an invalid SHA-1 directly to one of the gitlinks.
+ ObjectId badId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ directUpdateSubmodule("super-project", "refs/heads/master", "sub1", badId);
+ expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
+
+ String topic = "same-topic";
+ ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic);
+ ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic);
+
+ String changeId1 = getChangeId(sub1, sub1Id).get();
+ String changeId2 = getChangeId(sub2, sub2Id).get();
+ approve(changeId1);
+ approve(changeId2);
+
+ gApi.changes().id(changeId1).current().submit();
+
+ assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED);
+
+ // sub1 was skipped but sub2 succeeded.
+ expectToHaveSubmoduleState(superRepo, "master", "sub1", badId);
+ expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master");
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
new file mode 100644
index 0000000..2c32737
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -0,0 +1,191 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.apache.http.HttpStatus.SC_CREATED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.restapi.ParameterParser;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TraceIT extends AbstractDaemonTest {
+ @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
+ @Inject private DynamicSet<CommitValidationListener> commitValidationListeners;
+
+ private TraceValidatingProjectCreationValidationListener projectCreationListener;
+ private RegistrationHandle projectCreationListenerRegistrationHandle;
+ private TraceValidatingCommitValidationListener commitValidationListener;
+ private RegistrationHandle commitValidationRegistrationHandle;
+
+ @Before
+ public void setup() {
+ projectCreationListener = new TraceValidatingProjectCreationValidationListener();
+ projectCreationListenerRegistrationHandle =
+ projectCreationValidationListeners.add(projectCreationListener);
+ commitValidationListener = new TraceValidatingCommitValidationListener();
+ commitValidationRegistrationHandle = commitValidationListeners.add(commitValidationListener);
+ }
+
+ @After
+ public void cleanup() {
+ projectCreationListenerRegistrationHandle.remove();
+ commitValidationRegistrationHandle.remove();
+ }
+
+ @Test
+ public void restCallWithoutTrace() throws Exception {
+ RestResponse response = adminRestSession.put("/projects/new1");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void restCallWithTrace() throws Exception {
+ RestResponse response =
+ adminRestSession.put("/projects/new2?" + ParameterParser.TRACE_PARAMETER);
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
+ assertThat(projectCreationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void restCallWithTraceTrue() throws Exception {
+ RestResponse response =
+ adminRestSession.put("/projects/new3?" + ParameterParser.TRACE_PARAMETER + "=true");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNotNull();
+ assertThat(projectCreationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void restCallWithTraceFalse() throws Exception {
+ RestResponse response =
+ adminRestSession.put("/projects/new4?" + ParameterParser.TRACE_PARAMETER + "=false");
+ assertThat(response.getStatusCode()).isEqualTo(SC_CREATED);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(projectCreationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void pushWithoutTrace() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void pushWithTrace() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace"));
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void pushWithTraceTrue() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace=true"));
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void pushWithTraceFalse() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace=false"));
+ PushOneCommit.Result r = push.to("refs/heads/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void pushForReviewWithoutTrace() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void pushForReviewWithTrace() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void pushForReviewWithTraceTrue() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace=true"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isTrue();
+ }
+
+ @Test
+ public void pushForReviewWithTraceFalse() throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(ImmutableList.of("trace=false"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(commitValidationListener.foundTraceId).isFalse();
+ }
+
+ private static class TraceValidatingProjectCreationValidationListener
+ implements ProjectCreationValidationListener {
+ Boolean foundTraceId;
+
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+ }
+ }
+
+ private static class TraceValidatingCommitValidationListener implements CommitValidationListener {
+ Boolean foundTraceId;
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+ return ImmutableList.of();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 61a2d84..5580279 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -27,6 +27,7 @@
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -154,11 +155,12 @@
@Test
@TestProjectInput(createEmptyCommit = false)
public void submitToEmptyRepo() throws Exception {
- RevCommit initialHead = getRemoteHead();
+ assertThat(getRemoteHead()).isNull();
PushOneCommit.Result change = createChange();
+ assertThat(change.getCommit().getParents()).isEmpty();
Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
RevCommit headAfterSubmitPreview = getRemoteHead();
- assertThat(headAfterSubmitPreview).isEqualTo(initialHead);
+ assertThat(headAfterSubmitPreview).isNull();
assertThat(actual).hasSize(1);
submit(change.getChangeId());
@@ -1070,6 +1072,45 @@
change.current().submit();
}
+ @Test
+ @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
+ public void submitNonemptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
+ assertThat(getRemoteHead()).isNull();
+ PushOneCommit.Result change = createChange();
+ assertThat(change.getCommit().getParents()).isEmpty();
+ Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+ RevCommit headAfterSubmitPreview = getRemoteHead();
+ assertThat(headAfterSubmitPreview).isNull();
+ assertThat(actual).hasSize(1);
+
+ submit(change.getChangeId());
+ assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+ assertTrees(project, actual);
+ }
+
+ @Test
+ @TestProjectInput(createEmptyCommit = false, rejectEmptyCommit = InheritableBoolean.TRUE)
+ public void submitEmptyCommitToEmptyRepoWithRejectEmptyCommit_allowed() throws Exception {
+ assertThat(getRemoteHead()).isNull();
+ PushOneCommit.Result change =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, "Change 1", ImmutableMap.of())
+ .to("refs/for/master");
+ change.assertOkStatus();
+ // TODO(dborowitz): Use EMPTY_TREE_ID after upgrading to https://git.eclipse.org/r/127473
+ assertThat(change.getCommit().getTree())
+ .isEqualTo(ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"));
+
+ Map<Branch.NameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
+ RevCommit headAfterSubmitPreview = getRemoteHead();
+ assertThat(headAfterSubmitPreview).isNull();
+ assertThat(actual).hasSize(1);
+
+ submit(change.getChangeId());
+ assertThat(getRemoteHead().getId()).isEqualTo(change.getCommit());
+ assertTrees(project, actual);
+ }
+
private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
for (PushOneCommit.Result change : changes) {
try (BatchUpdate bu =
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 0017e08..df89686 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -61,7 +61,7 @@
@Test
public void createBranch_Forbidden() throws Exception {
setApiUser(user);
- assertCreateFails(testBranch, AuthException.class, "create not permitted for refs/heads/test");
+ assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
}
@Test
@@ -85,7 +85,7 @@
@Test
public void createBranchByAdminCreateReferenceBlocked_Forbidden() throws Exception {
blockCreateReference();
- assertCreateFails(testBranch, AuthException.class, "create not permitted for refs/heads/test");
+ assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
}
@Test
@@ -93,7 +93,7 @@
grantOwner();
blockCreateReference();
setApiUser(user);
- assertCreateFails(testBranch, AuthException.class, "create not permitted for refs/heads/test");
+ assertCreateFails(testBranch, AuthException.class, "not permitted: create on refs/heads/test");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 5e1b0bf..b426a37 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -193,7 +193,7 @@
private void assertDeleteForbidden(Branch.NameKey branch) throws Exception {
assertThat(branch(branch).get().canDelete).isNull();
exception.expect(AuthException.class);
- exception.expectMessage("delete not permitted");
+ exception.expectMessage("not permitted: delete");
branch(branch).delete();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 330f2b8..c1bd8f1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -75,7 +75,7 @@
project().deleteBranches(input);
fail("Expected AuthException");
} catch (AuthException e) {
- assertThat(e).hasMessageThat().isEqualTo("delete not permitted for refs/heads/test-1");
+ assertThat(e).hasMessageThat().isEqualTo("not permitted: delete on refs/heads/test-1");
}
setApiUser(admin);
assertBranches(BRANCHES);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index 0cbbe44..3ae0b44 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -125,7 +125,7 @@
private void assertDeleteForbidden() throws Exception {
assertThat(tag().get().canDelete).isNull();
exception.expect(AuthException.class);
- exception.expectMessage("delete not permitted");
+ exception.expectMessage("not permitted: delete");
tag().delete();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 714751d..d4edc0d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -254,7 +254,7 @@
TagInput input = new TagInput();
input.ref = "test";
exception.expect(AuthException.class);
- exception.expectMessage("create not permitted");
+ exception.expectMessage("not permitted: create");
tag(input.ref).create(input);
}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ReflogIT.java
deleted file mode 100644
index bcae987..0000000
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ReflogIT.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.server.notedb;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.UseLocalDisk;
-import com.google.gerrit.reviewdb.client.Change;
-import java.io.File;
-import org.eclipse.jgit.lib.ReflogEntry;
-import org.eclipse.jgit.lib.Repository;
-import org.junit.Test;
-
-@UseLocalDisk
-public class ReflogIT extends AbstractDaemonTest {
- @Test
- public void guessRestApiInReflog() throws Exception {
- assume().that(notesMigration.disableChangeReviewDb()).isTrue();
- PushOneCommit.Result r = createChange();
- Change.Id id = r.getChange().getId();
-
- try (Repository repo = repoManager.openRepository(r.getChange().project())) {
- File log = new File(repo.getDirectory(), "logs/" + changeMetaRef(id));
- if (!log.exists()) {
- log.getParentFile().mkdirs();
- assertThat(log.createNewFile()).isTrue();
- }
-
- gApi.changes().id(id.get()).topic("foo");
- ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
- assertThat(last).named("last RefLogEntry").isNotNull();
- assertThat(last.getComment()).isEqualTo("restapi.change.PutTopic");
- }
- }
-}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
new file mode 100644
index 0000000..8abb59d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/project/ReflogIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.ReflogEntryInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.project.testing.Util;
+import java.io.File;
+import java.util.List;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+@UseLocalDisk
+public class ReflogIT extends AbstractDaemonTest {
+ @Test
+ public void guessRestApiInReflog() throws Exception {
+ assume().that(notesMigration.disableChangeReviewDb()).isTrue();
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getChange().getId();
+
+ try (Repository repo = repoManager.openRepository(r.getChange().project())) {
+ File log = new File(repo.getDirectory(), "logs/" + changeMetaRef(id));
+ if (!log.exists()) {
+ log.getParentFile().mkdirs();
+ assertThat(log.createNewFile()).isTrue();
+ }
+
+ gApi.changes().id(id.get()).topic("foo");
+ ReflogEntry last = repo.getReflogReader(changeMetaRef(id)).getLastEntry();
+ assertThat(last).named("last RefLogEntry").isNotNull();
+ assertThat(last.getComment()).isEqualTo("restapi.change.PutTopic");
+ }
+ }
+
+ @Test
+ public void reflogUpdatedBySubmittingChange() throws Exception {
+ BranchApi branchApi = gApi.projects().name(project.get()).branch("master");
+ List<ReflogEntryInfo> reflog = branchApi.reflog();
+ assertThat(reflog).isNotEmpty();
+
+ // Current number of entries in the reflog
+ int refLogLen = reflog.size();
+
+ // Create and submit a change
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revision = r.getCommit().name();
+ ReviewInput in = ReviewInput.approve();
+ gApi.changes().id(changeId).revision(revision).review(in);
+ gApi.changes().id(changeId).revision(revision).submit();
+
+ // Submitting the change causes a new entry in the reflog
+ reflog = branchApi.reflog();
+ assertThat(reflog).hasSize(refLogLen + 1);
+ }
+
+ @Test
+ public void regularUserIsNotAllowedToGetReflog() throws Exception {
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ gApi.projects().name(project.get()).branch("master").reflog();
+ }
+
+ @Test
+ public void ownerUserIsAllowedToGetReflog() throws Exception {
+ GroupApi groupApi = gApi.groups().create(name("get-reflog"));
+ groupApi.addMembers("user");
+
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(
+ u.getConfig(), Permission.OWNER, new AccountGroup.UUID(groupApi.get().id), "refs/*");
+ u.save();
+ }
+
+ setApiUser(user);
+ gApi.projects().name(project.get()).branch("master").reflog();
+ }
+
+ @Test
+ public void adminUserIsAllowedToGetReflog() throws Exception {
+ setApiUser(admin);
+ gApi.projects().name(project.get()).branch("master").reflog();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
new file mode 100644
index 0000000..9c56d7e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -0,0 +1,64 @@
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.logging.LoggingContext;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.util.RequestId;
+import com.google.gerrit.server.validators.ProjectCreationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@UseSsh
+public class SshTraceIT extends AbstractDaemonTest {
+ @Inject private DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
+
+ private TraceValidatingProjectCreationValidationListener projectCreationListener;
+ private RegistrationHandle projectCreationListenerRegistrationHandle;
+
+ @Before
+ public void setup() {
+ projectCreationListener = new TraceValidatingProjectCreationValidationListener();
+ projectCreationListenerRegistrationHandle =
+ projectCreationValidationListeners.add(projectCreationListener);
+ }
+
+ @After
+ public void cleanup() {
+ projectCreationListenerRegistrationHandle.remove();
+ }
+
+ @Test
+ public void sshCallWithoutTrace() throws Exception {
+ adminSshSession.exec("gerrit create-project new1");
+ adminSshSession.assertSuccess();
+ assertThat(projectCreationListener.foundTraceId).isFalse();
+ }
+
+ @Test
+ public void sshCallWithTrace() throws Exception {
+ adminSshSession.exec("gerrit create-project --trace new2");
+
+ // The trace ID is written to stderr.
+ adminSshSession.assertFailure(RequestId.Type.TRACE_ID.name());
+
+ assertThat(projectCreationListener.foundTraceId).isTrue();
+ }
+
+ private static class TraceValidatingProjectCreationValidationListener
+ implements ProjectCreationValidationListener {
+ Boolean foundTraceId;
+
+ @Override
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
+ this.foundTraceId = LoggingContext.getInstance().getTagsAsMap().containsKey("TRACE_ID");
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index e89d1dc..29e9a0b 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -45,6 +45,7 @@
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
"//java/com/google/gerrit/server/group/testing",
"//java/com/google/gerrit/server/ioutil",
@@ -62,6 +63,7 @@
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
"//lib/commons:codec",
+ "//lib/flogger:api",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/jgit/org.eclipse.jgit.junit:junit",
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 5e93a09..81fd6d7 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -6,8 +6,8 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
-import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import java.lang.reflect.Type;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index ab88169..4950266 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -5,16 +5,7 @@
srcs = glob(["*.java"]),
deps = [
"//java/com/google/gerrit/server",
- "//java/com/google/gerrit/server/cache/testing",
- "//lib:guava",
- "//lib:gwtorm",
"//lib:junit",
- "//lib:protobuf",
- "//lib/auto:auto-value",
- "//lib/auto:auto-value-annotations",
- "//lib/jgit/org.eclipse.jgit:jgit",
"//lib/truth",
- "//lib/truth:truth-proto-extension",
- "//proto:cache_java_proto",
],
)
diff --git a/javatests/com/google/gerrit/server/cache/h2/BUILD b/javatests/com/google/gerrit/server/cache/h2/BUILD
index 63ae94b..2ee8e48 100644
--- a/javatests/com/google/gerrit/server/cache/h2/BUILD
+++ b/javatests/com/google/gerrit/server/cache/h2/BUILD
@@ -6,6 +6,7 @@
deps = [
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/cache/h2",
+ "//java/com/google/gerrit/server/cache/serialize",
"//lib:guava",
"//lib:h2",
"//lib:junit",
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 9bba996..147aeeb 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -19,9 +19,9 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.server.cache.StringSerializer;
import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
import com.google.inject.TypeLiteral;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -42,8 +42,8 @@
new SqlStore<>(
"jdbc:h2:mem:Test_" + id,
KEY_TYPE,
- StringSerializer.INSTANCE,
- StringSerializer.INSTANCE,
+ StringCacheSerializer.INSTANCE,
+ StringCacheSerializer.INSTANCE,
version,
1 << 20,
null);
@@ -87,9 +87,9 @@
@Test
public void stringSerializer() {
String input = "foo";
- byte[] serialized = StringSerializer.INSTANCE.serialize(input);
+ byte[] serialized = StringCacheSerializer.INSTANCE.serialize(input);
assertThat(serialized).isEqualTo(new byte[] {'f', 'o', 'o'});
- assertThat(StringSerializer.INSTANCE.deserialize(serialized)).isEqualTo(input);
+ assertThat(StringCacheSerializer.INSTANCE.deserialize(serialized)).isEqualTo(input);
}
@Test
diff --git a/javatests/com/google/gerrit/server/cache/serialize/BUILD b/javatests/com/google/gerrit/server/cache/serialize/BUILD
new file mode 100644
index 0000000..35d8527
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/BUILD
@@ -0,0 +1,20 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "tests",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/server/cache/serialize",
+ "//java/com/google/gerrit/server/cache/testing",
+ "//lib:guava",
+ "//lib:gwtorm",
+ "//lib:junit",
+ "//lib:protobuf",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ "//lib/truth",
+ "//lib/truth:truth-proto-extension",
+ "//proto:cache_java_proto",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/cache/BooleanCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/cache/BooleanCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
index 3186620..7504850 100644
--- a/javatests/com/google/gerrit/server/cache/BooleanCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/BooleanCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
diff --git a/javatests/com/google/gerrit/server/cache/EnumCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/cache/EnumCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
index 60bbb16..0b80fc7 100644
--- a/javatests/com/google/gerrit/server/cache/EnumCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/EnumCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
diff --git a/javatests/com/google/gerrit/server/cache/IntKeyCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/cache/IntKeyCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
index 7a7c27c..987a62a 100644
--- a/javatests/com/google/gerrit/server/cache/IntKeyCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntKeyCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
diff --git a/javatests/com/google/gerrit/server/cache/IntegerCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
similarity index 97%
rename from javatests/com/google/gerrit/server/cache/IntegerCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
index 962b797..c2db808 100644
--- a/javatests/com/google/gerrit/server/cache/IntegerCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/IntegerCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
diff --git a/javatests/com/google/gerrit/server/cache/JavaCacheSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
similarity index 96%
rename from javatests/com/google/gerrit/server/cache/JavaCacheSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
index 41d07b9..6596730 100644
--- a/javatests/com/google/gerrit/server/cache/JavaCacheSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/JavaCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
diff --git a/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java b/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
similarity index 93%
rename from javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
index 8bf9762..69694fe 100644
--- a/javatests/com/google/gerrit/server/cache/ProtoCacheSerializersTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/ProtoCacheSerializersTest.java
@@ -12,16 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.protobuf.ByteString;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -32,13 +32,13 @@
ObjectIdConverter idConverter = ObjectIdConverter.create();
assertThat(
idConverter.fromByteString(
- bytes(
+ byteString(
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa)))
.isEqualTo(ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
assertThat(
idConverter.fromByteString(
- bytes(
+ byteString(
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb)))
.isEqualTo(ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
@@ -61,14 +61,14 @@
idConverter.toByteString(
ObjectId.fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
.isEqualTo(
- bytes(
+ byteString(
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa));
assertThat(
idConverter.toByteString(
ObjectId.fromString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
.isEqualTo(
- bytes(
+ byteString(
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb));
}
diff --git a/javatests/com/google/gerrit/server/cache/StringSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
similarity index 69%
rename from javatests/com/google/gerrit/server/cache/StringSerializerTest.java
rename to javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
index 3035338..fa3b7d7 100644
--- a/javatests/com/google/gerrit/server/cache/StringSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/StringCacheSerializerTest.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.cache.serialize;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
@@ -21,12 +21,13 @@
import java.nio.charset.StandardCharsets;
import org.junit.Test;
-public class StringSerializerTest {
+public class StringCacheSerializerTest {
@Test
public void serialize() {
- assertThat(StringSerializer.INSTANCE.serialize("")).isEmpty();
- assertThat(StringSerializer.INSTANCE.serialize("abc")).isEqualTo(new byte[] {'a', 'b', 'c'});
- assertThat(StringSerializer.INSTANCE.serialize("a\u1234c"))
+ assertThat(StringCacheSerializer.INSTANCE.serialize("")).isEmpty();
+ assertThat(StringCacheSerializer.INSTANCE.serialize("abc"))
+ .isEqualTo(new byte[] {'a', 'b', 'c'});
+ assertThat(StringCacheSerializer.INSTANCE.serialize("a\u1234c"))
.isEqualTo(new byte[] {'a', (byte) 0xe1, (byte) 0x88, (byte) 0xb4, 'c'});
}
@@ -34,7 +35,7 @@
public void serializeInvalidChar() {
// Can't use UTF-8 for the test, since it can encode all Unicode code points.
try {
- StringSerializer.serialize(StandardCharsets.US_ASCII, "\u1234");
+ StringCacheSerializer.serialize(StandardCharsets.US_ASCII, "\u1234");
assert_().fail("expected IllegalStateException");
} catch (IllegalStateException expected) {
assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
@@ -43,10 +44,11 @@
@Test
public void deserialize() {
- assertThat(StringSerializer.INSTANCE.deserialize(new byte[0])).isEmpty();
- assertThat(StringSerializer.INSTANCE.deserialize(new byte[] {'a', 'b', 'c'})).isEqualTo("abc");
+ assertThat(StringCacheSerializer.INSTANCE.deserialize(new byte[0])).isEmpty();
+ assertThat(StringCacheSerializer.INSTANCE.deserialize(new byte[] {'a', 'b', 'c'}))
+ .isEqualTo("abc");
assertThat(
- StringSerializer.INSTANCE.deserialize(
+ StringCacheSerializer.INSTANCE.deserialize(
new byte[] {'a', (byte) 0xe1, (byte) 0x88, (byte) 0xb4, 'c'}))
.isEqualTo("a\u1234c");
}
@@ -54,7 +56,7 @@
@Test
public void deserializeInvalidChar() {
try {
- StringSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff});
+ StringCacheSerializer.INSTANCE.deserialize(new byte[] {(byte) 0xff});
assert_().fail("expected IllegalStateException");
} catch (IllegalStateException expected) {
assertThat(expected).hasCauseThat().isInstanceOf(CharacterCodingException.class);
diff --git a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
index 03e0d4e..b847ed7 100644
--- a/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/ChangeKindCacheImplTest.java
@@ -16,12 +16,12 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableMap;
-import com.google.gerrit.server.cache.CacheSerializer;
import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.change.ChangeKindCacheImpl.Key;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -39,9 +39,9 @@
assertThat(ChangeKindKeyProto.parseFrom(serialized))
.isEqualTo(
ChangeKindKeyProto.newBuilder()
- .setPrior(bytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
+ .setPrior(byteString(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
.setNext(
- bytes(
+ byteString(
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
.setStrategyName("aStrategy")
diff --git a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
index c8e6f2b..e10a236 100644
--- a/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
+++ b/javatests/com/google/gerrit/server/change/MergeabilityCacheImplTest.java
@@ -16,7 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableMap;
@@ -39,11 +39,11 @@
.isEqualTo(
MergeabilityKeyProto.newBuilder()
.setCommit(
- bytes(
+ byteString(
0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee,
0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee))
.setInto(
- bytes(
+ byteString(
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
.setSubmitType("MERGE_IF_NECESSARY")
diff --git a/javatests/com/google/gerrit/server/git/TagSetTest.java b/javatests/com/google/gerrit/server/git/TagSetTest.java
index 2591c3d..0c44df5 100644
--- a/javatests/com/google/gerrit/server/git/TagSetTest.java
+++ b/javatests/com/google/gerrit/server/git/TagSetTest.java
@@ -17,7 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Streams;
@@ -66,7 +66,7 @@
"refs/heads/master",
CachedRefProto.newBuilder()
.setId(
- bytes(
+ byteString(
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa))
.setFlag(1)
@@ -75,7 +75,7 @@
"refs/heads/branch",
CachedRefProto.newBuilder()
.setId(
- bytes(
+ byteString(
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb,
0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb, 0xbb))
.setFlag(2)
@@ -83,18 +83,18 @@
.addTag(
TagProto.newBuilder()
.setId(
- bytes(
+ byteString(
0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc,
0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc))
- .setFlags(bytes(0x2a))
+ .setFlags(byteString(0x2a))
.build())
.addTag(
TagProto.newBuilder()
.setId(
- bytes(
+ byteString(
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd))
- .setFlags(bytes(0x54))
+ .setFlags(byteString(0x54))
.build())
.build());
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java
new file mode 100644
index 0000000..bd04d8a
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareThreadFactoryTest.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.truth.Expect;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class LoggingContextAwareThreadFactoryTest {
+ @Rule public final Expect expect = Expect.create();
+
+ @Test
+ public void loggingContextPropagationToNewThread() throws Exception {
+ assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
+ assertThat(tagMap.keySet()).containsExactly("foo");
+ assertThat(tagMap.get("foo")).containsExactly("bar");
+
+ Thread thread =
+ new LoggingContextAwareThreadFactory(r -> new Thread(r, "test-thread"))
+ .newThread(
+ () -> {
+ // Verify that the tags have been propagated to the new thread.
+ SortedMap<String, SortedSet<Object>> threadTagMap =
+ LoggingContext.getInstance().getTags().asMap();
+ expect.that(threadTagMap.keySet()).containsExactly("foo");
+ expect.that(threadTagMap.get("foo")).containsExactly("bar");
+ });
+
+ // Execute in background.
+ thread.start();
+ thread.join();
+
+ // Verify that tags in the outer thread are still set.
+ tagMap = LoggingContext.getInstance().getTags().asMap();
+ assertThat(tagMap.keySet()).containsExactly("foo");
+ assertThat(tagMap.get("foo")).containsExactly("bar");
+ }
+ assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void loggingContextPropagationToSameThread() throws Exception {
+ assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ SortedMap<String, SortedSet<Object>> tagMap = LoggingContext.getInstance().getTags().asMap();
+ assertThat(tagMap.keySet()).containsExactly("foo");
+ assertThat(tagMap.get("foo")).containsExactly("bar");
+
+ Thread thread =
+ new LoggingContextAwareThreadFactory()
+ .newThread(
+ () -> {
+ // Verify that the tags have been propagated to the new thread.
+ SortedMap<String, SortedSet<Object>> threadTagMap =
+ LoggingContext.getInstance().getTags().asMap();
+ expect.that(threadTagMap.keySet()).containsExactly("foo");
+ expect.that(threadTagMap.get("foo")).containsExactly("bar");
+ });
+
+ // Execute in the same thread.
+ thread.run();
+
+ // Verify that tags in the outer thread are still set.
+ tagMap = LoggingContext.getInstance().getTags().asMap();
+ assertThat(tagMap.keySet()).containsExactly("foo");
+ assertThat(tagMap.get("foo")).containsExactly("bar");
+ }
+ assertThat(LoggingContext.getInstance().getTags().isEmpty()).isTrue();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/logging/MutableTagsTest.java b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
new file mode 100644
index 0000000..4fadbb4
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/MutableTagsTest.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MutableTagsTest {
+ private MutableTags tags;
+
+ @Before
+ public void setup() {
+ tags = new MutableTags();
+ }
+
+ @Test
+ public void addTag() {
+ assertThat(tags.add("name", "value")).isTrue();
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+ }
+
+ @Test
+ public void addTagsWithDifferentName() {
+ assertThat(tags.add("name1", "value1")).isTrue();
+ assertThat(tags.add("name2", "value2")).isTrue();
+ assertTags(
+ ImmutableMap.of("name1", ImmutableSet.of("value1"), "name2", ImmutableSet.of("value2")));
+ }
+
+ @Test
+ public void addTagsWithSameNameButDifferentValues() {
+ assertThat(tags.add("name", "value1")).isTrue();
+ assertThat(tags.add("name", "value2")).isTrue();
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value1", "value2")));
+ }
+
+ @Test
+ public void addTagsWithSameNameAndSameValue() {
+ assertThat(tags.add("name", "value")).isTrue();
+ assertThat(tags.add("name", "value")).isFalse();
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+ }
+
+ @Test
+ public void getEmptyTags() {
+ assertThat(tags.getTags().isEmpty()).isTrue();
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void isEmpty() {
+ assertThat(tags.isEmpty()).isTrue();
+
+ tags.add("foo", "bar");
+ assertThat(tags.isEmpty()).isFalse();
+
+ tags.remove("foo", "bar");
+ assertThat(tags.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void removeTags() {
+ tags.add("name1", "value1");
+ tags.add("name1", "value2");
+ tags.add("name2", "value");
+ assertTags(
+ ImmutableMap.of(
+ "name1", ImmutableSet.of("value1", "value2"), "name2", ImmutableSet.of("value")));
+
+ tags.remove("name2", "value");
+ assertTags(ImmutableMap.of("name1", ImmutableSet.of("value1", "value2")));
+
+ tags.remove("name1", "value1");
+ assertTags(ImmutableMap.of("name1", ImmutableSet.of("value2")));
+
+ tags.remove("name1", "value2");
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void removeNonExistingTag() {
+ tags.add("name", "value");
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+
+ tags.remove("foo", "bar");
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+
+ tags.remove("name", "foo");
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+ }
+
+ @Test
+ public void setTags() {
+ tags.add("name", "value");
+ assertTags(ImmutableMap.of("name", ImmutableSet.of("value")));
+
+ tags.set(ImmutableSetMultimap.of("foo", "bar", "foo", "baz", "bar", "baz"));
+ assertTags(
+ ImmutableMap.of("foo", ImmutableSet.of("bar", "baz"), "bar", ImmutableSet.of("baz")));
+ }
+
+ @Test
+ public void asMap() {
+ tags.add("name", "value");
+ assertThat(tags.asMap()).containsExactlyEntriesIn(ImmutableSetMultimap.of("name", "value"));
+
+ tags.set(ImmutableSetMultimap.of("foo", "bar", "foo", "baz", "bar", "baz"));
+ assertThat(tags.asMap())
+ .containsExactlyEntriesIn(
+ ImmutableSetMultimap.of("foo", "bar", "foo", "baz", "bar", "baz"));
+ }
+
+ @Test
+ public void clearTags() {
+ tags.add("name1", "value1");
+ tags.add("name1", "value2");
+ tags.add("name2", "value");
+ assertTags(
+ ImmutableMap.of(
+ "name1", ImmutableSet.of("value1", "value2"), "name2", ImmutableSet.of("value")));
+
+ tags.clear();
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void addInvalidTag() {
+ assertNullPointerException("tag name is required", () -> tags.add(null, "foo"));
+ assertNullPointerException("tag value is required", () -> tags.add("foo", null));
+ }
+
+ @Test
+ public void removeInvalidTag() {
+ assertNullPointerException("tag name is required", () -> tags.remove(null, "foo"));
+ assertNullPointerException("tag value is required", () -> tags.remove("foo", null));
+ }
+
+ private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
+ SortedMap<String, SortedSet<Object>> actualTagMap = tags.getTags().asMap();
+ assertThat(actualTagMap.keySet()).containsExactlyElementsIn(expectedTagMap.keySet());
+ for (Map.Entry<String, ImmutableSet<String>> expectedEntry : expectedTagMap.entrySet()) {
+ assertThat(actualTagMap.get(expectedEntry.getKey()))
+ .containsExactlyElementsIn(expectedEntry.getValue());
+ }
+ }
+
+ private void assertNullPointerException(String expectedMessage, Runnable r) {
+ try {
+ r.run();
+ assert_().fail("expected NullPointerException");
+ } catch (NullPointerException e) {
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/logging/TraceContextTest.java b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
new file mode 100644
index 0000000..c4ebd29
--- /dev/null
+++ b/javatests/com/google/gerrit/server/logging/TraceContextTest.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.server.util.RequestId;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import org.junit.After;
+import org.junit.Test;
+
+public class TraceContextTest {
+ @After
+ public void cleanup() {
+ LoggingContext.getInstance().clearTags();
+ }
+
+ @Test
+ public void openContext() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void openNestedContexts() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+
+ try (TraceContext traceContext2 = TraceContext.open().addTag("abc", "xyz")) {
+ assertTags(ImmutableMap.of("abc", ImmutableSet.of("xyz"), "foo", ImmutableSet.of("bar")));
+ }
+
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void openNestedContextsWithSameTagName() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+
+ try (TraceContext traceContext2 = TraceContext.open().addTag("foo", "baz")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar", "baz")));
+ }
+
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void openNestedContextsWithSameTagNameAndValue() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+
+ try (TraceContext traceContext2 = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+ }
+
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void openContextWithRequestId() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag(RequestId.Type.RECEIVE_ID, "foo")) {
+ assertTags(ImmutableMap.of("RECEIVE_ID", ImmutableSet.of("foo")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ @Test
+ public void addTag() {
+ assertTags(ImmutableMap.of());
+ try (TraceContext traceContext = TraceContext.open().addTag("foo", "bar")) {
+ assertTags(ImmutableMap.of("foo", ImmutableSet.of("bar")));
+
+ traceContext.addTag("foo", "baz");
+ traceContext.addTag("bar", "baz");
+ assertTags(
+ ImmutableMap.of("foo", ImmutableSet.of("bar", "baz"), "bar", ImmutableSet.of("baz")));
+ }
+ assertTags(ImmutableMap.of());
+ }
+
+ private void assertTags(ImmutableMap<String, ImmutableSet<String>> expectedTagMap) {
+ SortedMap<String, SortedSet<Object>> actualTagMap =
+ LoggingContext.getInstance().getTags().asMap();
+ assertThat(actualTagMap.keySet()).containsExactlyElementsIn(expectedTagMap.keySet());
+ for (Map.Entry<String, ImmutableSet<String>> expectedEntry : expectedTagMap.entrySet()) {
+ assertThat(actualTagMap.get(expectedEntry.getKey()))
+ .containsExactlyElementsIn(expectedEntry.getValue());
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
index 5a7d812..7b140b7 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
@@ -16,7 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableMap;
@@ -41,7 +41,7 @@
.setProject("project")
.setChangeId(1234)
.setId(
- bytes(
+ byteString(
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
.build());
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 574f6ac..7b41ba3 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -19,7 +19,7 @@
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
-import static com.google.gerrit.server.cache.ProtoCacheSerializers.toByteString;
+import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableList;
@@ -42,12 +42,12 @@
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
-import com.google.gerrit.server.cache.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
import com.google.gwtorm.client.KeyUtil;
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 7890de8..106e0c5 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -56,6 +56,7 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
@@ -204,6 +205,7 @@
@Inject private ThreadLocalRequestContext requestContext;
@Inject private DefaultRefFilter.Factory refFilterFactory;
@Inject private IdentifiedUser.GenericFactory identifiedUserFactory;
+ @Inject private TransferConfig transferConfig;
@Before
public void setUp() throws Exception {
@@ -971,6 +973,7 @@
repoManager,
commentLinks,
capabilityCollectionFactory,
+ transferConfig,
pc));
return repo;
}
diff --git a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
index b87bbf7..de2acf0 100644
--- a/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ConflictKeyTest.java
@@ -18,7 +18,7 @@
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
-import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.byteString;
import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
import com.google.common.collect.ImmutableMap;
@@ -68,11 +68,11 @@
.isEqualTo(
ConflictKeyProto.newBuilder()
.setCommit(
- bytes(
+ byteString(
0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee,
0xba, 0xdc, 0x0f, 0xee, 0xba, 0xdc, 0x0f, 0xee))
.setOtherCommit(
- bytes(
+ byteString(
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
.setSubmitType("MERGE_IF_NECESSARY")
diff --git a/plugins/replication b/plugins/replication
index 1086fac..b62f006 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 1086faccd0cf2aa53977854767fdc77f048b0253
+Subproject commit b62f006b1350180de0af02c82fb18fb290a2548f
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 660f54d..c119562 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -90,7 +90,7 @@
1. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-bazel.html#_gerrit_development_war_file)
2. Set up a local test site. Docs
- [here](https://gerrit-review.googlesource.com/Documentation/install-quick.html) and
+ [here](https://gerrit-review.googlesource.com/Documentation/linux-quickstart.html) and
[here](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
When your project is set up and works using the classic UI, run a test server
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
index 7cb1a11..97151f2 100644
--- a/polygerrit-ui/app/.eslintrc.json
+++ b/polygerrit-ui/app/.eslintrc.json
@@ -1,5 +1,8 @@
{
"extends": ["eslint:recommended", "google"],
+ "parserOptions": {
+ "ecmaVersion": 8
+ },
"env": {
"browser": true,
"es6": true
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 5977714..3606086 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -30,6 +30,7 @@
name: 'Work in progress',
query: 'is:open owner:${user} is:wip',
selfOnly: true,
+ hideIfEmpty: true,
},
{
// Non-WIP open changes owned by viewed user. Filter out changes ignored
@@ -160,16 +161,10 @@
_getUserDashboard(user, sections, title) {
sections = sections
.filter(section => (user === 'self' || !section.selfOnly))
- .map(section => {
- const dashboardSection = {
- name: section.name,
- query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
- };
- if (section.suffixForDashboard) {
- dashboardSection.suffixForDashboard = section.suffixForDashboard;
- }
- return dashboardSection;
- });
+ .map(section => Object.assign({}, section, {
+ name: section.name,
+ query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
+ }));
return Promise.resolve({title, sections});
},
@@ -197,45 +192,57 @@
// in an async so that attachment to the DOM can take place first.
const title = params.title || this._computeTitle(user);
this.async(() => this.fire('title-change', {title}));
+ return this._reload();
+ },
+ /**
+ * Reloads the element.
+ *
+ * @return {Promise<!Object>}
+ */
+ _reload() {
this._loading = true;
-
- const dashboardPromise = params.project ?
- this._getProjectDashboard(params.project, params.dashboard) :
+ const {project, dashboard, title, user, sections} = this.params;
+ const dashboardPromise = project ?
+ this._getProjectDashboard(project, dashboard) :
this._getUserDashboard(
- params.user || 'self',
- params.sections || DEFAULT_SECTIONS,
- params.title || this._computeTitle(params.user));
+ user || 'self',
+ sections || DEFAULT_SECTIONS,
+ title || this._computeTitle(user));
- return dashboardPromise.then(dashboard => {
- if (!dashboard) {
- this._loading = false;
- return;
+ return dashboardPromise.then(this._fetchDashboardChanges.bind(this))
+ .then(() => {
+ this.$.reporting.dashboardDisplayed();
+ }).catch(err => {
+ console.warn(err);
+ }).finally(() => { this._loading = false; });
+ },
+
+ /**
+ * Fetches the changes for each dashboard section and sets this._results
+ * with the response.
+ *
+ * @param {!Object} res
+ * @return {Promise}
+ */
+ _fetchDashboardChanges(res) {
+ if (!res) { return Promise.resolve(); }
+ const queries = res.sections.map(section => {
+ if (section.suffixForDashboard) {
+ return section.query + ' ' + section.suffixForDashboard;
}
- const queries = dashboard.sections.map(section => {
- if (section.suffixForDashboard) {
- return section.query + ' ' + section.suffixForDashboard;
- }
- return section.query;
- });
- const req =
- this.$.restAPI.getChanges(null, queries, null, this.options);
- return req.then(response => {
- this._loading = false;
- this._results = response.map((results, i) => {
- return {
- sectionName: dashboard.sections[i].name,
- query: dashboard.sections[i].query,
- results,
- };
- });
- });
- }).then(() => {
- this.$.reporting.dashboardDisplayed();
- }).catch(err => {
- this._loading = false;
- console.warn(err);
+ return section.query;
});
+
+ return this.$.restAPI.getChanges(null, queries, null, this.options)
+ .then(changes => {
+ this._results = changes.map((results, i) => ({
+ sectionName: res.sections[i].name,
+ query: res.sections[i].query,
+ results,
+ })).filter((section, i) => !res.sections[i].hideIfEmpty ||
+ section.results.length);
+ });
},
_computeUserHeaderClass(userParam) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index a1da018..cac2627 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -207,8 +207,11 @@
sections: [
{name: 'section 1', query: 'query 1'},
{name: 'section 2', query: 'query 2 for self'},
- {name: 'section 3', query: 'self only query'},
{
+ name: 'section 3',
+ query: 'self only query',
+ selfOnly: true,
+ }, {
name: 'section 4',
query: 'query 4',
suffixForDashboard: 'suffix',
@@ -239,6 +242,21 @@
});
});
+ test('hideIfEmpty sections', () => {
+ const sections = [
+ {name: 'test1', query: 'test1', hideIfEmpty: true},
+ {name: 'test2', query: 'test2', hideIfEmpty: true},
+ ];
+ getChangesStub.restore();
+ sandbox.stub(element.$.restAPI, 'getChanges')
+ .returns(Promise.resolve([[], ['nonempty']]));
+
+ return element._fetchDashboardChanges({sections}).then(() => {
+ assert.equal(element._results.length, 1);
+ assert.equal(element._results[0].sectionName, 'test2');
+ });
+ });
+
test('_computeUserHeaderClass', () => {
assert.equal(element._computeUserHeaderClass(undefined), '');
assert.equal(element._computeUserHeaderClass(''), '');
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
index 939ac67..1431887 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
@@ -56,7 +56,6 @@
float: right;
}
.title {
- font-weight: bold;
min-width: 10em;
padding: .75em .5em 0 var(--requirements-horizontal-padding);
vertical-align: top;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 243ebf1..e905e038 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -961,9 +961,10 @@
},
_determinePageBack() {
- // Default backPage to '/' if user came to change view page
+ // Default backPage to root if user came to change view page
// via an email link, etc.
- Gerrit.Nav.navigateToRelativeUrl(this.backPage || '/');
+ Gerrit.Nav.navigateToRelativeUrl(this.backPage ||
+ Gerrit.Nav.getUrlForRoot());
},
_handleLabelRemoved(splices, path) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index af87a7e..b5b8cd9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -89,12 +89,13 @@
assert(starStub.called);
});
- test('U should navigate to / if no backPage set', () => {
+ test('U should navigate to root if no backPage set', () => {
const relativeNavStub = sandbox.stub(Gerrit.Nav,
'navigateToRelativeUrl');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert.isTrue(relativeNavStub.called);
- assert.isTrue(relativeNavStub.lastCall.calledWithExactly('/'));
+ assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
+ Gerrit.Nav.getUrlForRoot()));
});
test('U should navigate to backPage if set', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 954507e..8ca40e7 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -40,6 +40,7 @@
setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'overrideCommentlinks', x => x);
});
teardown(() => { sandbox.restore(); });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index a4679a4..bc1b6be 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -390,6 +390,7 @@
if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
<gr-diff
no-auto-render
+ show-load-failure
display-line="[[_displayLine]]"
inline-index=[[index]]
hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index c2ce364..a7abf85 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -204,12 +204,46 @@
_computeChangeContainerClass(currentChange, relatedChange) {
const classes = ['changeContainer'];
- if (relatedChange.change_id === currentChange.change_id) {
+ if (this._changesEqual(relatedChange, currentChange)) {
classes.push('thisChange');
}
return classes.join(' ');
},
+ /**
+ * Do the given objects describe the same change? Compares the changes by
+ * their numbers.
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo
+ * @return {boolean}
+ */
+ _changesEqual(a, b) {
+ const aNum = this._getChangeNumber(a);
+ const bNum = this._getChangeNumber(b);
+ return aNum === bNum;
+ },
+
+ /**
+ * Get the change number from either a ChangeInfo (such as those included in
+ * SubmittedTogetherInfo responses) or get the change number from a
+ * RelatedChangeAndCommitInfo (such as those included in a
+ * RelatedChangesInfo response).
+ * @see /Documentation/rest-api-changes.html#change-info
+ * @see /Documentation/rest-api-changes.html#related-change-and-commit-info
+ *
+ * @param {!Object} change Either a ChangeInfo or a
+ * RelatedChangeAndCommitInfo object.
+ * @return {number}
+ */
+ _getChangeNumber(change) {
+ if (change.hasOwnProperty('_change_number')) {
+ return change._change_number;
+ }
+ return change._number;
+ },
+
_computeLinkClass(change) {
const statuses = [];
if (change.status == this.ChangeStatus.ABANDONED) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 3f3a255..ef4af16 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -223,13 +223,35 @@
});
test('_computeChangeContainerClass', () => {
- const change1 = {change_id: 123};
- const change2 = {change_id: 456};
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ const change3 = {change_id: 123, _number: 2};
assert.notEqual(element._computeChangeContainerClass(
change1, change1).indexOf('thisChange'), -1);
assert.equal(element._computeChangeContainerClass(
change1, change2).indexOf('thisChange'), -1);
+ assert.equal(element._computeChangeContainerClass(
+ change1, change3).indexOf('thisChange'), -1);
+ });
+
+ test('_changesEqual', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _number: 1};
+ const change3 = {change_id: 123, _number: 2};
+ const change4 = {change_id: 123, _change_number: 1};
+
+ assert.isTrue(element._changesEqual(change1, change1));
+ assert.isFalse(element._changesEqual(change1, change2));
+ assert.isFalse(element._changesEqual(change1, change3));
+ assert.isTrue(element._changesEqual(change2, change4));
+ });
+
+ test('_getChangeNumber', () => {
+ const change1 = {change_id: 123, _number: 0};
+ const change2 = {change_id: 456, _change_number: 1};
+ assert.equal(element._getChangeNumber(change1), 0);
+ assert.equal(element._getChangeNumber(change2), 1);
});
test('event for section loaded fires for each section ', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
index 072e7113..1e77d5d 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.html
@@ -46,6 +46,7 @@
.commandContainer:before {
background: #ebebeb;
bottom: 0;
+ box-sizing: border-box;
content: '$';
display: block;
left: 0;
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index dfe5410..aa04194 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -70,7 +70,15 @@
// - `repoName`, required, String: the name of the repo
// - `detail`, optional, String: the name of the repo detail view.
// Takes any value from Gerrit.Nav.RepoDetailView.
-
+ //
+ // - Gerrit.Nav.View.DASHBOARD
+ // - `repo`, optional, String.
+ // - `sections`, optional, Array of objects with `title` and `query`
+ // strings.
+ // - `user`, optional, String.
+ //
+ // - Gerrit.Nav.View.ROOT:
+ // - no possible parameters.
window.Gerrit = window.Gerrit || {};
@@ -96,6 +104,7 @@
GROUP: 'group',
PLUGIN_SCREEN: 'plugin-screen',
REPO: 'repo',
+ ROOT: 'root',
SEARCH: 'search',
SETTINGS: 'settings',
},
@@ -128,6 +137,9 @@
/** @type {Function} */
_generateWeblinks: uninitialized,
+ /** @type {Function} */
+ overrideCommentlinks: uninitialized,
+
/**
* @param {number=} patchNum
* @param {number|string=} basePatchNum
@@ -143,17 +155,20 @@
* @param {Function} navigate
* @param {Function} generateUrl
* @param {Function} generateWeblinks
+ * @param {Function} overrideCommentlinks
*/
- setup(navigate, generateUrl, generateWeblinks) {
+ setup(navigate, generateUrl, generateWeblinks, overrideCommentlinks) {
this._navigate = navigate;
this._generateUrl = generateUrl;
this._generateWeblinks = generateWeblinks;
+ this.overrideCommentlinks = overrideCommentlinks;
},
destroy() {
this._navigate = uninitialized;
this._generateUrl = uninitialized;
this._generateWeblinks = uninitialized;
+ this.overrideCommentlinks = uninitialized;
},
/**
@@ -412,6 +427,15 @@
},
/**
+ * @return {string}
+ */
+ getUrlForRoot() {
+ return this._getUrlFor({
+ view: Gerrit.Nav.View.ROOT,
+ });
+ },
+
+ /**
* @param {string} repo The name of the repo.
* @param {!Array} sections The sections to display in the dashboard
* @return {string}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 3b4f1ec..a307b85 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -176,6 +176,8 @@
const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
+ const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+
// Polymer makes `app` intrinsically defined on the window by virtue of the
// custom element having the id "app", but it is made explicit here.
const app = document.querySelector('#app');
@@ -248,6 +250,8 @@
url = this._generateGroupUrl(params);
} else if (params.view === Views.REPO) {
url = this._generateRepoUrl(params);
+ } else if (params.view === Views.ROOT) {
+ url = '/';
} else if (params.view === Views.SETTINGS) {
url = this._generateSettingsUrl(params);
} else {
@@ -403,20 +407,19 @@
* @return {string}
*/
_generateDashboardUrl(params) {
+ const repoName = params.repo || params.project || null;
if (params.sections) {
// Custom dashboard.
- const queryParams = params.sections.map(section => {
- return encodeURIComponent(section.name) + '=' +
- encodeURIComponent(section.query);
- });
+ const queryParams = this._sectionsToEncodedParams(params.sections,
+ repoName);
if (params.title) {
queryParams.push('title=' + encodeURIComponent(params.title));
}
const user = params.user ? params.user : '';
return `/dashboard/${user}?${queryParams.join('&')}`;
- } else if (params.project) {
+ } else if (repoName) {
// Project dashboard.
- return `/p/${params.project}/+/dashboard/${params.dashboard}`;
+ return `/p/${repoName}/+/dashboard/${params.dashboard}`;
} else {
// User dashboard.
return `/dashboard/${params.user || 'self'}`;
@@ -424,6 +427,23 @@
},
/**
+ * @param {!Array<!{name: string, query: string}>} sections
+ * @param {string=} opt_repoName
+ * @return {!Array<string>}
+ */
+ _sectionsToEncodedParams(sections, opt_repoName) {
+ return sections.map(section => {
+ // If there is a repo name provided, make sure to substitute it into the
+ // ${repo} (or legacy ${project}) query tokens.
+ const query = opt_repoName ?
+ section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
+ section.query;
+ return encodeURIComponent(section.name) + '=' +
+ encodeURIComponent(query);
+ });
+ },
+
+ /**
* @param {!Object} params
* @return {string}
*/
@@ -658,7 +678,8 @@
Gerrit.Nav.setup(
url => { page.show(url); },
this._generateUrl.bind(this),
- params => this._generateWeblinks(params)
+ params => this._generateWeblinks(params),
+ x => x
);
page.exit('*', (ctx, next) => {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index b68a5e9..2211039 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -375,6 +375,21 @@
'/dashboard/?section%201=query%201§ion%202=query%202');
});
+ test('custom repo dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ sections: [
+ {name: 'section 1', query: 'query 1 ${project}'},
+ {name: 'section 2', query: 'query 2 ${repo}'},
+ ],
+ repo: 'repo-name',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/dashboard/?section%201=query%201%20repo-name&' +
+ 'section%202=query%202%20repo-name');
+ });
+
test('custom user dashboard, with title', () => {
const params = {
view: Gerrit.Nav.View.DASHBOARD,
@@ -387,7 +402,18 @@
'/dashboard/user?name=query&title=custom%20dashboard');
});
- test('project dashboard', () => {
+ test('repo dashboard', () => {
+ const params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ repo: 'gerrit/repo',
+ dashboard: 'default:main',
+ };
+ assert.equal(
+ element._generateUrl(params),
+ '/p/gerrit/repo/+/dashboard/default:main');
+ });
+
+ test('project dashboard (legacy)', () => {
const params = {
view: Gerrit.Nav.View.DASHBOARD,
project: 'gerrit/project',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
index b47e516..b2fc64c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-binary.js
@@ -20,8 +20,8 @@
// Prevent redefinition.
if (window.GrDiffBuilderBinary) { return; }
- function GrDiffBuilderBinary(diff, comments, prefs, projectName, outputEl) {
- GrDiffBuilder.call(this, diff, comments, prefs, projectName, outputEl);
+ function GrDiffBuilderBinary(diff, comments, prefs, outputEl) {
+ GrDiffBuilder.call(this, diff, comments, null, prefs, outputEl);
}
GrDiffBuilderBinary.prototype = Object.create(GrDiffBuilder.prototype);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index ec78421..cb768ef 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -22,10 +22,10 @@
const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|jpeg|jpg|png|tiff|webp)$/;
- function GrDiffBuilderImage(
- diff, comments, prefs, projectName, outputEl, baseImage, revisionImage) {
- GrDiffBuilderSideBySide.call(
- this, diff, comments, prefs, projectName, outputEl, []);
+ function GrDiffBuilderImage(diff, comments, createThreadGroupFn, prefs,
+ outputEl, baseImage, revisionImage) {
+ GrDiffBuilderSideBySide.call(this, diff, comments, createThreadGroupFn,
+ prefs, outputEl, []);
this._baseImage = baseImage;
this._revisionImage = revisionImage;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
index 3d6dedd..fafae63 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -20,10 +20,10 @@
// Prevent redefinition.
if (window.GrDiffBuilderSideBySide) { return; }
- function GrDiffBuilderSideBySide(
- diff, comments, prefs, projectName, outputEl, layers) {
- GrDiffBuilder.call(
- this, diff, comments, prefs, projectName, outputEl, layers);
+ function GrDiffBuilderSideBySide(diff, comments, createThreadGroupFn, prefs,
+ outputEl, layers) {
+ GrDiffBuilder.call(this, diff, comments, createThreadGroupFn, prefs,
+ outputEl, layers);
}
GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
index 65a31a1..9a04b1f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -20,10 +20,10 @@
// Prevent redefinition.
if (window.GrDiffBuilderUnified) { return; }
- function GrDiffBuilderUnified(
- diff, comments, prefs, projectName, outputEl, layers) {
- GrDiffBuilder.call(
- this, diff, comments, prefs, projectName, outputEl, layers);
+ function GrDiffBuilderUnified(diff, comments, createThreadGroupFn, prefs,
+ outputEl, layers) {
+ GrDiffBuilder.call(this, diff, comments, createThreadGroupFn, prefs,
+ outputEl, layers);
}
GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index e8f4b21..8ca4f9d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -116,6 +116,12 @@
* @type {Defs.LineOfInterest|null}
*/
lineOfInterest: Object,
+
+ /**
+ * @type {function(number, booleam, !string)}
+ */
+ createCommentFn: Function,
+
_builder: Object,
_groups: Array,
_layers: Array,
@@ -250,12 +256,6 @@
GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
},
- createCommentThreadGroup(changeNum, patchNum, path,
- isOnParent, commentSide) {
- return this._builder.createCommentThreadGroup(changeNum, patchNum,
- path, isOnParent, commentSide);
- },
-
emitGroup(group, sectionEl) {
this._builder.emitGroup(group, sectionEl);
},
@@ -303,27 +303,24 @@
}
let builder = null;
+ const createFn = this.createCommentFn;
if (this.isImageDiff) {
- builder = new GrDiffBuilderImage(diff, comments, prefs,
- this.projectName, this.diffElement, this.baseImage,
- this.revisionImage);
+ builder = new GrDiffBuilderImage(diff, comments, createFn, prefs,
+ this.diffElement, this.baseImage, this.revisionImage);
} else if (diff.binary) {
// If the diff is binary, but not an image.
return new GrDiffBuilderBinary(diff, comments, prefs,
- this.projectName, this.diffElement);
+ this.diffElement);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
- builder = new GrDiffBuilderSideBySide(diff, comments, prefs,
- this.projectName, this.diffElement, this._layers);
+ builder = new GrDiffBuilderSideBySide(diff, comments, createFn,
+ prefs, this.diffElement, this._layers);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
- builder = new GrDiffBuilderUnified(diff, comments, prefs,
- this.projectName, this.diffElement, this._layers);
+ builder = new GrDiffBuilderUnified(diff, comments, createFn, prefs,
+ this.diffElement, this._layers);
}
if (!builder) {
throw Error('Unsupported diff view mode: ' + this.viewMode);
}
- if (this.parentIndex) {
- builder.setParentIndex(this.parentIndex);
- }
return builder;
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 409ccf9..96a160e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -42,15 +42,15 @@
*/
const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
- function GrDiffBuilder(diff, comments, prefs, projectName, outputEl, layers) {
+ function GrDiffBuilder(diff, comments, createThreadGroupFn, prefs, outputEl,
+ layers) {
this._diff = diff;
this._comments = comments;
+ this._createThreadGroupFn = createThreadGroupFn;
this._prefs = prefs;
- this._projectName = projectName;
this._outputEl = outputEl;
this.groups = [];
this._blameInfo = null;
- this._parentIndex = undefined;
this.layers = layers || [];
@@ -62,7 +62,6 @@
throw Error('Invalid line length from preferences.');
}
-
for (const layer of this.layers) {
if (layer.addListener) {
layer.addListener(this._handleLayerUpdate.bind(this));
@@ -354,28 +353,6 @@
};
/**
- * @param {number} changeNum
- * @param {number|string} patchNum
- * @param {string} path
- * @param {boolean} isOnParent
- * @param {string} commentSide
- * @return {!Object}
- */
- GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum,
- patchNum, path, isOnParent, commentSide) {
- const threadGroupEl =
- document.createElement('gr-diff-comment-thread-group');
- threadGroupEl.changeNum = changeNum;
- threadGroupEl.commentSide = commentSide;
- threadGroupEl.patchForNewThreads = patchNum;
- threadGroupEl.path = path;
- threadGroupEl.isOnParent = isOnParent;
- threadGroupEl.projectName = this._projectName;
- threadGroupEl.parentIndex = this._parentIndex;
- return threadGroupEl;
- };
-
- /**
* @param {number} line
* @param {string=} opt_side
* @return {!Object}
@@ -400,9 +377,8 @@
patchNum = this._comments.meta.patchRange.basePatchNum;
}
}
- const threadGroupEl = this.createCommentThreadGroup(
- this._comments.meta.changeNum, patchNum, this._comments.meta.path,
- isOnParent, opt_side);
+ const threadGroupEl = this._createThreadGroupFn(patchNum, isOnParent,
+ opt_side);
threadGroupEl.comments = comments;
if (opt_side) {
threadGroupEl.setAttribute('data-side', opt_side);
@@ -616,10 +592,6 @@
}
};
- GrDiffBuilder.prototype.setParentIndex = function(index) {
- this._parentIndex = index;
- };
-
/**
* Find the blame cell for a given line number.
* @param {number} lineNum
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index a00ecb3..7f1450c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -59,6 +59,7 @@
suite('gr-diff-builder tests', () => {
let element;
let builder;
+ let createThreadGroupFn;
let sandbox;
const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>';
@@ -74,9 +75,11 @@
show_tabs: true,
tab_size: 4,
};
- const projectName = 'my-project';
+ createThreadGroupFn = sinon.spy(() => ({
+ setAttribute: sinon.spy(),
+ }));
builder = new GrDiffBuilder(
- {content: []}, {left: [], right: []}, prefs, projectName);
+ {content: []}, {left: [], right: []}, createThreadGroupFn, prefs);
});
teardown(() => { sandbox.restore(); });
@@ -311,11 +314,8 @@
function checkThreadGroupProps(threadGroupEl, patchNum, isOnParent,
comments) {
- assert.equal(threadGroupEl.changeNum, '42');
- assert.equal(threadGroupEl.patchForNewThreads, patchNum);
- assert.equal(threadGroupEl.path, '/path/to/foo');
- assert.equal(threadGroupEl.isOnParent, isOnParent);
- assert.deepEqual(threadGroupEl.projectName, 'my-project');
+ assert.equal(createThreadGroupFn.lastCall.args[0], patchNum);
+ assert.equal(createThreadGroupFn.lastCall.args[1], isOnParent);
assert.deepEqual(threadGroupEl.comments, comments);
}
@@ -323,27 +323,33 @@
line.beforeNumber = 5;
line.afterNumber = 5;
let threadGroupEl = builder._commentThreadGroupForLine(line);
+ assert.isTrue(createThreadGroupFn.calledOnce);
checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
+ assert.isTrue(createThreadGroupFn.calledTwice);
checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
+ assert.isTrue(createThreadGroupFn.calledThrice);
checkThreadGroupProps(threadGroupEl, '3', true, [l5]);
builder._comments.meta.patchRange.basePatchNum = '1';
threadGroupEl = builder._commentThreadGroupForLine(line);
+ assert.equal(createThreadGroupFn.callCount, 4);
checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]);
threadEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.LEFT);
+ assert.equal(createThreadGroupFn.callCount, 5);
checkThreadGroupProps(threadEl, '1', false, [l5]);
threadGroupEl =
builder._commentThreadGroupForLine(line, GrDiffBuilder.Side.RIGHT);
+ assert.equal(createThreadGroupFn.callCount, 6);
checkThreadGroupProps(threadGroupEl, '3', false, [r5]);
builder._comments.meta.patchRange.basePatchNum = 'PARENT';
@@ -352,12 +358,14 @@
line.beforeNumber = 5;
line.afterNumber = 5;
threadGroupEl = builder._commentThreadGroupForLine(line);
+ assert.equal(createThreadGroupFn.callCount, 7);
checkThreadGroupProps(threadGroupEl, '3', true, [l5, r5]);
line = new GrDiffLine(GrDiffLine.Type.ADD);
line.beforeNumber = 3;
line.afterNumber = 5;
threadGroupEl = builder._commentThreadGroupForLine(line);
+ assert.equal(createThreadGroupFn.callCount, 8);
checkThreadGroupProps(threadGroupEl, '3', false, [l3, r5]);
});
@@ -920,7 +928,7 @@
outputEl = element.queryEffectiveChildren('#diffTable');
sandbox.stub(element, '_getDiffBuilder', () => {
const builder = new GrDiffBuilder(
- {content}, {left: [], right: []}, prefs, 'my-project', outputEl);
+ {content}, {left: [], right: []}, null, prefs, outputEl);
sandbox.stub(builder, 'addColumns');
builder.buildSectionElement = function(group) {
const section = document.createElement('stub');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 8a89937..2a00425 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -50,6 +50,9 @@
stub('gr-rest-api-interface', {
getLoggedIn() { return Promise.resolve(false); },
+ getDiff() {
+ return Promise.resolve(mockDiffResponse.diffResponse);
+ },
});
const fixtureElems = fixture('basic');
@@ -60,15 +63,12 @@
// Register the diff with the cursor.
cursorElement.push('diffs', diffElement);
+ diffElement.patchRange = {basePatchNum: 1, patchNum: 2};
diffElement.comments = {left: [], right: []};
diffElement.$.restAPI.getDiffPreferences().then(prefs => {
diffElement.prefs = prefs;
});
- sandbox.stub(diffElement, '_getDiff', () => {
- return Promise.resolve(mockDiffResponse.diffResponse);
- });
-
const setupDone = () => {
cursorElement._updateStops();
cursorElement.moveToFirstChunk();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 379bb34..1aba403 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -192,15 +192,20 @@
font-size: var(--font-size, var(--font-size-normal));
padding: 0.5em 0 0.5em 4em;
}
+ #loadingError,
#sizeWarning {
display: none;
margin: 1em auto;
max-width: 60em;
text-align: center;
}
+ #loadingError {
+ color: var(--error-text-color);
+ }
#sizeWarning gr-button {
margin: 1em;
}
+ #loadingError.showError,
#sizeWarning.warn {
display: block;
}
@@ -286,6 +291,7 @@
base-image="[[_baseImage]]"
revision-image="[[_revisionImage]]"
parent-index="[[_parentIndex]]"
+ create-comment-fn="[[_createThreadGroupFn]]"
line-of-interest="[[lineOfInterest]]">
<table
id="diffTable"
@@ -298,6 +304,9 @@
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
[[_newlineWarning]]
</div>
+ <div id="loadingError" class$="[[_computeErrorClass(_errorMessage)]]">
+ [[_errorMessage]]
+ </div>
<div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]">
<p>
Prevented render because "Whole file" is enabled and this diff is very
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 9cb83a3..5dd0bc6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -123,6 +123,14 @@
*/
lineOfInterest: Object,
+ /**
+ * If the diff fails to load, show the failure message in the diff rather
+ * than bubbling the error up to the whole page. This is useful for when
+ * loading inline diffs because one diff failing need not mark the whole
+ * page with a failure.
+ */
+ showLoadFailure: Boolean,
+
_loading: {
type: Boolean,
value: false,
@@ -162,6 +170,12 @@
_showWarning: Boolean,
+ /** @type {?string} */
+ _errorMessage: {
+ type: String,
+ value: null,
+ },
+
/** @type {?Object} */
_blame: {
type: Object,
@@ -182,6 +196,16 @@
type: String,
computed: '_computeNewlineWarning(_diff)',
},
+
+ /**
+ * @type {function(number, boolean, !string)}
+ */
+ _createThreadGroupFn: {
+ type: Function,
+ value() {
+ return this._createCommentThreadGroup.bind(this);
+ },
+ },
},
behaviors: [
@@ -214,16 +238,23 @@
this.clearBlame();
this._safetyBypass = null;
this._showWarning = false;
+ this._errorMessage = null;
this.clearDiffContent();
- const promises = [];
+ const diffRequest = this._getDiff();
- promises.push(this._getDiff().then(diff => {
- this._diff = diff;
+ const assetRequest = diffRequest.then(diff => {
+ // If the diff is null, then it's failed to load.
+ if (!diff) { return null; }
+
return this._loadDiffAssets();
- }));
+ });
- return Promise.all(promises).then(() => {
+ return Promise.all([diffRequest, assetRequest]).then(results => {
+ const diff = results[0];
+ if (!diff) {
+ return Promise.resolve();
+ }
if (this.prefs) {
return this._renderDiffTable();
}
@@ -452,20 +483,6 @@
},
/**
- * @param {string} commentSide
- * @param {!Object=} opt_range
- */
- _getRangeString(commentSide, opt_range) {
- return opt_range ?
- 'range-' +
- opt_range.startLine + '-' +
- opt_range.startChar + '-' +
- opt_range.endLine + '-' +
- opt_range.endChar + '-' +
- commentSide : 'line-' + commentSide;
- },
-
- /**
* Gets or creates a comment thread for a specific spot on a diff.
* May include a range, if the comment is a range comment.
*
@@ -481,8 +498,8 @@
// Check if thread group exists.
let threadGroupEl = this._getThreadGroupForLine(contentEl);
if (!threadGroupEl) {
- threadGroupEl = this.$.diffBuilder.createCommentThreadGroup(
- this.changeNum, patchNum, this.path, isOnParent, commentSide);
+ threadGroupEl = this._createCommentThreadGroup(patchNum, isOnParent,
+ commentSide);
contentEl.appendChild(threadGroupEl);
}
@@ -497,6 +514,25 @@
},
/**
+ * @param {number} patchNum
+ * @param {boolean} isOnParent
+ * @param {!string} commentSide
+ * @return {!Object}
+ */
+ _createCommentThreadGroup(patchNum, isOnParent, commentSide) {
+ const threadGroupEl =
+ document.createElement('gr-diff-comment-thread-group');
+ threadGroupEl.changeNum = this.changeNum;
+ threadGroupEl.commentSide = commentSide;
+ threadGroupEl.patchForNewThreads = patchNum;
+ threadGroupEl.path = this.path;
+ threadGroupEl.isOnParent = isOnParent;
+ threadGroupEl.projectName = this.projectName;
+ threadGroupEl.parentIndex = this._parentIndex;
+ return threadGroupEl;
+ },
+
+ /**
* The value to be used for the patch number of new comments created at the
* given line and content elements.
*
@@ -698,34 +734,58 @@
this.fire('server-error', {response});
return;
}
+
+ if (this.showLoadFailure) {
+ this._errorMessage = [
+ 'Encountered error when loading the diff:',
+ response.status,
+ response.statusText,
+ ].join(' ');
+ return;
+ }
+
this.fire('page-error', {response});
},
/** @return {!Promise<!Object>} */
_getDiff() {
- return this.$.restAPI.getDiff(
- this.changeNum,
- this.patchRange.basePatchNum,
- this.patchRange.patchNum,
- this.path,
- this._handleGetDiffError.bind(this)).then(diff => {
+ // Wrap the diff request in a new promise so that the error handler
+ // rejects the promise, allowing the error to be handled in the .catch.
+ const request = new Promise((resolve, reject) => {
+ this.$.restAPI.getDiff(
+ this.changeNum,
+ this.patchRange.basePatchNum,
+ this.patchRange.patchNum,
+ this.path,
+ reject)
+ .then(resolve);
+ });
+
+ return request
+ .then(diff => {
+ this.filesWeblinks = this._getFilesWeblinks(diff);
+ this._diff = diff;
this._reportDiff(diff);
- if (!this.commitRange) {
- this.filesWeblinks = {};
- return diff;
- }
- this.filesWeblinks = {
- meta_a: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.baseCommit, this.path,
- {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
- meta_b: Gerrit.Nav.getFileWebLinks(
- this.projectName, this.commitRange.commit, this.path,
- {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
- };
return diff;
+ })
+ .catch(e => {
+ this._handleGetDiffError(e);
+ return null;
});
},
+ _getFilesWeblinks(diff) {
+ if (!this.commitRange) { return {}; }
+ return {
+ meta_a: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.baseCommit, this.path,
+ {weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
+ meta_b: Gerrit.Nav.getFileWebLinks(
+ this.projectName, this.commitRange.commit, this.path,
+ {weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
+ };
+ },
+
/**
* Report info about the diff response.
*/
@@ -862,6 +922,14 @@
},
/**
+ * @param {string} errorMessage
+ * @return {string}
+ */
+ _computeErrorClass(errorMessage) {
+ return errorMessage ? 'showError' : '';
+ },
+
+ /**
* @return {number|null}
*/
_computeParentIndex(patchRangeRecord) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index b98602f..69bd330 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -343,20 +343,6 @@
});
});
- test('_getRangeString', () => {
- const side = 'PARENT';
- const range = {
- startLine: 1,
- startChar: 1,
- endLine: 1,
- endChar: 2,
- };
- assert.equal(element._getRangeString(side, range),
- 'range-1-1-1-2-PARENT');
- assert.equal(element._getRangeString(side, null),
- 'line-PARENT');
- }),
-
test('thread groups', () => {
const contentEl = document.createElement('div');
const commentSide = 'left';
@@ -410,7 +396,6 @@
suite('image diffs', () => {
let mockFile1;
let mockFile2;
- const stubs = [];
setup(() => {
mockFile1 = {
body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
@@ -445,18 +430,18 @@
};
const mockComments = {baseComments: [], comments: []};
- stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo',
- () => Promise.resolve(mockCommit)));
- stubs.push(sandbox.stub(element.$.restAPI,
+ sandbox.stub(element.$.restAPI, 'getCommitInfo')
+ .returns(Promise.resolve(mockCommit));
+ sandbox.stub(element.$.restAPI,
'getB64FileContents',
(changeId, patchNum, path, opt_parentIndex) => {
return Promise.resolve(opt_parentIndex === 1 ? mockFile1 :
mockFile2);
- }));
- stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments',
- () => Promise.resolve(mockComments)));
- stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts',
- () => Promise.resolve(mockComments)));
+ });
+ sandbox.stub(element.$.restAPI, '_getDiffComments')
+ .returns(Promise.resolve(mockComments));
+ sandbox.stub(element.$.restAPI, 'getDiffDrafts')
+ .returns(Promise.resolve(mockComments));
element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
element.comments = {left: [], right: []};
@@ -479,8 +464,8 @@
content: [{skip: 66}],
binary: true,
};
- stubs.push(sandbox.stub(element, '_getDiff',
- () => Promise.resolve(mockDiff)));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
@@ -559,8 +544,8 @@
content: [{skip: 66}],
binary: true,
};
- stubs.push(sandbox.stub(element, '_getDiff',
- () => Promise.resolve(mockDiff)));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
const rendered = () => {
// Recognizes that it should be an image diff.
@@ -640,8 +625,8 @@
content: [{skip: 66}],
binary: true,
};
- stubs.push(sandbox.stub(element, '_getDiff',
- () => Promise.resolve(mockDiff)));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
@@ -679,8 +664,8 @@
content: [{skip: 66}],
binary: true,
};
- stubs.push(sandbox.stub(element, '_getDiff',
- () => Promise.resolve(mockDiff)));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
@@ -720,8 +705,8 @@
};
mockFile1.type = 'image/jpeg-evil';
- stubs.push(sandbox.stub(element, '_getDiff',
- () => Promise.resolve(mockDiff)));
+ sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve(mockDiff));
element.addEventListener('render', () => {
// Recognizes that it should be an image diff.
@@ -793,6 +778,54 @@
element._getDiff().then(done);
});
+ test('_getDiff resolves as null on error', () => {
+ const onErrStub = sandbox.stub(element, '_handleGetDiffError');
+ const error = {ok: false, status: 500};
+ sandbox.stub(element.$.restAPI, 'getDiff',
+ (changeNum, basePatchNum, patchNum, path, onErr) => {
+ onErr(error);
+ });
+ return element._getDiff().then(diff => {
+ assert.isNull(diff);
+ assert.isTrue(onErrStub.calledOnce);
+ });
+ });
+
+ suite('_handleGetDiffError', () => {
+ let serverErrorStub;
+ let pageErrorStub;
+
+ setup(() => {
+ serverErrorStub = sinon.stub();
+ element.addEventListener('server-error', serverErrorStub);
+ pageErrorStub = sinon.stub();
+ element.addEventListener('page-error', pageErrorStub);
+ });
+
+ test('page error on HTTP-409', () => {
+ element._handleGetDiffError({status: 409});
+ assert.isTrue(serverErrorStub.calledOnce);
+ assert.isFalse(pageErrorStub.called);
+ assert.isNotOk(element._errorMessage);
+ });
+
+ test('server error on non-HTTP-409', () => {
+ element._handleGetDiffError({status: 500});
+ assert.isFalse(serverErrorStub.called);
+ assert.isTrue(pageErrorStub.calledOnce);
+ assert.isNotOk(element._errorMessage);
+ });
+
+ test('error message if showLoadFailure', () => {
+ element.showLoadFailure = true;
+ element._handleGetDiffError({status: 500, statusText: 'Failure!'});
+ assert.isFalse(serverErrorStub.called);
+ assert.isFalse(pageErrorStub.called);
+ assert.equal(element._errorMessage,
+ 'Encountered error when loading the diff: 500 Failure!');
+ });
+ });
+
suite('getCursorStops', () => {
const setupDiff = function() {
const mock = document.createElement('mock-diff-response');
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index c2d6cdf..f80e9f8 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -51,7 +51,6 @@
}
header gr-editable-label {
font-size: var(--font-size-large);
- font-weight: bold;
--label-style: {
text-overflow: initial;
white-space: initial;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 51ad0b7..0e8bb45 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -22,24 +22,35 @@
properties: {
name: String,
+ _urlsImported: {
+ type: Array,
+ value() { return []; },
+ },
+ _stylesApplied: {
+ type: Array,
+ value() { return []; },
+ },
},
_import(url) {
+ if (this._urlsImported.includes(url)) { return Promise.resolve(); }
+ this._urlsImported.push(url);
return new Promise((resolve, reject) => {
this.importHref(url, resolve, reject);
});
},
_applyStyle(name) {
+ if (this._stylesApplied.includes(name)) { return; }
+ this._stylesApplied.push(name);
const s = document.createElement('style', 'custom-style');
s.setAttribute('include', name);
Polymer.dom(this.root).appendChild(s);
},
- ready() {
- Gerrit.awaitPluginsLoaded().then(() => Promise.all(
- Gerrit._endpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl)))
+ _importAndApply() {
+ Promise.all(Gerrit._endpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl))
).then(() => {
const moduleNames = Gerrit._endpoints.getModules(this.name);
for (const name of moduleNames) {
@@ -47,5 +58,13 @@
}
});
},
+
+ attached() {
+ this._importAndApply();
+ },
+
+ ready() {
+ Gerrit.awaitPluginsLoaded().then(() => this._importAndApply());
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
index d1893ba..ec2888d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -32,38 +32,90 @@
<script>
suite('gr-external-style integration tests', () => {
+ const TEST_URL = 'http://some/plugin/url.html';
+
let sandbox;
let element;
+ let plugin;
- setup(done => {
- sandbox = sinon.sandbox.create();
-
- // NB: Order is important.
- let plugin;
+ const installPlugin = () => {
+ if (plugin) { return; }
Gerrit.install(p => {
plugin = p;
- plugin.registerStyleModule('foo', 'some-module');
- }, '0.1', 'http://some/plugin/url.html');
+ }, '0.1', TEST_URL);
+ };
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
-
+ const createElement = () => {
element = fixture('basic');
- sandbox.stub(element, '_applyStyle');
- sandbox.stub(element, 'importHref', (url, resolve) => { resolve(); });
+ sandbox.spy(element, '_applyStyle');
+ };
- flush(done);
+ /**
+ * Installs the plugin, creates the element, registers style module.
+ */
+ const lateRegister = () => {
+ installPlugin();
+ createElement();
+ plugin.registerStyleModule('foo', 'some-module');
+ };
+
+ /**
+ * Installs the plugin, registers style module, creates the element.
+ */
+ const earlyRegister = () => {
+ installPlugin();
+ plugin.registerStyleModule('foo', 'some-module');
+ createElement();
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ stub('gr-external-style', {
+ importHref: (url, resolve) => resolve(),
+ });
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
});
teardown(() => {
sandbox.restore();
});
- test('imports plugin-provided module', () => {
- assert.isTrue(element.importHref.calledWith(
- new URL('http://some/plugin/url.html')));
+ test('imports plugin-provided module', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(element.importHref.calledWith(new URL(TEST_URL)));
});
- test('applies plugin-provided styles', () => {
+ test('applies plugin-provided styles', async () => {
+ lateRegister();
+ await new Promise(flush);
+ assert.isTrue(element._applyStyle.calledWith('some-module'));
+ });
+
+ test('does not double import', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const urlsImported =
+ element._urlsImported.filter(url => url.toString() === TEST_URL);
+ assert.strictEqual(urlsImported.length, 1);
+ });
+
+ test('does not double apply', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ plugin.registerStyleModule('foo', 'some-module');
+ await new Promise(flush);
+ const stylesApplied =
+ element._stylesApplied.filter(name => name === 'some-module');
+ assert.strictEqual(stylesApplied.length, 1);
+ });
+
+ test('loads and applies preloaded modules', async () => {
+ earlyRegister();
+ await new Promise(flush);
+ assert.isTrue(element.importHref.calledWith(new URL(TEST_URL)));
assert.isTrue(element._applyStyle.calledWith('some-module'));
});
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index b75ae44..7476637 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -40,7 +40,7 @@
this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins)
.map(p => this._urlFor(p))
.filter(p => !Gerrit._isPluginPreloaded(p));
- const defaultTheme = config.default_theme;
+ const defaultTheme = this._urlFor(config.default_theme);
const pluginsPending =
[].concat(jsPlugins, htmlPlugins, defaultTheme || []);
Gerrit._setPluginsPending(pluginsPending);
@@ -98,6 +98,9 @@
},
_urlFor(pathOrUrl) {
+ if (!pathOrUrl) {
+ return pathOrUrl;
+ }
if (pathOrUrl.startsWith('preloaded:') ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index 641a800..26958ee 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -188,6 +188,15 @@
assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
+ test('default theme is loaded with html plugins', () => {
+ sandbox.stub(Gerrit, '_setPluginsPending');
+ element.config = {
+ default_theme: '/oof',
+ plugin: {},
+ };
+ assert.isTrue(Gerrit._setPluginsPending.calledWith([url + '/oof']));
+ });
+
test('skips preloaded plugins', () => {
sandbox.stub(Gerrit, '_isPluginPreloaded')
.withArgs(url + '/plugins/foo/bar').returns(true)
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index c1d0936..e0c7c37 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -78,6 +78,16 @@
assert.strictEqual(plugin, otherPlugin);
});
+ test('flushes preinstalls if provided', () => {
+ assert.doesNotThrow(() => {
+ Gerrit._flushPreinstalls();
+ });
+ window.Gerrit.flushPreinstalls = sandbox.stub();
+ Gerrit._flushPreinstalls();
+ assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
+ delete window.Gerrit.flushPreinstalls;
+ });
+
test('url', () => {
assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
assert.equal(plugin.url('/static/test.js'),
@@ -429,6 +439,16 @@
assert.strictEqual(pluginApi.getPluginName(), 'foo');
});
+ test('installing preloaded plugin', () => {
+ let plugin;
+ window.ASSETS_PATH = 'http://blips.com/chitz/';
+ Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
+ assert.strictEqual(plugin.getPluginName(), 'foo');
+ assert.strictEqual(plugin.url('/some/thing.html'),
+ 'http://blips.com/plugins/foo/some/thing.html');
+ delete window.ASSETS_PATH;
+ });
+
suite('test plugin with base url', () => {
setup(() => {
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 1efc176..36a428d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -103,6 +103,12 @@
// http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html
window.$wnd = window;
+ function flushPreinstalls() {
+ if (window.Gerrit.flushPreinstalls) {
+ window.Gerrit.flushPreinstalls();
+ }
+ }
+
function installPreloadedPlugins() {
if (!Gerrit._preloadedPlugins) { return; }
for (const name in Gerrit._preloadedPlugins) {
@@ -161,6 +167,13 @@
this._url = new URL(opt_url);
this._name = getPluginNameFromUrl(this._url);
+ if (this._url.protocol === PRELOADED_PROTOCOL) {
+ // Original plugin URL is used in plugin assets URLs calculation.
+ const assetsBaseUrl = window.ASSETS_PATH ||
+ (window.location.origin + Gerrit.BaseUrlBehavior.getBaseUrl());
+ this._url = new URL(assetsBaseUrl + '/plugins/' + this._name +
+ '/static/' + this._name + '.js');
+ }
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
@@ -435,6 +448,8 @@
},
};
+ flushPreinstalls();
+
const Gerrit = window.Gerrit || {};
let _resolveAllPluginsLoaded = null;
@@ -447,6 +462,7 @@
if (!app) {
// No gr-app found (running tests)
Gerrit._installPreloadedPlugins = installPreloadedPlugins;
+ Gerrit._flushPreinstalls = flushPreinstalls;
Gerrit._resetPlugins = () => {
_allPluginsPromise = null;
_pluginsInstalled = [];
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
index 2aead04..ca5c49f 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.html
@@ -29,12 +29,6 @@
<template strip-whitespace>
<style include="gr-voting-styles"></style>
<style include="shared-styles">
- .title {
- font-weight: bold;
- max-width: 20em;
- padding-right: .5em;
- word-break: break-word;
- }
.placeholder {
color: var(--deemphasized-text-color);
padding-top: .2em;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
index ec589fe..c35768f 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -16,6 +16,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../../styles/shared-styles.html">
<script src="../../../bower_components/ba-linkify/ba-linkify.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 22e14e9..091cb75 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -60,15 +60,16 @@
* commentLink patterns
*/
_contentOrConfigChanged(content, config) {
- var output = Polymer.dom(this.$.output);
+ config = Gerrit.Nav.overrideCommentlinks(config);
+ const output = Polymer.dom(this.$.output);
output.textContent = '';
- var parser = new GrLinkTextParser(config,
+ const parser = new GrLinkTextParser(config,
this._handleParseResult.bind(this), this.removeZeroWidthSpace);
parser.parse(content);
// Ensure that links originating from HTML commentlink configs open in a
// new tab. @see Issue 5567
- output.querySelectorAll('a').forEach(function(anchor) {
+ output.querySelectorAll('a').forEach(anchor => {
anchor.setAttribute('target', '_blank');
anchor.setAttribute('rel', 'noopener');
});
@@ -87,9 +88,9 @@
* @param {DocumentFragment|undefined} fragment
*/
_handleParseResult(text, href, fragment) {
- var output = Polymer.dom(this.$.output);
+ const output = Polymer.dom(this.$.output);
if (href) {
- var a = document.createElement('a');
+ const a = document.createElement('a');
a.href = href;
a.textContent = text;
a.target = '_blank';
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index baa025e..fc76da5 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -37,29 +37,30 @@
</test-fixture>
<script>
- suite('gr-linked-text tests', function() {
- var element;
- var sandbox;
+ suite('gr-linked-text tests', () => {
+ let element;
+ let sandbox;
- setup(function() {
+ setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
+ sandbox.stub(Gerrit.Nav, 'overrideCommentlinks', x => x);
element.config = {
ph: {
match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
- link: 'https://code.google.com/p/gerrit/issues/detail?id=$2'
+ link: 'https://code.google.com/p/gerrit/issues/detail?id=$2',
},
changeid: {
match: '(I[0-9a-f]{8,40})',
- link: '#/q/$1'
+ link: '#/q/$1',
},
changeid2: {
match: 'Change-Id: +(I[0-9a-f]{8,40})',
- link: '#/q/$1'
+ link: '#/q/$1',
},
googlesearch: {
match: 'google:(.+)',
- link: 'https://bing.com/search?q=$1', // html should supercede link.
+ link: 'https://bing.com/search?q=$1', // html should supercede link.
html: '<a href="https://google.com/search?q=$1">$1</a>',
},
hashedhtml: {
@@ -74,27 +75,27 @@
};
});
- teardown(function() {
+ teardown(() => {
sandbox.restore();
});
- test('URL pattern was parsed and linked.', function() {
- // Reguar inline link.
- var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+ test('URL pattern was parsed and linked.', () => {
+ // Regular inline link.
+ const url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
element.content = url;
- var linkEl = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[0];
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.rel, 'noopener');
assert.equal(linkEl.href, url);
assert.equal(linkEl.textContent, url);
});
- test('Bug pattern was parsed and linked', function() {
+ test('Bug pattern was parsed and linked', () => {
// "Issue/Bug" pattern.
element.content = 'Issue 3650';
- var linkEl = element.$.output.childNodes[0];
- var url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+ let linkEl = element.$.output.childNodes[0];
+ const url = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
assert.equal(linkEl.target, '_blank');
assert.equal(linkEl.href, url);
assert.equal(linkEl.textContent, 'Issue 3650');
@@ -107,26 +108,26 @@
assert.equal(linkEl.textContent, 'Bug 3650');
});
- test('Change-Id pattern was parsed and linked', function() {
+ test('Change-Id pattern was parsed and linked', () => {
// "Change-Id:" pattern.
- var changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- var prefix = 'Change-Id: ';
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
element.content = prefix + changeID;
- var textNode = element.$.output.childNodes[0];
- var linkEl = element.$.output.childNodes[1];
+ const textNode = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[1];
assert.equal(textNode.textContent, prefix);
- var url = '/q/' + changeID;
+ const url = '/q/' + changeID;
assert.equal(linkEl.target, '_blank');
// Since url is a path, the host is added automatically.
assert.isTrue(linkEl.href.endsWith(url));
assert.equal(linkEl.textContent, changeID);
});
- test('Multiple matches', function() {
+ test('Multiple matches', () => {
element.content = 'Issue 3650\nIssue 3450';
- var linkEl1 = element.$.output.childNodes[0];
- var linkEl2 = element.$.output.childNodes[2];
+ const linkEl1 = element.$.output.childNodes[0];
+ const linkEl2 = element.$.output.childNodes[2];
assert.equal(linkEl1.target, '_blank');
assert.equal(linkEl1.href,
@@ -139,22 +140,22 @@
assert.equal(linkEl2.textContent, 'Issue 3450');
});
- test('Change-Id pattern parsed before bug pattern', function() {
+ test('Change-Id pattern parsed before bug pattern', () => {
// "Change-Id:" pattern.
- var changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
- var prefix = 'Change-Id: ';
+ const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e';
+ const prefix = 'Change-Id: ';
// "Issue/Bug" pattern.
- var bug = 'Issue 3650';
+ const bug = 'Issue 3650';
- var changeUrl = '/q/' + changeID;
- var bugUrl = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
+ const changeUrl = '/q/' + changeID;
+ const bugUrl = 'https://code.google.com/p/gerrit/issues/detail?id=3650';
element.content = prefix + changeID + bug;
- var textNode = element.$.output.childNodes[0];
- var changeLinkEl = element.$.output.childNodes[1];
- var bugLinkEl = element.$.output.childNodes[2];
+ const textNode = element.$.output.childNodes[0];
+ const changeLinkEl = element.$.output.childNodes[1];
+ const bugLinkEl = element.$.output.childNodes[2];
assert.equal(textNode.textContent, prefix);
@@ -167,41 +168,41 @@
assert.equal(bugLinkEl.textContent, 'Issue 3650');
});
- test('html field in link config', function() {
+ test('html field in link config', () => {
element.content = 'google:do a barrel roll';
- var linkEl = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[0];
assert.equal(linkEl.getAttribute('href'),
'https://google.com/search?q=do a barrel roll');
assert.equal(linkEl.textContent, 'do a barrel roll');
});
- test('removing hash from links', function() {
+ test('removing hash from links', () => {
element.content = 'hash:foo';
- var linkEl = element.$.output.childNodes[0];
+ const linkEl = element.$.output.childNodes[0];
assert.isTrue(linkEl.href.endsWith('/awesomesauce'));
assert.equal(linkEl.textContent, 'foo');
});
- test('disabled config', function() {
+ test('disabled config', () => {
element.content = 'foo:baz';
assert.equal(element.$.output.innerHTML, 'foo:baz');
});
- test('R=email labels link correctly', function() {
+ test('R=email labels link correctly', () => {
element.removeZeroWidthSpace = true;
element.content = 'R=\u200Btest@google.com';
assert.equal(element.$.output.textContent, 'R=test@google.com');
assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1);
});
- test('CC=email labels link correctly', function() {
+ test('CC=email labels link correctly', () => {
element.removeZeroWidthSpace = true;
element.content = 'CC=\u200Btest@google.com';
assert.equal(element.$.output.textContent, 'CC=test@google.com');
assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1);
});
- test('only {http,https,mailto} protocols are linkified', function() {
+ test('only {http,https,mailto} protocols are linkified', () => {
element.content = 'xx mailto:test@google.com yy';
let links = element.$.output.querySelectorAll('a');
assert.equal(links.length, 1);
@@ -226,7 +227,7 @@
assert.equal(links.length, 0);
});
- test('overlapping links', function() {
+ test('overlapping links', () => {
element.config = {
b1: {
match: '(B:\\s*)(\\d+)',
@@ -238,7 +239,7 @@
},
};
element.content = '- B: 123, 45';
- var links = Polymer.dom(element.root).querySelectorAll('a');
+ const links = Polymer.dom(element.root).querySelectorAll('a');
assert.equal(links.length, 2);
assert.equal(element.$$('span').textContent, '- B: 123, 45');
@@ -250,31 +251,31 @@
assert.equal(links[1].textContent, '45');
});
- test('_contentOrConfigChanged called with config', function() {
- var contentStub = sandbox.stub(element, '_contentChanged');
- var contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ test('_contentOrConfigChanged called with config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
element.content = 'some text';
assert.isTrue(contentStub.called);
assert.isTrue(contentConfigStub.called);
});
});
- suite('gr-linked-text with null config', function() {
- var element;
- var sandbox;
+ suite('gr-linked-text with null config', () => {
+ let element;
+ let sandbox;
- setup(function() {
+ setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
});
- teardown(function() {
+ teardown(() => {
sandbox.restore();
});
- test('_contentOrConfigChanged not called without config', function() {
- var contentStub = sandbox.stub(element, '_contentChanged');
- var contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
+ test('_contentOrConfigChanged not called without config', () => {
+ const contentStub = sandbox.stub(element, '_contentChanged');
+ const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged');
element.content = 'some text';
assert.isTrue(contentStub.called);
assert.isFalse(contentConfigStub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
index 8b49ca0..8526c3e 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
@@ -41,8 +41,8 @@
* @param {Object|null|undefined} linkConfig Comment links as specified by the
* commentlinks field on a project config.
* @param {Function} callback The callback to be fired when an intermediate
- * parse result is emitted. The callback is passed text and href strings if
- * a link is to be created, or a document fragment otherwise.
+ * parse result is emitted. The callback is passed text and href strings
+ * if a link is to be created, or a document fragment otherwise.
* @param {boolean|undefined} opt_removeZeroWidthSpace If true, zero-width
* spaces will be removed from R=<email> and CC=<email> expressions.
*/
@@ -73,14 +73,14 @@
*/
GrLinkTextParser.prototype.processLinks = function(text, outputArray) {
this.sortArrayReverse(outputArray);
- var fragment = document.createDocumentFragment();
- var cursor = text.length;
+ const fragment = document.createDocumentFragment();
+ let cursor = text.length;
// Start inserting linkified URLs from the end of the String. That way, the
// string positions of the items don't change as we iterate through.
- outputArray.forEach(function(item) {
- // Add any text between the current linkified item and the item added before
- // if it exists.
+ outputArray.forEach(item => {
+ // Add any text between the current linkified item and the item added
+ // before if it exists.
if (item.position + item.length !== cursor) {
fragment.insertBefore(
document.createTextNode(
@@ -130,32 +130,32 @@
*/
GrLinkTextParser.prototype.addItem =
function(text, href, html, position, length, outputArray) {
- var htmlOutput = '';
+ let htmlOutput = '';
- if (href) {
- var a = document.createElement('a');
- a.href = href;
- a.textContent = text;
- a.target = '_blank';
- a.rel = 'noopener';
- htmlOutput = a;
- } else if (html) {
- var fragment = document.createDocumentFragment();
+ if (href) {
+ const a = document.createElement('a');
+ a.href = href;
+ a.textContent = text;
+ a.target = '_blank';
+ a.rel = 'noopener';
+ htmlOutput = a;
+ } else if (html) {
+ const fragment = document.createDocumentFragment();
// Create temporary div to hold the nodes in.
- var div = document.createElement('div');
- div.innerHTML = html;
- while (div.firstChild) {
- fragment.appendChild(div.firstChild);
- }
- htmlOutput = fragment;
- }
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ while (div.firstChild) {
+ fragment.appendChild(div.firstChild);
+ }
+ htmlOutput = fragment;
+ }
- outputArray.push({
- html: htmlOutput,
- position: position,
- length: length,
- });
- };
+ outputArray.push({
+ html: htmlOutput,
+ position,
+ length,
+ });
+ };
/**
* Create a CommentLinkItem for a link and append it to the given output
@@ -171,9 +171,9 @@
*/
GrLinkTextParser.prototype.addLink =
function(text, href, position, length, outputArray) {
- if (!text || this.hasOverlap(position, length, outputArray)) { return; }
- this.addItem(text, href, null, position, length, outputArray);
- };
+ if (!text || this.hasOverlap(position, length, outputArray)) { return; }
+ this.addItem(text, href, null, position, length, outputArray);
+ };
/**
* Create a CommentLinkItem specified by an HTMl string and append it to the
@@ -188,9 +188,9 @@
*/
GrLinkTextParser.prototype.addHTML =
function(html, position, length, outputArray) {
- if (this.hasOverlap(position, length, outputArray)) { return; }
- this.addItem(null, null, html, position, length, outputArray);
- };
+ if (this.hasOverlap(position, length, outputArray)) { return; }
+ this.addItem(null, null, html, position, length, outputArray);
+ };
/**
* Does the given range overlap with anything already in the item list.
@@ -200,18 +200,18 @@
*/
GrLinkTextParser.prototype.hasOverlap =
function(position, length, outputArray) {
- var endPosition = position + length;
- for (var i = 0; i < outputArray.length; i++) {
- var arrayItemStart = outputArray[i].position;
- var arrayItemEnd = outputArray[i].position + outputArray[i].length;
- if ((position >= arrayItemStart && position < arrayItemEnd) ||
+ const endPosition = position + length;
+ for (let i = 0; i < outputArray.length; i++) {
+ const arrayItemStart = outputArray[i].position;
+ const arrayItemEnd = outputArray[i].position + outputArray[i].length;
+ if ((position >= arrayItemStart && position < arrayItemEnd) ||
(endPosition > arrayItemStart && endPosition <= arrayItemEnd) ||
(position === arrayItemStart && position === arrayItemEnd)) {
return true;
- }
- }
- return false;
- };
+ }
+ }
+ return false;
+ };
/**
* Parse the given source text and emit callbacks for the items that are
@@ -241,9 +241,9 @@
text = text.replace(/^(CC|R)=\u200B/gm, '$1=');
}
- // If the href is provided then ba-linkify has recognized it as a URL. If the
- // source text does not include a protocol, the protocol will be added by
- // ba-linkify. Create the link if the href is provided and its protocol
+ // If the href is provided then ba-linkify has recognized it as a URL. If
+ // the source text does not include a protocol, the protocol will be added
+ // by ba-linkify. Create the link if the href is provided and its protocol
// matches the expected pattern.
if (href && URL_PROTOCOL_PATTERN.test(href)) {
this.addText(text, href);
@@ -262,9 +262,10 @@
* object.
*/
GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
- // The outputArray is used to store all of the matches found for all patterns.
- var outputArray = [];
- for (var p in patterns) {
+ // The outputArray is used to store all of the matches found for all
+ // patterns.
+ const outputArray = [];
+ for (const p in patterns) {
if (patterns[p].enabled != null && patterns[p].enabled == false) {
continue;
}
@@ -279,38 +280,37 @@
}
}
- var pattern = new RegExp(patterns[p].match, 'g');
+ const pattern = new RegExp(patterns[p].match, 'g');
- var match;
- var textToCheck = text;
- var susbtrIndex = 0;
+ let match;
+ let textToCheck = text;
+ let susbtrIndex = 0;
while ((match = pattern.exec(textToCheck)) != null) {
textToCheck = textToCheck.substr(match.index + match[0].length);
- var result = match[0].replace(pattern,
+ let result = match[0].replace(pattern,
patterns[p].html || patterns[p].link);
+ let i;
// Skip portion of replacement string that is equal to original.
- for (var i = 0; i < result.length; i++) {
- if (result[i] !== match[0][i]) {
- break;
- }
+ for (i = 0; i < result.length; i++) {
+ if (result[i] !== match[0][i]) { break; }
}
result = result.slice(i);
if (patterns[p].html) {
this.addHTML(
- result,
- susbtrIndex + match.index + i,
- match[0].length - i,
- outputArray);
+ result,
+ susbtrIndex + match.index + i,
+ match[0].length - i,
+ outputArray);
} else if (patterns[p].link) {
this.addLink(
- match[0],
- result,
- susbtrIndex + match.index + i,
- match[0].length - i,
- outputArray);
+ match[0],
+ result,
+ susbtrIndex + match.index + i,
+ match[0].length - i,
+ outputArray);
} else {
throw Error('linkconfig entry ' + p +
' doesn’t contain a link or html attribute.');
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index d014758..8d714eb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -61,7 +61,6 @@
* endpoint: string,
* patchNum: (string|number|null|undefined),
* errFn: (function(?Response, string=)|null|undefined),
- * cancelCondition: (function()|null|undefined),
* params: (Object|null|undefined),
* fetchOptions: (Object|null|undefined),
* anonymizedEndpoint: (string|undefined),
@@ -2112,10 +2111,8 @@
* @param {number|string} patchNum
* @param {string} path
* @param {function(?Response, string=)=} opt_errFn
- * @param {function()=} opt_cancelCondition
*/
- getDiff(changeNum, basePatchNum, patchNum, path,
- opt_errFn, opt_cancelCondition) {
+ getDiff(changeNum, basePatchNum, patchNum, path, opt_errFn) {
const params = {
context: 'ALL',
intraline: null,
@@ -2133,7 +2130,6 @@
endpoint,
patchNum,
errFn: opt_errFn,
- cancelCondition: opt_cancelCondition,
params,
anonymizedEndpoint: '/files/*/diff',
});
@@ -2796,7 +2792,6 @@
return this._fetchJSON({
url: url + req.endpoint,
errFn: req.errFn,
- cancelCondition: req.cancelCondition,
params: req.params,
fetchOptions: req.fetchOptions,
anonymizedUrl: anonymizedEndpoint ?
diff --git a/polygerrit-ui/app/externs/plugin.js b/polygerrit-ui/app/externs/plugin.js
index ed8ee95..c88c724 100644
--- a/polygerrit-ui/app/externs/plugin.js
+++ b/polygerrit-ui/app/externs/plugin.js
@@ -20,7 +20,7 @@
* @externs
*/
-// eslint-disable no-var
+/* eslint-disable no-var */
var Gerrit = {};
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.html b/polygerrit-ui/app/styles/dashboard-header-styles.html
index a88f68c..b82bf3a 100644
--- a/polygerrit-ui/app/styles/dashboard-header-styles.html
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.html
@@ -39,7 +39,7 @@
}
.info > div > span {
display: inline-block;
- font-weight: bold;
+ font-family: var(--font-family-bold);
text-align: right;
width: 4em;
}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 052de6b..f401735 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -19,7 +19,8 @@
/**
* @param canonicalPath
* @param staticResourcePath
- * @param? assetsUrl
+ * @param? assetsPath {string} URL to static assets root, if served from CDN.
+ * @param? assetsBundle {string} Assets bundle .html file, served from $assetsPath.
* @param? faviconPath
* @param? versionInfo
* @param? deprecateGwtUi
@@ -37,6 +38,7 @@
{if $deprecateGwtUi}window.DEPRECATE_GWT_UI = true;{/if}
{if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
{if $staticResourcePath != ''}window.STATIC_RESOURCE_PATH = '{$staticResourcePath}';{/if}
+ {if $assetsPath}window.ASSETS_PATH = '{$assetsPath}';{/if}
</script>{\n}
{if $faviconPath}
@@ -62,8 +64,8 @@
// CC them on any changes that load content before gr-app.html.
//
// github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
- {if $assetsUrl}
- <link rel="import" href="{$assetsUrl}">{\n}
+ {if $assetsPath and $assetsBundle}
+ <link rel="import" href="{$assetsPath + $assetsBundle}">{\n}
{/if}
<link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index d022c40..0c9d023 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -11,7 +11,6 @@
"//gerrit-gwtui:ui_tests",
"//javatests/com/google/gerrit/elasticsearch:elasticsearch_test_utils",
"//javatests/com/google/gerrit/server:server_tests",
- "//proto:reviewdb_java_proto",
]
DEPS = [
@@ -33,8 +32,7 @@
"//lib/gwt:w3c-css-sac",
"//lib/jetty:servlets",
"//lib/prolog:compiler-lib",
- # TODO(davido): I do not understand why it must be on the Eclipse classpath
- #'//Documentation:index',
+ "//proto:reviewdb_java_proto",
]
java_library(