Merge "Add ctrl+s as keyboard shortcut to save comment draft"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ed49276..31dc917 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3452,6 +3452,15 @@
[[site]]
=== Section site
+[[site.allowOriginRegex]]site.allowOriginRegex::
++
+List of regular expressions matching origins that should be permitted
+to use the Gerrit REST API to read content. These should be trusted
+applications as the sites may be able to use the user's credentials.
+Only applies to GET and HEAD requests.
++
+By default, unset, denying all cross-origin requests.
+
[[site.refreshHeaderFooter]]site.refreshHeaderFooter::
+
If true the server checks the site header, footer and CSS files for
diff --git a/ReleaseNotes/ReleaseNotes-2.11.10.txt b/ReleaseNotes/ReleaseNotes-2.11.10.txt
new file mode 100644
index 0000000..a352aac
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.10.txt
@@ -0,0 +1,28 @@
+= Release notes for Gerrit 2.11.10
+
+Gerrit 2.11.10 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.10.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.10.war]
+
+There are no schema changes from link:ReleaseNotes-2.11.9.html[2.11.9].
+
+== Bug Fixes
+
+* Fix synchronization of Myers diff and Histogram diff invocations.
++
+The fix for
+link:https://code.google.com/p/gerrit/issues/detail?id=3361[Issue 3361]
+that was included in Gerrit versions 2.10.7 and 2.11.4 introduced a
+regression that prevented more than one file header diff from being
+computed at the same time across the entire server.
+
+* Fix `sshd.idleTimeout` setting being ignored.
++
+The `sshd.idleTimeout` setting was not being correctly set on the SSHD
+backend, causing idle sessions to not time out.
+
+* Add the correct license for AsciiDoctor.
++
+AsciiDoctor is licensed under the MIT License, not Apache2 as previously
+documented.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.4.txt b/ReleaseNotes/ReleaseNotes-2.12.4.txt
index 21ee41d..411011e 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.4.txt
@@ -93,7 +93,7 @@
* Fix `sshd.idleTimeout` setting being ignored.
+
-Ths `sshd.idleTimeout` setting was not being correctly set on the SSHD
+The `sshd.idleTimeout` setting was not being correctly set on the SSHD
backend, causing idle sessions to not time out.
* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4324[Issue 4324]:
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 5534713..3ba24c5 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -14,6 +14,7 @@
[[s2_11]]
== Version 2.11.x
+* link:ReleaseNotes-2.11.10.html[2.11.10]
* link:ReleaseNotes-2.11.9.html[2.11.9]
* link:ReleaseNotes-2.11.8.html[2.11.8]
* link:ReleaseNotes-2.11.7.html[2.11.7]
diff --git a/WORKSPACE b/WORKSPACE
index 4911f44..97fdd05 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -24,24 +24,30 @@
sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0',
)
-GUICE_VERS = '4.0'
+GUICE_VERS = '4.1.0'
maven_jar(
name = 'guice_library',
artifact = 'com.google.inject:guice:' + GUICE_VERS,
- sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649',
+ sha1 = 'eeb69005da379a10071aa4948c48d89250febb07',
)
maven_jar(
name = 'guice_assistedinject',
artifact = 'com.google.inject.extensions:guice-assistedinject:' + GUICE_VERS,
- sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca',
+ sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb',
)
maven_jar(
name = 'guice_servlet',
artifact = 'com.google.inject.extensions:guice-servlet:' + GUICE_VERS,
- sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164',
+ sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb',
+)
+
+maven_jar(
+ name = 'multibindings',
+ artifact = 'com.google.inject.extensions:guice-multibindings:' + GUICE_VERS,
+ sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51',
)
maven_jar(
@@ -141,8 +147,8 @@
maven_jar(
name = 'gson',
- artifact = 'com.google.code.gson:gson:2.6.2',
- sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947',
+ artifact = 'com.google.code.gson:gson:2.7',
+ sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e',
)
maven_jar(
@@ -165,14 +171,14 @@
maven_jar(
name = 'joda_time',
- artifact = 'joda-time:joda-time:2.8',
- sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb',
+ artifact = 'joda-time:joda-time:2.9.4',
+ sha1 = '1c295b462f16702ebe720bbb08f62e1ba80da41b',
)
maven_jar(
name = 'joda_convert',
- artifact = 'org.joda:joda-convert:1.2',
- sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec',
+ artifact = 'org.joda:joda-convert:1.8.1',
+ sha1 = '675642ac208e0b741bc9118dcbcae44c271b992a',
)
maven_jar(
@@ -287,8 +293,8 @@
maven_jar(
name = 'commons_net',
- artifact = 'commons-net:commons-net:2.2',
- sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a',
+ artifact = 'commons-net:commons-net:3.5',
+ sha1 = '342fc284019f590e1308056990fdb24a08f06318',
)
maven_jar(
@@ -327,42 +333,42 @@
sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54',
)
-OW2_VERS = '5.0.3'
+OW2_VERS = '5.1'
maven_jar(
name = 'ow2_asm',
artifact = 'org.ow2.asm:asm:' + OW2_VERS,
- sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+ sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45',
)
maven_jar(
name = 'ow2_asm_analysis',
artifact = 'org.ow2.asm:asm-analysis:' + OW2_VERS,
- sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+ sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110',
)
maven_jar(
name = 'ow2_asm_commons',
artifact = 'org.ow2.asm:asm-commons:' + OW2_VERS,
- sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c',
+ sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e',
)
maven_jar(
name = 'ow2_asm_tree',
artifact = 'org.ow2.asm:asm-tree:' + OW2_VERS,
- sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+ sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34',
)
maven_jar(
name = 'ow2_asm_util',
artifact = 'org.ow2.asm:asm-util:' + OW2_VERS,
- sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+ sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47',
)
maven_jar(
name = 'auto_value',
- artifact = 'com.google.auto.value:auto-value:1.2',
- sha1 = '6873fed014fe1de1051aae2af68ba266d2934471',
+ artifact = 'com.google.auto.value:auto-value:1.3-rc1',
+ sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179',
)
maven_jar(
@@ -371,36 +377,36 @@
sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
)
-LUCENE_VERS = '5.4.1'
+LUCENE_VERS = '5.5.0'
maven_jar(
name = 'lucene_core',
artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS,
- sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab',
+ sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164',
)
maven_jar(
name = 'lucene_analyzers_common',
artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS,
- sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e',
+ sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc',
)
maven_jar(
name = 'backward_codecs',
artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS,
- sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f',
+ sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805',
)
maven_jar(
name = 'lucene_misc',
artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
- sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b',
+ sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca',
)
maven_jar(
name = 'lucene_queryparser',
artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS,
- sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618',
+ sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4',
)
maven_jar(
@@ -447,8 +453,8 @@
maven_jar(
name = 'jsr305',
- artifact = 'com.google.code.findbugs:jsr305:2.0.2',
- sha1 = '516c03b21d50a644d538de0f0369c620989cd8f0',
+ artifact = 'com.google.code.findbugs:jsr305:3.0.1',
+ sha1 = 'f7be08ec23c21485b9b5a1cf1654c2ec8c58168d',
)
maven_jar(
@@ -458,6 +464,19 @@
sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826',
)
+# Keep this version of Soy synchronized with the version used in Gitiles.
+maven_jar(
+ name = 'soy',
+ artifact = 'com.google.template:soy:2016-08-09',
+ sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4',
+)
+
+maven_jar(
+ name = 'icu4j',
+ artifact = 'com.ibm.icu:icu4j:57.1',
+ sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92',
+)
+
maven_jar(
name = 'dropwizard_core',
artifact = 'io.dropwizard.metrics:metrics-core:3.1.2',
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 79ff53a..5530be3 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -64,6 +64,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.change.Abandon;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Revisions;
@@ -238,6 +239,9 @@
@Inject
protected ChangeNotes.Factory notesFactory;
+ @Inject
+ protected Abandon changeAbandoner;
+
@Rule
public ExpectedException exception = ExpectedException.none();
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index 0196d1f..73e8ba5 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -151,8 +151,15 @@
public static PushResult pushHead(TestRepository<?> testRepo, String ref,
boolean pushTags, boolean force) throws GitAPIException {
+ return pushHead(testRepo, ref, pushTags, force, null);
+ }
+
+ public static PushResult pushHead(TestRepository<?> testRepo, String ref,
+ boolean pushTags, boolean force, List<String> pushOptions)
+ throws GitAPIException {
PushCommand pushCmd = testRepo.git().push();
pushCmd.setForce(force);
+ pushCmd.setPushOptions(pushOptions);
pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
if (pushTags) {
pushCmd.setPushTags();
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
index 390cae3..e9c6e96 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -16,6 +16,7 @@
import com.google.common.base.Preconditions;
+import org.apache.http.Header;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
@@ -52,7 +53,12 @@
}
public String getContentType() {
- return response.getFirstHeader("X-FYI-Content-Type").getValue();
+ return getHeader("X-FYI-Content-Type");
+ }
+
+ public String getHeader(String name) {
+ Header hdr = response.getFirstHeader(name);
+ return hdr != null ? hdr.getValue() : null;
}
public boolean hasContent() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
index 1e0920e..669b991 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -37,7 +37,11 @@
account.username, account.httpPassword);
}
- protected RestResponse execute(Request request) throws IOException {
+ public String url() {
+ return url;
+ }
+
+ public RestResponse execute(Request request) throws IOException {
return new RestResponse(executor.execute(request).returnResponse());
}
}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index c892877..e46176e 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
@@ -136,6 +137,7 @@
private String changeId;
private Tag tag;
private boolean force;
+ private List<String> pushOptions;
private final TestRepository<?>.CommitBuilder commitBuilder;
@@ -275,8 +277,8 @@
}
tagCommand.call();
}
- return new Result(ref, pushHead(testRepo, ref, tag != null, force), c,
- subject);
+ return new Result(ref,
+ pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject);
}
public void setTag(final Tag tag) {
@@ -287,6 +289,14 @@
this.force = force;
}
+ public List<String> getPushOptions() {
+ return pushOptions;
+ }
+
+ public void setPushOptions(List<String> pushOptions) {
+ this.pushOptions = pushOptions;
+ }
+
public void noParents() {
commitBuilder.noParents();
}
@@ -326,6 +336,10 @@
return commit;
}
+ public void assertPushOptions(List<String> pushOptions) {
+ assertEquals(pushOptions, getPushOptions());
+ }
+
public void assertChange(Change.Status expectedStatus,
String expectedTopic, TestAccount... expectedReviewers)
throws OrmException, NoSuchChangeException {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 9c59e10..90ece46 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -45,7 +45,7 @@
new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
}
- private RestResponse getWithHeader(String endPoint, Header header)
+ public RestResponse getWithHeader(String endPoint, Header header)
throws IOException {
Request get = Request.Get(url + "/a" + endPoint);
if (header != null) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index e4c39a1..00b48b4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -16,18 +16,32 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.fail;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.projects.BranchInfo;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
@@ -43,6 +57,16 @@
return cfg;
}
+ @BeforeClass
+ public static void setTimeForTesting() {
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
+ }
+
+ @AfterClass
+ public static void restoreTime() {
+ TestTimeUtil.useSystemTime();
+ }
+
@Before
public void setUp() throws Exception {
caAutoVerify = configureContributorAgreement(true);
@@ -90,6 +114,11 @@
// Sign the agreement
gApi.accounts().self().signAgreement(caAutoVerify.getName());
+
+ // Explicitly reset the user to force a new request context
+ setApiUser(user);
+
+ // Verify that the agreement was signed
result = gApi.accounts().self().listAgreements();
assertThat(result).hasSize(1);
AgreementInfo info = result.get(0);
@@ -117,6 +146,83 @@
gApi.accounts().self().listAgreements();
}
+ @Test
+ public void revertChangeWithoutCLA() throws Exception {
+ assume().that(isContributorAgreementsEnabled()).isTrue();
+
+ // Create a change succeeds when agreement is not required
+ setUseContributorAgreements(InheritableBoolean.FALSE);
+ ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+ // Approve and submit it
+ setApiUser(admin);
+ gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+ gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+ // Revert is not allowed when CLA is required but not signed
+ setApiUser(user);
+ setUseContributorAgreements(InheritableBoolean.TRUE);
+ exception.expect(AuthException.class);
+ exception.expectMessage("A Contributor Agreement must be completed");
+ gApi.changes().id(change.changeId).revert();
+ }
+
+ @Test
+ public void cherrypickChangeWithoutCLA() throws Exception {
+ assume().that(isContributorAgreementsEnabled()).isTrue();
+
+ // Create a new branch
+ setApiUser(admin);
+ BranchInfo dest = gApi.projects().name(project.get())
+ .branch("cherry-pick-to").create(new BranchInput()).get();
+
+ // Create a change succeeds when agreement is not required
+ setUseContributorAgreements(InheritableBoolean.FALSE);
+ ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+ // Approve and submit it
+ gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+ gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+ // Cherry-pick is not allowed when CLA is required but not signed
+ setApiUser(user);
+ setUseContributorAgreements(InheritableBoolean.TRUE);
+ CherryPickInput in = new CherryPickInput();
+ in.destination = dest.ref;
+ in.message = change.subject;
+ exception.expect(AuthException.class);
+ exception.expectMessage("A Contributor Agreement must be completed");
+ gApi.changes().id(change.changeId).current().cherryPick(in);
+ }
+
+ @Test
+ public void createChangeRespectsCLA() throws Exception {
+ assume().that(isContributorAgreementsEnabled()).isTrue();
+
+ // Create a change succeeds when agreement is not required
+ setUseContributorAgreements(InheritableBoolean.FALSE);
+ gApi.changes().create(newChangeInput());
+
+ // Create a change is not allowed when CLA is required but not signed
+ setUseContributorAgreements(InheritableBoolean.TRUE);
+ try {
+ gApi.changes().create(newChangeInput());
+ fail("Expected AuthException");
+ } catch (AuthException e) {
+ assertThat(e.getMessage()).contains(
+ "A Contributor Agreement must be completed");
+ }
+
+ // Sign the agreement
+ gApi.accounts().self().signAgreement(caAutoVerify.getName());
+
+ // Explicitly reset the user to force a new request context
+ setApiUser(user);
+
+ // Create a change succeeds after signing the agreement
+ gApi.changes().create(newChangeInput());
+ }
+
private void assertAgreement(AgreementInfo info, ContributorAgreement ca) {
assertThat(info.name).isEqualTo(ca.getName());
assertThat(info.description).isEqualTo(ca.getDescription());
@@ -128,4 +234,12 @@
assertThat(info.autoVerifyGroup).isNull();
}
}
+
+ private ChangeInput newChangeInput() {
+ ChangeInput in = new ChangeInput();
+ in.branch = "master";
+ in.subject = "test";
+ in.project = project.get();
+ return in;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index db9bb09..52cdc5c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -31,6 +31,7 @@
import static org.junit.Assert.fail;
import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -73,6 +74,7 @@
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.CurrentUser;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.git.ProjectConfig;
@@ -80,7 +82,6 @@
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.Util;
import com.google.gerrit.testutil.FakeEmailSender.Message;
-import com.google.gerrit.testutil.NoteDbMode;
import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -186,6 +187,64 @@
}
@Test
+ public void batchAbandon() throws Exception {
+ CurrentUser user = atrScope.get().getUser();
+ PushOneCommit.Result a = createChange();
+ List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user);
+ assertThat(controlA).hasSize(1);
+ PushOneCommit.Result b = createChange();
+ List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user);
+ assertThat(controlB).hasSize(1);
+ List<ChangeControl> list =
+ ImmutableList.of(controlA.get(0), controlB.get(0));
+ changeAbandoner.batchAbandon(
+ controlA.get(0).getProject().getNameKey(), user, list, "deadbeef");
+
+ ChangeInfo info = get(a.getChangeId());
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+ assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+ .contains("abandoned");
+ assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+ .contains("deadbeef");
+
+ info = get(b.getChangeId());
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+ assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+ .contains("abandoned");
+ assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+ .contains("deadbeef");
+ }
+
+ @Test
+ public void batchAbandonChangeProject() throws Exception {
+ String project1Name = name("Project1");
+ String project2Name = name("Project2");
+ gApi.projects().create(project1Name);
+ gApi.projects().create(project2Name);
+ TestRepository<InMemoryRepository> project1 =
+ cloneProject(new Project.NameKey(project1Name));
+ TestRepository<InMemoryRepository> project2 =
+ cloneProject(new Project.NameKey(project2Name));
+
+ CurrentUser user = atrScope.get().getUser();
+ PushOneCommit.Result a =
+ createChange(project1, "master", "x", "x", "x", "");
+ List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user);
+ assertThat(controlA).hasSize(1);
+ PushOneCommit.Result b =
+ createChange(project2, "master", "x", "x", "x", "");
+ List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user);
+ assertThat(controlB).hasSize(1);
+ List<ChangeControl> list =
+ ImmutableList.of(controlA.get(0), controlB.get(0));
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage(String.format(
+ "Project name \"%s\" doesn't match \"%s\"",
+ project2Name, project1Name));
+ changeAbandoner.batchAbandon(new Project.NameKey(project1Name), user, list);
+ }
+
+ @Test
public void abandonDraft() throws Exception {
PushOneCommit.Result r = createDraftChange();
String changeId = r.getChangeId();
@@ -1041,18 +1100,8 @@
.reviewer(user.getId().toString())
.votes();
- if (NoteDbMode.readWrite()) {
- // When NoteDb is enabled each reviewer is explicitly recorded in the
- // NoteDb and this record stays even when all votes of that user have been
- // deleted, hence there is no dummy 0 approval left when a vote is
- // deleted.
- assertThat(m).isEmpty();
- } else {
- // When NoteDb is disabled there is a dummy 0 approval on the change so
- // that the user is still returned as CC when all votes of that user have
- // been deleted.
- assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0));
- }
+ // Dummy 0 approval on the change to block vote copying to this patch set.
+ assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0));
ChangeInfo c = gApi.changes()
.id(r.getChangeId())
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 54fe28f..4da22d3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -311,6 +311,27 @@
assertVotes(c, user, 0, 0, REWORK);
}
+ @Test
+ public void deleteStickyVote() throws Exception {
+ String label = "Code-Review";
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ cfg.getLabelSections().get(label)
+ .setCopyMaxScore(true);
+ saveProjectConfig(project, cfg);
+
+
+ // Vote max score on PS1
+ String changeId = createChange(REWORK);
+ vote(admin, changeId, label, 2);
+ assertVotes(detailedChange(changeId), admin, label, 2, null);
+ updateChange(changeId, REWORK);
+ assertVotes(detailedChange(changeId), admin, label, 2, REWORK);
+
+ // Delete vote that was copied via sticky approval
+ deleteVote(admin, changeId, "Code-Review");
+ assertVotes(detailedChange(changeId), admin, label, 0, REWORK);
+ }
+
private ChangeInfo detailedChange(String changeId) throws Exception {
return gApi.changes().id(changeId)
.get(EnumSet.of(ListChangesOption.DETAILED_LABELS,
@@ -495,6 +516,15 @@
return c.revisions.get(c.currentRevision).kind;
}
+ private void vote(TestAccount user, String changeId, String label, int vote)
+ throws Exception {
+ setApiUser(user);
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(new ReviewInput().label(label, vote));
+ }
+
private void vote(TestAccount user, String changeId, int codeReviewVote,
int verifiedVote) throws Exception {
setApiUser(user);
@@ -504,6 +534,15 @@
gApi.changes().id(changeId).current().review(in);
}
+ private void deleteVote(TestAccount user, String changeId, String label)
+ throws Exception {
+ setApiUser(user);
+ gApi.changes()
+ .id(changeId)
+ .reviewer(user.getId().toString())
+ .deleteVote(label);
+ }
+
private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote,
int verifiedVote) {
assertVotes(c, user, codeReviewVote, verifiedVote, null);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 3629e29..e8b2924 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -52,7 +52,6 @@
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ETagView;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.change.GetRevisionActions;
@@ -567,16 +566,19 @@
}
@Test
- public void diffNonExistingFile() throws Exception {
- PushOneCommit.Result r = createChange();
-
- exception.expect(ResourceNotFoundException.class);
- exception.expectMessage("non-existing");
- gApi.changes()
+ public void diffDeletedFile() throws Exception {
+ pushFactory.create(db, admin.getIdent(), testRepo)
+ .to("refs/heads/master");
+ PushOneCommit.Result r =
+ pushFactory.create(db, admin.getIdent(), testRepo)
+ .rm("refs/for/master");
+ DiffInfo diff = gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
- .file("non-existing")
+ .file(FILE_NAME)
.diff();
+ assertThat(diff.metaA.lines).isEqualTo(1);
+ assertThat(diff.metaB).isNull();
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 7dee60f..c673e7b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -176,6 +176,21 @@
}
@Test
+ public void pushForMasterWithTopicOption() throws Exception {
+ String topicOption = "topic=myTopic";
+ List<String> pushOptions = new ArrayList<>();
+ pushOptions.add(topicOption);
+
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ push.setPushOptions(pushOptions);
+ PushOneCommit.Result r = push.to("refs/for/master");
+
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, "myTopic");
+ r.assertPushOptions(pushOptions);
+ }
+
+ @Test
public void pushForMasterWithNotify() throws Exception {
TestAccount user2 = accounts.user2();
String pushSpec = "refs/for/master"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
new file mode 100644
index 0000000..f5ae072
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
+import static com.google.common.net.HttpHeaders.ORIGIN;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.apache.http.Header;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class CorsIT extends AbstractDaemonTest {
+ @ConfigSuite.Default
+ public static Config allowExampleDotCom() {
+ Config cfg = new Config();
+ cfg.setStringList(
+ "site", null, "allowOriginRegex",
+ ImmutableList.of(
+ "https?://(.+[.])?example[.]com",
+ "http://friend[.]ly"));
+ return cfg;
+ }
+
+ @Test
+ public void origin() throws Exception {
+ Result change = createChange();
+
+ String url = "/changes/" + change.getChangeId() + "/detail";
+ RestResponse r = adminRestSession.get(url);
+ r.assertOK();
+ assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isNull();
+ assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS)).isNull();
+
+ check(url, true, "http://example.com");
+ check(url, true, "https://sub.example.com");
+ check(url, true, "http://friend.ly");
+
+ check(url, false, "http://evil.attacker");
+ check(url, false, "http://friendsly");
+ }
+
+ @Test
+ public void putWithOriginRefused() throws Exception {
+ Result change = createChange();
+ String origin = "http://example.com";
+ RestResponse r = adminRestSession.putWithHeader(
+ "/changes/" + change.getChangeId() + "/topic",
+ new BasicHeader(ORIGIN, origin),
+ "A");
+ r.assertOK();
+ checkCors(r, false, origin);
+ }
+
+ @Test
+ public void preflightOk() throws Exception {
+ Result change = createChange();
+
+ String origin = "http://example.com";
+ Request req = Request.Options(adminRestSession.url()
+ + "/a/changes/" + change.getChangeId() + "/detail");
+ req.addHeader(ORIGIN, origin);
+ req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Requested-With");
+
+ RestResponse res = adminRestSession.execute(req);
+ res.assertOK();
+ checkCors(res, true, origin);
+ }
+
+ @Test
+ public void preflightBadOrigin() throws Exception {
+ Result change = createChange();
+
+ Request req = Request.Options(adminRestSession.url()
+ + "/a/changes/" + change.getChangeId() + "/detail");
+ req.addHeader(ORIGIN, "http://evil.attacker");
+ req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+
+ adminRestSession.execute(req).assertBadRequest();
+ }
+
+ @Test
+ public void preflightBadMethod() throws Exception {
+ Result change = createChange();
+
+ for (String method : new String[] {"POST", "PUT", "DELETE", "PATCH"}) {
+ Request req = Request.Options(adminRestSession.url()
+ + "/a/changes/" + change.getChangeId() + "/detail");
+ req.addHeader(ORIGIN, "http://example.com");
+ req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, method);
+ adminRestSession.execute(req).assertBadRequest();
+ }
+ }
+
+ @Test
+ public void preflightBadHeader() throws Exception {
+ Result change = createChange();
+
+ Request req = Request.Options(adminRestSession.url()
+ + "/a/changes/" + change.getChangeId() + "/detail");
+ req.addHeader(ORIGIN, "http://example.com");
+ req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+ req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Gerrit-Auth");
+
+ adminRestSession.execute(req).assertBadRequest();
+ }
+
+ private RestResponse check(String url, boolean accept, String origin)
+ throws Exception {
+ Header hdr = new BasicHeader(ORIGIN, origin);
+ RestResponse r = adminRestSession.getWithHeader(url, hdr);
+ r.assertOK();
+ checkCors(r, accept, origin);
+ return r;
+ }
+
+ private void checkCors(RestResponse r, boolean accept, String origin) {
+ String allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN);
+ String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS);
+ String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS);
+ String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS);
+ if (accept) {
+ assertThat(allowOrigin).isEqualTo(origin);
+ assertThat(allowCred).isEqualTo("true");
+ assertThat(allowMethods).isEqualTo("GET, OPTIONS");
+ assertThat(allowHeaders).isEqualTo("X-Requested-With");
+ } else {
+ assertThat(allowOrigin).isNull();
+ assertThat(allowCred).isNull();
+ assertThat(allowMethods).isNull();
+ assertThat(allowHeaders).isNull();
+ }
+ }
+}
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 121d236..dda1290 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -74,6 +74,7 @@
paths = ['src/main/java'],
srcs = SRCS,
deps = [
+ '//lib:guava',
'//lib/guice:javax-inject',
'//lib/guice:guice_library',
'//lib/guice:guice-assistedinject',
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index a71ab37..2731476 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -47,7 +47,7 @@
Map<String, FileInfo> files() throws RestApiException;
Map<String, FileInfo> files(String base) throws RestApiException;
Map<String, FileInfo> files(int parentNum) throws RestApiException;
- FileApi file(String path) throws RestApiException;
+ FileApi file(String path);
MergeableInfo mergeable() throws RestApiException;
MergeableInfo mergeableOtherBranches() throws RestApiException;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
index 004ef1c..2056e25 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -15,10 +15,10 @@
package com.google.gerrit.extensions.client;
public enum AuthType {
- /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
+ /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> */
OPENID,
- /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */
+ /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> in Single Sign On mode */
OPENID_SSO,
/**
@@ -49,7 +49,7 @@
* Jetty's SSL channel to request client's SSL certificate. For this
* authentication to work a Gerrit administrator has to import the root
* certificate of the trust chain used to issue the client's certificate
- * into the <review-site>/etc/keystore.
+ * into the <review-site>/etc/keystore.
* <p>
* After the authentication is done Gerrit will obtain basic user
* registration (name and email) from LDAP, and some group memberships.
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index 943be7e..3d99883 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -82,7 +82,6 @@
Modes.I.htmlmixed(),
Modes.I.http(),
Modes.I.idl(),
- Modes.I.jade(),
Modes.I.javascript(),
Modes.I.jinja2(),
Modes.I.jsx(),
@@ -110,6 +109,7 @@
Modes.I.powershell(),
Modes.I.properties(),
Modes.I.protobuf(),
+ Modes.I.pug(),
Modes.I.puppet(),
Modes.I.python(),
Modes.I.q(),
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index 668a57f..218b96c 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -67,7 +67,6 @@
@Source("htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
@Source("http.js") @DoNotEmbed DataResource http();
@Source("idl.js") @DoNotEmbed DataResource idl();
- @Source("jade.js") @DoNotEmbed DataResource jade();
@Source("javascript.js") @DoNotEmbed DataResource javascript();
@Source("jinja2.js") @DoNotEmbed DataResource jinja2();
@Source("jsx.js") @DoNotEmbed DataResource jsx();
@@ -95,6 +94,7 @@
@Source("powershell.js") @DoNotEmbed DataResource powershell();
@Source("properties.js") @DoNotEmbed DataResource properties();
@Source("protobuf.js") @DoNotEmbed DataResource protobuf();
+ @Source("pug.js") @DoNotEmbed DataResource pug();
@Source("puppet.js") @DoNotEmbed DataResource puppet();
@Source("python.js") @DoNotEmbed DataResource python();
@Source("q.js") @DoNotEmbed DataResource q();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 943d824..e3f3fb1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -15,6 +15,14 @@
package com.google.gerrit.httpd.restapi;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
+import static com.google.common.net.HttpHeaders.ORIGIN;
+import static com.google.common.net.HttpHeaders.VARY;
import static java.math.RoundingMode.CEILING;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -35,9 +43,12 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
+import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
@@ -85,6 +96,7 @@
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.util.http.RequestUtil;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
@@ -103,6 +115,7 @@
import com.google.inject.Provider;
import com.google.inject.util.Providers;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.Heap;
import org.slf4j.Logger;
@@ -131,6 +144,7 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
@@ -150,6 +164,9 @@
// HTTP 422 Unprocessable Entity.
// TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
private static final int SC_UNPROCESSABLE_ENTITY = 422;
+ private static final String X_REQUESTED_WITH = "X-Requested-With";
+ private static final ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS =
+ ImmutableSet.of(X_REQUESTED_WITH);
private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
@@ -174,18 +191,29 @@
final Provider<ParameterParser> paramParser;
final AuditService auditService;
final RestApiMetrics metrics;
+ final Pattern allowOrigin;
@Inject
Globals(Provider<CurrentUser> currentUser,
DynamicItem<WebSession> webSession,
Provider<ParameterParser> paramParser,
AuditService auditService,
- RestApiMetrics metrics) {
+ RestApiMetrics metrics,
+ @GerritServerConfig Config cfg) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
this.auditService = auditService;
this.metrics = metrics;
+ allowOrigin = makeAllowOrigin(cfg);
+ }
+
+ private static Pattern makeAllowOrigin(Config cfg) {
+ String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
+ if (allow.length > 0) {
+ return Pattern.compile(Joiner.on('|').join(allow));
+ }
+ return null;
}
}
@@ -222,6 +250,11 @@
ViewData viewData = null;
try {
+ if (isCorsPreflight(req)) {
+ doCorsPreflight(req, res);
+ return;
+ }
+ checkCors(req, res);
checkUserSession(req);
List<IdString> path = splitPath(req);
@@ -232,7 +265,7 @@
viewData = new ViewData(null, null);
if (path.isEmpty()) {
- if (isGetOrHead(req)) {
+ if (isRead(req)) {
viewData = new ViewData(null, rc.list());
} else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
@@ -273,7 +306,7 @@
(RestCollection<RestResource, RestResource>) viewData.view;
if (path.isEmpty()) {
- if (isGetOrHead(req)) {
+ if (isRead(req)) {
viewData = new ViewData(null, c.list());
} else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
@@ -330,7 +363,7 @@
return;
}
- if (viewData.view instanceof RestReadView<?> && isGetOrHead(req)) {
+ if (viewData.view instanceof RestReadView<?> && isRead(req)) {
result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
} else if (viewData.view instanceof RestModifyView<?, ?>) {
@SuppressWarnings("unchecked")
@@ -428,6 +461,72 @@
}
}
+ private void checkCors(HttpServletRequest req, HttpServletResponse res) {
+ String origin = req.getHeader(ORIGIN);
+ if (isRead(req)
+ && !Strings.isNullOrEmpty(origin)
+ && isOriginAllowed(origin)) {
+ res.addHeader(VARY, ORIGIN);
+ setCorsHeaders(res, origin);
+ }
+ }
+
+ private static boolean isCorsPreflight(HttpServletRequest req) {
+ return "OPTIONS".equals(req.getMethod())
+ && !Strings.isNullOrEmpty(req.getHeader(ORIGIN))
+ && !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD));
+ }
+
+ private void doCorsPreflight(HttpServletRequest req,
+ HttpServletResponse res) throws BadRequestException {
+ CacheHeaders.setNotCacheable(res);
+ res.setHeader(VARY, Joiner.on(", ").join(ImmutableList.of(
+ ORIGIN,
+ ACCESS_CONTROL_REQUEST_METHOD)));
+
+ String origin = req.getHeader(ORIGIN);
+ if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
+ throw new BadRequestException("CORS not allowed");
+ }
+
+ String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD);
+ if (!"GET".equals(method) && !"HEAD".equals(method)) {
+ throw new BadRequestException(method + " not allowed in CORS");
+ }
+
+ String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS);
+ if (headers != null) {
+ res.addHeader(VARY, ACCESS_CONTROL_REQUEST_HEADERS);
+ String badHeader = Iterables.getFirst(
+ Iterables.filter(
+ Splitter.on(',').trimResults().split(headers),
+ Predicates.not(Predicates.in(ALLOWED_CORS_REQUEST_HEADERS))),
+ null);
+ if (badHeader != null) {
+ throw new BadRequestException(badHeader + " not allowed in CORS");
+ }
+ }
+
+ res.setStatus(SC_OK);
+ setCorsHeaders(res, origin);
+ res.setContentType("text/plain");
+ res.setContentLength(0);
+ }
+
+ private void setCorsHeaders(HttpServletResponse res, String origin) {
+ res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+ res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+ res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS");
+ res.setHeader(
+ ACCESS_CONTROL_ALLOW_HEADERS,
+ Joiner.on(", ").join(ALLOWED_CORS_REQUEST_HEADERS));
+ }
+
+ private boolean isOriginAllowed(String origin) {
+ return globals.allowOrigin != null
+ && globals.allowOrigin.matcher(origin).matches();
+ }
+
private static String messageOr(Throwable t, String defaultMessage) {
if (!Strings.isNullOrEmpty(t.getMessage())) {
return t.getMessage();
@@ -438,7 +537,7 @@
@SuppressWarnings({"unchecked", "rawtypes"})
private static boolean notModified(HttpServletRequest req, RestResource rsrc,
RestView<RestResource> view) {
- if (!isGetOrHead(req)) {
+ if (!isRead(req)) {
return false;
}
@@ -469,7 +568,7 @@
private static <R extends RestResource> void configureCaching(
HttpServletRequest req, HttpServletResponse res, R rsrc,
RestView<R> view, CacheControl c) {
- if (isGetOrHead(req)) {
+ if (isRead(req)) {
switch (c.getType()) {
case NONE:
default:
@@ -972,25 +1071,20 @@
private void checkUserSession(HttpServletRequest req)
throws AuthException {
CurrentUser user = globals.currentUser.get();
- if (isStateChange(req)) {
- if (user instanceof AnonymousUser) {
- throw new AuthException("Authentication required");
- } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
- throw new AuthException("Invalid authentication method. In order to authenticate, "
- + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
- }
+ if (isRead(req)) {
+ user.setAccessPath(AccessPath.REST_API);
+ } else if (user instanceof AnonymousUser) {
+ throw new AuthException("Authentication required");
+ } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
+ throw new AuthException("Invalid authentication method. In order to authenticate, "
+ + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
}
- user.setAccessPath(AccessPath.REST_API);
}
- private static boolean isGetOrHead(HttpServletRequest req) {
+ private static boolean isRead(HttpServletRequest req) {
return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
}
- private static boolean isStateChange(HttpServletRequest req) {
- return !isGetOrHead(req);
- }
-
private void checkRequiresCapability(ViewData viewData) throws AuthException {
CapabilityUtils.checkRequiresCapability(globals.currentUser,
viewData.pluginName, viewData.view.getClass());
@@ -1029,7 +1123,7 @@
static long replyText(@Nullable HttpServletRequest req,
HttpServletResponse res, String text) throws IOException {
- if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) {
+ if ((req == null || isRead(req)) && isMaybeHTML(text)) {
return replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
}
if (!text.endsWith("\n")) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index c137d1e..77b364c 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -377,7 +377,7 @@
close();
throw new OrmRuntimeException(e);
} catch (ExecutionException e) {
- Throwables.throwIfUnchecked(e.getCause());
+ Throwables.propagateIfPossible(e.getCause());
throw new OrmRuntimeException(e.getCause());
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
index 1b663ae..0c2ec78 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -64,10 +64,15 @@
}
private static byte[] message(HttpConnection conn) {
- String msg = conn.getHttpChannel().getResponse().getReason();
- if (msg == null) {
- msg = HttpStatus.getMessage(conn.getHttpChannel()
- .getResponse().getStatus());
+ String msg;
+ if (conn == null) {
+ msg = "";
+ } else {
+ msg = conn.getHttpChannel().getResponse().getReason();
+ if (msg == null) {
+ msg = HttpStatus.getMessage(conn.getHttpChannel()
+ .getResponse().getStatus());
+ }
}
return msg.getBytes(ISO_8859_1);
}
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index 2c18ca6..c761703 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -28,24 +28,33 @@
'//gerrit-extension-api:api',
'//gerrit-gwtexpui:server',
'//gerrit-reviewdb:server',
- '//lib:args4j',
- '//lib:blame-cache',
- '//lib/dropwizard:dropwizard-core',
- '//lib:guava',
- '//lib:gwtorm',
- '//lib:jsch',
- '//lib:mime-util',
- '//lib:servlet-api-3_1',
- '//lib:velocity',
'//lib/commons:lang',
+ '//lib/dropwizard:dropwizard-core',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
- '//lib/jgit/org.eclipse.jgit:jgit',
+ '//lib/guice:javax-inject',
+ '//lib/guice:multibindings',
'//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
+ '//lib/jgit/org.eclipse.jgit:jgit',
'//lib/joda:joda-time',
'//lib/log:api',
'//lib/mina:sshd',
+ '//lib/ow2:ow2-asm',
+ '//lib/ow2:ow2-asm-analysis',
+ '//lib/ow2:ow2-asm-commons',
+ '//lib/ow2:ow2-asm-util',
+ '//lib:args4j',
+ '//lib:blame-cache',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:icu4j',
+ '//lib:jsch',
+ '//lib:mime-util',
+ '//lib:protobuf',
+ '//lib:servlet-api-3_1',
+ '//lib:soy',
+ '//lib:velocity',
],
visibility = ['//visibility:public'],
)
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 443d7c4..080b52b 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -204,7 +204,6 @@
'//lib:guava',
'//lib:guava-retrying',
'//lib:protobuf',
- '//lib/commons:validator',
'//lib/dropwizard:dropwizard-core',
'//lib/guice:guice-assistedinject',
'//lib/prolog:runtime',
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index 5a6b50f..a591fba 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -48,6 +48,7 @@
'//lib:pegdown',
'//lib:protobuf',
'//lib:servlet-api-3_1',
+ '//lib:soy',
'//lib:tukaani-xz',
'//lib:velocity',
'//lib/antlr:java_runtime',
@@ -188,6 +189,7 @@
['src/test/java/**/*.java'],
exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
),
+ resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']),
deps = TESTUTIL_DEPS + [
':testutil',
'//gerrit-antlr:query_exception',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 5a5d16c..5c0723a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -221,7 +221,7 @@
need.add(authorId);
}
- if (committerId != null && canSee(db, update.getNotes(), authorId)) {
+ if (committerId != null && canSee(db, update.getNotes(), committerId)) {
need.add(committerId);
}
need.remove(change.getOwner());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
index 89e9419..f84d399 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
@@ -94,7 +94,7 @@
directory.fillAccountInfo(
Iterables.concat(created.values(), provided), options);
} catch (DirectoryException e) {
- Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+ Throwables.propagateIfPossible(e.getCause(), OrmException.class);
throw new OrmException(e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
index 2924f97..46d6f11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AgreementJson;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -44,21 +45,18 @@
private static final Logger log =
LoggerFactory.getLogger(GetAgreements.class);
- private final Provider<IdentifiedUser> self;
+ private final Provider<CurrentUser> self;
private final ProjectCache projectCache;
- private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final AgreementJson agreementJson;
private final boolean agreementsEnabled;
@Inject
- GetAgreements(Provider<IdentifiedUser> self,
+ GetAgreements(Provider<CurrentUser> self,
ProjectCache projectCache,
- IdentifiedUser.GenericFactory identifiedUserFactory,
AgreementJson agreementJson,
@GerritServerConfig Config config) {
this.self = self;
this.projectCache = projectCache;
- this.identifiedUserFactory = identifiedUserFactory;
this.agreementJson = agreementJson;
this.agreementsEnabled =
config.getBoolean("auth", "contributorAgreements", false);
@@ -71,12 +69,14 @@
throw new MethodNotAllowedException("contributor agreements disabled");
}
- if (self.get() != resource.getUser()) {
+ if (!self.get().isIdentifiedUser()) {
throw new AuthException("not allowed to get contributor agreements");
}
- IdentifiedUser user =
- identifiedUserFactory.create(self.get().getAccountId());
+ IdentifiedUser user = self.get().asIdentifiedUser();
+ if (user != resource.getUser()) {
+ throw new AuthException("not allowed to get contributor agreements");
+ }
List<AgreementInfo> results = new ArrayList<>();
Collection<ContributorAgreement> cas =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
index e47ceb3..81c860e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDetail.java
@@ -47,7 +47,7 @@
directory.fillAccountInfo(Collections.singleton(info),
EnumSet.allOf(FillOptions.class));
} catch (DirectoryException e) {
- Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+ Throwables.propagateIfPossible(e.getCause(), OrmException.class);
throw new OrmException(e);
}
return info;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 213f90d..6b5e83c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -321,13 +321,9 @@
}
@Override
- public FileApi file(String path) throws RestApiException {
- try {
- return fileApi.create(files.parse(revision,
- IdString.fromDecoded(path)));
- } catch (IOException e) {
- throw new RestApiException("Cannot retrieve file", e);
- }
+ public FileApi file(String path) {
+ return fileApi.create(files.parse(revision,
+ IdString.fromDecoded(path)));
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 354dc62..3567811 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -154,8 +154,8 @@
}
});
} catch (PrivilegedActionException e) {
- Throwables.throwIfInstanceOf(e.getException(), NamingException.class);
- Throwables.throwIfInstanceOf(e.getException(), RuntimeException.class);
+ Throwables.propagateIfPossible(e.getException(), NamingException.class);
+ Throwables.propagateIfPossible(e.getException(), RuntimeException.class);
LdapRealm.log.warn("Internal error", e.getException());
return null;
} finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index adbcf22..c4bd68d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -28,6 +28,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
@@ -50,6 +51,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collection;
+
@Singleton
public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
UiAction<ChangeResource> {
@@ -91,6 +94,11 @@
return json.create(ChangeJson.NO_OPTIONS).format(change);
}
+ public Change abandon(ChangeControl control)
+ throws RestApiException, UpdateException {
+ return abandon(control, "", NotifyHandling.ALL);
+ }
+
public Change abandon(ChangeControl control, String msgTxt)
throws RestApiException, UpdateException {
return abandon(control, msgTxt, NotifyHandling.ALL);
@@ -98,31 +106,76 @@
public Change abandon(ChangeControl control, String msgTxt,
NotifyHandling notifyHandling) throws RestApiException, UpdateException {
- CurrentUser user = control.getUser();
- Account account = user.isIdentifiedUser()
- ? user.asIdentifiedUser().getAccount()
- : null;
- Op op = new Op(msgTxt, account, notifyHandling);
- try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
- control.getProject().getNameKey(), user, TimeUtil.nowTs())) {
+ Op op = new Op(control.getUser(), msgTxt, notifyHandling);
+ try (BatchUpdate u =
+ batchUpdateFactory.create(
+ dbProvider.get(),
+ control.getProject().getNameKey(),
+ control.getUser(),
+ TimeUtil.nowTs())) {
u.addOp(control.getId(), op).execute();
}
return op.change;
}
+ /**
+ * If an extension has more than one changes to abandon that belong to the
+ * same project, they should use the batch instead of abandoning one by one.
+ * <p>
+ * It's the caller's responsibility to ensure that all jobs inside the same
+ * batch have the matching project from its ChangeControl. Violations will
+ * result in a ResourceConflictException.
+ */
+ public void batchAbandon(Project.NameKey project, CurrentUser user,
+ Collection<ChangeControl> controls, String msgTxt,
+ NotifyHandling notifyHandling) throws RestApiException, UpdateException {
+ if (controls.isEmpty()) {
+ return;
+ }
+ try (BatchUpdate u = batchUpdateFactory.create(
+ dbProvider.get(), project, user, TimeUtil.nowTs())) {
+ for (ChangeControl control : controls) {
+ if (!project.equals(control.getProject().getNameKey())) {
+ throw new ResourceConflictException(
+ String.format(
+ "Project name \"%s\" doesn't match \"%s\"",
+ control.getProject().getNameKey().get(),
+ project.get()));
+ }
+ u.addOp(
+ control.getId(), new Op(control.getUser(), msgTxt, notifyHandling));
+ }
+ u.execute();
+ }
+ }
+
+ public void batchAbandon(Project.NameKey project, CurrentUser user,
+ Collection<ChangeControl> controls, String msgTxt)
+ throws RestApiException, UpdateException {
+ batchAbandon(project, user, controls, msgTxt, NotifyHandling.ALL);
+ }
+
+ public void batchAbandon(Project.NameKey project, CurrentUser user,
+ Collection<ChangeControl> controls)
+ throws RestApiException, UpdateException {
+ batchAbandon(project, user, controls, "", NotifyHandling.ALL);
+ }
+
private class Op extends BatchUpdate.Op {
- private final Account account;
private final String msgTxt;
+ private final NotifyHandling notifyHandling;
+ private final Account account;
private Change change;
private PatchSet patchSet;
private ChangeMessage message;
- private NotifyHandling notifyHandling;
- private Op(String msgTxt, Account account, NotifyHandling notifyHandling) {
- this.account = account;
+ private Op(CurrentUser user, String msgTxt, NotifyHandling notifyHandling) {
this.msgTxt = msgTxt;
this.notifyHandling = notifyHandling;
+ account = user.isIdentifiedUser()
+ ? user.asIdentifiedUser().getAccount()
+ : null;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
index f84599d..205d959 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.change;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.config.ChangeCleanupConfig;
import com.google.gerrit.server.project.ChangeControl;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -37,10 +40,10 @@
private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
private final ChangeCleanupConfig cfg;
- private final InternalUser.Factory internalUserFactory;
private final ChangeQueryProcessor queryProcessor;
private final ChangeQueryBuilder queryBuilder;
private final Abandon abandon;
+ private final InternalUser internalUser;
@Inject
AbandonUtil(
@@ -50,10 +53,10 @@
ChangeQueryBuilder queryBuilder,
Abandon abandon) {
this.cfg = cfg;
- this.internalUserFactory = internalUserFactory;
this.queryProcessor = queryProcessor;
this.queryBuilder = queryBuilder;
this.abandon = abandon;
+ internalUser = internalUserFactory.create();
}
public void abandonInactiveOpenChanges() {
@@ -68,29 +71,42 @@
if (!cfg.getAbandonIfMergeable()) {
query += " -is:mergeable";
}
- List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
- .query(queryBuilder.parse(query)).entities();
- int count = 0;
+
+ List<ChangeData> changesToAbandon =
+ queryProcessor
+ .enforceVisibility(false)
+ .query(queryBuilder.parse(query))
+ .entities();
+ ImmutableMultimap.Builder<Project.NameKey, ChangeControl> builder =
+ ImmutableMultimap.builder();
for (ChangeData cd : changesToAbandon) {
+ ChangeControl control = cd.changeControl(internalUser);
+ builder.put(control.getProject().getNameKey(), control);
+ }
+
+ int count = 0;
+ Multimap<Project.NameKey, ChangeControl> abandons = builder.build();
+ String message = cfg.getAbandonMessage();
+ for (Project.NameKey project : abandons.keySet()) {
+ Collection<ChangeControl> changes = abandons.get(project);
try {
- abandon.abandon(changeControl(cd), cfg.getAbandonMessage());
- count++;
- } catch (ResourceConflictException e) {
- // Change was already merged or abandoned.
+ abandon.batchAbandon(project, internalUser, changes, message);
+ count += changes.size();
} catch (Throwable e) {
- log.error(String.format(
- "Failed to auto-abandon inactive open change %d.",
- cd.getId().get()), e);
+ StringBuilder msg =
+ new StringBuilder("Failed to auto-abandon inactive change(s):");
+ for (ChangeControl change : changes) {
+ msg.append(" ").append(change.getId().get());
+ }
+ msg.append(".");
+ log.error(msg.toString(), e);
}
}
log.info(String.format("Auto-Abandoned %d of %d changes.",
count, changesToAbandon.size()));
} catch (QueryParseException | OrmException e) {
- log.error("Failed to query inactive open changes for auto-abandoning.", e);
+ log.error(
+ "Failed to query inactive open changes for auto-abandoning.", e);
}
}
-
- private ChangeControl changeControl(ChangeData cd) throws OrmException {
- return cd.changeControl(internalUserFactory.create());
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index ab92a97..a6c2edf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -267,7 +267,7 @@
} catch (PatchListNotAvailableException | GpgException | OrmException
| IOException | RuntimeException e) {
if (!has(CHECK)) {
- Throwables.throwIfInstanceOf(e, OrmException.class);
+ Throwables.propagateIfPossible(e, OrmException.class);
throw new OrmException(e);
}
return checkOnly(cd);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 545ea17..1a063f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -30,6 +31,7 @@
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -71,8 +73,14 @@
throw new AuthException("Cherry pick not permitted");
}
+ ProjectControl projectControl = control.getProjectControl();
+ Capable capable = projectControl.canPushToAtLeastOneRef();
+ if (capable != Capable.OK) {
+ throw new AuthException(capable.getMessage());
+ }
+
String refName = RefNames.fullName(input.destination);
- RefControl refControl = control.getProjectControl().controlForRef(refName);
+ RefControl refControl = projectControl.controlForRef(refName);
if (!refControl.canUpload()) {
throw new AuthException("Not allowed to cherry pick "
+ revision.getChange().getId().toString() + " to "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index f1bdba5..e25e2292 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -14,8 +14,9 @@
package com.google.gerrit.server.change;
+import static com.google.common.base.Preconditions.checkNotNull;
+
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -28,6 +29,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -44,6 +46,7 @@
import com.google.gerrit.server.mail.DeleteVoteSender;
import com.google.gerrit.server.mail.ReplyToChangeSender;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.LabelVote;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -137,64 +140,66 @@
PatchSet.Id psId = change.currentPatchSetId();
ps = psUtil.current(db.get(), ctl.getNotes());
- PatchSetApproval psa = null;
- StringBuilder msg = new StringBuilder();
-
- // get all of the current approvals
+ boolean found = false;
LabelTypes labelTypes = ctx.getControl().getLabelTypes();
- Map<String, Short> currentApprovals = new HashMap<>();
- for (LabelType lt : labelTypes.getLabelTypes()) {
- currentApprovals.put(lt.getName(), (short) 0);
- for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
- ctx.getDb(), ctl, psId, accountId)) {
- if (lt.getLabelId().equals(a.getLabelId())) {
- currentApprovals.put(lt.getName(), a.getValue());
- }
- }
- }
- // removing votes so we need to determine the new set of approval scores
- newApprovals.putAll(currentApprovals);
+
for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
- ctx.getDb(), ctl, psId, accountId)) {
- if (ctl.canRemoveReviewer(a)) {
- if (a.getLabel().equals(label)) {
- // set the approval to 0 if vote is being removed
- newApprovals.put(a.getLabel(), (short) 0);
- // set old value only if the vote changed
- oldApprovals.put(a.getLabel(), a.getValue());
- msg.append("Removed ")
- .append(a.getLabel()).append(formatLabelValue(a.getValue()))
- .append(" by ").append(userFactory.create(a.getAccountId())
- .getNameEmail())
- .append("\n");
- psa = a;
- a.setValue((short)0);
- ctx.getUpdate(psId).removeApprovalFor(a.getAccountId(), label);
- break;
- }
- } else {
+ ctx.getDb(), ctl, psId, accountId)) {
+ if (labelTypes.byLabel(a.getLabelId()) == null) {
+ continue; // Ignore undefined labels.
+ } else if (!a.getLabel().equals(label)) {
+ // Populate map for non-matching labels, needed by VoteDeleted.
+ newApprovals.put(a.getLabel(), a.getValue());
+ continue;
+ } else if (!ctl.canRemoveReviewer(a)) {
throw new AuthException("delete vote not permitted");
}
+ // Set the approval to 0 if vote is being removed.
+ newApprovals.put(a.getLabel(), (short) 0);
+ found = true;
+
+ // Set old value, as required by VoteDeleted.
+ oldApprovals.put(a.getLabel(), a.getValue());
+ break;
}
- if (psa == null) {
+ if (!found) {
throw new ResourceNotFoundException();
}
- ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
- if (msg.length() > 0) {
- changeMessage =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(),
- ctx.getWhen(),
- change.currentPatchSetId());
- changeMessage.setMessage(msg.toString());
- cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
- changeMessage);
- }
+ ctx.getUpdate(psId).removeApprovalFor(accountId, label);
+ ctx.getDb().patchSetApprovals().upsert(
+ Collections.singleton(deletedApproval(ctx)));
+
+ changeMessage =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(),
+ ChangeUtil.messageUUID(ctx.getDb())),
+ ctx.getAccountId(),
+ ctx.getWhen(),
+ change.currentPatchSetId());
+ StringBuilder msg = new StringBuilder();
+ msg.append("Removed ");
+ LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label)));
+ changeMessage.setMessage(
+ msg.append(" by ")
+ .append(userFactory.create(accountId).getNameEmail())
+ .append("\n")
+ .toString());
+ cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
+ changeMessage);
+
return true;
}
+ private PatchSetApproval deletedApproval(ChangeContext ctx) {
+ return new PatchSetApproval(
+ new PatchSetApproval.Key(
+ ps.getId(),
+ accountId,
+ new LabelId(label)),
+ (short) 0,
+ ctx.getWhen());
+ }
+
@Override
public void postUpdate(Context ctx) {
if (changeMessage == null) {
@@ -220,11 +225,4 @@
user.getAccount(), ctx.getWhen());
}
}
-
- private static String formatLabelValue(short value) {
- if (value > 0) {
- return "+" + value;
- }
- return Short.toString(value);
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 1631d48..35dbec1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -29,7 +29,6 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -49,7 +48,6 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -69,15 +67,11 @@
public class Files implements ChildCollection<RevisionResource, FileResource> {
private final DynamicMap<RestView<FileResource>> views;
private final Provider<ListFiles> list;
- private final GitRepositoryManager repoManager;
@Inject
- Files(DynamicMap<RestView<FileResource>> views,
- Provider<ListFiles> list,
- GitRepositoryManager repoManager) {
+ Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
this.views = views;
this.list = list;
- this.repoManager = repoManager;
}
@Override
@@ -91,20 +85,8 @@
}
@Override
- public FileResource parse(RevisionResource rev, IdString id)
- throws ResourceNotFoundException, IOException {
- if (Patch.COMMIT_MSG.equals(id.get())) {
- return new FileResource(rev, id.get());
- }
- try (Repository repo = repoManager.openRepository(rev.getProject());
- RevWalk rw = new RevWalk(repo)) {
- RevTree tree = rw.parseTree(
- ObjectId.fromString(rev.getPatchSet().getRevision().get()));
- if (TreeWalk.forPath(repo, id.get(), tree) != null) {
- return new FileResource(rev, id.get());
- }
- }
- throw new ResourceNotFoundException(id);
+ public FileResource parse(RevisionResource rev, IdString id) {
+ return new FileResource(rev, id.get());
}
public static final class ListFiles implements RestReadView<RevisionResource> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index ade46be..3ca496a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.Capable;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -48,6 +49,7 @@
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -122,6 +124,13 @@
throws IOException, OrmException, RestApiException,
UpdateException, NoSuchChangeException {
RefControl refControl = req.getControl().getRefControl();
+ ProjectControl projectControl = req.getControl().getProjectControl();
+
+ Capable capable = projectControl.canPushToAtLeastOneRef();
+ if (capable != Capable.OK) {
+ throw new AuthException(capable.getMessage());
+ }
+
Change change = req.getChange();
if (!refControl.canUpload()) {
throw new AuthException("revert not permitted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index eb01de3..090d99d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -464,7 +464,7 @@
throw new ResourceNotFoundException(e.getMessage(), e);
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
+ Throwables.propagateIfPossible(e);
throw new UpdateException(e);
}
}
@@ -669,7 +669,7 @@
logDebug("No objects to flush");
}
} catch (Exception e) {
- Throwables.throwIfInstanceOf(e, RestApiException.class);
+ Throwables.propagateIfPossible(e, RestApiException.class);
throw new UpdateException(e);
}
}
@@ -744,8 +744,8 @@
maybeLogSlowUpdate(startNanos, "NoteDb");
}
} catch (ExecutionException | InterruptedException e) {
- Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class);
- Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), UpdateException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), RestApiException.class);
throw new UpdateException(e);
} catch (OrmException | IOException e) {
throw new UpdateException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 2f73360..00cab38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -42,6 +42,7 @@
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
@@ -315,6 +316,8 @@
private final RequestId receiveId;
private MagicBranchInput magicBranch;
private boolean newChangeForAllNotInTarget;
+ private final ListMultimap<String, String> pushOptions =
+ LinkedListMultimap.create();
private List<CreateRequest> newChanges = Collections.emptyList();
private final Map<Change.Id, ReplaceRequest> replaceByChange =
@@ -490,6 +493,7 @@
advHooks.add(new HackPushNegotiateHook());
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
rp.setPostReceiveHook(lazyPostReceive.get());
+ rp.setAllowPushOptions(true);
}
public void init() {
@@ -915,6 +919,18 @@
}
private void parseCommands(Collection<ReceiveCommand> commands) {
+ List<String> optionList = rp.getPushOptions();
+ if (optionList != null) {
+ for (String option : optionList) {
+ int e = option.indexOf('=');
+ if (e > 0) {
+ pushOptions.put(option.substring(0, e), option.substring(e + 1));
+ } else {
+ pushOptions.put(option, "");
+ }
+ }
+ }
+
logDebug("Parsing {} commands", commands.size());
for (ReceiveCommand cmd : commands) {
if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -1305,14 +1321,14 @@
return new MailRecipients(reviewer, cc);
}
- String parse(CmdLineParser clp, Repository repo, Set<String> refs)
- throws CmdLineException {
+ String parse(CmdLineParser clp, Repository repo, Set<String> refs,
+ ListMultimap<String, String> pushOptions) throws CmdLineException {
String ref = RefNames.fullName(
MagicBranch.getDestBranchName(cmd.getRefName()));
+ ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
int optionStart = ref.indexOf('%');
if (0 < optionStart) {
- ListMultimap<String, String> options = LinkedListMultimap.create();
for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
int e = s.indexOf('=');
if (0 < e) {
@@ -1321,10 +1337,13 @@
options.put(s, "");
}
}
- clp.parseOptionMap(options);
ref = ref.substring(0, optionStart);
}
+ if (!options.isEmpty()) {
+ clp.parseOptionMap(options);
+ }
+
// Split the destination branch by branch and topic. The topic
// suffix is entirely optional, so it might not even exist.
String head = readHEAD(repo);
@@ -1347,6 +1366,19 @@
}
}
+ /**
+ * Gets an unmodifiable view of the pushOptions.
+ * <p>
+ * The collection is empty if the client does not support push options, or if
+ * the client did not send any options.
+ *
+ * @return an unmodifiable view of pushOptions.
+ */
+ @Nullable
+ public ListMultimap<String, String> getPushOptions() {
+ return ImmutableListMultimap.copyOf(pushOptions);
+ }
+
private void parseMagicBranch(ReceiveCommand cmd) {
// Permit exactly one new change request per push.
if (magicBranch != null) {
@@ -1362,8 +1394,10 @@
String ref;
CmdLineParser clp = optionParserFactory.create(magicBranch);
magicBranch.clp = clp;
+
try {
- ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
+ ref = magicBranch.parse(
+ clp, repo, rp.getAdvertisedRefs().keySet(), pushOptions);
} catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) {
logDebug("Invalid branch syntax");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 8272aaf..249db3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -29,12 +29,12 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Enums;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
-import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
@@ -46,6 +46,7 @@
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.metrics.Timer1;
@@ -85,7 +86,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
@@ -97,6 +97,20 @@
private static final RevId PARTIAL_PATCH_SET =
new RevId("INVALID PARTIAL PATCH SET");
+ @AutoValue
+ static abstract class ApprovalKey {
+ abstract PatchSet.Id psId();
+ abstract Account.Id accountId();
+ abstract String label();
+ @Nullable abstract String tag();
+
+ private static ApprovalKey create(PatchSet.Id psId, Account.Id accountId,
+ String label, @Nullable String tag) {
+ return new AutoValue_ChangeNotesParser_ApprovalKey(
+ psId, accountId, label, tag);
+ }
+ }
+
// Private final members initialized in the constructor.
private final ChangeNoteUtil noteUtil;
private final NoteDbMetrics metrics;
@@ -114,8 +128,7 @@
private final TreeMap<PatchSet.Id, PatchSet> patchSets;
private final Set<PatchSet.Id> deletedPatchSets;
private final Map<PatchSet.Id, PatchSetState> patchSetStates;
- private final Map<PatchSet.Id,
- Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals;
+ private final Map<ApprovalKey, PatchSetApproval> approvals;
private final List<ChangeMessage> allChangeMessages;
private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
@@ -142,7 +155,7 @@
this.walk = walk;
this.noteUtil = noteUtil;
this.metrics = metrics;
- approvals = new HashMap<>();
+ approvals = new LinkedHashMap<>();
reviewers = HashBasedTable.create();
allPastReviewers = new ArrayList<>();
reviewerUpdates = new ArrayList<>();
@@ -210,14 +223,15 @@
}
private Multimap<PatchSet.Id, PatchSetApproval> buildApprovals() {
- Multimap<PatchSet.Id, PatchSetApproval> result =
- ArrayListMultimap.create(approvals.keySet().size(), 3);
- for (Table<?, ?, Optional<PatchSetApproval>> curr : approvals.values()) {
- for (Optional<PatchSetApproval> psa : curr.values()) {
- if (psa.isPresent()) {
- result.put(psa.get().getPatchSetId(), psa.get());
- }
+ Multimap<PatchSet.Id, PatchSetApproval> result = ArrayListMultimap.create();
+ for (PatchSetApproval a : approvals.values()) {
+ if (patchSetStates.get(a.getPatchSetId()) == PatchSetState.DELETED) {
+ continue; // Patch set was explicitly deleted.
+ } else if (allPastReviewers.contains(a.getAccountId())
+ && !reviewers.containsRow(a.getAccountId())) {
+ continue; // Reviewer was explicitly removed.
}
+ result.put(a.getPatchSetId(), a);
}
for (Collection<PatchSetApproval> v : result.asMap().values()) {
Collections.sort((List<PatchSetApproval>) v, ChangeNotes.PSA_BY_TIME);
@@ -606,7 +620,7 @@
"patch set %s requires an identified user as uploader", psId.get());
}
if (line.startsWith("-")) {
- parseRemoveApproval(psId, accountId, line);
+ parseRemoveApproval(psId, accountId, ts, line);
} else {
parseAddApproval(psId, accountId, ts, line);
}
@@ -637,39 +651,37 @@
throw pe;
}
- Entry<String, String> label = Maps.immutableEntry(l.label(), tag);
- Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
- getApprovalsTableIfNoVotePresent(psId, accountId, label);
- if (curr != null) {
- PatchSetApproval psa = new PatchSetApproval(
- new PatchSetApproval.Key(
- psId,
- accountId,
- new LabelId(l.label())),
- l.value(),
- ts);
- psa.setTag(tag);
- curr.put(accountId, label, Optional.of(psa));
+ PatchSetApproval psa = new PatchSetApproval(
+ new PatchSetApproval.Key(
+ psId,
+ accountId,
+ new LabelId(l.label())),
+ l.value(),
+ ts);
+ psa.setTag(tag);
+ ApprovalKey k = ApprovalKey.create(psId, accountId, l.label(), tag);
+ if (!approvals.containsKey(k)) {
+ approvals.put(k, psa);
}
}
private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
- String line) throws ConfigInvalidException {
+ Timestamp ts, String line) throws ConfigInvalidException {
Account.Id accountId;
- Entry<String, String> label;
+ String label;
int s = line.indexOf(' ');
if (s > 0) {
- label = Maps.immutableEntry(line.substring(1, s), tag);
+ label = line.substring(1, s);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
accountId = noteUtil.parseIdent(ident, id);
} else {
- label = Maps.immutableEntry(line.substring(1), tag);
+ label = line.substring(1);
accountId = committerId;
}
try {
- LabelType.checkNameInternal(label.getKey());
+ LabelType.checkNameInternal(label);
} catch (IllegalArgumentException e) {
ConfigInvalidException pe =
parseException("invalid %s: %s", FOOTER_LABEL, line);
@@ -677,38 +689,25 @@
throw pe;
}
- Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
- getApprovalsTableIfNoVotePresent(psId, accountId, label);
- if (curr != null) {
- curr.put(accountId, label, Optional.<PatchSetApproval> absent());
+ // Store an actual 0-vote approval in the map for a removed approval, for
+ // several reasons:
+ // - This is closer to the ReviewDb representation, which leads to less
+ // confusion and special-casing of NoteDb.
+ // - More importantly, ApprovalCopier needs an actual approval in order to
+ // block copying an earlier approval over a later delete.
+ PatchSetApproval remove = new PatchSetApproval(
+ new PatchSetApproval.Key(
+ psId,
+ accountId,
+ new LabelId(label)),
+ (short) 0,
+ ts);
+ ApprovalKey k = ApprovalKey.create(psId, accountId, label, tag);
+ if (!approvals.containsKey(k)) {
+ approvals.put(k, remove);
}
}
- private Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>
- getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId,
- Entry<String, String> label) {
-
- Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
- approvals.get(psId);
- if (curr != null) {
- if (curr.contains(accountId, label)) {
- return null;
- }
- } else {
- curr = Tables.newCustomTable(
- Maps.<Account.Id, Map<Entry<String, String>, Optional<PatchSetApproval>>>
- newHashMapWithExpectedSize(2),
- new Supplier<Map<Entry<String, String>, Optional<PatchSetApproval>>>() {
- @Override
- public Map<Entry<String, String>, Optional<PatchSetApproval>> get() {
- return new LinkedHashMap<>();
- }
- });
- approvals.put(psId, curr);
- }
- return curr;
- }
-
private void parseSubmitRecords(List<String> lines)
throws ConfigInvalidException {
SubmitRecord rec = null;
@@ -787,9 +786,6 @@
Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next();
if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
rit.remove();
- for (Table<Account.Id, ?, ?> curr : approvals.values()) {
- curr.rowKeySet().remove(e.getRowKey());
- }
}
}
}
@@ -832,13 +828,14 @@
// Post-process other collections to remove items corresponding to deleted
// patch sets. This is safer than trying to prevent insertion, as it will
// also filter out items racily added after the patch set was deleted.
+ //
+ // Approvals are filtered in buildApprovals().
NavigableSet<PatchSet.Id> all = patchSets.navigableKeySet();
if (!all.isEmpty()) {
currentPatchSetId = all.last();
} else {
currentPatchSetId = null;
}
- approvals.keySet().retainAll(all);
changeMessagesByPatchSet.keys().retainAll(all);
for (Iterator<ChangeMessage> it = allChangeMessages.iterator();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
index c47fd4f..071e12c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -197,7 +197,7 @@
limit = counter + count;
acquireCount++;
} catch (ExecutionException | RetryException e) {
- Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), OrmException.class);
throw new OrmException(e);
} catch (IOException e) {
throw new OrmException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index ae37c01..dd15cfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -93,7 +93,7 @@
} catch (ExecutionException e) {
// If there was an error computing the result, carry it
// up to the caller so the cache knows this key is invalid.
- Throwables.throwIfInstanceOf(e.getCause(), Exception.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), Exception.class);
throw new Exception(e.getMessage(), e.getCause());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 1156b91..faeaaf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -260,7 +260,7 @@
} catch (ExecutionException e) {
// If there was an error computing the result, carry it
// up to the caller so the cache knows this key is invalid.
- Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
throw new IOException(e.getMessage(), e.getCause());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 2097ebd..d27d4f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -148,7 +148,7 @@
} catch (ExecutionException e) {
if (!(e.getCause() instanceof RepositoryNotFoundException)) {
log.warn(String.format("Cannot read project %s", projectName.get()), e);
- Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
throw new IOException(e);
}
return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 25f80ae..22e5d69 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -384,7 +384,7 @@
}
final StringBuilder msg = new StringBuilder();
- msg.append(" A Contributor Agreement must be completed before uploading");
+ msg.append("A Contributor Agreement must be completed before uploading");
if (canonicalWebUrl != null) {
msg.append(":\n\n ");
msg.append(canonicalWebUrl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
index 8c850fb..ed50a54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPattern.java
@@ -49,7 +49,7 @@
try {
return exampleCache.get(refPattern);
} catch (ExecutionException e) {
- Throwables.throwIfUnchecked(e.getCause());
+ Throwables.propagateIfPossible(e.getCause());
throw new RuntimeException(e);
}
} else if (refPattern.endsWith("/*")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
index c2b8b03..168be5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndSource.java
@@ -84,7 +84,7 @@
try {
return readImpl();
} catch (OrmRuntimeException err) {
- Throwables.throwIfInstanceOf(err.getCause(), OrmException.class);
+ Throwables.propagateIfInstanceOf(err.getCause(), OrmException.class);
throw new OrmException(err);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index 70bdffb..d08f05c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -140,7 +140,7 @@
} catch (OrmRuntimeException e) {
throw new OrmException(e.getMessage(), e);
} catch (OrmException e) {
- Throwables.throwIfInstanceOf(e.getCause(), QueryParseException.class);
+ Throwables.propagateIfInstanceOf(e.getCause(), QueryParseException.class);
throw e;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
index fab0b34..030383a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
@@ -58,6 +58,16 @@
Short.parseShort(text.substring(e + 1), text.length()));
}
+ public static StringBuilder appendTo(StringBuilder sb, String label,
+ short value) {
+ if (value == (short) 0) {
+ return sb.append('-').append(label);
+ } else if (value < 0) {
+ return sb.append(label).append(value);
+ }
+ return sb.append(label).append('+').append(value);
+ }
+
public static LabelVote create(String label, short value) {
return new AutoValue_LabelVote(LabelType.checkNameInternal(label), value);
}
@@ -70,13 +80,9 @@
public abstract short value();
public String format() {
- if (value() == (short) 0) {
- return '-' + label();
- } else if (value() < 0) {
- return label() + value();
- } else {
- return label() + '+' + value();
- }
+ // Max short string length is "-32768".length() == 6.
+ return appendTo(new StringBuilder(label().length() + 6), label(), value())
+ .toString();
}
public String formatWithEquals() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index bdbb938..382485e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -129,7 +129,7 @@
try {
wrapped.call();
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
+ Throwables.propagateIfPossible(e);
throw new RuntimeException(e); // Not possible.
}
}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index d51547c..5a937b6 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -97,7 +97,7 @@
in = text/x-properties
ini = text/x-properties
intr = text/x-dylan
-jade = text/x-jade
+jade = text/x-pug
java = text/x-java
jl = text/x-julia
jruby = text/x-ruby
@@ -163,6 +163,7 @@
ps1 = application/x-powershell
psd1 = application/x-powershell
psm1 = application/x-powershell
+pug = text/x-pug
py = text/x-python
pyw = text/x-python
pyx = text/x-cython
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 0173b05..a29d397 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -322,7 +322,10 @@
update.commit();
notes = newNotes(c);
- assertThat(notes.getApprovals()).isEmpty();
+ assertThat(notes.getApprovals()).containsExactlyEntriesIn(
+ ImmutableMultimap.of(
+ psa.getPatchSetId(),
+ new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
}
@Test
@@ -344,7 +347,10 @@
update.commit();
notes = newNotes(c);
- assertThat(notes.getApprovals()).isEmpty();
+ assertThat(notes.getApprovals()).containsExactlyEntriesIn(
+ ImmutableMultimap.of(
+ psa.getPatchSetId(),
+ new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
// Add back approval on same label.
update = newUpdate(c, otherUser);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index 4ddca0c..fde3a66 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -97,7 +97,7 @@
try {
cmd.destroy();
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
+ Throwables.propagateIfPossible(e);
throw new RuntimeException(e);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index f3243c6..f2911dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -136,7 +136,7 @@
try {
cmd.destroy();
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
+ Throwables.propagateIfPossible(e);
throw new RuntimeException(e);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index c88a02c..24bd8c2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -153,7 +153,7 @@
try {
cmd.destroy();
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
+ Throwables.propagateIfPossible(e);
throw new RuntimeException(e);
}
}
diff --git a/lib/BUCK b/lib/BUCK
index efdf0eb..4534b44 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -67,10 +67,9 @@
maven_jar(
name = 'guava',
- id = 'com.google.guava:guava:20.0:20160818.201422-323',
- sha1 = '13af7470db1026c57aedd0144018e06fe79bba33',
+ id = 'com.google.guava:guava:19.0',
+ sha1 = '6ce200f6b23222af3d8abb6b6459e6c44f4bb0e9',
license = 'Apache2.0',
- repository = MAVEN_SNAPSHOT,
)
maven_jar(
diff --git a/lib/BUILD b/lib/BUILD
index e89e63c..a490038 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -151,6 +151,7 @@
visibility = ['//visibility:public'],
)
+
java_library(
name = 'h2',
exports = ['@h2//jar'],
@@ -202,3 +203,31 @@
exports = ['@derby//jar'],
visibility = ['//visibility:public'],
)
+
+java_library(
+ name = 'soy',
+ exports = ['@soy//jar'],
+ runtime_deps = [
+ ':args4j',
+ ':guava',
+ ':gson',
+ ':icu4j',
+ ':jsr305',
+ ':protobuf',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:multibindings',
+ '//lib/guice:javax-inject',
+ '//lib/ow2:ow2-asm',
+ '//lib/ow2:ow2-asm-analysis',
+ '//lib/ow2:ow2-asm-commons',
+ '//lib/ow2:ow2-asm-util',
+ ],
+ visibility = ['//visibility:public'],
+)
+
+java_library(
+ name = 'icu4j',
+ exports = [ '@icu4j//jar' ],
+ visibility = ['//visibility:public'],
+)
diff --git a/lib/JGIT_VERSION b/lib/JGIT_VERSION
index 6457323..5964a28 100644
--- a/lib/JGIT_VERSION
+++ b/lib/JGIT_VERSION
@@ -1,4 +1,4 @@
include_defs('//lib/maven.defs')
REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
-VERS = '4.4.1.201607150455-r.118-g1096652'
+VERS = '4.4.1.201607150455-r.137-gdd2a5a7'
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index a0e0e9a..56145ea 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
include_defs('//lib/maven.defs')
include_defs('//lib/codemirror/cm.defs')
-VERSION = '5.17.0'
+VERSION = '5.18.2'
TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
maven_jar(
name = 'codemirror-minified',
id = 'org.webjars.npm:codemirror-minified:' + VERSION,
- sha1 = '05ad901fc9be67eb7ba8997d896488093deb898e',
+ sha1 = '6755af157a7eaf2401468906bef67bbacc3c97f6',
attach_source = False,
license = 'codemirror-minified',
visibility = [],
@@ -17,7 +17,7 @@
maven_jar(
name = 'codemirror-original',
id = 'org.webjars.npm:codemirror:' + VERSION,
- sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85',
+ sha1 = '18c721ae88eed27cddb458c42f5d221fa3d9713e',
attach_source = False,
license = 'codemirror-original',
visibility = [],
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index baf2ce5..a1be90f 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -132,7 +132,6 @@
'htmlmixed',
'http',
'idl',
- 'jade',
'javascript',
'jinja2',
'jsx',
@@ -160,6 +159,7 @@
'powershell',
'properties',
'protobuf',
+ 'pug',
'puppet',
'python',
'q',
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index acade50..5850af2 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -3,6 +3,7 @@
exports = [
':guice_library',
':javax-inject',
+ ':multibindings',
],
visibility = ['//visibility:public'],
)
@@ -36,4 +37,11 @@
java_library(
name = 'javax-inject',
exports = ['@javax_inject//jar'],
+ visibility = ['//visibility:public'],
+)
+
+java_library(
+ name = 'multibindings',
+ exports = [ '@multibindings//jar' ],
+ visibility = ['//visibility:public'],
)
diff --git a/lib/jgit/org.eclipse.jgit.archive/BUCK b/lib/jgit/org.eclipse.jgit.archive/BUCK
index 384a5e0..29ba7ce 100644
--- a/lib/jgit/org.eclipse.jgit.archive/BUCK
+++ b/lib/jgit/org.eclipse.jgit.archive/BUCK
@@ -4,7 +4,7 @@
maven_jar(
name = 'jgit-archive',
id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
- sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a',
+ sha1 = '15cbe6b7e2b10ab94ffd7fa2091a3ed1b56f8c3f',
license = 'jgit',
repository = REPO,
deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.http.server/BUCK b/lib/jgit/org.eclipse.jgit.http.server/BUCK
index 2ade9ff..cbbb0e2 100644
--- a/lib/jgit/org.eclipse.jgit.http.server/BUCK
+++ b/lib/jgit/org.eclipse.jgit.http.server/BUCK
@@ -4,7 +4,7 @@
maven_jar(
name = 'jgit-servlet',
id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
- sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756',
+ sha1 = 'e6930052a609e7c61782bd46754765e7845fc3ee',
license = 'jgit',
repository = REPO,
deps = ['//lib/jgit/org.eclipse.jgit:jgit'],
diff --git a/lib/jgit/org.eclipse.jgit.junit/BUCK b/lib/jgit/org.eclipse.jgit.junit/BUCK
index a31ee6f..ed77543 100644
--- a/lib/jgit/org.eclipse.jgit.junit/BUCK
+++ b/lib/jgit/org.eclipse.jgit.junit/BUCK
@@ -4,7 +4,7 @@
maven_jar(
name = 'junit',
id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
- sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f',
+ sha1 = 'fc8e7ec3b61f8bde33d554c43beffbe47953b2c2',
license = 'DO_NOT_DISTRIBUTE',
repository = REPO,
unsign = True,
diff --git a/lib/jgit/org.eclipse.jgit/BUCK b/lib/jgit/org.eclipse.jgit/BUCK
index 7c06726..386ea04 100644
--- a/lib/jgit/org.eclipse.jgit/BUCK
+++ b/lib/jgit/org.eclipse.jgit/BUCK
@@ -4,8 +4,8 @@
maven_jar(
name = 'jgit',
id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
- bin_sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018',
- src_sha1 = '3e65e476bfb4a529e18752ffcd27b566e7ee7241',
+ bin_sha1 = '2c4482429f2c5064375cd1634023d0a7d65961a9',
+ src_sha1 = '3f1a513a2d8a17cc2ef7fe7105cd6c040ab06a8e',
license = 'jgit',
repository = REPO,
unsign = True,
diff --git a/plugins/replication b/plugins/replication
index 75af773..5cac325 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 75af77375b34133e85f3ee5f1b19dac19d3f3837
+Subproject commit 5cac325cca171205130c53df8b3ee9ab3b115979
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 1fe09e5..09d5b84 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -96,13 +96,12 @@
hidden></gr-confirm-rebase-dialog>
<gr-confirm-cherrypick-dialog id="confirmCherrypick"
class="confirmDialog"
- commit-info="[[commitInfo]]"
+ message="[[commitMessage]]"
on-confirm="_handleCherrypickConfirm"
on-cancel="_handleConfirmDialogCancel"
hidden></gr-confirm-cherrypick-dialog>
<gr-confirm-revert-dialog id="confirmRevertDialog"
class="confirmDialog"
- commit-info="[[commitInfo]]"
on-confirm="_handleRevertDialogConfirm"
on-cancel="_handleConfirmDialogCancel"
hidden></gr-confirm-revert-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 3445f4e..8deab6b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -75,7 +75,10 @@
},
changeNum: String,
patchNum: String,
- commitInfo: Object,
+ commitMessage: {
+ type: String,
+ value: '',
+ },
_loading: {
type: Boolean,
@@ -274,7 +277,7 @@
if (type === ActionType.REVISION) {
this._handleRevisionAction(key);
} else if (key === ChangeActions.REVERT) {
- this.$.confirmRevertDialog.populateRevertMessage();
+ this.$.confirmRevertDialog.populateRevertMessage(this.commitMessage);
this.$.confirmRevertDialog.message = this._modifyRevertMsg();
this._showActionDialog(this.$.confirmRevertDialog);
} else if (key === ChangeActions.ABANDON) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 8b51312..1782061 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -128,18 +128,6 @@
<span class="value">[[change.branch]]</span>
</section>
<section>
- <span class="title">Commit</span>
- <span class="value">
- <template is="dom-if" if="[[_showWebLink]]">
- <a target="_blank"
- href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
- </template>
- <template is="dom-if" if="[[!_showWebLink]]">
- [[_computeShortHash(commitInfo)]]
- </template>
- </span>
- </section>
- <section>
<span class="title">Topic</span>
<span class="value">
<gr-editable-label
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index af19703..20c117e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -27,17 +27,8 @@
properties: {
change: Object,
- commitInfo: Object,
mutable: Boolean,
serverConfig: Object,
- _showWebLink: {
- type: Boolean,
- computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
- },
- _webLink: {
- type: String,
- computed: '_computeWebLink(change, commitInfo, serverConfig)',
- },
_topicReadOnly: {
type: Boolean,
computed: '_computeTopicReadOnly(mutable, change)',
@@ -52,38 +43,6 @@
Gerrit.RESTClientBehavior,
],
- _computeShowWebLink: function(change, commitInfo, serverConfig) {
- var webLink = commitInfo.web_links && commitInfo.web_links.length;
- var gitWeb = serverConfig.gitweb && serverConfig.gitweb.url &&
- serverConfig.gitweb.type && serverConfig.gitweb.type.revision;
- return webLink || gitWeb;
- },
-
- _computeWebLink: function(change, commitInfo, serverConfig) {
- if (!this._computeShowWebLink(change, commitInfo, serverConfig)) {
- return;
- }
-
- if (serverConfig.gitweb && serverConfig.gitweb.url &&
- serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
- return serverConfig.gitweb.url +
- serverConfig.gitweb.type.revision
- .replace('${project}', change.project)
- .replace('${commit}', commitInfo.commit);
- }
-
- var webLink = commitInfo.web_links[0].url;
- if (!/^https?\:\/\//.test(webLink)) {
- webLink = '../../' + webLink;
- }
-
- return webLink;
- },
-
- _computeShortHash: function(commitInfo) {
- return commitInfo.commit.slice(0, 7);
- },
-
_computeHideStrategy: function(change) {
return !this.changeIsOpen(change.status);
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 01f0649..a2d4946 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -20,11 +20,9 @@
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-metadata.html">
-<script src="../../../scripts/util.js"></script>
<test-fixture id="basic">
<template>
@@ -68,79 +66,6 @@
assert.isTrue(element.$$('.strategy').hasAttribute('hidden'));
});
- test('no web link when unavailable', function() {
- element.commitInfo = {};
- element.serverConfig = {};
- element.change = {labels: []};
-
- assert.isNotOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- });
-
- test('use web link when available', function() {
- element.commitInfo = {web_links: [{url: 'link-url'}]};
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), '../../link-url');
- });
-
- test('does not relativize web links that begin with scheme', function() {
- element.commitInfo = {web_links: [{url: 'https://link-url'}]};
- element.serverConfig = {};
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'https://link-url');
- });
-
- test('use gitweb when available', function() {
- element.commitInfo = {commit: 'commit-sha'};
- element.serverConfig = {gitweb: {
- url: 'url-base/',
- type: {revision: 'xx ${project} xx ${commit} xx'},
- }};
- element.change = {
- project: 'project-name',
- labels: [],
- current_revision: element.commitInfo.commit
- };
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
-
- assert.equal(element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig), 'url-base/xx project-name xx commit-sha xx');
- });
-
- test('prefer gitweb when both are available', function() {
- element.commitInfo = {
- commit: 'commit-sha',
- web_links: [{url: 'link-url'}]
- };
- element.serverConfig = {gitweb: {
- url: 'url-base/',
- type: {revision: 'xx ${project} xx ${commit} xx'},
- }};
- element.change = {
- project: 'project-name',
- labels: [],
- current_revision: element.commitInfo.commit
- };
-
- assert.isOk(element._computeShowWebLink(element.change,
- element.commitInfo, element.serverConfig));
-
- var link = element._computeWebLink(element.change, element.commitInfo,
- element.serverConfig);
-
- assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
- assert.notEqual(link, '../../link-url');
- });
-
test('show CC section when NoteDb enabled', function() {
function hasCc() {
return element._showReviewersByState;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index cad4298..61b10e8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -29,6 +29,7 @@
<link rel="import" href="../gr-change-actions/gr-change-actions.html">
<link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
+<link rel="import" href="../gr-commit-info/gr-commit-info.html">
<link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
<link rel="import" href="../gr-file-list/gr-file-list.html">
<link rel="import" href="../gr-messages-list/gr-messages-list.html">
@@ -136,6 +137,12 @@
border: 1px solid #ddd;
margin: 1em var(--default-horizontal-margin);
}
+ .patchInfo--oldPatchSet .patchInfo-header {
+ background-color: #fff9c4;
+ }
+ .patchInfo--oldPatchSet .latestPatchContainer {
+ display: initial;
+ }
.patchInfo-header,
gr-file-list {
padding: .5em calc(var(--default-horizontal-margin) / 2);
@@ -143,6 +150,11 @@
.patchInfo-header {
background-color: #f6f6f6;
border-bottom: 1px solid #ebebeb;
+ display: flex;
+ justify-content: space-between;
+ }
+ .latestPatchContainer {
+ display: none;
}
@media screen and (max-width: 50em) {
.header {
@@ -210,7 +222,6 @@
<div class="changeInfo-column changeMetadata">
<gr-change-metadata
change="{{_change}}"
- commit-info="[[_commitInfo]]"
server-config="[[serverConfig]]"
mutable="[[_loggedIn]]"
on-show-reply-dialog="_handleShowReplyDialog">
@@ -226,8 +237,8 @@
change="[[_change]]"
actions="[[_change.actions]]"
change-num="[[_changeNum]]"
- patch-num="[[_patchRange.patchNum]]"
- commit-info="[[_commitInfo]]"
+ patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+ commit-message="[[_latestCommitMessage]]"
on-reload-change="_handleReloadChange"></gr-change-actions>
</div>
<div class="commitAndRelated">
@@ -252,12 +263,12 @@
</div>
</div>
</section>
- <section class="patchInfo">
+ <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum, _allPatchSets)]]">
<div class="patchInfo-header">
- <span>
+ <div>
<label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
<select id="patchSetSelect" on-change="_handlePatchChange">
- <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
+ <template is="dom-repeat" items="[[_allPatchSets]]" as="patchNumber">
<option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
<span>[[patchNumber]]</span>
/
@@ -265,13 +276,21 @@
</option>
</template>
</select>
- </span>
- <span class="downloadContainer">
- /
- <gr-button link
- class="download"
- on-tap="_handleDownloadTap">Download</gr-button>
- </span>
+ <span class="downloadContainer">
+ /
+ <gr-button link
+ class="download"
+ on-tap="_handleDownloadTap">Download</gr-button>
+ </span>
+ <span class="latestPatchContainer">
+ /
+ <a href$="/c/[[_change._number]]">Go to latest patch set</a>
+ </span>
+ </div>
+ <gr-commit-info
+ change="[[_change]]"
+ server-config="[[serverConfig]]"
+ commit-info="[[_commitInfo]]"></gr-commit-info>
</div>
<gr-file-list id="fileList"
change="[[_change]]"
@@ -306,8 +325,7 @@
with-backdrop>
<gr-reply-dialog id="replyDialog"
change="[[_change]]"
- patch-num="[[_patchRange.patchNum]]"
- revisions="[[_change.revisions]]"
+ patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
labels="[[_change.labels]]"
permitted-labels="[[_change.permitted_labels]]"
diff-drafts="[[_diffDrafts]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 726e7e5..b6c18e5 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
@@ -334,6 +334,12 @@
_resetFileListViewState: function() {
this.set('viewState.selectedFileIndex', 0);
+ if (!!this.viewState.changeNum &&
+ this.viewState.changeNum !== this._changeNum) {
+ // Reset the diff mode to null when navigating from one change to
+ // another, so that the user's preference is restored.
+ this.set('viewState.diffMode', null);
+ }
this.set('viewState.changeNum', this._changeNum);
this.set('viewState.patchRange', this._patchRange);
},
@@ -390,6 +396,14 @@
return allPatchSets[allPatchSets.length - 1];
},
+ _computePatchInfoClass: function(patchNum, allPatchSets) {
+ if (parseInt(patchNum, 10) ===
+ this._computeLatestPatchNum(allPatchSets)) {
+ return '';
+ }
+ return 'patchInfo--oldPatchSet';
+ },
+
_computeAllPatchSets: function(change) {
var patchNums = [];
for (var rev in change.revisions) {
@@ -587,7 +601,6 @@
var reloadPatchNumDependentResources = function() {
return Promise.all([
this._getCommitInfo(),
- this.$.actions.reload(),
this.$.fileList.reload(),
]);
}.bind(this);
@@ -596,6 +609,7 @@
return Promise.all([
this._getLatestCommitMessage(),
+ this.$.actions.reload(),
this.$.relatedChanges.reload(),
this._getProjectConfig(),
]);
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 7f9fb02..2cdeab4 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
@@ -165,6 +165,36 @@
assert.deepEqual(element._diffDrafts, {});
});
+ test('change num change', function() {
+ element._changeNum = null;
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ labels: {},
+ };
+ element.viewState.changeNum = null;
+ element.viewState.diffMode = 'UNIFIED';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+
+ element._changeNum = '1';
+ element.params = {changeNum: '1'};
+ element._change.newProp = '1';
+ flushAsynchronousOperations();
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ assert.equal(element.viewState.changeNum, '1');
+
+ element._changeNum = '2';
+ element.params = {changeNum: '2'};
+ element._change.newProp = '2';
+ flushAsynchronousOperations();
+ assert.isNull(element.viewState.diffMode);
+ assert.equal(element.viewState.changeNum, '2');
+ });
+
test('patch num change', function(done) {
element._changeNum = '42';
element._patchRange = {
@@ -183,7 +213,9 @@
status: 'NEW',
labels: {},
};
+ element.viewState.diffMode = 'UNIFIED';
flushAsynchronousOperations();
+
var selectEl = element.$$('.patchInfo-header select');
assert.ok(selectEl);
var optionEls = Polymer.dom(element.root).querySelectorAll(
@@ -201,6 +233,7 @@
var numEvents = 0;
selectEl.addEventListener('change', function(e) {
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
numEvents++;
if (numEvents == 1) {
assert(showStub.lastCall.calledWithExactly('/c/42/1'),
@@ -364,5 +397,14 @@
assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS),
'_openReplyDialog should have been passed CCS');
});
+
+ test('class is applied to file list on old patch set', function() {
+ var allPatcheSets = [1, 2, 4];
+ assert.equal(element._computePatchInfoClass('1', allPatcheSets),
+ 'patchInfo--oldPatchSet');
+ assert.equal(element._computePatchInfoClass('2', allPatcheSets),
+ 'patchInfo--oldPatchSet');
+ assert.equal(element._computePatchInfoClass('4', allPatcheSets), '');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
new file mode 100644
index 0000000..5cd65fa
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -0,0 +1,35 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-commit-info">
+ <template>
+ <style>
+ :host {
+ display: inline-block;
+ }
+ </style>
+ <template is="dom-if" if="[[_showWebLink]]">
+ <a target="_blank"
+ href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+ </template>
+ <template is="dom-if" if="[[!_showWebLink]]">
+ [[_computeShortHash(commitInfo)]]
+ </template>
+ </template>
+ <script src="gr-commit-info.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
new file mode 100644
index 0000000..5aa8601
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -0,0 +1,98 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-commit-info',
+
+ properties: {
+ change: Object,
+ commitInfo: Object,
+ serverConfig: Object,
+ _showWebLink: {
+ type: Boolean,
+ computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
+ },
+ _webLink: {
+ type: String,
+ computed: '_computeWebLink(change, commitInfo, serverConfig)',
+ },
+ },
+
+ _isWebLink: function(link) {
+ // This is a whitelist of web link types that provide direct links to
+ // the commit in the url property.
+ return link.name === 'gitiles' || link.name === 'gitweb';
+ },
+
+ _computeShowWebLink: function(change, commitInfo, serverConfig) {
+ if (serverConfig.gitweb && serverConfig.gitweb.url &&
+ serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
+ return true;
+ }
+
+ if (!commitInfo.web_links) {
+ return false;
+ }
+
+ for (var i = 0; i < commitInfo.web_links.length; i++) {
+ if (this._isWebLink(commitInfo.web_links[i])) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ _computeWebLink: function(change, commitInfo, serverConfig) {
+ if (!this._computeShowWebLink(change, commitInfo, serverConfig)) {
+ return;
+ }
+
+ if (serverConfig.gitweb && serverConfig.gitweb.url &&
+ serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
+ return serverConfig.gitweb.url +
+ serverConfig.gitweb.type.revision
+ .replace('${project}', change.project)
+ .replace('${commit}', commitInfo.commit);
+ }
+
+ var webLink = null;
+ for (var i = 0; i < commitInfo.web_links.length; i++) {
+ if (this._isWebLink(commitInfo.web_links[i])) {
+ webLink = commitInfo.web_links[i].url;
+ break;
+ }
+ }
+
+ if (!webLink) {
+ return;
+ }
+
+ if (!/^https?\:\/\//.test(webLink)) {
+ webLink = '../../' + webLink;
+ }
+
+ return webLink;
+ },
+
+ _computeShortHash: function(commitInfo) {
+ if (!commitInfo || !commitInfo.commit) {
+ return;
+ }
+ return commitInfo.commit.slice(0, 7);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
new file mode 100644
index 0000000..36b1628
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-commit-info</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-commit-info.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-commit-info></gr-commit-info>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-commit-info tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('no web link when unavailable', function() {
+ element.commitInfo = {};
+ element.serverConfig = {};
+ element.change = {labels: []};
+
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ });
+
+ test('use web link when available', function() {
+ element.commitInfo = {web_links: [{name: 'gitweb', url: 'link-url'}]};
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), '../../link-url');
+ });
+
+ test('does not relativize web links that begin with scheme', function() {
+ element.commitInfo = {
+ web_links: [{name: 'gitweb', url: 'https://link-url'}]
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+ });
+
+ test('use gitweb when available', function() {
+ element.commitInfo = {commit: 'commit-sha'};
+ element.serverConfig = {gitweb: {
+ url: 'url-base/',
+ type: {revision: 'xx ${project} xx ${commit} xx'},
+ }};
+ element.change = {
+ project: 'project-name',
+ labels: [],
+ current_revision: element.commitInfo.commit
+ };
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'url-base/xx project-name xx commit-sha xx');
+ });
+
+ test('prefer gitweb when both are available', function() {
+ element.commitInfo = {
+ commit: 'commit-sha',
+ web_links: [{url: 'link-url'}]
+ };
+ element.serverConfig = {gitweb: {
+ url: 'url-base/',
+ type: {revision: 'xx ${project} xx ${commit} xx'},
+ }};
+ element.change = {
+ project: 'project-name',
+ labels: [],
+ current_revision: element.commitInfo.commit
+ };
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+
+ var link = element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig);
+
+ assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
+ assert.notEqual(link, '../../link-url');
+ });
+
+ test('ignore web links that are neither gitweb nor gitiles', function() {
+ element.commitInfo = {
+ commit: 'commit-sha',
+ web_links: [
+ {
+ name: 'ignore',
+ url: 'ignore',
+ },
+ {
+ name: 'gitiles',
+ url: 'https://link-url',
+ }
+ ],
+ };
+ element.serverConfig = {};
+
+ assert.isOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.equal(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig), 'https://link-url');
+
+ // Remove gitiles link.
+ element.commitInfo.web_links.splice(1, 1);
+ assert.isNotOk(element._computeShowWebLink(element.change,
+ element.commitInfo, element.serverConfig));
+ assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
+ element.serverConfig));
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 97342d1..f27e4e2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -32,16 +32,6 @@
properties: {
branch: String,
message: String,
- commitInfo: {
- type: Object,
- readOnly: true,
- observer: '_commitInfoChanged',
- },
- },
-
- _commitInfoChanged: function(commitInfo) {
- // Pre-populate cherry-pick message for editing from commit info.
- this.message = commitInfo.message;
},
_handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index b4baa26..f38ef74 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -30,14 +30,12 @@
*/
properties: {
- branch: String,
message: String,
- commitInfo: Object,
},
- populateRevertMessage: function() {
+ populateRevertMessage: function(message) {
// Figure out what the revert title should be.
- var originalTitle = this.commitInfo.message.split('\n')[0];
+ var originalTitle = message.split('\n')[0];
var revertTitle = 'Revert of ' + originalTitle;
if (originalTitle.startsWith('Revert of ')) {
revertTitle = 'Reland of ' +
@@ -47,7 +45,7 @@
originalTitle.substring('Reland of '.length);
}
// Add '> ' in front of the original commit text.
- var originalCommitText = this.commitInfo.message.replace(/^/gm, '> ');
+ var originalCommitText = message.replace(/^/gm, '> ');
this.message = revertTitle + '\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n\n' +
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 1d53eef..3b3851a 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -40,9 +40,7 @@
test('single line', function() {
assert.isNotOk(element.message);
- element.commitInfo = {message: 'one line commit'};
- assert.isNotOk(element.message);
- element.populateRevertMessage();
+ element.populateRevertMessage('one line commit');
var expected = 'Revert of one line commit\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n\n' +
'Original issue\'s description:\n' +
@@ -52,9 +50,7 @@
test('multi line', function() {
assert.isNotOk(element.message);
- element.commitInfo = {message: 'many lines\ncommit\n\nmessage\n'};
- assert.isNotOk(element.message);
- element.populateRevertMessage();
+ element.populateRevertMessage('many lines\ncommit\n\nmessage\n');
var expected = 'Revert of many lines\n\n' +
'Reason for revert: <INSERT REASONING HERE>\n\n' +
'Original issue\'s description:\n' +
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index cec1e90..1432d8c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -97,9 +97,6 @@
border: none;
width: 100%;
}
- .labelsNotShown {
- color: #666;
- }
.labelContainer:not(:first-of-type) {
margin-top: .5em;
}
@@ -211,28 +208,20 @@
</iron-autogrow-textarea>
</section>
<section class="labelsContainer">
- <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]">
- <template is="dom-repeat"
- items="[[_computeLabelArray(permittedLabels)]]" as="label">
- <div class="labelContainer">
- <span class="labelName">[[label]]</span>
- <iron-selector data-label$="[[label]]"
- selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
- <template is="dom-repeat"
- items="[[_computePermittedLabelValues(permittedLabels, label)]]"
- as="value">
- <gr-button has-tooltip data-value$="[[value]]"
- title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
- </template>
- </iron-selector>
- </div>
- </template>
- </template>
- <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
- <span class="labelsNotShown">
- Labels are not shown because this is not the most recent patch set.
- <a href$="/c/[[change._number]]">Go to the latest patch set.</a>
- </span>
+ <template is="dom-repeat"
+ items="[[_computeLabelArray(permittedLabels)]]" as="label">
+ <div class="labelContainer">
+ <span class="labelName">[[label]]</span>
+ <iron-selector data-label$="[[label]]"
+ selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
+ <template is="dom-repeat"
+ items="[[_computePermittedLabelValues(permittedLabels, label)]]"
+ as="value">
+ <gr-button has-tooltip data-value$="[[value]]"
+ title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
+ </template>
+ </iron-selector>
+ </div>
</template>
</section>
<section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index d2b279d..963c8fa 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -48,7 +48,6 @@
properties: {
change: Object,
patchNum: String,
- revisions: Object,
disabled: {
type: Boolean,
value: false,
@@ -152,10 +151,6 @@
if (!this.permittedLabels.hasOwnProperty(label)) { continue; }
var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
-
- // The selector may not be present if it’s not at the latest patch set.
- if (!selectorEl) { continue; }
-
var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
selectedVal = parseInt(selectedVal, 10);
obj.labels[label] = selectedVal;
@@ -259,16 +254,6 @@
}.bind(this));
},
- _computeShowLabels: function(patchNum, revisions) {
- var num = parseInt(patchNum, 10);
- for (var rev in revisions) {
- if (revisions[rev]._number > num) {
- return false;
- }
- }
- return true;
- },
-
_computeHideDraftList: function(drafts) {
return Object.keys(drafts || {}).length == 0;
},
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 8fb4e45..639aeef 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -107,18 +107,7 @@
MockInteractions.tap(element.$$('.cancel'));
});
- test('show/hide labels', function() {
- var revisions = {
- rev1: {_number: 1},
- rev2: {_number: 2},
- };
- assert.isFalse(element._computeShowLabels('1', revisions));
- assert.isTrue(element._computeShowLabels('2', revisions));
- });
-
test('label picker', function(done) {
- var showLabelsStub = sinon.stub(element, '_computeShowLabels',
- function() { return true; });
element.revisions = {};
element.patchNum = '';
@@ -156,7 +145,6 @@
'Element should be enabled when done sending reply.');
assert.equal(element.draft.length, 0);
saveReviewStub.restore();
- showLabelsStub.restore();
done();
});
@@ -312,7 +300,10 @@
assert.equal(body, 'first error, second error');
});
});
- element.send().then(done);
+
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ flush(function() { element.send().then(done); });
});
test('ccs are displayed if NoteDb is enabled', function() {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 2971ed2..4ad2a37 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-reporting/gr-reporting.html">
<script src="../../../bower_components/page/page.js"></script>
<script src="gr-router.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 8441372..c69e43f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -17,10 +17,15 @@
// Polymer makes `app` intrinsically defined on the window by virtue of the
// custom element having the id "app", but it is made explicit here.
var app = document.querySelector('#app');
- var restAPI = document.createElement('gr-rest-api-interface');
- var reporting = document.createElement('gr-reporting');
+ if (!app) {
+ console.log('No gr-app found (running tests)');
+ return;
+ }
window.addEventListener('WebComponentsReady', function() {
+ var restAPI = document.createElement('gr-rest-api-interface');
+ var reporting = document.createElement('gr-reporting');
+
reporting.timeEnd('WebComponentsReady');
reporting.pageLoaded();
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 18c0602..40a90b7 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
@@ -77,6 +77,7 @@
_builder: Object,
_groups: Array,
_layers: Array,
+ _showTabs: Boolean,
},
get diffElement() {
@@ -92,6 +93,7 @@
this._layers = [
this.$.syntaxLayer,
this._createIntralineLayer(),
+ this._createTabIndicatorLayer(),
this.$.rangeLayer,
];
@@ -102,6 +104,7 @@
render: function(comments, prefs) {
this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
+ this._showTabs = !!prefs.show_tabs;
// Stop the processor (if it's running).
this.$.processor.cancel();
@@ -330,6 +333,31 @@
};
},
+ _createTabIndicatorLayer: function() {
+ var show = (function() { return this._showTabs; }).bind(this);
+ return {
+ addListener: function() {},
+ annotate: function(el, line) {
+ // If visible tabs are disabled, do nothing.
+ if (!show()) { return; }
+
+ // Find and annotate the locations of tabs.
+ var split = line.text.split('\t');
+ if (!split) { return; }
+ for (var i = 0, pos = 0; i < split.length - 1; i++) {
+ // Skip forward by the length of the content
+ pos += split[i].length;
+
+ GrAnnotation.annotateElement(el, pos, 1,
+ 'style-scope gr-diff tab-indicator');
+
+ // Skip forward by one tab character.
+ pos++;
+ }
+ },
+ };
+ },
+
/**
* In pages with large diffs, creating the first comment thread can be
* slow because nested Polymer elements (particularly
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 2090e98..670885a 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
@@ -493,7 +493,7 @@
for (var i = 0; i < split.length - 1; i++) {
offset += split[i].length;
width = tabSize - (offset % tabSize);
- result += split[i] + this._getTabWrapper(width, this._prefs.show_tabs);
+ result += split[i] + this._getTabWrapper(width);
offset += width;
}
if (split.length) {
@@ -503,7 +503,7 @@
return result;
};
- GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
+ GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
// Force this to be a number to prevent arbitrary injection.
tabSize = +tabSize;
if (isNaN(tabSize)) {
@@ -511,9 +511,6 @@
}
var str = '<span class="style-scope gr-diff tab ';
- if (showTabs) {
- str += 'withIndicator';
- }
str += '" style="';
// TODO(andybons): CSS tab-size is not supported in IE.
str += 'tab-size:' + tabSize + ';';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 187a5cd..af44629 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -414,6 +414,117 @@
});
});
+ suite('tab indicators', function() {
+ var sandbox;
+ var element;
+ var layer;
+
+ setup(function() {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element._showTabs = true;
+ layer = element._createTabIndicatorLayer();
+ });
+
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ test('does nothing with empty line', function() {
+ var line = {text: ''};
+ var el = document.createElement('div');
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', function() {
+ var str = 'lorem ipsum no tabs';
+ var line = {text: str};
+ var el = document.createElement('div');
+ el.textContent = str;
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', function() {
+ var str = '\tlorem upsum';
+ var line = {text: str};
+ var el = document.createElement('div');
+ el.textContent = str;
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ var args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('does not annotate when disabled', function() {
+ element._showTabs = false;
+
+ var str = '\tlorem upsum';
+ var line = {text: str};
+ var el = document.createElement('div');
+ el.textContent = str;
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', function() {
+ var str = '\t\tlorem upsum';
+ var line = {text: str};
+ var el = document.createElement('div');
+ el.textContent = str;
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.equal(annotateElementStub.callCount, 2);
+
+ var args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+
+ args = annotateElementStub.getCalls()[1].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 1, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('annotates intermediate tabs', function() {
+ var str = 'lorem\tupsum';
+ var line = {text: str};
+ var el = document.createElement('div');
+ el.textContent = str;
+ var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, line);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ var args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 5, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+ });
+
suite('rendering', function() {
var content;
var outputEl;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index d6a3bc0..7111ee5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -105,6 +105,15 @@
}
}.bind(this));
+ if (this.changeViewState.diffMode === null) {
+ // Initialize with user's diff mode preference. Default to
+ // SIDE_BY_SIDE in the meantime.
+ this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
+ this.$.restAPI.getPreferences().then(function(prefs) {
+ this.set('changeViewState.diffMode', prefs.diff_view);
+ }.bind(this));
+ }
+
if (this._path) {
this.fire('title-change',
{title: this._computeFileDisplayName(this._path)});
@@ -113,11 +122,6 @@
this.$.cursor.push('diffs', this.$.diff);
},
- detached: function() {
- // Reset the diff mode to null so that it reverts to the user preference.
- this.changeViewState.diffMode = null;
- },
-
_getLoggedIn: function() {
return this.$.restAPI.getLoggedIn();
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 0a4d6b6..c21bd90 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -32,6 +32,12 @@
</template>
</test-fixture>
+<test-fixture id="blank">
+ <template>
+ <div></div>
+ </template>
+</text-fixture>
+
<script>
suite('gr-diff-view tests', function() {
var element;
@@ -394,6 +400,29 @@
assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
});
+ test('diff mode selector initializes from preferences', function() {
+ var resolvePrefs;
+ var prefsPromise = new Promise(function(resolve) {
+ resolvePrefs = resolve;
+ });
+ var getPreferencesStub = sinon.stub(element.$.restAPI, 'getPreferences',
+ function() { return prefsPromise; });
+
+ // Attach a new gr-diff-view so we can intercept the preferences fetch.
+ var view = document.createElement('gr-diff-view');
+ var select = view.$.modeSelect;
+ fixture('blank').appendChild(view);
+ flushAsynchronousOperations();
+
+ // At this point the diff mode doesn't yet have the user's preference.
+ assert.equal(select.value, 'SIDE_BY_SIDE');
+
+ // Receive the overriding preference.
+ resolvePrefs({diff_view: 'UNIFIED'});
+ flushAsynchronousOperations();
+ assert.equal(select.value, 'SIDE_BY_SIDE');
+ });
+
test('_loadHash', function() {
assert.isNotOk(element.$.cursor.initialLineNumber);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 46612a0..2431fb0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -152,11 +152,11 @@
}
.tab {
display: inline-block;
- position: relative;
}
- .tab.withIndicator {
- color: #D68E47;
- text-decoration: line-through;
+ .tab-indicator:before {
+ color: #C62828;
+ /* >> character */
+ content: '\00BB';
}
</style>
<style include="gr-theme-default"></style>
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index ca26ed0..0c23c4f 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -25,7 +25,7 @@
<test-fixture id="basic">
<template>
- <gr-app></gr-app>
+ <gr-app id="app"></gr-app>
</template>
</test-fixture>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index cc0da66..164bb2d 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -120,13 +120,14 @@
:host([secondary]:active) {
border-color: #941c0c;
}
- :host([primary][loading]),
- :host([primary][disabled]) {
+ :host([primary][loading]) {
background-color: #7caeff;
border-color: transparent;
color: #fff;
}
-
+ :host([primary][disabled]) {
+ background-color: #888;
+ }
</style>
<content></content>
</template>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 2714f48..a608cae 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -25,15 +25,15 @@
var basePath = '../elements/';
[
- 'change-list/gr-change-list-item/gr-change-list-item_test.html',
- 'change-list/gr-change-list/gr-change-list_test.html',
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
'change/gr-change-actions/gr-change-actions_test.html',
'change/gr-change-metadata/gr-change-metadata_test.html',
'change/gr-change-view/gr-change-view_test.html',
'change/gr-comment-list/gr-comment-list_test.html',
+ 'change/gr-commit-info/gr-commit-info_test.html',
'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
+ 'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
'change/gr-download-dialog/gr-download-dialog_test.html',
'change/gr-file-list/gr-file-list_test.html',
'change/gr-message/gr-message_test.html',
@@ -41,13 +41,18 @@
'change/gr-related-changes-list/gr-related-changes-list_test.html',
'change/gr-reply-dialog/gr-reply-dialog_test.html',
'change/gr-reviewer-list/gr-reviewer-list_test.html',
+ 'change-list/gr-change-list/gr-change-list_test.html',
+ 'change-list/gr-change-list-item/gr-change-list-item_test.html',
'core/gr-account-dropdown/gr-account-dropdown_test.html',
'core/gr-error-manager/gr-error-manager_test.html',
'core/gr-main-header/gr-main-header_test.html',
+ 'core/gr-reporting/gr-reporting_test.html',
'core/gr-search-bar/gr-search-bar_test.html',
+ 'diff/gr-diff/gr-diff-group_test.html',
+ 'diff/gr-diff/gr-diff_test.html',
'diff/gr-diff-builder/gr-diff-builder_test.html',
- 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
'diff/gr-diff-comment/gr-diff-comment_test.html',
+ 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html',
@@ -55,8 +60,6 @@
'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html',
'diff/gr-diff-view/gr-diff-view_test.html',
- 'diff/gr-diff/gr-diff-group_test.html',
- 'diff/gr-diff/gr-diff_test.html',
'diff/gr-patch-range-select/gr-patch-range-select_test.html',
'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
'diff/gr-selection-action-box/gr-selection-action-box_test.html',