Merge "Remove comments about OpenJDK bug 100167"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6bcf851..a3a813e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3541,6 +3541,15 @@
+
Default is zero, no limit.
+[[receive.maxBatchCommits]]receive.maxBatchCommits::
++
+The maximum number of commits that Gerrit allows to be pushed in a batch
+directly to a branch when link:user-upload.html#bypass_review[bypassing review].
+This limit can be bypassed if a user link:user-upload.html#skip_validation[skips
+validation].
++
+Default is 10000.
+
[[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
+
Maximum allowed Git object size that 'receive-pack' will accept.
diff --git a/Documentation/dev-plugins-pg.txt b/Documentation/dev-plugins-pg.txt
index 9d36758..e1bf39e 100644
--- a/Documentation/dev-plugins-pg.txt
+++ b/Documentation/dev-plugins-pg.txt
@@ -45,14 +45,14 @@
decoration case, a hook is set with a `content` attribute that points to the DOM
element.
-1. Get the DOM hook API instance via `plugin.getDomHook(endpointName)`
+1. Get the DOM hook API instance via `plugin.hook(endpointName)`
2. Set up an `onAttached` callback
3. Callback is called when the hook element is created and inserted into DOM
4. Use element.content to get UI element
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('reply-text');
+ const domHook = plugin.hook('reply-text');
domHook.onAttached(element => {
if (!element.content) { return; }
// element.content is a reply dialog text area.
@@ -70,7 +70,7 @@
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('reply-text');
+ const domHook = plugin.hook('reply-text');
domHook.onAttached(element => {
if (!element.content) { return; }
element.content.style.border = '1px red dashed';
@@ -86,7 +86,7 @@
``` js
Gerrit.install(function(plugin) {
- const domHook = plugin.getDomHook('header-title', {replace: true});
+ const domHook = plugin.hook('header-title', {replace: true});
domHook.onAttached(element => {
element.appendChild(document.createElement('my-site-header'));
});
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 26193f4..2f6a7d6 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -359,6 +359,13 @@
git merge stable
----
+[[update-api-version-in-bazlets-repository]]
+
+Bazlets is used by gerrit plugins to simplify build process. To allow the
+new released version to be used by gerrit plugins,
+link:https://gerrit.googlesource.com/bazlets/+/master/gerrit_api.bzl#8[gerrit_api.bzl]
+must reference the new version. Upload a change to bazlets repository with
+api version upgrade.
GERRIT
------
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index 2632254..ca8dc75 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -32,6 +32,7 @@
* link:error-prohibited-by-gerrit.html[prohibited by Gerrit]
* link:error-project-not-found.html[Project not found: ...]
* link:error-same-change-id-in-multiple-changes.html[same Change-Id in multiple changes]
+* link:error-too-many-commits.html[too many commits]
* link:error-upload-denied.html[Upload denied for project \'...']
* link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
diff --git a/Documentation/error-too-many-commits.txt b/Documentation/error-too-many-commits.txt
new file mode 100644
index 0000000..3e16220
--- /dev/null
+++ b/Documentation/error-too-many-commits.txt
@@ -0,0 +1,20 @@
+= too many commits
+
+This error occurs when a push directly to a branch
+link:user-upload.html#bypass_review[bypassing review] contains more commits than
+the server is able to validate in a single batch.
+
+The recommended way to avoid this message is to use the
+link:user-upload.html#skip_validation[`skip-validation` push option]. Depending
+on the number of commits, it may also be feasible to split the push into smaller
+batches.
+
+The actual limit is controlled by a
+link:config-gerrit.html#receive.maxBatchCommits[server config option].
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
index e0fe1b3..03aaabf 100644
--- a/Documentation/pgm-LocalUsernamesToLowerCase.txt
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -7,7 +7,7 @@
== SYNOPSIS
[verse]
--
-_java_ -jar gerrit.war _LocalUsernamesToLowerCase
+_java_ -jar gerrit.war _LocalUsernamesToLowerCase_
-d <SITE_PATH>
--
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 54ddcff..c95cf2c 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -224,6 +224,7 @@
The defined maximum Git object size limit is inherited by any child
project.
+[[require-signed-off-by]]
=== Require Signed-off-by
The `Require Signed-off-by in commit message` option defines whether a
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fad4b9c..79c7a1a 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1077,6 +1077,36 @@
If the change had no assignee the response is "`204 No Content`".
+[[get-pure-revert]]
+=== Get Pure Revert
+--
+'GET /changes/link:#change-id[\{change-id\}]/pure_revert'
+--
+
+Check if the given change is a pure revert of the change it references in `revertOf`.
+Optionally, the query parameter `o` can be passed in to specify a commit (SHA1 in
+40 digit hex representation) to check against. It takes precedence over `revertOf`.
+If the change has no reference in `revertOf`, the parameter is mandatory.
+
+As response a link:#pure-revert-info[PureRevertInfo] entity is returned.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/pure_revert?o=247bccf56ae47634650bcc08b8aa784c3580ccas HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "is_pure_revert" : false
+ }
+----
+
[[abandon-change]]
=== Abandon Change
--
@@ -6557,6 +6587,16 @@
of recipient type to link:#notify-info[NotifyInfo] entity.
|=============================
+[[pure-revert-info]]
+=== PureRevertInfo
+The `PureRevertInfo` entity describes the result of a pure revert check.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name |Description
+|`is_pure_revert` |Outcome of the check as boolean.
+|======================
+
[[push-certificate-info]]
=== PushCertificateInfo
The `PushCertificateInfo` entity contains information about a push
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 210f9e9..556a8bb 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -517,6 +517,44 @@
make undesired changes to the public repository.
+[[skip_validation]]
+=== Skip Validation
+
+Even when a user has permission to push directly to a branch
+link:#bypass_review[bypassing review], by default Gerrit will still validate any
+new commits, for example to check author/committer identities, and run
+link:config-validation.html#new-commit-validation[validation plugins]. This
+behavior can be bypassed with a push option:
+
+----
+git push -o skip-validation HEAD:master
+----
+
+Using the `skip-validation` option requires the user to have a specific set
+of permissions, *in addition* to those permissions already required to bypass
+review:
+
+* link:access-control.html#category_forge_author[Forge Author]
+* link:access-control.html#category_forge_committer[Forge Committer]
+* link:access-control.html#category_forge_server[Forge Server]
+* link:access-control.html#category_push_merge[Push Merge Commits]
+
+Plus these additional requirements on the project:
+
+* Project must not link:project-configuration.html#require-signed-off-by[require
+Signed-off-by].
+* Project must not have `refs/meta/reject-commits`.
+
+This option only applies when pushing directly to a branch bypassing review.
+Validation also occurs when pushing new changes for review, and that type of
+validation cannot be skipped.
+
+The `skip-validation` option is always required when pushing
+link:error-too-many-commits.html[more than a certain number of commits]. This is
+the recommended approach when pushing lots of old history, since some validators
+would require rewriting history in order to make them pass.
+
+
[[auto_merge]]
=== Auto-Merge during Push
diff --git a/WORKSPACE b/WORKSPACE
index 5057012..d76b8d4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -6,9 +6,9 @@
http_archive(
name = "io_bazel_rules_closure",
- sha256 = "af1f5a31b8306faed9d09a38c8e2c1d6afc4c4a2dada3b5de11cceae8c7f4596",
- strip_prefix = "rules_closure-f68d4b5a55c04ee50a3196590dce1ca8e7dbf438",
- url = "https://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/f68d4b5a55c04ee50a3196590dce1ca8e7dbf438.tar.gz", # 2017-05-05
+ sha256 = "25f5399f18d8bf9ce435f85c6bbf671ec4820bc4396b3022cc5dc4bc66303609",
+ strip_prefix = "rules_closure-0.4.2",
+ url = "https://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/0.4.2.tar.gz", # 2017-08-29
)
# File is specific to Polymer and copied from the Closure Github -- should be
@@ -608,8 +608,8 @@
maven_jar(
name = "dropwizard_core",
- artifact = "io.dropwizard.metrics:metrics-core:3.2.2",
- sha1 = "cd9886f498ee2ab2d994f0c779e5553b2c450416",
+ artifact = "io.dropwizard.metrics:metrics-core:3.2.4",
+ sha1 = "36af4975e38bb39686a63ba5139dce8d3f410669",
)
# When updading Bouncy Castle, also update it in bazlets.
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index fb9eb12..d734cd1 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -191,7 +191,7 @@
try:
gerrit.post("/changes/" + change_id + "/abandon",
- data='{"message" : "%s"}' % abandon_message)
+ data={"message" : "%s" % abandon_message})
abandoned += 1
except Exception as e:
errors += 1
diff --git a/fake_pom.xml b/fake_pom.xml
index e8910b6..6ec45e5 100644
--- a/fake_pom.xml
+++ b/fake_pom.xml
@@ -53,6 +53,9 @@
<name>Logan Hanks</name>
</developer>
<developer>
+ <name>Luca Milanesio</name>
+ </developer>
+ <developer>
<name>Martin Fick</name>
</developer>
<developer>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
index 6edb3f0..43477ae 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GlobalPluginConfig.java
@@ -25,7 +25,7 @@
@Retention(RUNTIME)
@Repeatable(GlobalPluginConfigs.class)
public @interface GlobalPluginConfig {
- /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.comfig}. */
+ /** Name of the plugin, corresponding to {@code $site/etc/@pluginName.config}. */
String pluginName();
/** @see GerritConfig#name() */
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b7d368a..cb33959 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -45,6 +45,7 @@
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
@@ -90,6 +91,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gerrit.server.project.RefPattern;
import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -533,9 +535,7 @@
Set<String> currentEmails = getEmails();
for (String email : emails) {
assertThat(currentEmails).doesNotContain(email);
- EmailInput input = new EmailInput();
- input.email = email;
- input.noConfirmation = true;
+ EmailInput input = newEmailInput(email);
gApi.accounts().self().addEmail(input);
accountIndexedCounter.assertReindexOf(admin);
}
@@ -560,9 +560,7 @@
// Non-supported TLD (see tlds-alpha-by-domain.txt)
"new.email@example.africa");
for (String email : emails) {
- EmailInput input = new EmailInput();
- input.email = email;
- input.noConfirmation = true;
+ EmailInput input = newEmailInput(email);
try {
gApi.accounts().self().addEmail(input);
fail("Expected BadRequestException for invalid email address: " + email);
@@ -576,20 +574,41 @@
@Test
public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception {
TestAccount account = accountCreator.create(name("user"));
- EmailInput input = new EmailInput();
- input.email = "test@test.com";
- input.noConfirmation = true;
+ EmailInput input = newEmailInput("test@test.com");
setApiUser(user);
exception.expect(AuthException.class);
gApi.accounts().id(account.username).addEmail(input);
}
@Test
+ public void cannotAddEmailAddressUsedByAnotherAccount() throws Exception {
+ String email = "new.email@example.com";
+ EmailInput input = newEmailInput(email);
+ gApi.accounts().self().addEmail(input);
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("Identity 'mailto:" + email + "' in use by another account");
+ gApi.accounts().id(user.username).addEmail(input);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "auth.registerEmailPrivateKey",
+ value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co="
+ )
+ public void addEmailSendsConfirmationEmail() throws Exception {
+ String email = "new.email@example.com";
+ EmailInput input = newEmailInput(email, false);
+ gApi.accounts().self().addEmail(input);
+
+ assertThat(sender.getMessages()).hasSize(1);
+ Message m = sender.getMessages().get(0);
+ assertThat(m.rcpt()).containsExactly(new Address(email));
+ }
+
+ @Test
public void deleteEmail() throws Exception {
String email = "foo.bar@example.com";
- EmailInput input = new EmailInput();
- input.email = email;
- input.noConfirmation = true;
+ EmailInput input = newEmailInput(email);
gApi.accounts().self().addEmail(input);
resetCurrentApiUser();
@@ -1655,6 +1674,17 @@
assertThat(newAccount.getMetaId()).isEqualTo(getMetaId(accountId));
}
+ private EmailInput newEmailInput(String email, boolean noConfirmation) {
+ EmailInput input = new EmailInput();
+ input.email = email;
+ input.noConfirmation = noConfirmation;
+ return input;
+ }
+
+ private EmailInput newEmailInput(String email) {
+ return newEmailInput(email, true);
+ }
+
private String getMetaId(Account.Id accountId) throws IOException {
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo);
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 bfa21cb..4ce412c 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
@@ -19,6 +19,7 @@
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
@@ -31,6 +32,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -81,11 +83,13 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -3127,6 +3131,150 @@
gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId()));
}
+ @Test
+ public void fourByteEmoji() throws Exception {
+ // U+1F601 GRINNING FACE WITH SMILING EYES
+ String smile = new String(Character.toChars(0x1f601));
+ assertThat(smile).isEqualTo("😁");
+ assertThat(smile).hasLength(2); // Thanks, Java.
+ assertThat(smile.getBytes(UTF_8)).hasLength(4);
+
+ String subject = "A happy change " + smile;
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
+ .to("refs/for/master");
+ r.assertOkStatus();
+ String id = r.getChangeId();
+
+ ReviewInput ri = ReviewInput.approve();
+ ri.message = "I like it " + smile;
+ ReviewInput.CommentInput ci = new ReviewInput.CommentInput();
+ ci.path = FILE_NAME;
+ ci.side = Side.REVISION;
+ ci.message = "Good " + smile;
+ ri.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(ci));
+ gApi.changes().id(id).current().review(ri);
+
+ ChangeInfo info =
+ gApi.changes()
+ .id(id)
+ .get(
+ EnumSet.of(
+ ListChangesOption.MESSAGES,
+ ListChangesOption.CURRENT_COMMIT,
+ ListChangesOption.CURRENT_REVISION));
+ assertThat(info.subject).isEqualTo(subject);
+ assertThat(Iterables.getLast(info.messages).message).endsWith(ri.message);
+ assertThat(Iterables.getOnlyElement(info.revisions.values()).commit.message)
+ .startsWith(subject);
+
+ List<CommentInfo> comments =
+ Iterables.getOnlyElement(gApi.changes().id(id).comments().values());
+ assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message);
+ }
+
+ @Test
+ public void pureRevertReturnsTrueForPureRevert() throws Exception {
+ PushOneCommit.Result r = createChange();
+ merge(r);
+ String revertId = gApi.changes().id(r.getChangeId()).revert().get().id;
+ // Without query parameter
+ assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+ // With query parameter
+ assertThat(
+ gApi.changes()
+ .id(revertId)
+ .pureRevert(getRemoteHead().toObjectId().name())
+ .isPureRevert)
+ .isTrue();
+ }
+
+ @Test
+ public void pureRevertReturnsFalseOnContentChange() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ merge(r1);
+ // Create a revert and expect pureRevert to be true
+ String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+ assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+
+ // Create a new PS and expect pureRevert to be false
+ PushOneCommit.Result result = amendChange(revertId);
+ result.assertOkStatus();
+ assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse();
+ }
+
+ @Test
+ public void pureRevertParameterTakesPrecedence() throws Exception {
+ PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+ merge(r1);
+ String oldHead = getRemoteHead().toObjectId().name();
+
+ PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2");
+ merge(r2);
+
+ String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId;
+ assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+ assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse();
+ }
+
+ @Test
+ public void pureRevertReturnsFalseOnInvalidInput() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ merge(r1);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("invalid object ID");
+ gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id");
+ }
+
+ @Test
+ public void pureRevertReturnsTrueWithCleanRebase() throws Exception {
+ PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+ merge(r1);
+
+ PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2");
+ merge(r2);
+
+ String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId;
+ // Rebase revert onto HEAD
+ gApi.changes().id(revertId).rebase();
+ // Check that pureRevert is true which implies that the commit can be rebased onto the original
+ // commit.
+ assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue();
+ }
+
+ @Test
+ public void pureRevertReturnsFalseWithRebaseConflict() throws Exception {
+ // Create an initial commit to serve as claimed original
+ PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1");
+ merge(r1);
+ String claimedOriginal = getRemoteHead().toObjectId().name();
+
+ // Change contents of the file to provoke a conflict
+ merge(createChange("commit message", "a.txt", "content2"));
+
+ // Create a commit that we can revert
+ PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3");
+ merge(r2);
+
+ // Create a revert of r2
+ String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id();
+ // Assert that the change is a pure revert of it's 'revertOf'
+ assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue();
+ // Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying
+ // to rebase this on claimed original, which fails.
+ PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal);
+ assertThat(pureRevert.isPureRevert).isFalse();
+ }
+
+ @Test
+ public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception {
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("no ID was provided and change isn't a revert");
+ gApi.changes().id(createChange().getChangeId()).pureRevert();
+ }
+
private String getCommitMessage(String changeId) throws RestApiException, IOException {
return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
}
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 6face43..0dc1389 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
@@ -23,10 +23,12 @@
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
@@ -35,6 +37,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
@@ -62,10 +65,12 @@
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.client.RefNames;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.receive.ReceiveConstants;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.project.Util;
import com.google.gerrit.server.query.change.ChangeData;
@@ -1766,6 +1771,29 @@
assertThat(getPublishedComments(r.getChangeId())).isEmpty();
}
+ @GerritConfig(name = "receive.maxBatchCommits", value = "2")
+ @Test
+ public void maxBatchCommits() throws Exception {
+ List<RevCommit> commits = new ArrayList<>();
+ commits.addAll(initChanges(2));
+ String master = "refs/heads/master";
+ assertPushOk(pushHead(testRepo, master), master);
+
+ commits.addAll(initChanges(3));
+ assertPushRejected(pushHead(testRepo, master), master, "too many commits");
+
+ grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS);
+ PushResult r =
+ pushHead(testRepo, master, false, false, ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
+ assertPushOk(r, master);
+
+ // No open changes; branch was advanced.
+ String q = commits.stream().map(ObjectId::name).collect(joining(" OR commit:", "commit:", ""));
+ assertThat(gApi.changes().query(q).get()).isEmpty();
+ assertThat(gApi.projects().name(project.get()).branch(master).get().revision)
+ .isEqualTo(Iterables.getLast(commits).name());
+ }
+
private DraftInput newDraft(String path, int line, String message) {
DraftInput d = new DraftInput();
d.path = path;
@@ -1839,11 +1867,21 @@
}
private List<RevCommit> createChanges(int n, String refsFor) throws Exception {
- return createChanges(n, refsFor, ImmutableList.<String>of());
+ return createChanges(n, refsFor, ImmutableList.of());
}
private List<RevCommit> createChanges(int n, String refsFor, List<String> footerLines)
throws Exception {
+ List<RevCommit> commits = initChanges(n, footerLines);
+ assertPushOk(pushHead(testRepo, refsFor, false), refsFor);
+ return commits;
+ }
+
+ private List<RevCommit> initChanges(int n) throws Exception {
+ return initChanges(n, ImmutableList.of());
+ }
+
+ private List<RevCommit> initChanges(int n, List<String> footerLines) throws Exception {
List<RevCommit> commits = new ArrayList<>(n);
for (int i = 1; i <= n; i++) {
String msg = "Change " + i;
@@ -1863,7 +1901,6 @@
testRepo.getRevWalk().parseBody(c);
commits.add(c);
}
- assertPushOk(pushHead(testRepo, refsFor, false), refsFor);
return commits;
}
@@ -1934,4 +1971,15 @@
config.getProject().setCreateNewChangeForAllNotInTarget(InheritableBoolean.TRUE);
saveProjectConfig(project, config);
}
+
+ private void grantSkipValidation(Project.NameKey project, String ref, AccountGroup.UUID groupUuid)
+ throws Exception {
+ // See SKIP_VALIDATION implementation in default permission backend.
+ ProjectConfig config = projectCache.checkedGet(project).getConfig();
+ Util.allow(config, Permission.FORGE_AUTHOR, groupUuid, ref);
+ Util.allow(config, Permission.FORGE_COMMITTER, groupUuid, ref);
+ Util.allow(config, Permission.FORGE_SERVER, groupUuid, ref);
+ Util.allow(config, Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref);
+ saveProjectConfig(project, config);
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index b99e99d..b3a53b0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -834,7 +834,7 @@
try (BatchUpdate bu = newUpdate(ctl.getChange().getOwner())) {
ins =
patchSetInserterFactory
- .create(ctl, nextPatchSetId(ctl), commit)
+ .create(ctl.getNotes(), nextPatchSetId(ctl), commit)
.setValidate(false)
.setFireRevisionCreated(false)
.setNotify(NotifyHandling.NONE);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index f713ad2..53bf6a0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -22,6 +22,7 @@
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -257,6 +258,12 @@
void index() throws RestApiException;
+ /** Check if this change is a pure revert of the change stored in revertOf. */
+ PureRevertInfo pureRevert() throws RestApiException;
+
+ /** Check if this change is a pure revert of claimedOriginal (SHA1 in 40 digit hex). */
+ PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException;
+
abstract class SuggestedReviewersRequest {
private String query;
private int limit;
@@ -548,5 +555,15 @@
public void mute(boolean mute) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public PureRevertInfo pureRevert() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java
new file mode 100644
index 0000000..7f0d7a8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PureRevertInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class PureRevertInfo {
+ public boolean isPureRevert;
+
+ public PureRevertInfo() {}
+
+ public PureRevertInfo(boolean isPureRevert) {
+ this.isPureRevert = isPureRevert;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 2629cec..b556519 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -31,6 +31,8 @@
String defaultRevisionSpec();
+ String annotation();
+
String buttonDeleteIncludedGroup();
String buttonAddIncludedGroup();
@@ -183,6 +185,8 @@
String columnTagRevision();
+ String columnTagAnnotation();
+
String initialRevision();
String revision();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 54f5c8b..5521da0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -3,6 +3,7 @@
defaultBranchName = Branch Name
defaultTagName = Tag Name
defaultRevisionSpec = Revision (Branch or SHA-1)
+annotation = Annotation (optional)
buttonDeleteIncludedGroup = Delete
buttonAddIncludedGroup = Add
@@ -87,6 +88,7 @@
columnBranchRevision = Revision
columnTagName = Tag Name
columnTagRevision = Revision
+columnTagAnnotation = Annotation
initialRevision = Initial Revision
revision = Revision
buttonAddBranch = Create Branch
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
index 3bad430..18e4176 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -71,6 +71,7 @@
private Button addTag;
private HintTextBox nameTxtBox;
private HintTextBox irevTxtBox;
+ private HintTextBox annotationTxtBox;
private FlowPanel addPanel;
private NpTextBox filterTxt;
private Query query;
@@ -105,6 +106,7 @@
addTag.setEnabled(true);
nameTxtBox.setEnabled(true);
irevTxtBox.setEnabled(true);
+ annotationTxtBox.setEnabled(true);
}
@Override
@@ -120,14 +122,11 @@
addPanel = new FlowPanel();
- Grid addGrid = new Grid(2, 2);
+ Grid addGrid = new Grid(3, 2);
addGrid.setStyleName(Gerrit.RESOURCES.css().addBranch());
int texBoxLength = 50;
- nameTxtBox = new HintTextBox();
- nameTxtBox.setVisibleLength(texBoxLength);
- nameTxtBox.setHintText(AdminConstants.I.defaultTagName());
- nameTxtBox.addKeyPressHandler(
+ KeyPressHandler onKeyPress =
new KeyPressHandler() {
@Override
public void onKeyPress(KeyPressEvent event) {
@@ -135,25 +134,29 @@
doAddNewTag();
}
}
- });
+ };
+
+ nameTxtBox = new HintTextBox();
+ nameTxtBox.setVisibleLength(texBoxLength);
+ nameTxtBox.setHintText(AdminConstants.I.defaultTagName());
+ nameTxtBox.addKeyPressHandler(onKeyPress);
addGrid.setText(0, 0, AdminConstants.I.columnTagName() + ":");
addGrid.setWidget(0, 1, nameTxtBox);
irevTxtBox = new HintTextBox();
irevTxtBox.setVisibleLength(texBoxLength);
irevTxtBox.setHintText(AdminConstants.I.defaultRevisionSpec());
- irevTxtBox.addKeyPressHandler(
- new KeyPressHandler() {
- @Override
- public void onKeyPress(KeyPressEvent event) {
- if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
- doAddNewTag();
- }
- }
- });
+ irevTxtBox.addKeyPressHandler(onKeyPress);
addGrid.setText(1, 0, AdminConstants.I.revision() + ":");
addGrid.setWidget(1, 1, irevTxtBox);
+ annotationTxtBox = new HintTextBox();
+ annotationTxtBox.setVisibleLength(texBoxLength);
+ annotationTxtBox.setHintText(AdminConstants.I.annotation());
+ annotationTxtBox.addKeyPressHandler(onKeyPress);
+ addGrid.setText(2, 0, AdminConstants.I.columnTagAnnotation() + ":");
+ addGrid.setWidget(2, 1, annotationTxtBox);
+
addTag = new Button(AdminConstants.I.buttonAddTag());
addTag.addClickHandler(
new ClickHandler() {
@@ -237,17 +240,24 @@
return;
}
+ String annotation = annotationTxtBox.getText().trim();
+ if (annotation.isEmpty()) {
+ annotation = null;
+ }
+
addTag.setEnabled(false);
ProjectApi.createTag(
getProjectKey(),
tagName,
rev,
+ annotation,
new GerritCallback<TagInfo>() {
@Override
public void onSuccess(TagInfo tag) {
showAddedTag(tag);
nameTxtBox.setText("");
irevTxtBox.setText("");
+ annotationTxtBox.setText("");
query = new Query(match).start(start).run();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index acee478..3766dd9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -62,9 +62,14 @@
/** Create a new tag */
public static void createTag(
- Project.NameKey name, String ref, String revision, AsyncCallback<TagInfo> cb) {
+ Project.NameKey name,
+ String ref,
+ String revision,
+ String annotation,
+ AsyncCallback<TagInfo> cb) {
TagInput input = TagInput.create();
input.setRevision(revision);
+ input.setMessage(annotation);
project(name).view("tags").id(ref).ifNoneMatch().put(input, cb);
}
@@ -381,6 +386,8 @@
protected TagInput() {}
final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
+
+ final native void setMessage(String m) /*-{ if(m)this.message=m; }-*/;
}
private static class BranchInput extends JavaScriptObject {
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 558cc78..51c60af 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -41,6 +41,10 @@
// @see https://github.com/w3c/preload/issues/32 regarding crossorigin
<link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
<link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Medium.woff2" as="font" type="font/woff2" crossorigin>{\n}
+ <link rel="preload" href="{$staticResourcePath}/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin>{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
<script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 4efbecc..072d1ed 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -619,7 +619,6 @@
// If the build system provides us with a source root, use that.
try (InputStream stream = self.getResourceAsStream(SOURCE_ROOT_RESOURCE)) {
- System.err.println("URL: " + stream);
if (stream != null) {
try (Scanner scan = new Scanner(stream, UTF_8.name()).useDelimiter("\n")) {
if (scan.hasNext()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 2068540..444f64f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -199,7 +199,7 @@
T def, A options, String fmt, Object... args) {
final String prompt = String.format(fmt, args);
for (; ; ) {
- String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
+ String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString().toLowerCase());
if (r == null) {
throw abort();
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index f1eef0b..89de9dc 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -102,19 +102,13 @@
}
public static String changeMetaRef(Change.Id id) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_CHANGES);
- r.append(shard(id.get()));
- r.append(META_SUFFIX);
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_CHANGES);
+ return shard(id.get(), r).append(META_SUFFIX).toString();
}
public static String robotCommentsRef(Change.Id id) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_CHANGES);
- r.append(shard(id.get()));
- r.append(ROBOT_COMMENTS_SUFFIX);
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_CHANGES);
+ return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString();
}
public static boolean isNoteDbMetaRef(String ref) {
@@ -129,16 +123,12 @@
}
public static String refsUsers(Account.Id accountId) {
- StringBuilder r = new StringBuilder();
- r.append(REFS_USERS);
- r.append(shard(accountId.get()));
- return r.toString();
+ StringBuilder r = newStringBuilder().append(REFS_USERS);
+ return shard(accountId.get(), r).toString();
}
public static String refsDraftComments(Change.Id changeId, Account.Id accountId) {
- StringBuilder r = buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get());
- r.append(accountId.get());
- return r.toString();
+ return buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get()).append(accountId.get()).toString();
}
public static String refsDraftCommentsPrefix(Change.Id changeId) {
@@ -146,9 +136,7 @@
}
public static String refsStarredChanges(Change.Id changeId, Account.Id accountId) {
- StringBuilder r = buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get());
- r.append(accountId.get());
- return r.toString();
+ return buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get()).append(accountId.get()).toString();
}
public static String refsStarredChangesPrefix(Change.Id changeId) {
@@ -156,11 +144,8 @@
}
private static StringBuilder buildRefsPrefix(String prefix, int id) {
- StringBuilder r = new StringBuilder();
- r.append(prefix);
- r.append(shard(id));
- r.append('/');
- return r;
+ StringBuilder r = newStringBuilder().append(prefix);
+ return shard(id, r).append('/');
}
public static String refsCacheAutomerge(String hash) {
@@ -171,15 +156,18 @@
if (id < 0) {
return null;
}
- StringBuilder r = new StringBuilder();
+ return shard(id, newStringBuilder()).toString();
+ }
+
+ private static StringBuilder shard(int id, StringBuilder sb) {
int n = id % 100;
if (n < 10) {
- r.append('0');
+ sb.append('0');
}
- r.append(n);
- r.append('/');
- r.append(id);
- return r.toString();
+ sb.append(n);
+ sb.append('/');
+ sb.append(id);
+ return sb;
}
/**
@@ -363,5 +351,12 @@
return Integer.valueOf(name.substring(i, name.length()));
}
+ private static StringBuilder newStringBuilder() {
+ // Many refname types in this file are always are longer than the default of 16 chars, so
+ // presize StringBuilders larger by default. This hurts readability less than accurate
+ // calculations would, at a negligible cost to memory overhead.
+ return new StringBuilder(64);
+ }
+
private RefNames() {}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index 6771616..538c7c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.git.LabelNormalizer;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
@@ -183,7 +184,7 @@
ChangeKind kind =
changeKindCache.getChangeKind(
- project.getProject().getNameKey(),
+ project.getNameKey(),
rw,
repoConfig,
ObjectId.fromString(priorPs.getRevision().get()),
@@ -204,7 +205,7 @@
}
}
return labelNormalizer.normalize(ctl, byUser.values()).getNormalized();
- } catch (IOException e) {
+ } catch (IOException | PermissionBackendException e) {
throw new OrmException(e);
}
}
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 7750729..d858da5 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
@@ -31,8 +31,6 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -48,6 +46,7 @@
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
@@ -308,7 +307,7 @@
* @param update change update.
* @param labelTypes label types for the containing project.
* @param ps patch set being approved.
- * @param changeCtl change control for user adding approvals.
+ * @param user user adding approvals.
* @param approvals approvals to add.
* @throws RestApiException
* @throws OrmException
@@ -318,10 +317,10 @@
ChangeUpdate update,
LabelTypes labelTypes,
PatchSet ps,
- ChangeControl changeCtl,
+ CurrentUser user,
Map<String, Short> approvals)
- throws RestApiException, OrmException {
- Account.Id accountId = changeCtl.getUser().getAccountId();
+ throws RestApiException, OrmException, PermissionBackendException {
+ Account.Id accountId = user.getAccountId();
checkArgument(
accountId.equals(ps.getUploader()),
"expected user %s to match patch set uploader %s",
@@ -330,12 +329,12 @@
if (approvals.isEmpty()) {
return ImmutableList.of();
}
- checkApprovals(approvals, changeCtl);
+ checkApprovals(approvals, permissionBackend.user(user).database(db).change(update.getNotes()));
List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
Date ts = update.getWhen();
for (Map.Entry<String, Short> vote : approvals.entrySet()) {
LabelType lt = labelTypes.byLabel(vote.getKey());
- cells.add(newApproval(ps.getId(), changeCtl.getUser(), lt.getLabelId(), vote.getValue(), ts));
+ cells.add(newApproval(ps.getId(), user, lt.getLabelId(), vote.getValue(), ts));
}
for (PatchSetApproval psa : cells) {
update.putApproval(psa.getLabel(), psa.getValue());
@@ -356,13 +355,15 @@
}
}
- private static void checkApprovals(Map<String, Short> approvals, ChangeControl changeCtl)
- throws AuthException {
+ private static void checkApprovals(
+ Map<String, Short> approvals, PermissionBackend.ForChange forChange)
+ throws AuthException, PermissionBackendException {
for (Map.Entry<String, Short> vote : approvals.entrySet()) {
String name = vote.getKey();
Short value = vote.getValue();
- PermissionRange range = changeCtl.getRange(Permission.forLabel(name));
- if (range == null || !range.contains(value)) {
+ try {
+ forChange.check(new LabelPermission.WithValue(name, value));
+ } catch (AuthException e) {
throw new AuthException(
String.format("applying label \"%s\": %d is restricted", name, value));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
index e9152d0..9bd7783 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -135,7 +135,7 @@
.getProvider()
.get()
.suggestReviewers(
- projectState.getProject().getNameKey(),
+ projectState.getNameKey(),
changeNotes.getChangeId(),
query,
reviewerScores.keySet()));
@@ -239,8 +239,7 @@
List<Predicate<ChangeData>> predicates = new ArrayList<>();
for (Account.Id id : candidates) {
try {
- Predicate<ChangeData> projectQuery =
- changeQueryBuilder.project(projectState.getProject().getName());
+ Predicate<ChangeData> projectQuery = changeQueryBuilder.project(projectState.getName());
// Get all labels for this project and create a compound OR query to
// fetch all changes where users have applied one of these labels
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 3224601..cafdaed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -345,7 +345,8 @@
ExternalId extId = externalIds.get(who.getExternalIdKey());
if (extId != null) {
if (!extId.accountId().equals(to)) {
- throw new AccountException("Identity in use by another account");
+ throw new AccountException(
+ "Identity '" + extId.key().get() + "' in use by another account");
}
update(who, extId);
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index dfa0e7c..2dc9f6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -42,6 +42,7 @@
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
+import com.google.gerrit.extensions.common.PureRevertInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.IdString;
@@ -59,6 +60,7 @@
import com.google.gerrit.server.change.GetAssignee;
import com.google.gerrit.server.change.GetHashtags;
import com.google.gerrit.server.change.GetPastAssignees;
+import com.google.gerrit.server.change.GetPureRevert;
import com.google.gerrit.server.change.GetTopic;
import com.google.gerrit.server.change.Ignore;
import com.google.gerrit.server.change.Index;
@@ -142,6 +144,7 @@
private final SetWorkInProgress setWip;
private final SetReadyForReview setReady;
private final PutMessage putMessage;
+ private final GetPureRevert getPureRevert;
@Inject
ChangeApiImpl(
@@ -186,6 +189,7 @@
SetWorkInProgress setWip,
SetReadyForReview setReady,
PutMessage putMessage,
+ GetPureRevert getPureRevert,
@Assisted ChangeResource change) {
this.changeApi = changeApi;
this.revert = revert;
@@ -228,6 +232,7 @@
this.setWip = setWip;
this.setReady = setReady;
this.putMessage = putMessage;
+ this.getPureRevert = getPureRevert;
this.change = change;
}
@@ -669,4 +674,18 @@
unmute.apply(change, new Unmute.Input());
}
}
+
+ @Override
+ public PureRevertInfo pureRevert() throws RestApiException {
+ return pureRevert(null);
+ }
+
+ @Override
+ public PureRevertInfo pureRevert(@Nullable String claimedOriginal) throws RestApiException {
+ try {
+ return getPureRevert.setClaimedOriginal(claimedOriginal).apply(change);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot compute pure revert", e);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 385baba..e3e3e32 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -378,7 +378,7 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws RestApiException, OrmException, IOException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException {
change = ctx.getChange(); // Use defensive copy created by ChangeControl.
ReviewDb db = ctx.getDb();
ChangeControl ctl = ctx.getControl();
@@ -444,7 +444,7 @@
filterOnChangeVisibility(db, ctx.getNotes(), reviewersToAdd),
Collections.<Account.Id>emptySet());
approvalsUtil.addApprovalsForNewPatchSet(
- db, update, labelTypes, patchSet, ctx.getControl(), approvals);
+ db, update, labelTypes, patchSet, ctx.getUser(), approvals);
// Check if approvals are changing in with this update. If so, add current user to reviewers.
// Note that this is done separately as addReviewers is filtering out the change owner as
// reviewer which is needed in several other code paths.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 6e555e5..12c0483 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -48,7 +48,6 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
@@ -246,8 +245,7 @@
if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick
// will be added as a new patch set.
- ChangeControl destCtl = projectControl.controlFor(destChanges.get(0).notes());
- result = insertPatchSet(bu, git, destCtl, cherryPickCommit, input);
+ result = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
} else {
// Change key not found on destination branch. We can create a new
// change.
@@ -321,15 +319,15 @@
private Change.Id insertPatchSet(
BatchUpdate bu,
Repository git,
- ChangeControl destCtl,
+ ChangeNotes destNotes,
CodeReviewCommit cherryPickCommit,
CherryPickInput input)
throws IOException, OrmException, BadRequestException, ConfigInvalidException {
- Change destChange = destCtl.getChange();
+ Change destChange = destNotes.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
- PatchSet current = psUtil.current(dbProvider.get(), destCtl.getNotes());
+ PatchSet current = psUtil.current(dbProvider.get(), destNotes);
- PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit);
+ PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
inserter
.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
.setDraft(current.isDraft())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index a3927b5..0444e0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -76,7 +76,7 @@
input.message = message.isEmpty() ? commit.getFullMessage() : message;
String destination = Strings.nullToEmpty(input.destination).trim();
input.parent = input.parent == null ? 1 : input.parent;
- Project.NameKey projectName = rsrc.getProjectState().getProject().getNameKey();
+ Project.NameKey projectName = rsrc.getProjectState().getNameKey();
if (destination.isEmpty()) {
throw new BadRequestException("destination must be non-empty");
@@ -99,7 +99,7 @@
projectName,
commit,
input,
- new Branch.NameKey(rsrc.getProjectState().getProject().getNameKey(), refName));
+ new Branch.NameKey(rsrc.getProjectState().getNameKey(), refName));
return json.noOptions().format(projectName, cherryPickedChangeId);
} catch (InvalidChangeOperationException e) {
throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 46d8063..76d5550 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -515,7 +515,7 @@
(psIdToDelete != null && reuseOldPsId)
? psIdToDelete
: ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId());
- PatchSetInserter inserter = patchSetInserterFactory.create(ctl, psId, commit);
+ PatchSetInserter inserter = patchSetInserterFactory.create(ctl.getNotes(), psId, commit);
try (BatchUpdate bu = newBatchUpdate()) {
bu.setRepository(repo, rw, oi);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
index b02f31b..072052b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -154,7 +154,7 @@
PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId());
PatchSetInserter psInserter =
- patchSetInserterFactory.create(rsrc.getControl(), nextPsId, newCommit);
+ patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) {
bu.setRepository(git, rw, oi);
bu.addOp(
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 ee93da0..c725089 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
@@ -127,7 +127,7 @@
try (BatchUpdate bu =
updateFactory.create(
- db.get(), change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) {
+ db.get(), change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
bu.addOp(
change.getId(),
new Op(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 181505d..ff5fb0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -315,6 +315,6 @@
private Repository openRepository(ProjectState project)
throws RepositoryNotFoundException, IOException {
- return repoManager.openRepository(project.getProject().getNameKey());
+ return repoManager.openRepository(project.getNameKey());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 1ac5a88f..eec318b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -218,7 +218,7 @@
List<DiffWebLinkInfo> links =
webLinks.getDiffLinks(
- state.getProject().getName(),
+ state.getName(),
resource.getPatchKey().getParentKey().getParentKey().get(),
basePatchSet != null ? basePatchSet.getId().get() : null,
revA,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
new file mode 100644
index 0000000..c849134
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPureRevert.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+public class GetPureRevert implements RestReadView<ChangeResource> {
+ private final MergeUtil.Factory mergeUtilFactory;
+ private final GitRepositoryManager repoManager;
+ private final ProjectCache projectCache;
+ private final ChangeNotes.Factory notesFactory;
+ private final Provider<ReviewDb> dbProvider;
+ private final PatchSetUtil psUtil;
+
+ @Option(
+ name = "--claimed-original",
+ aliases = {"-o"},
+ usage = "SHA1 (40 digit hex) of the original commit"
+ )
+ @Nullable
+ private String claimedOriginal;
+
+ @Inject
+ GetPureRevert(
+ MergeUtil.Factory mergeUtilFactory,
+ GitRepositoryManager repoManager,
+ ProjectCache projectCache,
+ ChangeNotes.Factory notesFactory,
+ Provider<ReviewDb> dbProvider,
+ PatchSetUtil psUtil) {
+ this.mergeUtilFactory = mergeUtilFactory;
+ this.repoManager = repoManager;
+ this.projectCache = projectCache;
+ this.notesFactory = notesFactory;
+ this.dbProvider = dbProvider;
+ this.psUtil = psUtil;
+ }
+
+ @Override
+ public PureRevertInfo apply(ChangeResource rsrc)
+ throws ResourceConflictException, IOException, BadRequestException, OrmException,
+ AuthException {
+ PatchSet currentPatchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
+ if (currentPatchSet == null) {
+ throw new ResourceConflictException("current revision is missing");
+ } else if (!rsrc.getControl().isPatchVisible(currentPatchSet, dbProvider.get())) {
+ throw new AuthException("current revision not accessible");
+ }
+
+ if (claimedOriginal == null) {
+ if (rsrc.getChange().getRevertOf() == null) {
+ throw new BadRequestException("no ID was provided and change isn't a revert");
+ }
+ PatchSet ps =
+ psUtil.current(
+ dbProvider.get(),
+ notesFactory.createChecked(
+ dbProvider.get(), rsrc.getProject(), rsrc.getChange().getRevertOf()));
+ claimedOriginal = ps.getRevision().get();
+ }
+
+ try (Repository repo = repoManager.openRepository(rsrc.getProject());
+ ObjectInserter oi = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(repo)) {
+ RevCommit claimedOriginalCommit;
+ try {
+ claimedOriginalCommit = rw.parseCommit(ObjectId.fromString(claimedOriginal));
+ } catch (InvalidObjectIdException | MissingObjectException e) {
+ throw new BadRequestException("invalid object ID");
+ }
+ if (claimedOriginalCommit.getParentCount() == 0) {
+ throw new BadRequestException("can't check against initial commit");
+ }
+ RevCommit claimedRevertCommit =
+ rw.parseCommit(ObjectId.fromString(currentPatchSet.getRevision().get()));
+ if (claimedRevertCommit.getParentCount() == 0) {
+ throw new BadRequestException("claimed revert has no parents");
+ }
+ // Rebase claimed revert onto claimed original
+ ThreeWayMerger merger =
+ mergeUtilFactory
+ .create(projectCache.checkedGet(rsrc.getProject()))
+ .newThreeWayMerger(oi, repo.getConfig());
+ merger.setBase(claimedRevertCommit.getParent(0));
+ merger.merge(claimedRevertCommit, claimedOriginalCommit);
+ if (merger.getResultTreeId() == null) {
+ // Merge conflict during rebase
+ return new PureRevertInfo(false);
+ }
+
+ // Any differences between claimed original's parent and the rebase result indicate that the
+ // claimedRevert is not a pure revert but made content changes
+ try (DiffFormatter df = new DiffFormatter(new ByteArrayOutputStream())) {
+ df.setRepository(repo);
+ List<DiffEntry> entries =
+ df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
+ return new PureRevertInfo(entries.isEmpty());
+ }
+ }
+ }
+
+ public GetPureRevert setClaimedOriginal(String claimedOriginal) {
+ this.claimedOriginal = claimedOriginal;
+ return this;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index bf76af9..5089574 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -70,6 +70,7 @@
get(CHANGE_KIND, "robotcomments").to(ListChangeRobotComments.class);
get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
get(CHANGE_KIND, "check").to(Check.class);
+ get(CHANGE_KIND, "pure_revert").to(GetPureRevert.class);
post(CHANGE_KIND, "check").to(Check.class);
put(CHANGE_KIND, "topic").to(PutTopic.class);
delete(CHANGE_KIND, "topic").to(PutTopic.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 5e26305..77f4e5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -27,7 +27,6 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -44,12 +43,14 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.NoSshInfo;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -70,7 +71,7 @@
private static final Logger log = LoggerFactory.getLogger(PatchSetInserter.class);
public interface Factory {
- PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, ObjectId commitId);
+ PatchSetInserter create(ChangeNotes notes, PatchSet.Id psId, ObjectId commitId);
}
// Injected fields.
@@ -78,6 +79,7 @@
private final PatchSetInfoFactory patchSetInfoFactory;
private final CommitValidators.Factory commitValidatorsFactory;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+ private final ProjectCache projectCache;
private final RevisionCreated revisionCreated;
private final ApprovalsUtil approvalsUtil;
private final ApprovalCopier approvalCopier;
@@ -88,9 +90,9 @@
private final PatchSet.Id psId;
private final ObjectId commitId;
// Read prior to running the batch update, so must only be used during
- // updateRepo; updateChange and later must use the control from the
+ // updateRepo; updateChange and later must use the notes from the
// ChangeContext.
- private final ChangeControl origCtl;
+ private final ChangeNotes origNotes;
// Fields exposed as setters.
private String message;
@@ -123,7 +125,8 @@
ReplacePatchSetSender.Factory replacePatchSetFactory,
PatchSetUtil psUtil,
RevisionCreated revisionCreated,
- @Assisted ChangeControl ctl,
+ ProjectCache projectCache,
+ @Assisted ChangeNotes notes,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
this.permissionBackend = permissionBackend;
@@ -135,8 +138,9 @@
this.replacePatchSetFactory = replacePatchSetFactory;
this.psUtil = psUtil;
this.revisionCreated = revisionCreated;
+ this.projectCache = projectCache;
- this.origCtl = ctl;
+ this.origNotes = notes;
this.psId = psId;
this.commitId = commitId.copy();
}
@@ -316,7 +320,7 @@
permissionBackend
.user(ctx.getUser())
.database(ctx.getDb())
- .change(origCtl.getNotes())
+ .change(origNotes)
.check(ChangePermission.ADD_PATCH_SET);
}
if (!validate) {
@@ -324,7 +328,7 @@
}
PermissionBackend.ForRef perm =
- permissionBackend.user(ctx.getUser()).ref(origCtl.getChange().getDest());
+ permissionBackend.user(ctx.getUser()).ref(origNotes.getChange().getDest());
String refName = getPatchSetId().toRefName();
try (CommitReceivedEvent event =
@@ -333,16 +337,15 @@
ObjectId.zeroId(),
commitId,
refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
- origCtl.getProjectControl().getProject(),
- origCtl.getRefControl().getRefName(),
+ projectCache.checkedGet(origNotes.getProjectName()).getProject(),
+ origNotes.getChange().getDest().get(),
ctx.getRevWalk().getObjectReader(),
commitId,
ctx.getIdentifiedUser())) {
commitValidatorsFactory
.forGerritCommits(
perm,
- new Branch.NameKey(
- origCtl.getProject().getNameKey(), origCtl.getRefControl().getRefName()),
+ origNotes.getChange().getDest(),
ctx.getIdentifiedUser(),
new NoSshInfo(),
ctx.getRevWalk())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
index 7d17dfb..a1a5ab7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -141,8 +141,7 @@
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId());
ObjectId newCommit =
createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
- PatchSetInserter inserter =
- psInserterFactory.create(resource.getControl(), psId, newCommit);
+ PatchSetInserter inserter = psInserterFactory.create(resource.getNotes(), psId, newCommit);
inserter.setMessage(
String.format("Patch Set %s: Commit message was updated.", psId.getId()));
inserter.setDescription("Edit commit message");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 34d239c..465a1b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -172,7 +172,7 @@
ctl.getChange().currentPatchSetId());
patchSetInserter =
patchSetInserterFactory
- .create(ctl, rebasedPatchSetId, rebasedCommit)
+ .create(ctl.getNotes(), rebasedPatchSetId, rebasedCommit)
.setDescription("Rebase")
.setDraft(originalPatchSet.isDraft())
.setNotify(NotifyHandling.NONE)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index be31c99..310f700 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -20,7 +20,6 @@
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.reviewdb.client.Account;
@@ -84,7 +83,7 @@
ChangeData cd = null;
for (ReviewerResource rsrc : rsrcs) {
if (cd == null || !cd.getId().equals(rsrc.getChangeId())) {
- cd = changeDataFactory.create(db.get(), rsrc.getControl().getNotes());
+ cd = changeDataFactory.create(db.get(), rsrc.getChangeResource().getNotes());
}
ReviewerInfo info =
format(
@@ -125,13 +124,9 @@
out.approvals = new TreeMap<>(labelTypes.nameComparator());
for (PatchSetApproval ca : approvals) {
- for (PermissionRange pr : cd.changeControl().getLabelRanges()) {
- if (!pr.isEmpty()) {
- LabelType at = labelTypes.byLabel(ca.getLabelId());
- if (at != null) {
- out.approvals.put(at.getName(), formatValue(ca.getValue()));
- }
- }
+ LabelType at = labelTypes.byLabel(ca.getLabelId());
+ if (at != null) {
+ out.approvals.put(at.getName(), formatValue(ca.getValue()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
index f6f7919..47e25b04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -23,7 +23,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
@@ -114,25 +113,4 @@
public boolean isByEmail() {
return user == null;
}
-
- /**
- * Get the control for the caller's user.
- *
- * @return the control for the caller's user (as opposed to the reviewer's user as returned by
- * {@link #getReviewerControl()}).
- */
- public ChangeControl getControl() {
- return change.getControl();
- }
-
- /**
- * Get the control for the reviewer's user.
- *
- * @return the control for the reviewer's user (as opposed to the caller's user as returned by
- * {@link #getControl()}).
- */
- public ChangeControl getReviewerControl() {
- checkArgument(user != null, "no user provided");
- return change.getControl().forUser(user);
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
index ddf48fd..980e6e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -84,7 +84,7 @@
Iterable<PatchSetApproval> byPatchSetUser =
approvalsUtil.byPatchSetUser(
db.get(),
- rsrc.getControl(),
+ rsrc.getChangeResource().getControl(),
rsrc.getChange().currentPatchSetId(),
rsrc.getReviewerUser().getAccountId(),
null,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 743df20..917c005 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -180,7 +180,7 @@
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
PatchSetInserter inserter =
patchSetInserterFactory
- .create(ctl, psId, squashed)
+ .create(ctl.getNotes(), psId, squashed)
.setNotify(notify)
.setAccountsToNotify(accountsToNotify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index a3f5093..2eeed24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -24,13 +24,15 @@
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -76,15 +78,18 @@
private final Provider<ReviewDb> db;
private final ChangeControl.GenericFactory changeFactory;
private final IdentifiedUser.GenericFactory userFactory;
+ private final PermissionBackend permissionBackend;
@Inject
LabelNormalizer(
Provider<ReviewDb> db,
ChangeControl.GenericFactory changeFactory,
- IdentifiedUser.GenericFactory userFactory) {
+ IdentifiedUser.GenericFactory userFactory,
+ PermissionBackend permissionBackend) {
this.db = db;
this.changeFactory = changeFactory;
this.userFactory = userFactory;
+ this.permissionBackend = permissionBackend;
}
/**
@@ -96,7 +101,7 @@
* @throws OrmException
*/
public Result normalize(Change change, Collection<PatchSetApproval> approvals)
- throws OrmException {
+ throws OrmException, PermissionBackendException {
IdentifiedUser user = userFactory.create(change.getOwner());
return normalize(changeFactory.controlFor(db.get(), change, user), approvals);
}
@@ -108,7 +113,8 @@
* for the user. Approvals for unknown labels are not included in the output, nor are
* approvals where the user has no permissions for that label.
*/
- public Result normalize(ChangeControl ctl, Collection<PatchSetApproval> approvals) {
+ public Result normalize(ChangeControl ctl, Collection<PatchSetApproval> approvals)
+ throws PermissionBackendException {
List<PatchSetApproval> unchanged = Lists.newArrayListWithCapacity(approvals.size());
List<PatchSetApproval> updated = Lists.newArrayListWithCapacity(approvals.size());
List<PatchSetApproval> deleted = Lists.newArrayListWithCapacity(approvals.size());
@@ -132,7 +138,7 @@
}
PatchSetApproval copy = copy(psa);
applyTypeFloor(label, copy);
- if (!applyRightFloor(ctl, label, copy)) {
+ if (!applyRightFloor(ctl.getNotes(), label, copy)) {
deleted.add(psa);
} else if (copy.getValue() != psa.getValue()) {
updated.add(copy);
@@ -147,19 +153,24 @@
return new PatchSetApproval(src.getPatchSetId(), src);
}
- private PermissionRange getRange(ChangeControl ctl, LabelType lt, Account.Id id) {
- String permission = Permission.forLabel(lt.getName());
- IdentifiedUser user = userFactory.create(id);
- return ctl.forUser(user).getRange(permission);
- }
-
- private boolean applyRightFloor(ChangeControl ctl, LabelType lt, PatchSetApproval a) {
- PermissionRange range = getRange(ctl, lt, a.getAccountId());
- if (range.isEmpty()) {
+ private boolean applyRightFloor(ChangeNotes notes, LabelType lt, PatchSetApproval a)
+ throws PermissionBackendException {
+ PermissionBackend.ForChange forChange =
+ permissionBackend.user(userFactory.create(a.getAccountId())).database(db).change(notes);
+ // Check if the user is allowed to vote on the label at all
+ try {
+ forChange.check(new LabelPermission(lt.getName()));
+ } catch (AuthException e) {
return false;
}
- a.setValue((short) range.squash(a.getValue()));
- return true;
+ // Squash vote to nearest allowed value
+ try {
+ forChange.check(new LabelPermission.WithValue(lt.getName(), a.getValue()));
+ return true;
+ } catch (AuthException e) {
+ a.setValue(forChange.squashThenCheck(lt, a.getValue()));
+ return true;
+ }
}
private void applyTypeFloor(LabelType lt, PatchSetApproval a) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index e7303e8..29b2548 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -95,7 +95,7 @@
}
Project.NameKey getProjectName() {
- return project.getProject().getNameKey();
+ return project.getNameKey();
}
public CodeReviewRevWalk getCodeReviewRevWalk() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 07ea774..9d7dd19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -77,6 +77,7 @@
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> user;
private final PermissionBackend permissionBackend;
+ private final PermissionBackend.ForProject perm;
private final ProjectState projectState;
private final Repository git;
private ProjectControl projectCtl;
@@ -100,6 +101,8 @@
this.db = db;
this.user = user;
this.permissionBackend = permissionBackend;
+ this.perm =
+ permissionBackend.user(user).database(db).project(projectState.getProject().getNameKey());
this.projectState = projectState;
this.git = git;
}
@@ -185,7 +188,7 @@
if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeparately)) {
TagMatcher tags =
tagCache
- .get(projectState.getProject().getNameKey())
+ .get(projectState.getNameKey())
.matcher(
tagCache,
git,
@@ -265,31 +268,25 @@
}
private Map<Change.Id, Branch.NameKey> visibleChangesBySearch() {
- Project project = projectCtl.getProject();
+ Project.NameKey project = projectState.getNameKey();
try {
Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
- for (ChangeData cd : changeCache.getChangeData(db.get(), project.getNameKey())) {
- if (permissionBackend
- .user(user)
- .indexedChange(cd, changeNotesFactory.createFromIndexedChange(cd.change()))
- .database(db)
- .test(ChangePermission.READ)) {
+ for (ChangeData cd : changeCache.getChangeData(db.get(), project)) {
+ ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
+ if (perm.indexedChange(cd, notes).test(ChangePermission.READ)) {
visibleChanges.put(cd.getId(), cd.change().getDest());
}
}
return visibleChanges;
} catch (OrmException | PermissionBackendException e) {
log.error(
- "Cannot load changes for project "
- + project.getName()
- + ", assuming no changes are visible",
- e);
+ "Cannot load changes for project " + project + ", assuming no changes are visible", e);
return Collections.emptyMap();
}
}
private Map<Change.Id, Branch.NameKey> visibleChangesByScan() {
- Project.NameKey p = projectCtl.getProject().getNameKey();
+ Project.NameKey p = projectState.getNameKey();
Stream<ChangeNotesResult> s;
try {
s = changeNotesFactory.scan(git, db.get(), p);
@@ -309,7 +306,7 @@
return null;
}
try {
- if (permissionBackend.user(user).change(r.notes()).database(db).test(ChangePermission.READ)) {
+ if (perm.change(r.notes()).test(ChangePermission.READ)) {
return r.notes();
}
} catch (PermissionBackendException e) {
@@ -330,17 +327,13 @@
private boolean canReadRef(String ref) {
try {
- permissionBackend
- .user(user)
- .project(projectCtl.getProject().getNameKey())
- .ref(ref)
- .check(RefPermission.READ);
+ perm.ref(ref).check(RefPermission.READ);
+ return true;
} catch (AuthException e) {
return false;
} catch (PermissionBackendException e) {
log.error("unable to check permissions", e);
return false;
}
- return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 80c896f..042dad2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -23,6 +23,7 @@
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
import static com.google.gerrit.server.git.receive.ReceiveConstants.ONLY_OWNER_CAN_MODIFY_WIP;
+import static com.google.gerrit.server.git.receive.ReceiveConstants.PUSH_OPTION_SKIP_VALIDATION;
import static com.google.gerrit.server.git.receive.ReceiveConstants.SAME_CHANGE_ID_IN_MULTIPLE_CHANGES;
import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
@@ -198,7 +199,6 @@
/** Receives change upload using the Git receive-pack protocol. */
class ReceiveCommits {
private static final Logger log = LoggerFactory.getLogger(ReceiveCommits.class);
- private static final String BYPASS_REVIEW = "bypass-review";
private enum ReceiveError {
CONFIG_UPDATE(
@@ -362,6 +362,7 @@
// Other settings populated during processing.
private MagicBranchInput magicBranch;
private boolean newChangeForAllNotInTarget;
+ private String setFullNameTo;
// Handles for outputting back over the wire to the end user.
private Task newProgress;
@@ -587,6 +588,9 @@
}
}
+ // Update account info with details discovered during commit walking.
+ updateAccountInfo();
+
closeProgress.end();
commandProgress.end();
progress.end();
@@ -1000,9 +1004,10 @@
}
Branch.NameKey branch = new Branch.NameKey(project.getName(), cmd.getRefName());
- String rejectReason = createRefControl.canCreateRef(rp.getRepository(), obj, user, branch);
- if (rejectReason != null) {
- reject(cmd, "prohibited by Gerrit: " + rejectReason);
+ try {
+ createRefControl.checkCreateRef(rp.getRepository(), branch, obj);
+ } catch (AuthException denied) {
+ reject(cmd, "prohibited by Gerrit: " + denied.getMessage());
return;
}
@@ -2681,11 +2686,11 @@
if (!RefNames.REFS_CONFIG.equals(cmd.getRefName())
&& !(MagicBranch.isMagicBranch(cmd.getRefName())
|| NEW_PATCHSET_PATTERN.matcher(cmd.getRefName()).matches())
- && pushOptions.containsKey(BYPASS_REVIEW)) {
+ && pushOptions.containsKey(PUSH_OPTION_SKIP_VALIDATION)) {
try {
- perm.check(RefPermission.BYPASS_REVIEW);
+ perm.check(RefPermission.SKIP_VALIDATION);
if (!Iterables.isEmpty(rejectCommits)) {
- throw new AuthException("reject-commits prevents " + BYPASS_REVIEW);
+ throw new AuthException("reject-commits prevents " + PUSH_OPTION_SKIP_VALIDATION);
}
logDebug("Short-circuiting new commit validation");
} catch (AuthException denied) {
@@ -2694,7 +2699,7 @@
return;
}
- boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName());
+ boolean missingFullName = Strings.isNullOrEmpty(user.getAccount().getFullName());
RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.NONE);
@@ -2706,39 +2711,35 @@
ListMultimap<ObjectId, Ref> existing = changeRefsById();
walk.markStart((RevCommit) parsedObject);
markHeadsAsUninteresting(walk, cmd.getRefName());
- int i = 0;
+ int limit = receiveConfig.maxBatchCommits;
+ int n = 0;
for (RevCommit c; (c = walk.next()) != null; ) {
- i++;
+ if (++n > limit) {
+ logDebug("Number of new commits exceeds limit of {}", limit);
+ addMessage(
+ "Cannot push more than "
+ + limit
+ + " commits to "
+ + branch.get()
+ + " without "
+ + PUSH_OPTION_SKIP_VALIDATION
+ + " option");
+ reject(cmd, "too many commits");
+ return;
+ }
if (existing.keySet().contains(c)) {
continue;
} else if (!validCommit(walk, perm, branch, cmd, c)) {
break;
}
- if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
- try {
- String committerName = c.getCommitterIdent().getName();
- Account account =
- accountsUpdate
- .create()
- .update(
- user.getAccountId(),
- a -> {
- if (Strings.isNullOrEmpty(a.getFullName())) {
- a.setFullName(committerName);
- }
- });
- if (account != null && Strings.isNullOrEmpty(account.getFullName())) {
- user.getAccount().setFullName(account.getFullName());
- }
- } catch (IOException | ConfigInvalidException e) {
- logWarn("Cannot default full_name", e);
- } finally {
- defaultName = false;
- }
+ if (missingFullName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
+ logDebug("Will update full name of caller");
+ setFullNameTo = c.getCommitterIdent().getName();
+ missingFullName = false;
}
}
- logDebug("Validated {} new commits", i);
+ logDebug("Validated {} new commits", n);
} catch (IOException err) {
cmd.setResult(REJECTED_MISSING_OBJECT);
logError("Invalid pack upload; one or more objects weren't sent", err);
@@ -2880,6 +2881,30 @@
}
}
+ private void updateAccountInfo() {
+ if (setFullNameTo == null) {
+ return;
+ }
+ logDebug("Updating full name of caller");
+ try {
+ Account account =
+ accountsUpdate
+ .create()
+ .update(
+ user.getAccountId(),
+ a -> {
+ if (Strings.isNullOrEmpty(a.getFullName())) {
+ a.setFullName(setFullNameTo);
+ }
+ });
+ if (account != null) {
+ user.getAccount().setFullName(account.getFullName());
+ }
+ } catch (IOException | ConfigInvalidException e) {
+ logWarn("Failed to update full name of caller", e);
+ }
+ }
+
private Map<Change.Key, ChangeNotes> openChangesByBranch(Branch.NameKey branch)
throws OrmException {
Map<Change.Key, ChangeNotes> r = new HashMap<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
index 7be6dcc..30d5071 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConfig.java
@@ -28,6 +28,7 @@
final boolean checkMagicRefs;
final boolean checkReferencedObjectsAreReachable;
final boolean allowDrafts;
+ final int maxBatchCommits;
private final int systemMaxBatchChanges;
private final AccountLimits.Factory limitsFactory;
@@ -37,6 +38,7 @@
checkReferencedObjectsAreReachable =
config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
allowDrafts = config.getBoolean("change", null, "allowDrafts", true);
+ maxBatchCommits = config.getInt("receive", null, "maxBatchCommits", 10000);
systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
this.limitsFactory = limitsFactory;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index 99742f3..92723e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -17,6 +17,8 @@
import com.google.common.annotations.VisibleForTesting;
public final class ReceiveConstants {
+ public static final String PUSH_OPTION_SKIP_VALIDATION = "skip-validation";
+
@VisibleForTesting
public static final String ONLY_OWNER_CAN_MODIFY_WIP =
"only change owner can modify Work-in-Progress";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0187304..3399071 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -52,6 +52,7 @@
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.change.ChangeData;
@@ -224,7 +225,7 @@
@Override
public boolean updateChange(ChangeContext ctx)
- throws RestApiException, OrmException, IOException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException {
notes = ctx.getNotes();
Change change = notes.getChange();
if (change == null || change.getStatus().isClosed()) {
@@ -306,7 +307,7 @@
update,
projectControl.getProjectState().getLabelTypes(),
newPatchSet,
- ctx.getControl(),
+ ctx.getUser(),
approvals);
approvalCopier.copyInReviewDb(
ctx.getDb(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index a3163c3..affa918 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -45,6 +45,7 @@
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.SubmoduleException;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -330,7 +331,7 @@
}
private void setApproval(ChangeContext ctx, IdentifiedUser user)
- throws OrmException, IOException {
+ throws OrmException, IOException, PermissionBackendException {
Change.Id id = ctx.getChange().getId();
List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
PatchSet.Id oldPsId = toMerge.getPatchsetId();
@@ -352,7 +353,7 @@
}
private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
- throws OrmException, IOException {
+ throws OrmException, IOException, PermissionBackendException {
PatchSet.Id psId = update.getPatchSetId();
Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
for (PatchSetApproval psa :
@@ -525,7 +526,7 @@
try (Repository git = args.repoManager.openRepository(getProject())) {
git.setGitwebDescription(p.getProject().getDescription());
} catch (IOException e) {
- log.error("cannot update description of " + p.getProject().getName(), e);
+ log.error("cannot update description of " + p.getName(), e);
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index b384405..f4deee8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -391,7 +391,7 @@
+ " tried to push an invalid project configuration "
+ receiveEvent.command.getNewId().name()
+ " for project "
- + receiveEvent.project.getName(),
+ + receiveEvent.project,
e);
throw new CommitValidationException("invalid project configuration", messages);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index fd524b4..51e47a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -145,7 +145,7 @@
if (RefNames.REFS_CONFIG.equals(destBranch.get())) {
final Project.NameKey newParent;
try {
- ProjectConfig cfg = new ProjectConfig(destProject.getProject().getNameKey());
+ ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
cfg.load(repo, commit);
newParent = cfg.getProject().getParent(allProjectsName);
final Project.NameKey oldParent = destProject.getProject().getParent(allProjectsName);
@@ -256,7 +256,7 @@
IdentifiedUser caller)
throws MergeValidationException {
Account.Id accountId = Account.Id.fromRef(destBranch.get());
- if (!allUsersName.equals(destProject.getProject().getNameKey()) || accountId == null) {
+ if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
return;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
index 7d83c5c..2d1fc56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -258,9 +258,7 @@
log.warn(
String.format(
"Cannot load %s from %s in %s",
- c.key.filename,
- patchList.getNewId().name(),
- projectState.getProject().getName()),
+ c.key.filename, patchList.getNewId().name(), projectState.getName()),
e);
currentGroup.fileData = null;
}
@@ -586,7 +584,7 @@
private Repository getRepository() {
try {
- return args.server.openRepository(projectState.getProject().getNameKey());
+ return args.server.openRepository(projectState.getNameKey());
} catch (IOException e) {
return null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index d1434bc..e1b6e36 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -103,7 +103,7 @@
} catch (QueryParseException e) {
log.warn(
"Project {} has invalid notify {} filter \"{}\": {}",
- state.getProject().getName(),
+ state.getName(),
nc.getName(),
nc.getFilter(),
e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 5c0cf44..6db9357 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -268,6 +268,15 @@
return ref(notes.getChange().getDest().get()).change(notes);
}
+ /**
+ * @return instance scoped for the change loaded from index, and its destination ref and
+ * project. This method should only be used when database access is harmful and potentially
+ * stale data from the index is acceptable.
+ */
+ public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+ return ref(notes.getChange().getDest().get()).indexedChange(cd, notes);
+ }
+
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(ProjectPermission perm)
throws AuthException, PermissionBackendException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
index e03272b..8b5d8fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -29,7 +29,7 @@
FORGE_COMMITTER(Permission.FORGE_COMMITTER),
FORGE_SERVER(Permission.FORGE_SERVER),
MERGE,
- BYPASS_REVIEW,
+ SKIP_VALIDATION,
/** Create a change to code review a commit. */
CREATE_CHANGE,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index b9649ed..f5e4542 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -48,7 +48,6 @@
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
-import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -269,13 +268,8 @@
return canAbandon(db) && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
}
- /** All value ranges of any allowed label permission. */
- public List<PermissionRange> getLabelRanges() {
- return getRefControl().getLabelRanges(isOwner());
- }
-
/** The range of permitted values associated with a label permission. */
- public PermissionRange getRange(String permission) {
+ private PermissionRange getRange(String permission) {
return getRefControl().getRange(permission, isOwner());
}
@@ -315,7 +309,7 @@
}
/** Is this user the owner of the change? */
- boolean isOwner() {
+ private boolean isOwner() {
if (getUser().isIdentifiedUser()) {
Account.Id id = getUser().asIdentifiedUser().getAccountId();
return id.equals(getChange().getOwner());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
index edd3fb6..b372b38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -41,6 +41,6 @@
public boolean isDirectChild() {
ProjectState firstParent = Iterables.getFirst(child.parents(), null);
- return firstParent != null && parent.getNameKey().equals(firstParent.getProject().getNameKey());
+ return firstParent != null && parent.getNameKey().equals(firstParent.getNameKey());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
index 3152e97..5b36916 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitIncludedIn.java
@@ -38,7 +38,7 @@
public IncludedInInfo apply(CommitResource rsrc)
throws RestApiException, OrmException, IOException {
RevCommit commit = rsrc.getCommit();
- Project.NameKey project = rsrc.getProjectState().getProject().getNameKey();
+ Project.NameKey project = rsrc.getProjectState().getNameKey();
return includedIn.apply(project, commit.getId().getName());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
index e38f442..a504a1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -101,7 +101,7 @@
/** @return true if {@code commit} is visible to the caller. */
public boolean canRead(ProjectState state, Repository repo, RevCommit commit) {
- Project.NameKey project = state.getProject().getNameKey();
+ Project.NameKey project = state.getNameKey();
// Look for changes associated with the commit.
try {
@@ -126,7 +126,7 @@
log.error(
String.format(
"Cannot verify permissions to commit object %s in repository %s",
- commit.name(), state.getProject().getNameKey()),
+ commit.name(), state.getNameKey()),
e);
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index 34c3287..eb0dde4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -159,7 +159,7 @@
p.type = configEntry.getType();
p.permittedValues = configEntry.getPermittedValues();
p.editable = configEntry.isEditable(project) ? true : null;
- if (configEntry.isInheritable() && !allProjects.equals(project.getProject().getNameKey())) {
+ if (configEntry.isInheritable() && !allProjects.equals(project.getNameKey())) {
PluginConfig cfgWithInheritance =
cfgFactory.getFromProjectConfigWithInheritance(project, e.getPluginName());
p.inheritable = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 77fb86b..f15571c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -121,10 +121,7 @@
}
}
- String rejectReason = createRefControl.canCreateRef(repo, object, identifiedUser.get(), name);
- if (rejectReason != null) {
- throw new AuthException("Cannot create \"" + ref + "\": " + rejectReason);
- }
+ createRefControl.checkCreateRef(repo, name, object);
try {
final RefUpdate u = repo.updateRef(ref);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java
index aa48a73..2034cc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateRefControl.java
@@ -14,14 +14,14 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
+import com.google.inject.Provider;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -41,137 +41,103 @@
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
+ private final Provider<CurrentUser> user;
@Inject
- CreateRefControl(PermissionBackend permissionBackend, ProjectCache projectCache) {
+ CreateRefControl(
+ PermissionBackend permissionBackend, ProjectCache projectCache, Provider<CurrentUser> user) {
this.permissionBackend = permissionBackend;
this.projectCache = projectCache;
+ this.user = user;
}
/**
- * Determines whether the user can create a new Git ref.
+ * Checks whether the {@link CurrentUser} can create a new Git ref.
*
* @param repo repository on which user want to create
- * @param object the object the user will start the reference with
- * @param user the current identified user
* @param branch the branch the new {@link RevObject} should be created on
- * @return {@code null} if the user specified can create a new Git ref, or a String describing why
- * the creation is not allowed.
- * @throws PermissionBackendException on failure of permission checks
+ * @param object the object the user will start the reference with
+ * @throws AuthException if creation is denied; the message explains the denial.
+ * @throws PermissionBackendException on failure of permission checks.
*/
- @Nullable
- public String canCreateRef(
- Repository repo, RevObject object, IdentifiedUser user, Branch.NameKey branch)
- throws PermissionBackendException, NoSuchProjectException, IOException {
+ public void checkCreateRef(Repository repo, Branch.NameKey branch, RevObject object)
+ throws AuthException, PermissionBackendException, NoSuchProjectException, IOException {
ProjectState ps = projectCache.checkedGet(branch.getParentKey());
if (ps == null) {
throw new NoSuchProjectException(branch.getParentKey());
}
if (!ps.getProject().getState().permitsWrite()) {
- return "project state does not permit write";
+ throw new AuthException("project state does not permit write");
}
PermissionBackend.ForRef perm = permissionBackend.user(user).ref(branch);
if (object instanceof RevCommit) {
- if (!testAuditLogged(perm, RefPermission.CREATE)) {
- return user.getAccountId() + " lacks permission: " + Permission.CREATE;
- }
- return canCreateCommit(repo, (RevCommit) object, ps, user, perm);
+ perm.check(RefPermission.CREATE);
+ checkCreateCommit(repo, (RevCommit) object, ps, perm);
} else if (object instanceof RevTag) {
- final RevTag tag = (RevTag) object;
+ RevTag tag = (RevTag) object;
try (RevWalk rw = new RevWalk(repo)) {
rw.parseBody(tag);
} catch (IOException e) {
- String msg =
- String.format("RevWalk(%s) for pushing tag %s:", branch.getParentKey(), tag.name());
- log.error(msg, e);
-
- return "I/O exception for revwalk";
+ log.error(String.format("RevWalk(%s) parsing %s:", branch.getParentKey(), tag.name()), e);
+ throw e;
}
// If tagger is present, require it matches the user's email.
- //
- final PersonIdent tagger = tag.getTaggerIdent();
- if (tagger != null) {
- boolean valid;
- if (user.isIdentifiedUser()) {
- final String addr = tagger.getEmailAddress();
- valid = user.asIdentifiedUser().hasEmailAddress(addr);
- } else {
- valid = false;
- }
- if (!valid && !testAuditLogged(perm, RefPermission.FORGE_COMMITTER)) {
- return user.getAccountId() + " lacks permission: " + Permission.FORGE_COMMITTER;
- }
+ PersonIdent tagger = tag.getTaggerIdent();
+ if (tagger != null
+ && (!user.get().isIdentifiedUser()
+ || !user.get().asIdentifiedUser().hasEmailAddress(tagger.getEmailAddress()))) {
+ perm.check(RefPermission.FORGE_COMMITTER);
}
- RevObject tagObject = tag.getObject();
- if (tagObject instanceof RevCommit) {
- String rejectReason = canCreateCommit(repo, (RevCommit) tagObject, ps, user, perm);
- if (rejectReason != null) {
- return rejectReason;
- }
+ RevObject target = tag.getObject();
+ if (target instanceof RevCommit) {
+ checkCreateCommit(repo, (RevCommit) target, ps, perm);
} else {
- String rejectReason = canCreateRef(repo, tagObject, user, branch);
- if (rejectReason != null) {
- return rejectReason;
- }
+ checkCreateRef(repo, branch, target);
}
// If the tag has a PGP signature, allow a lower level of permission
// than if it doesn't have a PGP signature.
- //
- RefControl refControl = ps.controlFor(user).controlForRef(branch);
+ RefControl refControl = ps.controlFor(user.get()).controlForRef(branch);
if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
- return refControl.canPerform(Permission.CREATE_SIGNED_TAG)
- ? null
- : user.getAccountId() + " lacks permission: " + Permission.CREATE_SIGNED_TAG;
+ if (!refControl.canPerform(Permission.CREATE_SIGNED_TAG)) {
+ throw new AuthException(Permission.CREATE_SIGNED_TAG + " not permitted");
+ }
+ } else if (!refControl.canPerform(Permission.CREATE_TAG)) {
+ throw new AuthException(Permission.CREATE_TAG + " not permitted");
}
- return refControl.canPerform(Permission.CREATE_TAG)
- ? null
- : user.getAccountId() + " lacks permission " + Permission.CREATE_TAG;
}
-
- return null;
}
/**
- * Check if the user is allowed to create a new commit object if this introduces a new commit to
- * the project. If not allowed, returns a string describing why it's not allowed. The userId
- * argument is only used for the error message.
+ * Check if the user is allowed to create a new commit object if this creation would introduce a
+ * new commit to the repository.
*/
- @Nullable
- private String canCreateCommit(
- Repository repo,
- RevCommit commit,
- ProjectState projectState,
- IdentifiedUser user,
- PermissionBackend.ForRef forRef)
- throws PermissionBackendException {
- if (projectState.controlFor(user).isReachableFromHeadsOrTags(repo, commit)) {
+ private void checkCreateCommit(
+ Repository repo, RevCommit commit, ProjectState projectState, PermissionBackend.ForRef forRef)
+ throws AuthException, PermissionBackendException {
+ try {
+ // If the user has update (push) permission, they can create the ref regardless
+ // of whether they are pushing any new objects along with the create.
+ forRef.check(RefPermission.UPDATE);
+ return;
+ } catch (AuthException denied) {
+ // Fall through to check reachability.
+ }
+
+ if (projectState.controlFor(user.get()).isReachableFromHeadsOrTags(repo, commit)) {
// If the user has no push permissions, check whether the object is
// merged into a branch or tag readable by this user. If so, they are
// not effectively "pushing" more objects, so they can create the ref
// even if they don't have push permission.
- return null;
- } else if (testAuditLogged(forRef, RefPermission.UPDATE)) {
- // If the user has push permissions, they can create the ref regardless
- // of whether they are pushing any new objects along with the create.
- return null;
+ return;
}
- return user.getAccountId()
- + " lacks permission "
- + Permission.PUSH
- + " for creating new commit object";
- }
- private boolean testAuditLogged(PermissionBackend.ForRef forRef, RefPermission p)
- throws PermissionBackendException {
- try {
- forRef.check(p);
- } catch (AuthException e) {
- return false;
- }
- return true;
+ throw new AuthException(
+ String.format(
+ "%s for creating new commit object not permitted",
+ RefPermission.UPDATE.describeForException()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 8c1c1e2..82462b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -34,7 +34,7 @@
public static FileResource create(
GitRepositoryManager repoManager, ProjectState projectState, ObjectId rev, String path)
throws ResourceNotFoundException, IOException {
- try (Repository repo = repoManager.openRepository(projectState.getProject().getNameKey());
+ try (Repository repo = repoManager.openRepository(projectState.getNameKey());
RevWalk rw = new RevWalk(repo)) {
RevTree tree = rw.parseTree(rev);
if (TreeWalk.forPath(repo, path, tree) != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
index 53e1baa..afffdfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -39,6 +39,6 @@
if (recursive || rsrc.isDirectChild()) {
return json.format(rsrc.getChild().getProject());
}
- throw new ResourceNotFoundException(rsrc.getChild().getProject().getName());
+ throw new ResourceNotFoundException(rsrc.getChild().getName());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Index.java
index 8a8e8d3..8c8314b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Index.java
@@ -32,6 +32,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.util.concurrent.Future;
import org.eclipse.jgit.util.io.NullOutputStream;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@@ -60,7 +61,11 @@
.beginSubTask("", MultiProgressMonitor.UNKNOWN);
AllChangesIndexer allChangesIndexer = allChangesIndexerProvider.get();
allChangesIndexer.setVerboseOut(NullOutputStream.INSTANCE);
- executor.submit(allChangesIndexer.reindexProject(indexer, project, mpt, mpt));
+ // The REST call is just a trigger for async reindexing, so it is safe to ignore the future's
+ // return value.
+ @SuppressWarnings("unused")
+ Future<Void> ignored =
+ executor.submit(allChangesIndexer.reindexProject(indexer, project, mpt, mpt));
return Response.accepted("Project " + project + " submitted for reindexing");
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
index 23a4417..e5fe37d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -77,7 +77,7 @@
for (Project.NameKey name : projectCache.all()) {
ProjectState c = projectCache.get(name);
if (c != null && parent.equals(c.getProject().getParent(allProjects))) {
- children.put(c.getProject().getNameKey(), c.getProject());
+ children.put(c.getNameKey(), c.getProject());
}
}
return permissionBackend
@@ -105,7 +105,7 @@
for (Project.NameKey name : projectCache.all()) {
ProjectState c = projectCache.get(name);
if (c != null) {
- projects.put(c.getProject().getNameKey(), c.getProject());
+ projects.put(c.getNameKey(), c.getProject());
}
}
return projects;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index e1d6c14..f81e84b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -93,7 +93,7 @@
private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException {
Map<Project.NameKey, ProjectState> tree = new LinkedHashMap<>();
for (ProjectState ps : rsrc.getProjectState().tree()) {
- tree.put(ps.getProject().getNameKey(), ps);
+ tree.put(ps.getNameKey(), ps);
}
tree.keySet()
.retainAll(permissionBackend.user(user).filter(ProjectPermission.ACCESS, tree.keySet()));
@@ -102,10 +102,8 @@
private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
throws ResourceNotFoundException, IOException, PermissionBackendException {
- Project.NameKey projectName = state.getProject().getNameKey();
- PermissionBackend.ForProject perm =
- permissionBackend.user(user).project(state.getProject().getNameKey());
- try (Repository git = gitManager.openRepository(projectName);
+ PermissionBackend.ForProject perm = permissionBackend.user(user).project(state.getNameKey());
+ try (Repository git = gitManager.openRepository(state.getNameKey());
RevWalk rw = new RevWalk(git)) {
List<DashboardInfo> all = new ArrayList<>();
for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index a284d7d..522aa89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -373,12 +373,12 @@
ProjectState parent = Iterables.getFirst(e.parents(), null);
if (parent != null) {
if (isParentAccessible(accessibleParents, perm, parent)) {
- info.parent = parent.getProject().getName();
+ info.parent = parent.getName();
} else {
- info.parent = hiddenNames.get(parent.getProject().getName());
+ info.parent = hiddenNames.get(parent.getName());
if (info.parent == null) {
info.parent = "?-" + (hiddenNames.size() + 1);
- hiddenNames.put(parent.getProject().getName(), info.parent);
+ hiddenNames.put(parent.getName(), info.parent);
}
}
}
@@ -506,8 +506,7 @@
} else {
log.warn(
String.format(
- "parent project %s of project %s not found",
- parent.get(), ps.getProject().getName()));
+ "parent project %s of project %s not found", parent.get(), ps.getName()));
}
}
}
@@ -518,7 +517,7 @@
private boolean isParentAccessible(
Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState p)
throws PermissionBackendException {
- Project.NameKey name = p.getProject().getNameKey();
+ Project.NameKey name = p.getNameKey();
Boolean b = checked.get(name);
if (b == null) {
try {
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 2011cd5..a3bee39 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
@@ -274,8 +274,7 @@
if (!canPerformOnAnyRef(Permission.PUSH)
&& !canPerformOnAnyRef(Permission.CREATE_TAG)
&& !isOwner()) {
- String pName = state.getProject().getName();
- return new Capable("Upload denied for project '" + pName + "'");
+ return new Capable("Upload denied for project '" + state.getName() + "'");
}
if (state.isUseContributorAgreements()) {
return verifyActiveContributorAgreement();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
index 9d9e5bb..ac8d536 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -44,7 +44,7 @@
allProjectsName = all;
seen = Sets.newLinkedHashSet();
- seen.add(firstResult.getProject().getNameKey());
+ seen.add(firstResult.getNameKey());
next = firstResult;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index d4c344b..72e5ee6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -180,7 +180,7 @@
}
private boolean isRevisionOutOfDate() {
- try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+ try (Repository git = gitMgr.openRepository(getNameKey())) {
Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
if (ref == null || ref.getObjectId() == null) {
return true;
@@ -203,7 +203,7 @@
public PrologEnvironment newPrologEnvironment() throws CompileException {
PrologMachineCopy pmc = rulesMachine;
if (pmc == null) {
- pmc = rulesCache.loadMachine(getProject().getNameKey(), config.getRulesId());
+ pmc = rulesCache.loadMachine(getNameKey(), config.getRulesId());
rulesMachine = pmc;
}
return envFactory.create(pmc);
@@ -226,6 +226,14 @@
return config.getProject();
}
+ public Project.NameKey getNameKey() {
+ return getProject().getNameKey();
+ }
+
+ public String getName() {
+ return getNameKey().get();
+ }
+
public ProjectConfig getConfig() {
return config;
}
@@ -236,10 +244,10 @@
}
ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
- try (Repository git = gitMgr.openRepository(getProject().getNameKey())) {
+ try (Repository git = gitMgr.openRepository(getNameKey())) {
cfg.load(git);
} catch (IOException | ConfigInvalidException e) {
- log.warn("Failed to load " + fileName + " for " + getProject().getName(), e);
+ log.warn("Failed to load " + fileName + " for " + getName(), e);
}
configs.put(fileName, cfg);
@@ -268,7 +276,7 @@
section.setPermissions(copy);
}
- SectionMatcher matcher = SectionMatcher.wrap(getProject().getNameKey(), section);
+ SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section);
if (matcher != null) {
sm.add(matcher);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 0775bcf..c4a7eb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -105,7 +105,7 @@
public ConfigInfo apply(ProjectState projectState, ConfigInput input)
throws ResourceNotFoundException, BadRequestException, ResourceConflictException {
- Project.NameKey projectName = projectState.getProject().getNameKey();
+ Project.NameKey projectName = projectState.getNameKey();
if (input == null) {
throw new BadRequestException("config is required");
}
@@ -309,7 +309,7 @@
throw new BadRequestException(
String.format(
"Not allowed to set parameter '%s' of plugin '%s' on project '%s'.",
- parameterName, pluginName, projectState.getProject().getName()));
+ parameterName, pluginName, projectState.getName()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index a749759..3736360 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -323,27 +323,6 @@
return canForcePerform(Permission.EDIT_TOPIC_NAME);
}
- /** All value ranges of any allowed label permission. */
- List<PermissionRange> getLabelRanges(boolean isChangeOwner) {
- List<PermissionRange> r = new ArrayList<>();
- for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
- if (Permission.isLabel(e.getKey())) {
- int min = 0;
- int max = 0;
- for (PermissionRule rule : e.getValue()) {
- if (projectControl.match(rule, isChangeOwner)) {
- min = Math.min(min, rule.getMin());
- max = Math.max(max, rule.getMax());
- }
- }
- if (min != 0 || max != 0) {
- r.add(new PermissionRange(e.getKey(), min, max));
- }
- }
- }
- return r;
- }
-
/** The range of permitted values associated with a label permission. */
PermissionRange getRange(String permission) {
return getRange(permission, false);
@@ -609,7 +588,7 @@
case UPDATE_BY_SUBMIT:
return projectControl.controlForRef("refs/for/" + getRefName()).canSubmit(true);
- case BYPASS_REVIEW:
+ case SKIP_VALIDATION:
return canForgeAuthor()
&& canForgeCommitter()
&& canForgeGerritServerIdentity()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 591fcc2..b042183 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -87,7 +87,9 @@
}
}
// The change owner may remove any zero or positive score.
- if (changeControl.isOwner() && 0 <= value) {
+ if (currentUser.isIdentifiedUser()
+ && currentUser.getAccountId().equals(notes.getChange().getOwner())
+ && 0 <= value) {
return true;
}
// Users with the remove reviewer permission, the branch owner, project
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
index 9cba53b..e875388 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -14,18 +14,10 @@
package com.google.gerrit.server.project;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.extensions.api.access.AccessSectionInfo;
-import com.google.gerrit.extensions.api.access.PermissionInfo;
-import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -37,10 +29,8 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -49,44 +39,35 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class SetAccess implements RestModifyView<ProjectResource, ProjectAccessInput> {
protected final GroupBackend groupBackend;
private final PermissionBackend permissionBackend;
- private final GroupsCollection groupsCollection;
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
- private final AllProjectsName allProjects;
- private final Provider<SetParent> setParent;
private final GetAccess getAccess;
private final ProjectCache projectCache;
private final Provider<IdentifiedUser> identifiedUser;
+ private final SetAccessUtil accessUtil;
@Inject
private SetAccess(
GroupBackend groupBackend,
PermissionBackend permissionBackend,
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
- AllProjectsName allProjects,
- Provider<SetParent> setParent,
- GroupsCollection groupsCollection,
ProjectCache projectCache,
GetAccess getAccess,
- Provider<IdentifiedUser> identifiedUser) {
+ Provider<IdentifiedUser> identifiedUser,
+ SetAccessUtil accessUtil) {
this.groupBackend = groupBackend;
this.permissionBackend = permissionBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
- this.allProjects = allProjects;
- this.setParent = setParent;
- this.groupsCollection = groupsCollection;
this.getAccess = getAccess;
this.projectCache = projectCache;
this.identifiedUser = identifiedUser;
+ this.accessUtil = accessUtil;
}
@Override
@@ -94,113 +75,39 @@
throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException,
BadRequestException, UnprocessableEntityException, OrmException,
PermissionBackendException {
- List<AccessSection> removals = getAccessSections(input.remove);
- List<AccessSection> additions = getAccessSections(input.add);
MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
- ProjectControl projectControl = rsrc.getControl();
ProjectConfig config;
- Project.NameKey newParentProjectName =
- input.parent == null ? null : new Project.NameKey(input.parent);
-
+ List<AccessSection> removals = accessUtil.getAccessSections(input.remove);
+ List<AccessSection> additions = accessUtil.getAccessSections(input.add);
try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
config = ProjectConfig.read(md);
- // Perform removal checks
- for (AccessSection section : removals) {
+ // Check that the user has the right permissions.
+ boolean checkedAdmin = false;
+ for (AccessSection section : Iterables.concat(additions, removals)) {
boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
-
if (isGlobalCapabilities) {
- checkGlobalCapabilityPermissions(config.getName());
- } else if (!projectControl.controlForRef(section.getName()).isOwner()) {
+ if (!checkedAdmin) {
+ permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
+ checkedAdmin = true;
+ }
+ } else if (!rsrc.getControl().controlForRef(section.getName()).isOwner()) {
throw new AuthException(
- "You are not allowed to edit permissionsfor ref: " + section.getName());
- }
- }
- // Perform addition checks
- for (AccessSection section : additions) {
- String name = section.getName();
- boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
-
- if (isGlobalCapabilities) {
- checkGlobalCapabilityPermissions(config.getName());
- } else {
- if (!AccessSection.isValid(name)) {
- throw new BadRequestException("invalid section name");
- }
- if (!projectControl.controlForRef(name).isOwner()) {
- throw new AuthException("You are not allowed to edit permissionsfor ref: " + name);
- }
- RefPattern.validate(name);
- }
-
- // Check all permissions for soundness
- for (Permission p : section.getPermissions()) {
- if (isGlobalCapabilities && !GlobalCapability.isCapability(p.getName())) {
- throw new BadRequestException(
- "Cannot add non-global capability " + p.getName() + " to global capabilities");
- }
+ "You are not allowed to edit permissions for ref: " + section.getName());
}
}
- // Apply removals
- for (AccessSection section : removals) {
- if (section.getPermissions().isEmpty()) {
- // Remove entire section
- config.remove(config.getAccessSection(section.getName()));
- }
- // Remove specific permissions
- for (Permission p : section.getPermissions()) {
- if (p.getRules().isEmpty()) {
- config.remove(config.getAccessSection(section.getName()), p);
- } else {
- for (PermissionRule r : p.getRules()) {
- config.remove(config.getAccessSection(section.getName()), p, r);
- }
- }
- }
- }
+ accessUtil.validateChanges(config, removals, additions);
+ accessUtil.applyChanges(config, removals, additions);
- // Apply additions
- for (AccessSection section : additions) {
- AccessSection currentAccessSection = config.getAccessSection(section.getName());
-
- if (currentAccessSection == null) {
- // Add AccessSection
- config.replace(section);
- } else {
- for (Permission p : section.getPermissions()) {
- Permission currentPermission = currentAccessSection.getPermission(p.getName());
- if (currentPermission == null) {
- // Add Permission
- currentAccessSection.addPermission(p);
- } else {
- for (PermissionRule r : p.getRules()) {
- // AddPermissionRule
- currentPermission.add(r);
- }
- }
- }
- }
- }
-
- if (newParentProjectName != null
- && !config.getProject().getNameKey().equals(allProjects)
- && !config.getProject().getParent(allProjects).equals(newParentProjectName)) {
- try {
- setParent
- .get()
- .validateParentUpdate(
- projectControl.getProject().getNameKey(),
- projectControl.getUser().asIdentifiedUser(),
- MoreObjects.firstNonNull(newParentProjectName, allProjects).get(),
- true);
- } catch (UnprocessableEntityException e) {
- throw new ResourceConflictException(e.getMessage(), e);
- }
- config.getProject().setParentName(newParentProjectName);
- }
+ accessUtil.setParentName(
+ identifiedUser.get(),
+ config,
+ rsrc.getNameKey(),
+ input.parent == null ? null : new Project.NameKey(input.parent),
+ !checkedAdmin);
if (!Strings.isNullOrEmpty(input.message)) {
if (!input.message.endsWith("\n")) {
@@ -221,68 +128,4 @@
return getAccess.apply(rsrc.getNameKey());
}
-
- private List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
- throws UnprocessableEntityException {
- if (sectionInfos == null) {
- return Collections.emptyList();
- }
-
- List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
- for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) {
- AccessSection accessSection = new AccessSection(entry.getKey());
-
- if (entry.getValue().permissions == null) {
- continue;
- }
-
- for (Map.Entry<String, PermissionInfo> permissionEntry :
- entry.getValue().permissions.entrySet()) {
- Permission p = new Permission(permissionEntry.getKey());
- if (permissionEntry.getValue().exclusive != null) {
- p.setExclusiveGroup(permissionEntry.getValue().exclusive);
- }
-
- if (permissionEntry.getValue().rules == null) {
- continue;
- }
- for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
- permissionEntry.getValue().rules.entrySet()) {
- PermissionRuleInfo pri = permissionRuleInfoEntry.getValue();
-
- GroupDescription.Basic group = groupsCollection.parseId(permissionRuleInfoEntry.getKey());
- if (group == null) {
- throw new UnprocessableEntityException(
- permissionRuleInfoEntry.getKey() + " is not a valid group ID");
- }
- PermissionRule r = new PermissionRule(GroupReference.forGroup(group));
- if (pri != null) {
- if (pri.max != null) {
- r.setMax(pri.max);
- }
- if (pri.min != null) {
- r.setMin(pri.min);
- }
- r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
- if (pri.force != null) {
- r.setForce(pri.force);
- }
- }
- p.add(r);
- }
- accessSection.getPermissions().add(p);
- }
- sections.add(accessSection);
- }
- return sections;
- }
-
- private void checkGlobalCapabilityPermissions(Project.NameKey projectName)
- throws BadRequestException, AuthException, PermissionBackendException {
- if (!allProjects.equals(projectName)) {
- throw new BadRequestException(
- "Cannot edit global capabilities for projects other than " + allProjects.get());
- }
- permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER);
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java
new file mode 100644
index 0000000..848d68c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccessUtil.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class SetAccessUtil {
+ private final GroupsCollection groupsCollection;
+ private final AllProjectsName allProjects;
+ private final Provider<SetParent> setParent;
+
+ @Inject
+ private SetAccessUtil(
+ GroupsCollection groupsCollection,
+ AllProjectsName allProjects,
+ Provider<SetParent> setParent) {
+ this.groupsCollection = groupsCollection;
+ this.allProjects = allProjects;
+ this.setParent = setParent;
+ }
+
+ List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
+ throws UnprocessableEntityException {
+ if (sectionInfos == null) {
+ return Collections.emptyList();
+ }
+
+ List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
+ for (Map.Entry<String, AccessSectionInfo> entry : sectionInfos.entrySet()) {
+ if (entry.getValue().permissions == null) {
+ continue;
+ }
+
+ AccessSection accessSection = new AccessSection(entry.getKey());
+ for (Map.Entry<String, PermissionInfo> permissionEntry :
+ entry.getValue().permissions.entrySet()) {
+ if (permissionEntry.getValue().rules == null) {
+ continue;
+ }
+
+ Permission p = new Permission(permissionEntry.getKey());
+ if (permissionEntry.getValue().exclusive != null) {
+ p.setExclusiveGroup(permissionEntry.getValue().exclusive);
+ }
+
+ for (Map.Entry<String, PermissionRuleInfo> permissionRuleInfoEntry :
+ permissionEntry.getValue().rules.entrySet()) {
+ GroupDescription.Basic group = groupsCollection.parseId(permissionRuleInfoEntry.getKey());
+ if (group == null) {
+ throw new UnprocessableEntityException(
+ permissionRuleInfoEntry.getKey() + " is not a valid group ID");
+ }
+
+ PermissionRuleInfo pri = permissionRuleInfoEntry.getValue();
+ PermissionRule r = new PermissionRule(GroupReference.forGroup(group));
+ if (pri != null) {
+ if (pri.max != null) {
+ r.setMax(pri.max);
+ }
+ if (pri.min != null) {
+ r.setMin(pri.min);
+ }
+ r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
+ if (pri.force != null) {
+ r.setForce(pri.force);
+ }
+ }
+ p.add(r);
+ }
+ accessSection.getPermissions().add(p);
+ }
+ sections.add(accessSection);
+ }
+ return sections;
+ }
+
+ /**
+ * Checks that the removals and additions are logically valid, but doesn't check current user's
+ * permission.
+ */
+ void validateChanges(
+ ProjectConfig config, List<AccessSection> removals, List<AccessSection> additions)
+ throws BadRequestException, InvalidNameException {
+ // Perform permission checks
+ for (AccessSection section : Iterables.concat(additions, removals)) {
+ boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+ if (isGlobalCapabilities) {
+ if (!allProjects.equals(config.getName())) {
+ throw new BadRequestException(
+ "Cannot edit global capabilities for projects other than " + allProjects.get());
+ }
+ }
+ }
+
+ // Perform addition checks
+ for (AccessSection section : additions) {
+ String name = section.getName();
+ boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(name);
+
+ if (!isGlobalCapabilities) {
+ if (!AccessSection.isValid(name)) {
+ throw new BadRequestException("invalid section name");
+ }
+ RefPattern.validate(name);
+ } else {
+ // Check all permissions for soundness
+ for (Permission p : section.getPermissions()) {
+ if (!GlobalCapability.isCapability(p.getName())) {
+ throw new BadRequestException(
+ "Cannot add non-global capability " + p.getName() + " to global capabilities");
+ }
+ }
+ }
+ }
+ }
+
+ void applyChanges(
+ ProjectConfig config, List<AccessSection> removals, List<AccessSection> additions) {
+ // Apply removals
+ for (AccessSection section : removals) {
+ if (section.getPermissions().isEmpty()) {
+ // Remove entire section
+ config.remove(config.getAccessSection(section.getName()));
+ continue;
+ }
+
+ // Remove specific permissions
+ for (Permission p : section.getPermissions()) {
+ if (p.getRules().isEmpty()) {
+ config.remove(config.getAccessSection(section.getName()), p);
+ } else {
+ for (PermissionRule r : p.getRules()) {
+ config.remove(config.getAccessSection(section.getName()), p, r);
+ }
+ }
+ }
+ }
+
+ // Apply additions
+ for (AccessSection section : additions) {
+ AccessSection currentAccessSection = config.getAccessSection(section.getName());
+
+ if (currentAccessSection == null) {
+ // Add AccessSection
+ config.replace(section);
+ } else {
+ for (Permission p : section.getPermissions()) {
+ Permission currentPermission = currentAccessSection.getPermission(p.getName());
+ if (currentPermission == null) {
+ // Add Permission
+ currentAccessSection.addPermission(p);
+ } else {
+ for (PermissionRule r : p.getRules()) {
+ // AddPermissionRule
+ currentPermission.add(r);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void setParentName(
+ IdentifiedUser identifiedUser,
+ ProjectConfig config,
+ Project.NameKey projectName,
+ Project.NameKey newParentProjectName,
+ boolean checkAdmin)
+ throws ResourceConflictException, AuthException, PermissionBackendException {
+ if (newParentProjectName != null
+ && !config.getProject().getNameKey().equals(allProjects)
+ && !config.getProject().getParent(allProjects).equals(newParentProjectName)) {
+ try {
+ setParent
+ .get()
+ .validateParentUpdate(
+ projectName, identifiedUser, newParentProjectName.get(), checkAdmin);
+ } catch (UnprocessableEntityException e) {
+ throw new ResourceConflictException(e.getMessage(), e);
+ }
+ config.getProject().setParentName(newParentProjectName);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 07f7ead..37cfcdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -77,8 +77,7 @@
IdentifiedUser user = rsrc.getUser().asIdentifiedUser();
String parentName =
MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
- validateParentUpdate(
- rsrc.getProjectState().getProject().getNameKey(), user, parentName, checkIfAdmin);
+ validateParentUpdate(rsrc.getProjectState().getNameKey(), user, parentName, checkIfAdmin);
try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
ProjectConfig config = ProjectConfig.read(md);
Project project = config.getProject();
@@ -128,11 +127,11 @@
if (Iterables.tryFind(
parent.tree(),
p -> {
- return p.getProject().getNameKey().equals(project);
+ return p.getNameKey().equals(project);
})
.isPresent()) {
throw new ResourceConflictException(
- "cycle exists between " + project.get() + " and " + parent.getProject().getName());
+ "cycle exists between " + project.get() + " and " + parent.getName());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 7274100..4595933 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -600,8 +600,7 @@
try {
parentEnv = parentState.newPrologEnvironment();
} catch (CompileException err) {
- throw new RuleEvalException(
- "Cannot consult rules.pl for " + parentState.getProject().getName(), err);
+ throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
}
parentEnv.copyStoredValues(childEnv);
@@ -619,12 +618,12 @@
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
- err.getMessage(), cd.getId().get(), parentState.getProject().getName()));
+ err.getMessage(), cd.getId().get(), parentState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
- filterRule, cd.getId().get(), parentState.getProject().getName()),
+ filterRule, cd.getId().get(), parentState.getName()),
err);
} finally {
reductionsConsumed += env.getReductions();
@@ -690,6 +689,6 @@
}
private String getProjectName() {
- return control.getProjectControl().getProjectState().getProject().getName();
+ return control.getProjectControl().getProjectState().getName();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index f5e8d69..19c0515 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -56,7 +56,7 @@
}
List<Predicate<ChangeData>> r = new ArrayList<>();
- r.add(new ProjectPredicate(projectState.getProject().getName()));
+ r.add(new ProjectPredicate(projectState.getName()));
try {
ProjectResource proj = new ProjectResource(projectState.controlFor(self.get()));
ListChildProjects children = listChildProjects.get();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ea9af4c..0153004 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -2330,7 +2330,7 @@
PatchSetInserter inserter =
patchSetFactory
- .create(ctl, new PatchSet.Id(c.getId(), n), commit)
+ .create(ctl.getNotes(), new PatchSet.Id(c.getId(), n), commit)
.setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(false)
.setValidate(false);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index 58492b2..b9a98b9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -69,8 +69,7 @@
@Override
public Project.NameKey getProjectName() {
- Project project = projectControl.getProjectState().getProject();
- return project.getNameKey();
+ return projectControl.getProjectState().getNameKey();
}
});
} finally {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 35788fd..0d7fa24 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -226,6 +226,6 @@
if (ps == null) {
return Collections.emptySet();
}
- return ps.parents().transform(s -> s.getProject().getNameKey()).toSet();
+ return ps.parents().transform(s -> s.getNameKey()).toSet();
}
}
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 0a75bfc..f2d07ed 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -395,7 +395,7 @@
MyParser(Object bean) {
super(bean);
- parseAdditionalOptions("", bean, new HashSet<>());
+ parseAdditionalOptions(bean, new HashSet<>());
ensureOptionsInitialized();
}
@@ -433,7 +433,7 @@
}
}
- private void parseAdditionalOptions(String prefix, Object bean, Set<Object> parsedBeans) {
+ private void parseAdditionalOptions(Object bean, Set<Object> parsedBeans) {
for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (f.isAnnotationPresent(Options.class)) {
@@ -443,8 +443,7 @@
} catch (IllegalAccessException e) {
throw new IllegalAnnotationError(e);
}
- parseWithPrefix(
- prefix + f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
+ parseWithPrefix(f.getAnnotation(Options.class).prefix(), additionalBean, parsedBeans);
}
}
}
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD
index 57429f3..025b93e 100644
--- a/lib/fonts/BUILD
+++ b/lib/fonts/BUILD
@@ -3,8 +3,12 @@
# Roboto Mono. Version 2.136
# https://github.com/google/roboto/releases/tag/v2.136
filegroup(
- name = "robotomono",
+ name = "robotofonts",
srcs = [
+ "Roboto-Medium.woff",
+ "Roboto-Medium.woff2",
+ "Roboto-Regular.woff",
+ "Roboto-Regular.woff2",
"RobotoMono-Regular.woff",
"RobotoMono-Regular.woff2",
],
diff --git a/lib/fonts/Roboto-Medium.woff b/lib/fonts/Roboto-Medium.woff
new file mode 100644
index 0000000..720bd3e
--- /dev/null
+++ b/lib/fonts/Roboto-Medium.woff
Binary files differ
diff --git a/lib/fonts/Roboto-Medium.woff2 b/lib/fonts/Roboto-Medium.woff2
new file mode 100644
index 0000000..c003fba
--- /dev/null
+++ b/lib/fonts/Roboto-Medium.woff2
Binary files differ
diff --git a/lib/fonts/Roboto-Regular.woff b/lib/fonts/Roboto-Regular.woff
new file mode 100644
index 0000000..03e84eb
--- /dev/null
+++ b/lib/fonts/Roboto-Regular.woff
Binary files differ
diff --git a/lib/fonts/Roboto-Regular.woff2 b/lib/fonts/Roboto-Regular.woff2
new file mode 100644
index 0000000..6fa4939
--- /dev/null
+++ b/lib/fonts/Roboto-Regular.woff2
Binary files differ
diff --git a/plugins/replication b/plugins/replication
index 297b749..643d635 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 297b749038153527291b43cb08b162eb475adcd7
+Subproject commit 643d635a4502e2a2df6cb02edade88bad3fd953a
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index d4d2322..31ab6aa 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -29,7 +29,7 @@
genrule2(
name = "fonts",
srcs = [
- "//lib/fonts:robotomono",
+ "//lib/fonts:robotofonts",
],
outs = ["fonts.zip"],
cmd = " && ".join([
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
index c2b28d5..529e14c 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -109,7 +109,8 @@
permission="{{permission}}"
labels="[[labels]]"
section="[[section.id]]"
- editing="[[editing]]">
+ editing="[[editing]]"
+ groups="[[groups]]">
</gr-permission>
</template>
<div id="addPermission">
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index 16e6207..07a7a62 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -33,6 +33,7 @@
notify: true,
observer: '_sectionChanged',
},
+ groups: Object,
labels: Object,
editing: {
type: Boolean,
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 00aeb91..72439e2 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -70,6 +70,7 @@
attached() {
this._getCreateGroupCapability();
this.fire('title-change', {title: 'Groups'});
+ this._maybeOpenCreateOverlay(this.params);
},
_paramsChanged(params) {
@@ -81,6 +82,16 @@
this._offset);
},
+ /**
+ * Opens the create overlay if the route has a hash 'create'
+ * @param {!Object} params
+ */
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
+ this.$.createOverlay.open();
+ }
+ },
+
_computeGroupUrl(id) {
return this.getUrl(this._path + '/', id);
},
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index 4cc1afe..06428c7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -90,6 +90,18 @@
test('_shownGroups', () => {
assert.equal(element._shownGroups.length, 25);
});
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
});
suite('test with less then 25 groups', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index c804933..928c24e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -24,10 +24,7 @@
<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
<link rel="import" href="../../shared/gr-placeholder/gr-placeholder.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../gr-create-group-dialog/gr-create-group-dialog.html">
-<link rel="import" href="../gr-create-project-dialog/gr-create-project-dialog.html">
<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html">
-<link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html">
<link rel="import" href="../gr-group/gr-group.html">
<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html">
<link rel="import" href="../gr-group-members/gr-group-members.html">
@@ -36,6 +33,7 @@
<link rel="import" href="../gr-project-access/gr-project-access.html">
<link rel="import" href="../gr-project-commands/gr-project-commands.html">
<link rel="import" href="../gr-project-detail-list/gr-project-detail-list.html">
+<link rel="import" href="../gr-project-list/gr-project-list.html">
<dom-module id="gr-admin-view">
<template>
@@ -75,8 +73,7 @@
</gr-page-nav>
<template is="dom-if" if="[[_showProjectList]]" restamp="true">
<main class="table">
- <gr-admin-project-list class="table" params="[[params]]">
- </gr-admin-project-list>
+ <gr-project-list class="table" params="[[params]]"></gr-project-list>
</main>
</template>
<template is="dom-if" if="[[_showProjectMain]]" restamp="true">
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index e75c636..2d8da15 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -17,7 +17,7 @@
const ADMIN_LINKS = [{
name: 'Projects',
url: '/admin/projects',
- view: 'gr-admin-project-list',
+ view: 'gr-project-list',
viewableToAll: true,
children: [],
}, {
@@ -191,7 +191,7 @@
params.adminView === 'gr-project-commands');
this.set('_showProjectMain', params.adminView === 'gr-project');
this.set('_showProjectList',
- params.adminView === 'gr-admin-project-list');
+ params.adminView === 'gr-project-list');
this.set('_showProjectDetailList',
params.adminView === 'gr-project-detail-list');
this.set('_showPluginList', params.adminView === 'gr-plugin-list');
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index 746b7bb..117940a3 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -70,18 +70,18 @@
element._filteredLinks = [{
name: 'Projects',
url: '/admin/projects',
- view: 'gr-admin-project-list',
+ view: 'gr-project-list',
children: [],
}];
element.params = {
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
};
flushAsynchronousOperations();
assert.equal(Polymer.dom(element.root).querySelectorAll(
'.selected').length, 1);
- assert.ok(element.$$('gr-admin-project-list'));
+ assert.ok(element.$$('gr-project-list'));
assert.isNotOk(element.$$('gr-admin-create-project'));
});
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index bfced55..9371b17 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -64,7 +64,7 @@
}
th {
border-bottom: 1px solid #eee;
- font-weight: bold;
+ font-family: var(--font-family-bold);
text-align: left;
}
</style>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index 1dfb06c..31171f7 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -92,7 +92,8 @@
<gr-rule-editor
label="[[_label]]"
editing="[[editing]]"
- group="[[rule.id]]"
+ group-id="[[rule.id]]"
+ group-name="[[_computeGroupName(groups, rule.id)]]"
permission="[[permission.id]]"
rule="{{rule}}"
section="[[section]]"></gr-rule-editor>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 97d4fe5..b99327c 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -28,6 +28,7 @@
observer: '_sortPermission',
notify: true,
},
+ groups: Object,
section: String,
editing: {
type: Boolean,
@@ -129,6 +130,11 @@
return groups;
},
+ _computeGroupName(groups, groupId) {
+ return groups && groups[groupId] && groups[groupId].name ?
+ groups[groupId].name : groupId;
+ },
+
_getGroupSuggestions() {
return this.$.restAPI.getSuggestedGroups(
this._groupFilter,
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index 3c229d1..73eac54 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -158,6 +158,14 @@
'editing deleted');
});
+ test('_computeGroupName', () => {
+ const groups = {
+ abc123: {name: 'test group'},
+ bcd234: {},
+ };
+ assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
+ assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
+ });
test('_computeGroupsWithRules', () => {
const rules = [
diff --git a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.html b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.html
index d776763..686aa10 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.html
@@ -40,7 +40,8 @@
capabilities="[[_capabilities]]"
section="{{section}}"
labels="[[_labels]]"
- editing="[[_editing]]"></gr-access-section>
+ editing="[[_editing]]"
+ groups="[[_groups]]"></gr-access-section>
</template>
<template is="dom-if" if="[[_inheritsFrom]]">
<h3 id="inheritsFrom">Rights Inherit From
diff --git a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
index 808279a..8e1b1f5 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access.js
@@ -24,6 +24,7 @@
},
_capabilities: Object,
+ _groups: Object,
/** @type {?} */
_inheritsFrom: Object,
_labels: Object,
@@ -41,7 +42,12 @@
Gerrit.URLEncodingBehavior,
],
+ /**
+ * @param {string} project
+ * @return {!Promise}
+ */
_projectChanged(project) {
+ if (!project) { return Promise.resolve(); }
const promises = [];
if (!this._sections) {
this._sections = [];
@@ -49,6 +55,7 @@
promises.push(this.$.restAPI.getProjectAccessRights(project).then(res => {
this._inheritsFrom = res.inherits_from;
this._local = res.local;
+ this._groups = res.groups;
return this.toSortedArray(this._local);
}));
diff --git a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access_test.html b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access_test.html
index ef7412e..70b0f8c 100644
--- a/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-access/gr-project-access_test.html
@@ -78,7 +78,7 @@
},
};
const accessStub = sandbox.stub(element.$.restAPI,
- 'getProjectAccessRights') .returns(Promise.resolve(accessRes));
+ 'getProjectAccessRights').returns(Promise.resolve(accessRes));
const capabilitiesStub = sandbox.stub(element.$.restAPI,
'getCapabilities').returns(Promise.resolve(capabilitiesRes));
const projectStub = sandbox.stub(element.$.restAPI, 'getProject').returns(
@@ -97,6 +97,46 @@
});
});
+ test('_projectChanged when project changes to undefined returns', done => {
+ const capabilitiesRes = {
+ accessDatabase: {
+ id: 'accessDatabase',
+ name: 'Access Database',
+ },
+ };
+ const accessRes = {
+ local: {
+ GLOBAL_CAPABILITIES: {
+ permissions: {
+ accessDatabase: {
+ rules: {
+ 123: {},
+ },
+ },
+ },
+ },
+ },
+ };
+ const projectRes = {
+ labels: {
+ 'Code-Review': {},
+ },
+ };
+ const accessStub = sandbox.stub(element.$.restAPI,
+ 'getProjectAccessRights').returns(Promise.resolve(accessRes));
+ const capabilitiesStub = sandbox.stub(element.$.restAPI,
+ 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
+ const projectStub = sandbox.stub(element.$.restAPI, 'getProject').returns(
+ Promise.resolve(projectRes));
+
+ element._projectChanged().then(() => {
+ assert.isFalse(accessStub.called);
+ assert.isFalse(capabilitiesStub.called);
+ assert.isFalse(projectStub.called);
+ done();
+ });
+ });
+
test('_computeParentHref', () => {
const projectName = 'test-project';
assert.equal(element._computeParentHref(projectName),
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
similarity index 97%
rename from polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
rename to polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
index 1b6da09..6c45704 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.html
@@ -26,7 +26,7 @@
<link rel="import" href="../gr-create-project-dialog/gr-create-project-dialog.html">
-<dom-module id="gr-admin-project-list">
+<dom-module id="gr-project-list">
<template>
<style include="shared-styles"></style>
<style include="gr-table-styles"></style>
@@ -94,5 +94,5 @@
</gr-overlay>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
- <script src="gr-admin-project-list.js"></script>
+ <script src="gr-project-list.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
similarity index 91%
rename from polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
rename to polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
index 4f93e98..070cc2f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list.js
@@ -15,7 +15,7 @@
'use strict';
Polymer({
- is: 'gr-admin-project-list',
+ is: 'gr-project-list',
properties: {
/**
@@ -70,6 +70,7 @@
attached() {
this._getCreateProjectCapability();
this.fire('title-change', {title: 'Projects'});
+ this._maybeOpenCreateOverlay(this.params);
},
_paramsChanged(params) {
@@ -81,6 +82,16 @@
this._offset);
},
+ /**
+ * Opens the create overlay if the route has a hash 'create'
+ * @param {!Object} params
+ */
+ _maybeOpenCreateOverlay(params) {
+ if (params && params.openCreateModal) {
+ this.$.createOverlay.open();
+ }
+ },
+
_computeProjectUrl(name) {
return this.getUrl(this._path + '/', name);
},
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
similarity index 88%
rename from polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
rename to polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
index ee3f832..87732b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-project-list/gr-project-list_test.html
@@ -16,19 +16,19 @@
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-admin-project-list</title>
+<title>gr-project-list</title>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-admin-project-list.html">
+<link rel="import" href="gr-project-list.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
- <gr-admin-project-list></gr-admin-project-list>
+ <gr-project-list></gr-project-list>
</template>
</test-fixture>
@@ -47,7 +47,7 @@
};
};
- suite('gr-admin-project-list tests', () => {
+ suite('gr-project-list tests', () => {
let element;
let projects;
let sandbox;
@@ -66,13 +66,11 @@
suite('list with projects', () => {
setup(done => {
projects = _.times(26, projectGenerator);
-
stub('gr-rest-api-interface', {
getProjects(num, offset) {
return Promise.resolve(projects);
},
});
-
element._paramsChanged(value).then(() => { flush(done); });
});
@@ -86,6 +84,18 @@
test('_shownProjects', () => {
assert.equal(element._shownProjects.length, 25);
});
+
+ test('_maybeOpenCreateOverlay', () => {
+ const overlayOpen = sandbox.stub(element.$.createOverlay, 'open');
+ element._maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ const params = {};
+ element._maybeOpenCreateOverlay(params);
+ assert.isFalse(overlayOpen.called);
+ params.openCreateModal = true;
+ element._maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
});
suite('list with less then 25 projects', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index 0a8f382..18612c8 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -16,7 +16,9 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -64,6 +66,9 @@
#deletedContainer.deleted {
display: block;
}
+ .groupPath {
+ color: #666;
+ }
</style>
<style include="gr-form-styles"></style>
<div id="mainContainer"
@@ -100,7 +105,9 @@
</select>
</gr-select>
</template>
- <span>[[group]]</span>
+ <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
+ [[groupName]]
+ </a>
<gr-select
id="force"
class$="[[_computeForceClass(permission)]]"
@@ -126,7 +133,7 @@
<div
id="deletedContainer"
class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
- [[group]] was deleted
+ [[groupName]] was deleted
<gr-button id="undoRemoveBtn" on-tap="_handleUndoRemove">Undo</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 6f404bf..7f1a245 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -57,7 +57,8 @@
type: Boolean,
value: false,
},
- group: String,
+ groupId: String,
+ groupName: String,
permission: String,
/** @type {?} */
rule: {
@@ -78,6 +79,8 @@
behaviors: [
Gerrit.AccessBehavior,
+ Gerrit.BaseUrlBehavior,
+ Gerrit.URLEncodingBehavior,
],
observers: [
@@ -107,6 +110,10 @@
return this._computeForce(permission) ? 'force' : '';
},
+ _computeGroupPath(group) {
+ return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`;
+ },
+
_computeSectionClass(editing, deleted) {
const classList = [];
if (editing) {
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 2b02419..3594e4b 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -240,6 +240,12 @@
MockInteractions.tap(element.$.undoRemoveBtn);
assert.isNotOk(element.rule.value.deleted);
});
+
+ test('_computeGroupPath', () => {
+ const group = '123';
+ assert.equal(element._computeGroupPath(group),
+ `/admin/groups/123`);
+ });
});
suite('new edit rule', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 70275b3..4ff10cb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -39,7 +39,7 @@
background-color: #ebf5fb;
}
:host([needs-review]) {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
:host([assigned]) {
background-color: #fcfad6;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index bec7429..f04b7c6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -16,8 +16,9 @@
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-dashboard-view">
<template>
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 18ba163..f2ed33a 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
@@ -74,9 +74,12 @@
margin: .5em;
text-align: center;
}
+ #mainContent.mobileOverlayOpened {
+ display: none;
+ }
}
</style>
- <div>
+ <div id="mainContent">
<span
id="actionLoadingMessage"
hidden$="[[!_actionLoadingMessage]]">
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 4691836..3bccdd8 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
@@ -291,6 +291,11 @@
'_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)',
],
+ listeners: {
+ 'fullscreen-overlay-opened': '_handleHideBackgroundContent',
+ 'fullscreen-overlay-closed': '_handleShowBackgroundContent',
+ },
+
ready() {
this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
this._loading = false;
@@ -939,6 +944,14 @@
this._fireAction('/wip', this.actions.wip, false);
},
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ },
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ },
+
/**
* Merge sources of change actions into a single ordered array of action
* values.
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 2390c21..55cd982 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -346,6 +346,20 @@
});
});
+ test('fullscreen-overlay-opened hides content', () => {
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.overlay.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
suite('cherry-pick', () => {
let fireActionStub;
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 5a834b7..388a3f7 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
@@ -43,7 +43,7 @@
}
.title {
color: #666;
- font-weight: bold;
+ font-family: var(--font-family-bold);
max-width: 20em;
word-break: break-word;
}
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 0290cb1..48fcfd7 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
@@ -78,7 +78,7 @@
.header-title {
flex: 1;
font-size: 1.2em;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.prefsButton {
float: right;
@@ -173,7 +173,7 @@
display: initial;
}
.patchInfo-header,
- gr-file-list {
+ .fileList {
padding: .5em calc(var(--default-horizontal-margin) / 2);
}
.patchInfo-header {
@@ -252,6 +252,30 @@
.editLoaded .showOnEdit {
display: initial;
}
+ .fileList-header {
+ display: flex;
+ font-weight: bold;
+ justify-content: space-between;
+ margin-bottom: .5em;
+ }
+ .rightControls {
+ display: flex;
+ flex-wrap: wrap;
+ font-weight: normal;
+ justify-content: flex-end;
+ }
+ .separator {
+ margin: 0 .25em;
+ }
+ .expandInline {
+ padding-right: .25em;
+ }
+ .patchSetSelect {
+ max-width: 8em;
+ }
+ .scrollable {
+ overflow: auto;
+ }
@media screen and (min-width: 80em) {
.commitMessage {
max-width: var(--commit-message-max-width, 100ch);
@@ -323,16 +347,19 @@
flex: initial;
margin-right: 0;
}
- .scrollable {
- overflow: auto;
+ /* Change actions are the only thing thant need to remain visible due
+ to the fact that they may have the currently visible overlay open. */
+ #mainContent.overlayOpen .hideOnMobileOverlay {
+ display: none;
}
}
</style>
<div class="container loading" hidden$="[[!_loading]]">Loading...</div>
<div
+ id="mainContent"
class$="container [[_computeEditLoadedClass(_editLoaded)]]"
hidden$="{{_loading}}">
- <div class$="[[_computeHeaderClass(_change)]]">
+ <div class$="hideOnMobileOverlay [[_computeHeaderClass(_change)]]">
<span class="header-title">
<gr-change-star
id="changeStar"
@@ -360,7 +387,7 @@
</span>
</div>
<section class="changeInfo">
- <div class="changeInfo-column changeMetadata">
+ <div class="changeInfo-column changeMetadata hideOnMobileOverlay">
<gr-change-metadata
change="{{_change}}"
commit-info="[[_commitInfo]]"
@@ -394,7 +421,7 @@
on-download-tap="_handleDownloadTap"></gr-change-actions>
</div>
<hr class="mobile">
- <div id="commitAndRelated">
+ <div id="commitAndRelated" class="hideOnMobileOverlay">
<div class="commitContainer">
<div
id="commitMessage"
@@ -459,7 +486,7 @@
</div>
</div>
</section>
- <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum,
+ <section class$="patchInfo hideOnMobileOverlay [[_computePatchInfoClass(_patchRange.patchNum,
_allPatchSets)]]">
<div class="patchInfo-header">
<div class="patchInfo-header-wrapper">
@@ -521,22 +548,79 @@
</span>
</div>
</div>
- <gr-file-list id="fileList"
- diff-prefs="{{_diffPrefs}}"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- patch-range="{{_patchRange}}"
- comments="[[_comments]]"
- drafts="[[_diffDrafts]]"
- revisions="[[_sortedRevisions]]"
- project-config="[[_projectConfig]]"
- selected-index="{{viewState.selectedFileIndex}}"
- diff-view-mode="{{viewState.diffMode}}"
- edit-loaded="[[_editLoaded]]"
- num-files-shown="{{_numFilesShown}}"
- file-list-increment="{{_numFilesShown}}"></gr-file-list>
+ <div class="fileList">
+ <div class="fileList-header">
+ <div>Files</div>
+ <div class="rightControls">
+ <template is="dom-if"
+ if="[[_fileListActionsVisible(_shownFileCount, _maxFilesForBulkActions)]]">
+ <gr-button
+ id="expandBtn"
+ link
+ on-tap="_expandAllDiffs">Show diffs</gr-button>
+ <span class="separator">/</span>
+ <gr-button
+ id="collapseBtn"
+ link
+ on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+ </template>
+ <template is="dom-if"
+ if="[[!_fileListActionsVisible(_shownFileCount, _maxFilesForBulkActions)]]">
+ <div class="warning">
+ Bulk actions disabled because there are too many files.
+ </div>
+ </template>
+ <span class="separator">/</span>
+ <gr-select
+ id="modeSelect"
+ bind-value="{{viewState.diffMode}}">
+ <select>
+ <option value="SIDE_BY_SIDE">Side By Side</option>
+ <option value="UNIFIED_DIFF">Unified</option>
+ </select>
+ </gr-select>
+ <span class="separator">/</span>
+ <label>
+ Diff against
+ <gr-select id="patchChange" bind-value="{{_diffAgainst}}"
+ class="patchSetSelect" on-change="_handleBasePatchChange">
+ <select>
+ <option value="PARENT">Base</option>
+ <template
+ is="dom-repeat"
+ items="[[_allPatchSets]]"
+ as="patchNum">
+ <option
+ disabled$="[[_computeBasePatchDisabled(patchNum.num, _patchRange.patchNum, _sortedRevisions)]]"
+ value$="[[patchNum.num]]">
+ [[patchNum.num]]
+ [[patchNum.desc]]
+ </option>
+ </template>
+ </select>
+ </gr-select>
+ </label>
+ </div>
+ </div>
+ <gr-file-list id="fileList"
+ diff-prefs="{{_diffPrefs}}"
+ change="[[_change]]"
+ change-num="[[_changeNum]]"
+ patch-range="{{_patchRange}}"
+ comments="[[_comments]]"
+ drafts="[[_diffDrafts]]"
+ revisions="[[_sortedRevisions]]"
+ project-config="[[_projectConfig]]"
+ selected-index="{{viewState.selectedFileIndex}}"
+ diff-view-mode="[[viewState.diffMode]]"
+ edit-loaded="[[_editLoaded]]"
+ num-files-shown="{{_numFilesShown}}"
+ file-list-increment="{{_numFilesShown}}"
+ on-files-shown-changed="_setShownFiles"></gr-file-list>
+ </div>
</section>
<gr-messages-list id="messageList"
+ class="hideOnMobileOverlay"
change-num="[[_changeNum]]"
messages="[[_change.messages]]"
reviewer-updates="[[_change.reviewer_updates]]"
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 edf7dd3..b60453b 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
@@ -123,6 +123,7 @@
computed: '_computeHideEditCommitMessage(_loggedIn, ' +
'_editingCommitMessage, _change)',
},
+ _diffAgainst: String,
/** @type {?string} */
_latestCommitMessage: {
type: String,
@@ -134,6 +135,13 @@
computed:
'_computeChangeIdCommitMessageError(_latestCommitMessage, _change)',
},
+ // Caps the number of files that can be shown and have the 'show diffs' /
+ // 'hide diffs' buttons still be functional.
+ _maxFilesForBulkActions: {
+ type: Number,
+ readOnly: true,
+ value: 225,
+ },
/** @type {?} */
_patchRange: {
type: Object,
@@ -162,6 +170,7 @@
computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
},
_selectedPatchSet: String,
+ _shownFileCount: Number,
_initialLoadComplete: {
type: Boolean,
value: false,
@@ -204,6 +213,13 @@
listeners: {
'topic-changed': '_handleTopicChanged',
+ // When an overlay is opened in a mobile viewport, the overlay has a full
+ // screen view. When it has a full screen view, we do not want the
+ // background to be scrollable. This will eliminate background scroll by
+ // hiding most of the contents on the screen upon opening, and showing
+ // again upon closing.
+ 'fullscreen-overlay-opened': '_handleHideBackgroundContent',
+ 'fullscreen-overlay-closed': '_handleShowBackgroundContent',
},
observers: [
'_labelsChanged(_change.labels.*)',
@@ -234,6 +250,7 @@
this._account = acct;
});
}
+ this._setDiffViewMode();
});
this.addEventListener('comment-save', this._handleCommentSave.bind(this));
@@ -257,6 +274,20 @@
}
},
+ _setDiffViewMode() {
+ if (!this.viewState.diffViewMode) {
+ return this.$.restAPI.getPreferences().then( prefs => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', prefs.default_diff_view);
+ }
+ }).then(() => {
+ if (!this.viewState.diffMode) {
+ this.set('viewState.diffMode', 'SIDE_BY_SIDE');
+ }
+ });
+ }
+ },
+
_updateSortedRevisions(revisionsRecord) {
const revisions = revisionsRecord.base;
this._sortedRevisions = this.sortRevisions(Object.values(revisions));
@@ -378,8 +409,12 @@
this._diffDrafts = diffDrafts;
},
+ _handleBasePatchChange(e) {
+ this._changePatchNum(this._selectedPatchSet, e.target.value, true);
+ },
+
_handlePatchChange(e) {
- this._changePatchNum(e.target.value, true);
+ this._changePatchNum(e.target.value, this._diffAgainst, true);
},
_handleReplyTap(e) {
@@ -420,6 +455,14 @@
}
},
+ _handleHideBackgroundContent() {
+ this.$.mainContent.classList.add('overlayOpen');
+ },
+
+ _handleShowBackgroundContent() {
+ this.$.mainContent.classList.remove('overlayOpen');
+ },
+
_handleReplySent(e) {
this.$.replyOverlay.close();
this._reload();
@@ -447,16 +490,28 @@
}, 150);
},
+ _setShownFiles(e) {
+ this._shownFileCount = e.detail.length;
+ },
+
+ _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) {
+ return shownFileCount <= maxFilesForBulkActions;
+ },
+
+ _expandAllDiffs() {
+ this.$.fileList.expandAllDiffs();
+ },
+
+ _collapseAllDiffs() {
+ this.$.fileList.collapseAllDiffs();
+ },
+
_paramsChanged(value) {
if (value.view !== Gerrit.Nav.View.CHANGE) {
this._initialLoadComplete = false;
return;
}
- // If the patch changed, and was not set to undefined/undefined, we need
- // not reload all resources -- only the commit info and the file list.
- // If the patch range was set to undefined/undefined, the user is looking
- // to refresh the whole view.
const patchChanged = this._patchRange &&
(value.patchNum !== undefined && value.basePatchNum !== undefined) &&
(this._patchRange.patchNum !== value.patchNum ||
@@ -466,24 +521,28 @@
this._initialLoadComplete = false;
}
- const patchNum = value.patchNum ||
- this.computeLatestPatchNum(this._allPatchSets);
-
- const basePatchNum = value.basePatchNum || 'PARENT';
-
- this._patchRange = {patchNum, basePatchNum};
+ const patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || 'PARENT',
+ };
+ this.$.fileList.collapseAllDiffs();
if (this._initialLoadComplete && patchChanged) {
+ if (patchRange.patchNum == null) {
+ patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
+ }
+ this._patchRange = patchRange;
this._reloadPatchNumDependentResources().then(() => {
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
change: this._change,
- patchNum,
+ patchNum: patchRange.patchNum,
});
});
return;
}
this._changeNum = value.changeNum;
+ this._patchRange = patchRange;
this.$.relatedChanges.clear();
this._reload().then(() => {
@@ -615,12 +674,13 @@
/**
* Change active patch to the provided patch num.
- * @param {number|string} patchNum the patchn number to be viewed.
+ * @param {number|string} basePatchNum the base patch to be viewed.
+ * @param {number|string} patchNum the patch number to be viewed.
* @param {boolean} opt_forceParams When set to true, the resulting URL will
* always include the patch range, even if the requested patchNum is
* known to be the latest.
*/
- _changePatchNum(patchNum, opt_forceParams) {
+ _changePatchNum(patchNum, basePatchNum, opt_forceParams) {
if (!opt_forceParams) {
let currentPatchNum;
if (this._change.current_revision) {
@@ -630,13 +690,13 @@
currentPatchNum = this.computeLatestPatchNum(this._allPatchSets);
}
if (this.patchNumEquals(patchNum, currentPatchNum) &&
- this._patchRange.basePatchNum === 'PARENT') {
+ basePatchNum === 'PARENT') {
Gerrit.Nav.navigateToChange(this._change);
return;
}
}
Gerrit.Nav.navigateToChange(this._change, patchNum,
- this._patchRange.basePatchNum);
+ basePatchNum);
},
_computeChangeUrl(change) {
@@ -719,6 +779,11 @@
this.findSortedIndex(basePatchNum, this._sortedRevisions);
},
+ _computeBasePatchDisabled(patchNum, currentPatchNum) {
+ return this.findSortedIndex(patchNum, this._sortedRevisions) >=
+ this.findSortedIndex(currentPatchNum, this._sortedRevisions);
+ },
+
_computeLabelNames(labels) {
return Object.keys(labels).sort();
},
@@ -1100,6 +1165,7 @@
_updateSelected() {
this._selectedPatchSet = this._patchRange.patchNum;
+ this._diffAgainst = this._patchRange.basePatchNum;
},
_computePatchSetDescription(change, patchNum) {
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 9bf7a7d..5389f1c6 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
@@ -33,6 +33,12 @@
</template>
</test-fixture>
+<test-fixture id="blank">
+ <template>
+ <div></div>
+ </template>
+</test-fixture>
+
<script>
suite('gr-change-view tests', () => {
let element;
@@ -46,6 +52,7 @@
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
getAccount() { return Promise.resolve(null); },
+ _fetchSharedCacheURL() { return Promise.resolve({}); },
});
element = fixture('basic');
});
@@ -121,6 +128,49 @@
});
});
+ test('fullscreen-overlay-opened hides content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleHideBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-opened');
+ assert.isTrue(element._handleHideBackgroundContent.called);
+ assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
+ assert.equal(getComputedStyle(element.$.actions).display, 'block');
+ });
+
+ test('fullscreen-overlay-closed shows content', () => {
+ element._loggedIn = true;
+ element._loading = false;
+ element._change = {
+ owner: {_account_id: 1},
+ labels: {},
+ actions: {
+ abandon: {
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon',
+ },
+ },
+ };
+ sandbox.spy(element, '_handleShowBackgroundContent');
+ element.$.replyDialog.fire('fullscreen-overlay-closed');
+ assert.isTrue(element._handleShowBackgroundContent.called);
+ assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
+ });
+
test('X should expand all messages', () => {
const handleExpand =
sandbox.stub(element.$.messageList, 'handleExpandCollapse');
@@ -622,6 +672,132 @@
element.fire('change', {}, {node: selectEl.nativeSelect});
});
+ test('diffMode defaults to side by side without preferences', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({}));
+ // No user prefs or diff view mode set.
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('diffMode defaults to preference when not already set', done => {
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'UNIFIED');
+ done();
+ });
+ });
+
+ test('existing diffMode overrides preference', done => {
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(
+ Promise.resolve({default_diff_view: 'UNIFIED'}));
+ element._setDiffViewMode().then(() => {
+ assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
+ done();
+ });
+ });
+
+ test('diff against dropdown', done => {
+ element._changeNum = '42';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ rev2: {_number: 2},
+ rev3: {_number: 'edit', basePatchNum: 2},
+ rev4: {_number: 3},
+ },
+ status: 'NEW',
+ labels: {},
+ };
+
+ flush(() => {
+ const selectEl = element.$.patchChange;
+ assert.equal(selectEl.nativeSelect.value, 'PARENT');
+ assert.isTrue(element.$$('#patchChange option[value="3"]')
+ .hasAttribute('disabled'));
+ selectEl.addEventListener('change', () => {
+ assert.equal(selectEl.nativeSelect.value, 'edit');
+ assert(navigateToChangeStub.lastCall.calledWithExactly(
+ element._change, '3', 'edit'),
+ 'Should navigate to /c/42/edit..3');
+ done();
+ });
+ selectEl.nativeSelect.value = 'edit';
+ element.fire('change', {}, {node: selectEl.nativeSelect});
+ });
+ });
+
+ test('expandAllDiffs called when expand button clicked', () => {
+ element._shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element.$.fileList, 'expandAllDiffs');
+ MockInteractions.tap(Polymer.dom(element.root).querySelector(
+ '#expandBtn'));
+ assert.isTrue(element.$.fileList.expandAllDiffs.called);
+ });
+
+ test('collapseAllDiffs called when expand button clicked', () => {
+ element._shownFileCount = 1;
+ flushAsynchronousOperations();
+ sandbox.stub(element.$.fileList, 'collapseAllDiffs');
+ MockInteractions.tap(Polymer.dom(element.root).querySelector(
+ '#collapseBtn'));
+ assert.isTrue(element.$.fileList.collapseAllDiffs.called);
+ });
+
+ test('show/hide diffs disabled for large amounts of files', done => {
+ const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
+ element._files = [];
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element._shownFileCount = 1;
+ flush(() => {
+ assert.isTrue(computeSpy.lastCall.returnValue);
+ _.times(element._maxFilesForBulkActions + 1, () => {
+ element._shownFileCount = element._shownFileCount + 1;
+ });
+ assert.isFalse(computeSpy.lastCall.returnValue);
+ done();
+ });
+ });
+
+ test('diff mode selector initializes from preferences', () => {
+ let resolvePrefs;
+ const prefsPromise = new Promise(resolve => {
+ resolvePrefs = resolve;
+ });
+ sandbox.stub(element.$.restAPI, 'getPreferences').returns(prefsPromise);
+
+ // Attach a new gr-change-view so we can intercept the preferences fetch.
+ const view = document.createElement('gr-change-view');
+ const 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.nativeSelect.value, 'SIDE_BY_SIDE');
+
+ // Receive the overriding preference.
+ resolvePrefs({default_diff_view: 'UNIFIED'});
+ flushAsynchronousOperations();
+ assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
+ document.getElementById('blank').restore();
+ });
+
test('don’t reload entire page when patchRange changes', () => {
const reloadStub = sandbox.stub(element, '_reload',
() => { return Promise.resolve(); });
@@ -629,6 +805,7 @@
'_reloadPatchNumDependentResources',
() => { return Promise.resolve(); });
const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
const value = {
view: Gerrit.Nav.View.CHANGE,
@@ -646,26 +823,22 @@
assert.isFalse(reloadStub.calledTwice);
assert.isTrue(reloadPatchDependentStub.calledOnce);
assert.isTrue(relatedClearSpy.calledOnce);
+ assert.isTrue(collapseStub.calledTwice);
});
test('reload entire page when patchRange doesnt change', () => {
- const mockPatchRange = {patchNum: '1337', basePatchNum: 'PARENT'};
const reloadStub = sandbox.stub(element, '_reload',
() => { return Promise.resolve(); });
- element._patchRange = {};
- sandbox.stub(element, 'computeLatestPatchNum').returns('1337');
+ const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
const value = {
view: Gerrit.Nav.View.CHANGE,
};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledOnce);
- assert.deepEqual(element._patchRange, mockPatchRange);
-
element._initialLoadComplete = true;
- element._patchRange = {};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledTwice);
- assert.deepEqual(element._patchRange, mockPatchRange);
+ assert.isTrue(collapseStub.calledTwice);
});
test('include base patch when not parent', () => {
@@ -686,13 +859,13 @@
labels: {},
};
- element._changePatchNum(13);
+ element._changePatchNum(13, 2);
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element._change, 13, '2'));
+ element._change, 13, 2));
element._patchRange.basePatchNum = 'PARENT';
- element._changePatchNum(3);
+ element._changePatchNum(3, 'PARENT');
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change, 3, 'PARENT'));
});
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 2b7b0fd..705cf52 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -36,7 +36,7 @@
}
.file {
border-top: 1px solid #ddd;
- font-weight: bold;
+ font-family: var(--font-family-bold);
margin: 10px 0 3px;
padding: 10px 0 5px;
}
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 1d07cfe..fdd9b26 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -77,10 +77,15 @@
<div class="patchFiles">
<label>Patch file</label>
<div>
- <a id="download" href$="[[_computeDownloadLink(change, patchNum)]]">
+ <a
+ id="download"
+ href$="[[_computeDownloadLink(change, patchNum)]]"
+ download>
[[_computeDownloadFilename(change, patchNum)]]
</a>
- <a href$="[[_computeZipDownloadLink(change, patchNum)]]">
+ <a
+ href$="[[_computeZipDownloadLink(change, patchNum)]]"
+ download>
[[_computeZipDownloadFilename(change, patchNum)]]
</a>
</div>
@@ -89,7 +94,9 @@
<label>Archive</label>
<div id="archives" class="archives">
<template is="dom-repeat" items="[[config.archives]]" as="format">
- <a href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]">
+ <a
+ href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]"
+ download>
[[format]]
</a>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index b52363b..f6e1748 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -115,31 +115,39 @@
let sandbox;
setup(() => {
- element = fixture('basic');
sandbox = sinon.sandbox.create();
+
+ element = fixture('basic');
+ element.patchNum = '1';
+ element.config = {
+ schemes: {
+ 'anonymous http': {},
+ 'http': {},
+ 'repo': {},
+ 'ssh': {},
+ },
+ archives: ['tgz', 'tar', 'tbz2', 'txz'],
+ };
+
+ flushAsynchronousOperations();
});
teardown(() => {
sandbox.restore();
});
+ test('anchors use download attribute', () => {
+ const anchors = Polymer.dom(element.root).querySelectorAll('a');
+ assert.isTrue(!anchors.some(a => !a.hasAttribute('download')));
+ });
+
suite('gr-download-dialog tests with no fetch options', () => {
setup(() => {
element.change = getChangeObjectNoFetch();
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
+ flushAsynchronousOperations();
});
test('focuses on first download link if no copy links', () => {
- flushAsynchronousOperations();
const focusStub = sandbox.stub(element.$.download, 'focus');
element.focus();
assert.isTrue(focusStub.called);
@@ -150,20 +158,10 @@
suite('gr-download-dialog with fetch options', () => {
setup(() => {
element.change = getChangeObject();
- element.patchNum = '1';
- element.config = {
- schemes: {
- 'anonymous http': {},
- 'http': {},
- 'repo': {},
- 'ssh': {},
- },
- archives: ['tgz', 'tar', 'tbz2', 'txz'],
- };
+ flushAsynchronousOperations();
});
test('focuses on first copy link', () => {
- flushAsynchronousOperations();
const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy');
element.focus();
flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index b218f02..95bfc70 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -44,21 +44,6 @@
:host(.loading) .row {
opacity: .5;
};
- header {
- display: flex;
- font-weight: bold;
- justify-content: space-between;
- margin-bottom: .5em;
- }
- .rightControls {
- display: flex;
- flex-wrap: wrap;
- font-weight: normal;
- justify-content: flex-end;
- }
- .separator {
- margin: 0 .25em;
- }
.reviewed,
.status {
align-items: center;
@@ -119,7 +104,7 @@
}
.drafts {
color: #C62828;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.show-hide {
margin-left: .4em;
@@ -132,9 +117,6 @@
padding-right: 2.6em;
text-align: right;
}
- .expandInline {
- padding-right: .25em;
- }
.warning {
color: #666;
}
@@ -155,9 +137,6 @@
margin: .25em 0 1em;
overflow-x: auto;
}
- .patchSetSelect {
- max-width: 8em;
- }
.truncatedFileName {
display: none;
}
@@ -201,53 +180,6 @@
}
}
</style>
- <header>
- <div>Files</div>
- <div class="rightControls">
- <template is="dom-if"
- if="[[_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
- <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
- <span class="separator">/</span>
- <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
- </template>
- <template is="dom-if"
- if="[[!_fileListActionsVisible(_shownFiles.*, _maxFilesForBulkActions)]]">
- <div class="warning">
- Bulk actions disabled because there are too many files.
- </div>
- </template>
- <span class="separator">/</span>
- <gr-select
- id="modeSelect"
- bind-value="{{diffViewMode}}">
- <select>
- <option value="SIDE_BY_SIDE">Side By Side</option>
- <option value="UNIFIED_DIFF">Unified</option>
- </select>
- </gr-select>
- <span class="separator">/</span>
- <label>
- Diff against
- <gr-select id="patchChange" bind-value="{{_diffAgainst}}"
- class="patchSetSelect" on-change="_handlePatchChange">
- <select>
- <option value="PARENT">Base</option>
- <template
- is="dom-repeat"
- items="[[computeAllPatchSets(change)]]"
- as="patchNum">
- <option
- disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum, revisions)]]"
- value$="[[patchNum.num]]">
- [[patchNum.num]]
- [[patchNum.desc]]
- </option>
- </template>
- </select>
- </gr-select>
- </label>
- </div>
- </header>
<div
id="container"
class$="[[_computeContainerClass(editLoaded)]]"
@@ -351,7 +283,7 @@
project-config="[[projectConfig]]"
on-line-selected="_onLineSelected"
no-render-on-prefs-change
- view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
+ view-mode="[[diffViewMode]]"></gr-diff>
</template>
</template>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 7164a16..100bdee 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -36,10 +36,7 @@
properties: {
/** @type {?} */
- patchRange: {
- type: Object,
- observer: '_updateSelected',
- },
+ patchRange: Object,
patchNum: String,
changeNum: String,
comments: Object,
@@ -76,7 +73,6 @@
type: Array,
value() { return []; },
},
- _diffAgainst: String,
diffPrefs: {
type: Object,
notify: true,
@@ -108,13 +104,6 @@
type: Array,
computed: '_computeFilesShown(numFilesShown, _files.*)',
},
- // Caps the number of files that can be shown and have the 'show diffs' /
- // 'hide diffs' buttons still be functional.
- _maxFilesForBulkActions: {
- type: Number,
- readOnly: true,
- value: 225,
- },
_expandedFilePaths: {
type: Array,
value() { return []; },
@@ -162,7 +151,7 @@
this._loading = true;
- this._collapseAllDiffs();
+ this.collapseAllDiffs();
const promises = [];
promises.push(this._getFiles().then(files => {
@@ -188,9 +177,6 @@
promises.push(this._getPreferences().then(prefs => {
this._userPrefs = prefs;
- if (!this.diffViewMode) {
- this.set('diffViewMode', prefs.default_diff_view);
- }
}));
return Promise.all(promises).then(() => {
@@ -239,11 +225,6 @@
return this.$.restAPI.getPreferences();
},
- _computePatchSetDisabled(patchNum, currentPatchNum) {
- return this.findSortedIndex(patchNum, this.revisions) >=
- this.findSortedIndex(currentPatchNum, this.revisions);
- },
-
_togglePathExpanded(path) {
// Is the path in the list of expanded diffs? IF so remove it, otherwise
// add it to the list.
@@ -259,14 +240,6 @@
this._togglePathExpanded(this._files[index].__path);
},
- _handlePatchChange(e) {
- const patchRange = Object.assign({}, this.patchRange);
- patchRange.basePatchNum = Polymer.dom(e).rootTarget.value;
-
- Gerrit.Nav.navigateToChange(this.change, patchRange.patchNum,
- patchRange.basePatchNum);
- },
-
_updateDiffPreferences() {
if (!this.diffs.length) { return; }
// Re-render all expanded diffs sequentially.
@@ -287,7 +260,7 @@
}
},
- _expandAllDiffs() {
+ expandAllDiffs() {
this._showInlineDiffs = true;
// Find the list of paths that are in the file list, but not in the
@@ -304,7 +277,7 @@
this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths));
},
- _collapseAllDiffs() {
+ collapseAllDiffs() {
this._showInlineDiffs = false;
this._expandedFilePaths = [];
this.$.diffCursor.handleDiffUpdate();
@@ -640,9 +613,9 @@
_toggleInlineDiffs() {
if (this._showInlineDiffs) {
- this._collapseAllDiffs();
+ this.collapseAllDiffs();
} else {
- this._expandAllDiffs();
+ this.expandAllDiffs();
}
},
@@ -751,7 +724,9 @@
},
_computeFilesShown(numFilesShown, files) {
- return files.base.slice(0, numFilesShown);
+ const filesShown = files.base.slice(0, numFilesShown);
+ this.fire('files-shown-changed', {length: filesShown.length});
+ return filesShown;
},
_setReviewedFiles(shownFiles, files, reviewedRecord, loggedIn) {
@@ -816,35 +791,6 @@
this.numFilesShown = this._files.length;
},
- _updateSelected(patchRange) {
- this._diffAgainst = patchRange.basePatchNum;
- },
-
- /**
- * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
- * the current state.
- *
- * The expected behavior is to use the mode specified in the user's
- * preferences unless they have manually chosen the alternative view.
- *
- * Use side-by-side if there is no view mode or preferences.
- *
- * @return {string}
- */
- _getDiffViewMode(diffViewMode, userPrefs) {
- if (diffViewMode) {
- return diffViewMode;
- } else if (userPrefs) {
- return this.diffViewMode = userPrefs.default_diff_view;
- }
- return 'SIDE_BY_SIDE';
- },
-
- _fileListActionsVisible(shownFilesRecord,
- maxFilesForBulkActions) {
- return shownFilesRecord.base.length <= maxFilesForBulkActions;
- },
-
_computePatchSetDescription(revisions, patchNum) {
const rev = this.getRevisionByPatchNum(revisions, patchNum);
return (rev && rev.description) ?
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index ff62845..4b407c4 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -35,12 +35,6 @@
</template>
</test-fixture>
-<test-fixture id="blank">
- <template>
- <div></div>
- </template>
-</test-fixture>
-
<script>
suite('gr-file-list tests', () => {
let element;
@@ -653,44 +647,6 @@
}
});
- test('diff against dropdown', done => {
- const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange');
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '3',
- };
- element.change = {
- revisions: {
- rev1: {_number: 1},
- rev2: {_number: 2},
- rev3: {_number: 'edit', basePatchNum: 2},
- rev4: {_number: 3},
- },
- };
- element.revisions = [
- {_number: 1},
- {_number: 2},
- {_number: 'edit', basePatchNum: 2},
- {_number: 3},
- ];
-
- flush(() => {
- const selectEl = element.$.patchChange;
- assert.equal(selectEl.nativeSelect.value, 'PARENT');
- assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled'));
- selectEl.addEventListener('change', () => {
- assert.equal(selectEl.nativeSelect.value, 'edit');
- assert(navStub.lastCall.calledWithExactly(element.change, '3', 'edit'),
- 'Should navigate to /c/42/edit..3');
- navStub.restore();
- done();
- });
- selectEl.nativeSelect.value = 'edit';
- element.fire('change', {}, {node: selectEl.nativeSelect});
- });
- });
-
test('checkbox shows/hides diff inline', () => {
element._files = [
{__path: 'myfile.txt'},
@@ -737,57 +693,11 @@
flushAsynchronousOperations();
const diffDisplay = element.diffs[0];
element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
- assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
- assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE');
element.set('diffViewMode', 'UNIFIED_DIFF');
assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
assert.isTrue(element._updateDiffPreferences.called);
});
- test('diff mode selector initializes from preferences', () => {
- let resolvePrefs;
- const prefsPromise = new Promise(resolve => {
- resolvePrefs = resolve;
- });
- sandbox.stub(element, '_getPreferences').returns(prefsPromise);
-
- // Attach a new gr-file-list so we can intercept the preferences fetch.
- const view = document.createElement('gr-file-list');
- const 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.nativeSelect.value, 'SIDE_BY_SIDE');
-
- // Receive the overriding preference.
- resolvePrefs({default_diff_view: 'UNIFIED'});
- flushAsynchronousOperations();
- assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
- document.getElementById('blank').restore();
- });
-
- test('show/hide diffs disabled for large amounts of files', done => {
- const computeSpy = sandbox.spy(element, '_fileListActionsVisible');
- element._files = [];
- element.changeNum = '42';
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: '2',
- };
- element.$.fileCursor.setCursorAtIndex(0);
- flush(() => {
- assert.isTrue(computeSpy.lastCall.returnValue);
- const arr = [];
- _.times(element._maxFilesForBulkActions + 1, () => {
- arr.push({__path: 'myfile.txt'});
- });
- element._files = arr;
- element.numFilesShown = arr.length;
- assert.isFalse(computeSpy.lastCall.returnValue);
- done();
- });
- });
test('expanded attribute not set on path when not expanded', () => {
element._files = [
@@ -796,19 +706,6 @@
assert.isNotOk(element.$$('.expanded'));
});
- test('_getDiffViewMode', () => {
- // No user prefs or diff view mode set.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- // User prefs but no diff view mode set.
- element.diffViewMode = null;
- element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
- assert.equal(
- element._getDiffViewMode(null, element._userPrefs), 'UNIFIED_DIFF');
- // User prefs and diff view mode set.
- element.diffViewMode = 'SIDE_BY_SIDE';
- assert.equal(element._getDiffViewMode(
- element.diffViewMode, element._userPrefs), 'SIDE_BY_SIDE');
- });
test('expand_inline_diffs user preference', () => {
element._files = [
{__path: '/COMMIT_MSG'},
@@ -859,6 +756,24 @@
assert.notInclude(element._expandedFilePaths, path);
});
+ test('collapseAllDiffs', () => {
+ sandbox.stub(element, '_renderInOrder')
+ .returns(Promise.resolve());
+ const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
+ 'handleDiffUpdate');
+
+ const path = 'path/to/my/file.txt';
+ element.files = [{__path: path}];
+ element._expandedFilePaths = [path];
+ element._showInlineDiffs = true;
+
+ element.collapseAllDiffs();
+ flushAsynchronousOperations();
+ assert.equal(element._expandedFilePaths.length, 0);
+ assert.isFalse(element._showInlineDiffs);
+ assert.isTrue(cursorUpdateStub.calledOnce);
+ });
+
test('_expandedPathsChanged', done => {
sandbox.stub(element, '_reviewFile');
const path = 'path/to/my/file.txt';
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index ae43edf..c0e55ef 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -76,7 +76,7 @@
width: 2.5em;
}
.name {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.message {
--gr-formatted-text-prose-max-width: 80ch;
@@ -136,7 +136,7 @@
}
gr-account-label {
--gr-account-label-text-style: {
- font-weight: bold;
+ font-family: var(--font-family-bold);
};
}
</style>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 8c02e65..2ebf7c7 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -68,7 +68,7 @@
}
.status {
color: #666;
- font-weight: bold;
+ font-family: var(--font-family-bold);
margin-left: .25em;
}
.notCurrent {
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 04a1b16..68c7fd7 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
@@ -87,7 +87,7 @@
margin-top: 1em;
}
.groupName {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.groupSize {
font-style: italic;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index c147456..e95bd41 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -66,7 +66,7 @@
if (switchAccountUrl) {
const replacements = {path};
const url = this._interpolateUrl(switchAccountUrl, replacements);
- links.push({name: 'Switch account', url});
+ links.push({name: 'Switch account', url, external: true});
}
links.push({name: 'Sign out', url: '/logout'});
return links;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 6017cb9..1183d9c 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -83,14 +83,20 @@
// Unparameterized switch account link.
let links = element._getLinks('/switch-account');
assert.equal(links.length, 3);
- assert.deepEqual(links[1],
- {name: 'Switch account', url: '/switch-account'});
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account',
+ external: true,
+ });
// Parameterized switch account link.
links = element._getLinks('/switch-account${path}', '/c/123');
assert.equal(links.length, 3);
- assert.deepEqual(links[1],
- {name: 'Switch account', url: '/switch-account/c/123'});
+ assert.deepEqual(links[1], {
+ name: 'Switch account',
+ url: '/switch-account/c/123',
+ external: true,
+ });
});
test('_interpolateUrl', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 3f3350e..2318657 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -48,12 +48,12 @@
text-align: right;
}
.header {
- font-weight: bold;
+ font-family: var(--font-family-bold);
padding-top: 1em;
}
.key {
display: inline-block;
- font-weight: bold;
+ font-family: var(--font-family-bold);
border-radius: 3px;
background-color: #f1f2f3;
padding: .1em .5em;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 04db296..09f3029 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -78,6 +78,9 @@
flex: 1;
justify-content: flex-end;
}
+ .rightItems gr-endpoint-decorator:not(:empty) {
+ margin-left: 1em;
+ }
gr-search-bar {
flex-grow: 1;
margin-left: .5em;
@@ -117,10 +120,11 @@
@media screen and (max-width: 50em) {
.bigTitle {
font-size: 14px;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
gr-search-bar,
.browse,
+ .rightItems .hideOnMobile,
.links > li.hideOnMobile {
display: none;
}
@@ -159,9 +163,11 @@
</ul>
<div class="rightItems">
<gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-browse-source"></gr-endpoint-decorator>
<div class="accountContainer" id="accountContainer">
- <a class="loginButton" href$="[[_loginURL]]"
- on-tap="_loginTapHandler">Sign in</a>
+ <a class="loginButton" href$="[[_loginURL]]">Sign in</a>
<gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index c7a3815..fe4f7cf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -219,6 +219,12 @@
// makes assumptions that work for the GWT UI, but not PolyGerrit,
// so we'll just disable it altogether for now.
delete linkObj.target;
+
+ // Becasue the "my menu" links may be arbitrary URLs, we don't know
+ // whether they correspond to any client routes. Mark all such links as
+ // external.
+ linkObj.external = true;
+
return linkObj;
},
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index e4cc7bd..3f45bbf 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -58,9 +58,9 @@
{url: 'https://awesometown.com/#hashyhash'},
{url: 'url', target: '_blank'},
].map(element._fixMyMenuItem), [
- {url: '/q/owner:self+is:draft'},
- {url: 'https://awesometown.com/#hashyhash'},
- {url: 'url'},
+ {url: '/q/owner:self+is:draft', external: true},
+ {url: 'https://awesometown.com/#hashyhash', external: true},
+ {url: 'url', external: true},
]);
});
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 f049c99..2de74e1 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -21,6 +21,10 @@
AGREEMENTS: /^\/settings\/(agreements|new-agreement)/,
REGISTER: /^\/register(\/.*)?/,
+ // Pattern for login and logout URLs intended to be passed-through. May
+ // include a return URL.
+ LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
+
// Pattern for a catchall route when no other pattern is matched.
DEFAULT: /.*/,
@@ -42,6 +46,12 @@
GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
+ // Matches /admin/create-project
+ LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
+
+ // Matches /admin/create-project
+ LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
+
// Matches /admin/projects/<project>
PROJECT: /^\/admin\/projects\/([^,]+)$/,
@@ -68,6 +78,8 @@
TAG_LIST_FILTER_OFFSET:
'/admin/projects/:project,tags/q/filter::filter,:offset',
+ PLUGINS: /^\/plugins\/(.+)$/,
+
PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
// Matches /admin/plugins[,<offset>][/].
@@ -439,7 +451,7 @@
'_handleProjectCommandsRoute', true);
this._mapRoute(RoutePattern.PROJECT_ACCESS,
- '_handleProjectAccessRoute', true);
+ '_handleProjectAccessRoute');
this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
'_handleBranchListOffsetRoute');
@@ -459,6 +471,12 @@
this._mapRoute(RoutePattern.TAG_LIST_FILTER,
'_handleTagListFilterRoute');
+ this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
+ '_handleCreateGroupRoute', true);
+
+ this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
+ '_handleCreateProjectRoute', true);
+
this._mapRoute(RoutePattern.PROJECT_LIST_OFFSET,
'_handleProjectListOffsetRoute');
@@ -470,6 +488,8 @@
this._mapRoute(RoutePattern.PROJECT, '_handleProjectRoute');
+ this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
+
this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
'_handlePluginListOffsetRoute', true);
@@ -507,6 +527,8 @@
this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
+ this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
+
// Note: this route should appear last so it only catches URLs unmatched
// by other patterns.
this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
@@ -606,6 +628,7 @@
adminView: 'gr-admin-group-list',
offset: data.params[1] || 0,
filter: null,
+ openCreateModal: data.hash === 'create',
});
},
@@ -719,16 +742,17 @@
_handleProjectListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
offset: data.params[1] || 0,
filter: null,
+ openCreateModal: data.hash === 'create',
});
},
_handleProjectListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
offset: data.params.offset,
filter: data.params.filter,
});
@@ -737,11 +761,23 @@
_handleProjectListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
filter: data.params.filter || null,
});
},
+ _handleCreateProjectRoute(data) {
+ // Redirects the legacy route to the new route, which displays the project
+ // list with a hash 'create'.
+ this._redirect('/admin/projects#create');
+ },
+
+ _handleCreateGroupRoute(data) {
+ // Redirects the legacy route to the new route, which displays the group
+ // list with a hash 'create'.
+ this._redirect('/admin/groups#create');
+ },
+
_handleProjectRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
@@ -914,6 +950,14 @@
},
/**
+ * Handler for routes that should pass through the router and not be caught
+ * by the catchall _handleDefaultRoute handler.
+ */
+ _handlePassThroughRoute() {
+ location.reload();
+ },
+
+ /**
* Catchall route for when no other route is matched.
*/
_handleDefaultRoute() {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 2253bc0..831b905 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -125,6 +125,8 @@
const shouldRequireAutoAuth = [
'_handleAdminPlaceholderRoute',
'_handleAgreementsRoute',
+ '_handleCreateGroupRoute',
+ '_handleCreateProjectRoute',
'_handleDiffEditRoute',
'_handleGroupAuditLogRoute',
'_handleGroupInfoRoute',
@@ -136,7 +138,6 @@
'_handlePluginListFilterRoute',
'_handlePluginListOffsetRoute',
'_handlePluginListRoute',
- '_handleProjectAccessRoute',
'_handleProjectCommandsRoute',
'_handleSettingsLegacyRoute',
'_handleSettingsRoute',
@@ -153,6 +154,8 @@
'_handleChangeLegacyRoute',
'_handleDiffLegacyRoute',
'_handleGroupMembersRoute',
+ '_handlePassThroughRoute',
+ '_handleProjectAccessRoute',
'_handleProjectListFilterOffsetRoute',
'_handleProjectListFilterRoute',
'_handleProjectListOffsetRoute',
@@ -640,6 +643,7 @@
adminView: 'gr-admin-group-list',
offset: 0,
filter: null,
+ openCreateModal: false,
});
data.params[1] = 42;
@@ -648,6 +652,16 @@
adminView: 'gr-admin-group-list',
offset: 42,
filter: null,
+ openCreateModal: false,
+ });
+
+ data.hash = 'create';
+ assertDataToParams(data, '_handleGroupListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: true,
});
});
@@ -808,17 +822,28 @@
const data = {params: {}};
assertDataToParams(data, '_handleProjectListOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
offset: 0,
filter: null,
+ openCreateModal: false,
});
data.params[1] = 42;
assertDataToParams(data, '_handleProjectListOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
offset: 42,
filter: null,
+ openCreateModal: false,
+ });
+
+ data.hash = 'create';
+ assertDataToParams(data, '_handleProjectListOffsetRoute', {
+ view: Gerrit.Nav.View.ADMIN,
+ adminView: 'gr-project-list',
+ offset: 42,
+ filter: null,
+ openCreateModal: true,
});
});
@@ -826,7 +851,7 @@
const data = {params: {filter: 'foo', offset: 42}};
assertDataToParams(data, '_handleProjectListFilterOffsetRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
offset: 42,
filter: 'foo',
});
@@ -836,14 +861,14 @@
const data = {params: {}};
assertDataToParams(data, '_handleProjectListFilterRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
filter: null,
});
data.params.filter = 'foo';
assertDataToParams(data, '_handleProjectListFilterRoute', {
view: Gerrit.Nav.View.ADMIN,
- adminView: 'gr-admin-project-list',
+ adminView: 'gr-project-list',
filter: 'foo',
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index 9ad0bf9..ca22942 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -69,7 +69,7 @@
.authorName,
.draftLabel,
.draftTooltip {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.draftLabel,
.draftTooltip {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index 99a7054..f9ca92e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -45,7 +45,7 @@
}
.header {
border-bottom: 1px solid #ddd;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.mainContainer {
padding: 1em 0;
@@ -69,7 +69,7 @@
justify-content: space-between;
}
.beta {
- font-weight: bold;
+ font-family: var(--font-family-bold);
color: #888;
}
</style>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 440f44b..10d30e4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -113,7 +113,7 @@
}
.dropdown-content a[selected] {
color: #000;
- font-weight: bold;
+ font-family: var(--font-family-bold);
pointer-events: none;
text-decoration: none;
}
@@ -194,7 +194,7 @@
.mobileNavLink {
color: #000;
font-size: 1.5em;
- font-weight: bold;
+ font-family: var(--font-family-bold);
text-decoration: none;
}
.mobileNavLink:not([href]) {
@@ -280,7 +280,9 @@
</gr-patch-range-select>
<span class="download desktop">
<span class="separator">/</span>
- <a class="downloadLink"
+ <a
+ class="downloadLink"
+ download
href$="[[_computeDownloadLink(_changeNum, _patchRange, _path)]]">
Download
</a>
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 e239bf0..73dd94a 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
@@ -455,8 +455,10 @@
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
element._path = 'glados.txt';
flushAsynchronousOperations();
- assert.equal(element.$$('.downloadLink').getAttribute('href'),
+ const link = element.$$('.downloadLink');
+ assert.equal(link.getAttribute('href'),
'/changes/42/revisions/10/patch?zip&path=glados.txt');
+ assert.isTrue(link.hasAttribute('download'));
});
test('file review status', done => {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index defbe8a..406a4a7 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -46,6 +46,7 @@
<link rel="import" href="./core/gr-reporting/gr-reporting.html">
<link rel="import" href="./core/gr-router/gr-router.html">
<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
+<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="./plugins/gr-external-style/gr-external-style.html">
<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html">
@@ -201,6 +202,7 @@
on-close="_handleRegistrationDialogClose">
</gr-registration-dialog>
</gr-overlay>
+ <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator>
<gr-error-manager id="errorManager"></gr-error-manager>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 84aa438..4a38b85 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -145,12 +145,6 @@
this.$.header.unfloat();
},
- _loginTapHandler(e) {
- e.preventDefault();
- page.show('/login/' + encodeURIComponent(
- window.location.pathname + window.location.hash));
- },
-
// Argument used for binding update only.
_computeLoggedIn(account) {
return !!(account && Object.keys(account).length > 0);
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 02a2085..889333b 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,49 +14,122 @@
(function(window) {
'use strict';
- function GrDomHooks(plugin) {
+ function GrDomHooksManager(plugin) {
this._plugin = plugin;
this._hooks = {};
}
- GrDomHooks.prototype._getName = function(endpointName) {
- return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
+ GrDomHooksManager.prototype._getHookName = function(endpointName,
+ opt_moduleName) {
+ if (opt_moduleName) {
+ return endpointName + ' ' + opt_moduleName;
+ } else {
+ return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
+ }
};
- GrDomHooks.prototype.getDomHook = function(endpointName) {
- const hookName = this._getName(endpointName);
+ GrDomHooksManager.prototype.getDomHook = function(endpointName,
+ opt_moduleName) {
+ const hookName = this._getHookName(endpointName, opt_moduleName);
if (!this._hooks[hookName]) {
- this._hooks[hookName] = new GrDomHook(hookName);
+ this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName);
}
return this._hooks[hookName];
};
- function GrDomHook(hookName) {
+ function GrDomHook(hookName, opt_moduleName) {
+ this._instances = [];
this._callbacks = [];
- // Expose to closure.
- const callbacks = this._callbacks;
- this._componentClass = Polymer({
+ if (opt_moduleName) {
+ this._moduleName = opt_moduleName;
+ } else {
+ this._moduleName = hookName;
+ this._createPlaceholder(hookName);
+ }
+ }
+
+ GrDomHook.prototype._createPlaceholder = function(hookName) {
+ Polymer({
is: hookName,
properties: {
plugin: Object,
content: Object,
},
- attached() {
- callbacks.forEach(callback => {
- callback(this);
- });
- },
});
- }
+ };
+ GrDomHook.prototype.handleInstanceDetached = function(instance) {
+ const index = this._instances.indexOf(instance);
+ if (index !== -1) {
+ this._instances.splice(index, 1);
+ }
+ };
+
+ GrDomHook.prototype.handleInstanceAttached = function(instance) {
+ this._instances.push(instance);
+ this._callbacks.forEach(callback => callback(instance));
+ };
+
+ /**
+ * Get instance of last DOM hook element attached into the endpoint.
+ * Returns a Promise, that's resolved when attachment is done.
+ * @return {!Promise<!Element>}
+ */
+ GrDomHook.prototype.getLastAttached = function() {
+ if (this._instances.length) {
+ return Promise.resolve(this._instances.slice(-1)[0]);
+ }
+ if (!this._lastAttachedPromise) {
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ this._callbacks.push(resolve);
+ this._lastAttachedPromise = promise.then(element => {
+ this._lastAttachedPromise = null;
+ const index = this._callbacks.indexOf(resolve);
+ if (index !== -1) {
+ this._callbacks.splice(index, 1);
+ }
+ return element;
+ });
+ }
+ return this._lastAttachedPromise;
+ };
+
+ /**
+ * Get all DOM hook elements.
+ */
+ GrDomHook.prototype.getAllAttached = function() {
+ return this._instances;
+ };
+
+ /**
+ * Install a new callback to invoke when a new instance of DOM hook element
+ * is attached.
+ * @param {function(Element)} callback
+ */
GrDomHook.prototype.onAttached = function(callback) {
this._callbacks.push(callback);
return this;
};
+ /**
+ * Name of DOM hook element that will be installed into the endpoint.
+ */
GrDomHook.prototype.getModuleName = function() {
- return this._componentClass.prototype.is;
+ return this._moduleName;
};
- window.GrDomHooks = GrDomHooks;
+ GrDomHook.prototype.getPublicAPI = function() {
+ const result = {};
+ const exposedMethods = [
+ 'onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName',
+ ];
+ for (const p of exposedMethods) {
+ result[p] = this[p].bind(this);
+ }
+ return result;
+ };
+
+ window.GrDomHook = GrDomHook;
+ window.GrDomHooksManager = GrDomHooksManager;
})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
index f92f5c5..f5a7f6f 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -33,28 +33,110 @@
<script>
suite('gr-dom-hooks tests', () => {
+ const PUBLIC_METHODS =
+ ['onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName'];
+
let instance;
let sandbox;
+ let hook;
+ let hookInternal;
setup(() => {
sandbox = sinon.sandbox.create();
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- instance = new GrDomHooks(plugin);
+ instance = new GrDomHooksManager(plugin);
});
teardown(() => {
sandbox.restore();
});
- test('defines a Polymer components', () => {
- const onAttachedSpy = sandbox.spy();
- instance.getDomHook('foo-bar').onAttached(onAttachedSpy);
- const hookName = Object.keys(instance._hooks).pop();
- assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
- const el = fixture('basic').appendChild(document.createElement(hookName));
- assert.isTrue(onAttachedSpy.calledWithExactly(el));
+ suite('placeholder', () => {
+ setup(()=>{
+ sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
+ hookInternal = instance.getDomHook('foo-bar');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('registers placeholder class', () => {
+ assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
+ 'testplugin-autogenerated-foo-bar'));
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
+ assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
+ });
+ });
+
+ suite('custom element', () => {
+ setup(() => {
+ hookInternal = instance.getDomHook('foo-bar', 'my-el');
+ hook = hookInternal.getPublicAPI();
+ });
+
+ test('public hook API has only public methods', () => {
+ assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
+ });
+
+ test('getModuleName()', () => {
+ const hookName = Object.keys(instance._hooks).pop();
+ assert.equal(hookName, 'foo-bar my-el');
+ assert.equal(hook.getModuleName(), 'my-el');
+ });
+
+ test('onAttached', () => {
+ const onAttachedSpy = sandbox.spy();
+ hook.onAttached(onAttachedSpy);
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
+ assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
+ });
+
+ test('getAllAttached', () => {
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ assert.deepEqual([el1, el2], hook.getAllAttached());
+ hookI.handleInstanceDetached(el1);
+ assert.deepEqual([el2], hook.getAllAttached());
+ });
+
+ test('getLastAttached', () => {
+ const beforeAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el1, el));
+ const [el1, el2] = [
+ document.createElement(hook.getModuleName()),
+ document.createElement(hook.getModuleName()),
+ ];
+ el1.textContent = 'one';
+ el2.textContent = 'two';
+ hookInternal.handleInstanceAttached(el1);
+ hookInternal.handleInstanceAttached(el2);
+ const afterAttachedPromise = hook.getLastAttached().then(
+ el => assert.strictEqual(el2, el));
+ return Promise.all([
+ beforeAttachedPromise,
+ afterAttachedPromise,
+ ]);
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
index 49424a1..0928534 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
@@ -18,7 +18,7 @@
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<dom-module id="gr-endpoint-decorator">
- <template>
+ <template strip-whitespace>
<content></content>
</template>
<script src="gr-endpoint-decorator.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 7e74494..bcd2378 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -19,6 +19,17 @@
properties: {
name: String,
+ /** @type {!Map} */
+ _domHooks: {
+ type: Map,
+ value() { return new Map(); },
+ },
+ },
+
+ detached() {
+ for (const [el, domHook] of this._domHooks) {
+ domHook.handleInstanceDetached(el);
+ }
},
_import(url) {
@@ -31,33 +42,48 @@
const el = document.createElement(name);
el.plugin = plugin;
el.content = this.getContentChildren()[0];
- return Polymer.dom(this.root).appendChild(el);
+ this._appendChild(el);
+ return el;
},
_initReplacement(name, plugin) {
this.getContentChildren().forEach(node => node.remove());
const el = document.createElement(name);
el.plugin = plugin;
- return Polymer.dom(this.root).appendChild(el);
+ this._appendChild(el);
+ return el;
+ },
+
+ _appendChild(el) {
+ Polymer.dom(this.root).appendChild(el);
+ },
+
+ _initModule({moduleName, plugin, type, domHook}) {
+ let el;
+ switch (type) {
+ case 'decorate':
+ el = this._initDecoration(moduleName, plugin);
+ break;
+ case 'replace':
+ el = this._initReplacement(moduleName, plugin);
+ break;
+ }
+ if (el) {
+ domHook.handleInstanceAttached(el);
+ }
+ this._domHooks.set(el, domHook);
},
ready() {
+ Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
Gerrit.awaitPluginsLoaded().then(() => Promise.all(
Gerrit._endpoints.getPlugins(this.name).map(
pluginUrl => this._import(pluginUrl)))
- ).then(() => {
- const modulesData = Gerrit._endpoints.getDetails(this.name);
- for (const {moduleName, plugin, type} of modulesData) {
- switch (type) {
- case 'decorate':
- this._initDecoration(moduleName, plugin);
- break;
- case 'replace':
- this._initReplacement(moduleName, plugin);
- break;
- }
- }
- });
+ ).then(() =>
+ Gerrit._endpoints
+ .getDetails(this.name)
+ .forEach(this._initModule, this)
+ );
},
});
})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 8b96dee..e7d1930 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -34,10 +34,20 @@
let sandbox;
let element;
let plugin;
+ let domHookStub;
setup(done => {
+ Gerrit._endpoints = new GrPluginEndpoints();
+
sandbox = sinon.sandbox.create();
+ domHookStub = {
+ handleInstanceAttached: sandbox.stub(),
+ handleInstanceDetached: sandbox.stub(),
+ };
+ sandbox.stub(
+ GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub);
+
// NB: Order is important.
Gerrit.install(p => {
plugin = p;
@@ -45,11 +55,12 @@
plugin.registerCustomComponent('foo', 'other-module', {replace: true});
}, '0.1', 'http://some/plugin/url.html');
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
element = fixture('basic');
- sandbox.stub(element, '_initDecoration');
- sandbox.stub(element, '_initReplacement');
+ sandbox.stub(element, '_initDecoration').returns({});
+ sandbox.stub(element, '_initReplacement').returns({});
sandbox.stub(element, 'importHref', (url, resolve) => resolve());
flush(done);
@@ -65,13 +76,39 @@
});
test('inits decoration dom hook', () => {
- assert.isTrue(
- element._initDecoration.calledWith('some-module', plugin));
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[0], 'some-module');
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[1], plugin);
});
test('inits replacement dom hook', () => {
- assert.isTrue(
- element._initReplacement.calledWith('other-module', plugin));
+ assert.strictEqual(
+ element._initReplacement.lastCall.args[0], 'other-module');
+ assert.strictEqual(
+ element._initReplacement.lastCall.args[1], plugin);
+ });
+
+ test('calls dom hook handleInstanceAttached', () => {
+ assert.equal(domHookStub.handleInstanceAttached.callCount, 2);
+ });
+
+ test('calls dom hook handleInstanceDetached', () => {
+ element.detached();
+ assert.equal(domHookStub.handleInstanceDetached.callCount, 2);
+ });
+
+ test('installs modules on late registration', done => {
+ domHookStub.handleInstanceAttached.reset();
+ plugin.registerCustomComponent('foo', 'noob-noob');
+ flush(() => {
+ assert.equal(domHookStub.handleInstanceAttached.callCount, 1);
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[0], 'noob-noob');
+ assert.strictEqual(
+ element._initDecoration.lastCall.args[1], plugin);
+ done();
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
new file mode 100644
index 0000000..3ccb3fd
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.html
@@ -0,0 +1,28 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+
+<dom-module id="gr-plugin-popup">
+ <template>
+ <style include="shared-styles"></style>
+ <gr-overlay id="overlay" with-backdrop>
+ <content></content>
+ </gr-overlay>
+ </template>
+ <script src="gr-plugin-popup.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
new file mode 100644
index 0000000..8286eae
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -0,0 +1,28 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+ 'use strict';
+ Polymer({
+ is: 'gr-plugin-popup',
+ get opened() {
+ return this.$.overlay.opened;
+ },
+ open() {
+ return this.$.overlay.open();
+ },
+ close() {
+ this.$.overlay.close();
+ },
+ });
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
new file mode 100644
index 0000000..2dbf96d
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-plugin-popup</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-plugin-popup.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-plugin-popup></gr-plugin-popup>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-plugin-popup tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ stub('gr-overlay', {
+ open: sandbox.stub().returns(Promise.resolve()),
+ close: sandbox.stub(),
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('exists', () => {
+ assert.isOk(element);
+ });
+
+ test('open uses open() from gr-overlay', () => {
+ return element.open().then(() => {
+ assert.isTrue(element.$.overlay.open.called);
+ });
+ });
+
+ test('close uses close() from gr-overlay', () => {
+ element.close();
+ assert.isTrue(element.$.overlay.close.called);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
new file mode 100644
index 0000000..6bf37de
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html
@@ -0,0 +1,23 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+<link rel="import" href="gr-plugin-popup.html">
+
+<dom-module id="gr-popup-interface">
+ <script src="gr-popup-interface.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
new file mode 100644
index 0000000..e62e882
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -0,0 +1,71 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+ 'use strict';
+
+ /**
+ * Plugin popup API.
+ * Provides method for opening and closing popups from plugin.
+ * opt_moduleName is a name of custom element that will be automatically
+ * inserted on popup opening.
+ * @param {!Object} plugin
+ * @param {opt_moduleName=} string
+ */
+ function GrPopupInterface(plugin, opt_moduleName) {
+ this.plugin = plugin;
+ this._openingPromise = null;
+ this._popup = null;
+ this._moduleName = opt_moduleName || null;
+ }
+
+ GrPopupInterface.prototype._getElement = function() {
+ return Polymer.dom(this._popup);
+ };
+
+ /**
+ * Opens the popup, inserts it into DOM over current UI.
+ * Creates the popup if not previously created. Creates popup content element,
+ * if it was provided with constructor.
+ * @returns {!Promise<!Object>}
+ */
+ GrPopupInterface.prototype.open = function() {
+ if (!this._openingPromise) {
+ this._openingPromise =
+ this.plugin.hook('plugin-overlay').getLastAttached()
+ .then(hookEl => {
+ const popup = document.createElement('gr-plugin-popup');
+ if (this._moduleName) {
+ const el = Polymer.dom(popup).appendChild(
+ document.createElement(this._moduleName));
+ el.plugin = this.plugin;
+ }
+ this._popup = Polymer.dom(hookEl).appendChild(popup);
+ Polymer.dom.flush();
+ return this._popup.open().then(() => this);
+ });
+ }
+ return this._openingPromise;
+ };
+
+ /**
+ * Hides the popup.
+ */
+ GrPopupInterface.prototype.close = function() {
+ if (!this._popup) { return; }
+ this._popup.close();
+ this._openingPromise = null;
+ };
+
+ window.GrPopupInterface = GrPopupInterface;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
new file mode 100644
index 0000000..7d9dd28
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-popup-interface</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-popup-interface.html"/>
+
+<script>void(0);</script>
+
+<test-fixture id="container">
+ <template>
+ <div></div>
+ </template>
+</test-fixture>
+
+<dom-module id="gr-user-test-popup">
+ <template>
+ <div id="barfoo">some test module</div>
+ </template>
+ <script>Polymer({is: 'gr-user-test-popup'});</script>
+</dom-module>
+
+<script>
+ suite('gr-popup-interface tests', () => {
+ let container;
+ let instance;
+ let plugin;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ container = fixture('container');
+ sandbox.stub(plugin, 'hook').returns({
+ getLastAttached() {
+ return Promise.resolve(container);
+ },
+ });
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ suite('manual', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin);
+ });
+
+ test('open', () => {
+ return instance.open().then(api => {
+ assert.strictEqual(api, instance);
+ const manual = document.createElement('div');
+ manual.id = 'foobar';
+ manual.innerHTML = 'manual content';
+ api._getElement().appendChild(manual);
+ flushAsynchronousOperations();
+ assert.equal(
+ container.querySelector('#foobar').textContent, 'manual content');
+ });
+ });
+
+ test('close', () => {
+ return instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ });
+ });
+ });
+
+ suite('components', () => {
+ setup(() => {
+ instance = new GrPopupInterface(plugin, 'gr-user-test-popup');
+ });
+
+ test('open', () => {
+ return instance.open().then(api => {
+ assert.isNotNull(
+ Polymer.dom(container).querySelector('gr-user-test-popup'));
+ });
+ });
+
+ test('close', () => {
+ return instance.open().then(api => {
+ assert.isTrue(api._getElement().node.opened);
+ api.close();
+ assert.isFalse(api._getElement().node.opened);
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index e91ab0a..d57b301 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -22,7 +22,7 @@
}
GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
- this.plugin.getDomHook('header-title', {replace: true}).onAttached(
+ this.plugin.hook('header-title', {replace: true}).onAttached(
element => {
const customHeader =
document.createElement('gr-custom-plugin-header');
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 34f0b16..1bd345e 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -36,7 +36,7 @@
}
header {
border-bottom: 1px solid #ddd;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
header,
main,
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index bf5e4e0..51fa616 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -53,7 +53,7 @@
}
.action {
color: #a1c2fa;
- font-weight: bold;
+ font-family: var(--font-family-bold);
margin-left: 1em;
text-decoration: none;
}
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 1fecbe7..2cbcfa2 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -32,7 +32,7 @@
display: inline-block;
font-family: var(--font-family);
font-size: 12px;
- font-weight: bold;
+ font-family: var(--font-family-bold);
outline-width: 0;
padding: .4em .85em;
position: relative;
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
index d4a98e4..27c0355 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -33,7 +33,7 @@
header {
border-bottom: 1px solid #ddd;
flex-shrink: 0;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
main {
display: flex;
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
index 05075bd..68c3848 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -40,11 +40,11 @@
display: block;
}
label {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
li[selected] gr-button {
color: #000;
- font-weight: bold;
+ font-family: var(--font-family-bold);
text-decoration: none;
}
.schemes {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 8f2994d..f4813fa 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -61,7 +61,7 @@
list-style: none;
}
ul .accountName {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
li .accountInfo,
li .itemAction {
@@ -92,7 +92,7 @@
padding: .85em 1em;
}
.bold-text {
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
:host:not([down-arrow]) .downArrow { display: none; }
:host([down-arrow]) .downArrow {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index a66ab2f..ec8f04c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,6 +14,9 @@
(function() {
'use strict';
+ const REL_NOOPENER = 'noopener';
+ const REL_EXTERNAL = 'external';
+
Polymer({
is: 'gr-dropdown',
@@ -83,6 +86,10 @@
'up': '_handleUp',
},
+ /**
+ * Handle the up key.
+ * @param {!Event} e
+ */
_handleUp(e) {
if (this.$.dropdown.opened) {
e.preventDefault();
@@ -93,6 +100,10 @@
}
},
+ /**
+ * Handle the down key.
+ * @param {!Event} e
+ */
_handleDown(e) {
if (this.$.dropdown.opened) {
e.preventDefault();
@@ -103,6 +114,10 @@
}
},
+ /**
+ * Handle the tab key.
+ * @param {!Event} e
+ */
_handleTab(e) {
if (this.$.dropdown.opened) {
// Tab in a native select is a no-op. Emulate this.
@@ -111,6 +126,10 @@
}
},
+ /**
+ * Handle the enter key.
+ * @param {!Event} e
+ */
_handleEnter(e) {
e.preventDefault();
e.stopPropagation();
@@ -125,6 +144,10 @@
}
},
+ /**
+ * Handle a click on the iron-dropdown element.
+ * @param {!Event} e
+ */
_handleDropdownTap(e) {
// async is needed so that that the click event is fired before the
// dropdown closes (This was a bug for touch devices).
@@ -133,10 +156,17 @@
}, 1);
},
+ /**
+ * Hanlde a click on the button to open the dropdown.
+ * @param {!Event} e
+ */
_showDropdownTapHandler(e) {
this._open();
},
+ /**
+ * Open the dropdown and initialize the cursor.
+ */
_open() {
this.$.dropdown.open();
this.$.cursor.setCursorAtIndex(0);
@@ -144,19 +174,43 @@
this.$.cursor.target.focus();
},
+ /**
+ * Get the class for a top-content item based on the given boolean.
+ * @param {boolean} bold Whether the item is bold.
+ * @return {string} The class for the top-content item.
+ */
_getClassIfBold(bold) {
return bold ? 'bold-text' : '';
},
+ /**
+ * Build a URL for the given host and path. If there is a base URL, it will
+ * be included between the host and the path.
+ * @param {!string} host
+ * @param {!string} path
+ * @return {!string} The scheme-relative URL.
+ */
_computeURLHelper(host, path) {
return '//' + host + this.getBaseUrl() + path;
},
+ /**
+ * Build a scheme-relative URL for the current host. Will include the base
+ * URL if one is present. Note: the URL will be scheme-relative but absolute
+ * with regard to the host.
+ * @param {!string} path The path for the URL.
+ * @return {!string} The scheme-relative URL.
+ */
_computeRelativeURL(path) {
const host = window.location.host;
return this._computeURLHelper(host, path);
},
+ /**
+ * Compute the URL for a link object.
+ * @param {!Object} link The object describing the link.
+ * @return {!string} The URL.
+ */
_computeLinkURL(link) {
if (typeof link.url === 'undefined') {
return '';
@@ -167,10 +221,24 @@
return this._computeRelativeURL(link.url);
},
+ /**
+ * Compute the value for the rel attribute of an anchor for the given link
+ * object. If the link has a target value, then the rel must be "noopener"
+ * for security reasons.
+ * @param {!Object} link The object describing the link.
+ * @return {?string} The rel value for the link.
+ */
_computeLinkRel(link) {
- return link.target ? 'noopener' : null;
+ // Note: noopener takes precedence over external.
+ if (link.target) { return REL_NOOPENER; }
+ if (link.external) { return REL_EXTERNAL; }
+ return null;
},
+ /**
+ * Handle a click on an item of the dropdown.
+ * @param {!Event} e
+ */
_handleItemTap(e) {
const id = e.target.getAttribute('data-id');
const item = this.items.find(item => item.id === id);
@@ -182,10 +250,20 @@
}
},
+ /**
+ * If a dropdown item is shown as a button, get the class for the button.
+ * @param {string} id
+ * @param {!Object} disabledIdsRecord The change record for the disabled IDs
+ * list.
+ * @return {!string} The class for the item button.
+ */
_computeDisabledClass(id, disabledIdsRecord) {
return disabledIdsRecord.base.includes(id) ? 'disabled' : '';
},
+ /**
+ * Recompute the stops for the dropdown item cursor.
+ */
_resetCursorStops() {
Polymer.dom.flush();
this._listElements = Polymer.dom(this.root).querySelectorAll('li');
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 8654ac8..ab31f7c 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -72,10 +72,17 @@
});
test('link rel', () => {
- assert.isNull(element._computeLinkRel({url: '/test'}));
- assert.equal(
- element._computeLinkRel({url: '/test', target: '_blank'}),
- 'noopener');
+ let link = {url: '/test'};
+ assert.isNull(element._computeLinkRel(link));
+
+ link = {url: '/test', target: '_blank'};
+ assert.equal(element._computeLinkRel(link), 'noopener');
+
+ link = {url: '/test', external: true};
+ assert.equal(element._computeLinkRel(link), 'external');
+
+ link = {url: '/test', target: '_blank', external: true};
+ assert.equal(element._computeLinkRel(link), 'noopener');
});
test('_getClassIfBold', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index df407a9..65aa364 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -49,7 +49,7 @@
GrChangeReplyInterface.prototype.addReplyTextChangedCallback =
function(handler) {
- this.plugin.getDomHook('reply-text').onAttached(el => {
+ this.plugin.hook('reply-text').onAttached(el => {
if (!el.content) { return; }
el.content.addEventListener('value-changed', e => {
handler(e.detail.value);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index f6e2b64..53f889f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
+<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index ca0f372..dec2dc3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -120,6 +120,26 @@
});
});
+ test('delete works', () => {
+ const response = {status: 204};
+ sendStub.returns(Promise.resolve(response));
+ return plugin.delete('/url', r => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.strictEqual(r, response);
+ });
+ });
+
+ test('delete fails', () => {
+ sendStub.returns(Promise.resolve(
+ {status: 400, text() { return Promise.resolve('text'); }}));
+ return plugin.delete('/url', r => {
+ throw new Error('Should not resolve');
+ }).catch(err => {
+ assert.isTrue(sendStub.calledWith('DELETE', '/url'));
+ assert.equal('text', err);
+ });
+ });
+
test('history event', done => {
plugin.on(element.EventType.HISTORY, throwErrFn);
plugin.on(element.EventType.HISTORY, path => {
@@ -338,5 +358,35 @@
'http://test.com/r/plugins/testplugin/static/test.js');
});
});
+
+ suite('popup', () => {
+ test('popup(element) is deprecated', () => {
+ assert.throws(() => {
+ plugin.popup(document.createElement('div'));
+ });
+ });
+
+ test('popup(moduleName) creates popup with component', () => {
+ const openStub = sandbox.stub();
+ sandbox.stub(window, 'GrPopupInterface').returns({
+ open: openStub,
+ });
+ plugin.popup('some-name');
+ assert.isTrue(openStub.calledOnce);
+ assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name'));
+ });
+
+ test('deprecated.popup(element) creates popup with element', () => {
+ const el = document.createElement('div');
+ el.textContent = 'some text here';
+ const openStub = sandbox.stub(GrPopupInterface.prototype, 'open');
+ openStub.returns(Promise.resolve({
+ _getElement() {
+ return document.createElement('div');
+ }}));
+ plugin.deprecated.popup(el);
+ assert.isTrue(openStub.calledOnce);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 52b1fb7..1ee9eec 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -16,19 +16,32 @@
function GrPluginEndpoints() {
this._endpoints = {};
+ this._callbacks = {};
}
+ GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
+ if (!this._callbacks[endpoint]) {
+ this._callbacks[endpoint] = [];
+ }
+ this._callbacks[endpoint].push(callback);
+ };
+
GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
- moduleName) {
+ moduleName, domHook) {
if (!this._endpoints[endpoint]) {
this._endpoints[endpoint] = [];
}
- this._endpoints[endpoint].push({
+ const moduleInfo = {
moduleName,
plugin,
pluginUrl: plugin._url,
type,
- });
+ domHook,
+ };
+ this._endpoints[endpoint].push(moduleInfo);
+ if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) {
+ this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
+ }
};
/**
@@ -44,6 +57,7 @@
* plugin: Plugin,
* pluginUrl: String,
* type: EndpointType,
+ * domHook: !Object
* }>}
*/
GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index 2c1f4e9..a61cdc8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -29,16 +29,21 @@
let instance;
let pluginFoo;
let pluginBar;
+ let domHook;
setup(() => {
sandbox = sinon.sandbox.create();
+ domHook = {};
instance = new GrPluginEndpoints();
Gerrit.install(p => { pluginFoo = p; }, '0.1',
'http://test.com/plugins/testplugin/static/foo.html');
- instance.registerModule(pluginFoo, 'a-place', 'decorate', 'foo-module');
+ instance.registerModule(
+ pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
Gerrit.install(p => { pluginBar = p; }, '0.1',
'http://test.com/plugins/testplugin/static/bar.html');
- instance.registerModule(pluginBar, 'a-place', 'style', 'bar-module');
+ instance.registerModule(
+ pluginBar, 'a-place', 'style', 'bar-module', domHook);
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
});
teardown(() => {
@@ -52,12 +57,14 @@
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
type: 'decorate',
+ domHook,
},
{
moduleName: 'bar-module',
plugin: pluginBar,
pluginUrl: pluginBar._url,
type: 'style',
+ domHook,
},
]);
});
@@ -69,6 +76,7 @@
plugin: pluginBar,
pluginUrl: pluginBar._url,
type: 'style',
+ domHook,
},
]);
});
@@ -82,6 +90,7 @@
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
type: 'decorate',
+ domHook,
},
]);
});
@@ -95,5 +104,19 @@
assert.deepEqual(
instance.getPlugins('a-place'), [pluginFoo._url, pluginBar._url]);
});
+
+ test('onNewEndpoint', () => {
+ const newModuleStub = sandbox.stub();
+ instance.onNewEndpoint('a-place', newModuleStub);
+ instance.registerModule(
+ pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+ assert.deepEqual(newModuleStub.lastCall.args[0], {
+ moduleName: 'zaz-module',
+ plugin: pluginFoo,
+ pluginUrl: pluginFoo._url,
+ type: 'replace',
+ domHook,
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d1b9417..52db873 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -55,8 +55,7 @@
window.$wnd = window;
function Plugin(opt_url) {
- this._generatedHookNames = [];
- this._domHooks = new GrDomHooks(this);
+ this._domHooks = new GrDomHooksManager(this);
if (!opt_url) {
console.warn('Plugin not being loaded from /plugins base path.',
@@ -77,6 +76,10 @@
return;
}
this._name = pathname.split('/')[2];
+
+ this.deprecated = {
+ popup: deprecatedAPI.popup.bind(this),
+ };
}
Plugin._sharedAPIElement = document.createElement('gr-js-api-interface');
@@ -93,11 +96,22 @@
};
Plugin.prototype.registerCustomComponent = function(
- endpointName, moduleName, opt_options) {
+ endpointName, opt_moduleName, opt_options) {
const type = opt_options && opt_options.replace ?
EndpointType.REPLACE : EndpointType.DECORATE;
+ const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
+ const moduleName = opt_moduleName || hook.getModuleName();
Gerrit._endpoints.registerModule(
- this, endpointName, type, moduleName);
+ this, endpointName, type, moduleName, hook);
+ return hook.getPublicAPI();
+ };
+
+ /**
+ * Returns instance of DOM hook API for endpoint. Creates a placeholder
+ * element for the first call.
+ */
+ Plugin.prototype.hook = function(endpointName, opt_options) {
+ return this.registerCustomComponent(endpointName, undefined, opt_options);
};
Plugin.prototype.getServerInfo = function() {
@@ -143,6 +157,24 @@
return this._send('POST', url, opt_callback, payload);
},
+ Plugin.prototype.delete = function(url, opt_callback) {
+ return getRestAPI().send('DELETE', url, opt_callback).then(response => {
+ if (response.status !== 204) {
+ return response.text().then(text => {
+ if (text) {
+ return Promise.reject(text);
+ } else {
+ return Promise.reject(response.status);
+ }
+ });
+ }
+ if (opt_callback) {
+ opt_callback(response);
+ }
+ return response;
+ });
+ },
+
Plugin.prototype.changeActions = function() {
return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement(
Plugin._sharedAPIElement.Element.CHANGE_ACTIONS));
@@ -166,12 +198,22 @@
return new GrAttributeHelper(element);
};
- Plugin.prototype.getDomHook = function(endpointName, opt_options) {
- const hook = this._domHooks.getDomHook(endpointName);
- const moduleName = hook.getModuleName();
- const type = opt_options && opt_options.type || EndpointType.DECORATE;
- Gerrit._endpoints.registerModule(this, endpointName, type, moduleName);
- return hook;
+ Plugin.prototype.popup = function(moduleName) {
+ if (typeof moduleName !== 'string') {
+ throw new Error('deprecated, use deprecated.popup');
+ }
+ const api = new GrPopupInterface(this, moduleName);
+ return api.open();
+ };
+
+ const deprecatedAPI = {};
+ deprecatedAPI.popup = function(el) {
+ console.warn('plugin.deprecated.popup() is deprecated!');
+ if (!el) {
+ throw new Error('Popup contents not found');
+ }
+ const api = new GrPopupInterface(this);
+ api.open().then(api => api._getElement().appendChild(el));
};
const Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
index 419d2f7..1b59d35 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -25,6 +25,16 @@
background: #fff;
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
}
+
+ @media screen and (max-width: 50em) {
+ :host {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ right: 0;
+ top: 0;
+ }
+ }
</style>
<content></content>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index 8db8004..ebf2f02 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -16,21 +16,61 @@
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
+ const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
Polymer({
is: 'gr-overlay',
+ /**
+ * Fired when a fullscreen overlay is closed
+ *
+ * @event fullscreen-overlay-closed
+ */
+
+ /**
+ * Fired when an overlay is opened in full screen mode
+ *
+ * @event fullscreen-overlay-opened
+ */
+
+ properties: {
+ _fullScreenOpen: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
behaviors: [
Polymer.IronOverlayBehavior,
],
+ listeners: {
+ 'iron-overlay-closed': '_close',
+ 'iron-overlay-cancelled': '_close',
+ },
+
open(...args) {
return new Promise(resolve => {
Polymer.IronOverlayBehaviorImpl.open.apply(this, args);
+ if (this._isMobile()) {
+ this.fire('fullscreen-overlay-opened');
+ this._fullScreenOpen = true;
+ }
this._awaitOpen(resolve);
});
},
+ _isMobile() {
+ return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
+ },
+
+ _close() {
+ if (this._fullScreenOpen) {
+ this.fire('fullscreen-overlay-closed');
+ this._fullScreenOpen = false;
+ }
+ },
+
/**
* Override the focus stops that iron-overlay-behavior tries to find.
*/
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
new file mode 100644
index 0000000..3f427ca
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-overlay</title>
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../test/common-test-setup.html"/>
+
+<link rel="import" href="gr-overlay.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-overlay>
+ <div>content</div>
+ </gr-overlay>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-overlay tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('events are fired on fullscreen view', done => {
+ sandbox.stub(element, '_isMobile').returns(true);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isTrue(element._fullScreenOpen);
+ assert.isTrue(openHandler.called);
+
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isTrue(closeHandler.called);
+ done();
+ });
+ });
+
+ test('events are not fired on desktop view', done => {
+ sandbox.stub(element, '_isMobile').returns(false);
+ const openHandler = sandbox.stub();
+ const closeHandler = sandbox.stub();
+ element.addEventListener('fullscreen-overlay-opened', openHandler);
+ element.addEventListener('fullscreen-overlay-closed', closeHandler);
+
+ element.open().then(() => {
+ assert.isTrue(element._isMobile.called);
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(openHandler.called);
+
+ element._close();
+ assert.isFalse(element._fullScreenOpen);
+ assert.isFalse(closeHandler.called);
+ done();
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 2d48d36..21aa6cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -55,7 +55,16 @@
};
GrEtagDecorator.prototype.getCachedPayload = function(url) {
- return this._payloadCache.get(url);
+ let payload = this._payloadCache.get(url);
+
+ if (typeof payload === 'object') {
+ // Note: For the sake of cache transparency, deep clone the response
+ // object so that cache hits are not equal object references. Some code
+ // expects every network response to deserialize to a fresh object.
+ payload = JSON.parse(JSON.stringify(payload));
+ }
+
+ return payload;
};
GrEtagDecorator.prototype._truncateCache = function() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
index 8be2352..40e639e 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -84,7 +84,7 @@
});
test('getCachedPayload', () => {
- const payload = {};
+ const payload = 'payload';
etag.collect('/foo', fakeRequest('bar'), payload);
assert.strictEqual(etag.getCachedPayload('/foo'), payload);
etag.collect('/foo', fakeRequest('bar', 304), 'garbage');
@@ -92,5 +92,25 @@
etag.collect('/foo', fakeRequest('bar', 200), 'new payload');
assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload');
});
+
+ test('getCachedPayload does not preserve object equality', () => {
+ const payload = {foo: 'bar'};
+ etag.collect('/foo', fakeRequest('bar'), payload);
+ assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+ assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 304), {foo: 'baz'});
+ assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+ assert.notStrictEqual(etag.getCachedPayload('/foo'), payload);
+ etag.collect('/foo', fakeRequest('bar', 200), {foo: 'bar baz'});
+ assert.deepEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+ assert.notStrictEqual(etag.getCachedPayload('/foo'), {foo: 'bar baz'});
+ });
+
+ test('getCachedPayload clones the response deeply', () => {
+ const payload = {foo: {bar: 'baz'}};
+ etag.collect('/foo', fakeRequest('bar'), payload);
+ assert.deepEqual(etag.getCachedPayload('/foo'), payload);
+ assert.notStrictEqual(etag.getCachedPayload('/foo').foo, payload.foo);
+ });
});
</script>
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
index 46fc46b..080c345 100644
--- a/polygerrit-ui/app/index.html
+++ b/polygerrit-ui/app/index.html
@@ -26,6 +26,10 @@
-->
<link rel="preload" href="/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Regular.woff" as="font" type="font/woff" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Medium.woff2" as="font" type="font/woff2" crossorigin>
+<link rel="preload" href="/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin>
<link rel="stylesheet" href="/styles/fonts.css">
<link rel="stylesheet" href="/styles/main.css">
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index be80c13..a77ad50 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -72,7 +72,7 @@
name + "_app_sources",
name + "_css_sources",
name + "_top_sources",
- "//lib/fonts:robotomono",
+ "//lib/fonts:robotofonts",
"//lib/js:highlightjs_files",
# we extract from the zip, but depend on the component for license checking.
"@webcomponentsjs//:zipfile",
@@ -82,7 +82,7 @@
cmd = " && ".join([
"mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}",
"for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + appName + ".$$ext; done",
- "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/",
+ "cp $(locations //lib/fonts:robotofonts) $$TMP/polygerrit_ui/fonts/",
"for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done",
"for f in $(locations "+ name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done",
"for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done",
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 4728fe6..4318757 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -29,11 +29,12 @@
--default-text-color: #000;
--view-background-color: #fff;
--default-horizontal-margin: 1rem;
- --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ --font-family-bold: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
--iron-overlay-backdrop: {
transition: none;
- };
+ }
}
@media screen and (max-width: 50em) {
:root {
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index d339e16..6a5da44 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -17,4 +17,46 @@
url('../fonts/RobotoMono-Regular.woff2') format('woff2'),
url('../fonts/RobotoMono-Regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('Roboto-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
+ unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('RobotoMono-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+
+/* latin-ext */
+@font-face {
+ font-family: 'Roboto Medium';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto Medium'), local('Roboto-Medium'),
+ url('../fonts/Roboto-Medium.woff2') format('woff2'),
+ url('../fonts/Roboto-Medium.woff') format('woff');
+ unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Roboto Medium';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto Medium'), local('Roboto-Medium'),
+ url('../fonts/Roboto-Medium.woff2') format('woff2'),
+ url('../fonts/Roboto-Medium.woff') format('woff');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
}
\ No newline at end of file
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index d283aac..00ea613 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -22,7 +22,7 @@
.topHeader,
.groupHeader {
border-bottom: 1px solid #eee;
- font-weight: bold;
+ font-family: var(--font-family-bold);
padding: .3em .5em;
}
.topHeader {
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index 7e0bf5a..603f610 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -39,7 +39,7 @@
}
.gr-form-styles .title {
color: #666;
- font-weight: bold;
+ font-family: var(--font-family-bold);
padding-right: .5em;
width: 15em;
}
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.html b/polygerrit-ui/app/styles/gr-page-nav-styles.html
index e6e9c96..0c4d20f 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.html
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.html
@@ -44,14 +44,14 @@
margin-top: 1em;
}
.navStyles .title {
- font-weight: bold;
+ font-family: var(--font-family-bold);
margin: .4em 0;
}
.navStyles .selected {
background-color: #fff;
border-bottom: 1px dotted #808080;
border-top: 1px dotted #808080;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
.navStyles a {
color: black;
diff --git a/polygerrit-ui/app/styles/gr-table-styles.html b/polygerrit-ui/app/styles/gr-table-styles.html
index d296530..6b8c88d0 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.html
+++ b/polygerrit-ui/app/styles/gr-table-styles.html
@@ -34,7 +34,7 @@
.genericList th {
background-color: #ddd;
border-bottom: 1px solid #eee;
- font-weight: bold;
+ font-family: var(--font-family-bold);
padding: .3em .5em;
text-align: left;
}
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index b18543a..045821c 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -37,6 +37,6 @@
*/
-webkit-text-size-adjust: none;
font-size: 13px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
line-height: 1.4;
}
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index cc1dabd..5c11d60 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -38,6 +38,12 @@
margin: 0;
padding: 0;
}
+ input,
+ textarea,
+ select,
+ button {
+ font: inherit;
+ }
body {
line-height: 1;
}
@@ -59,15 +65,15 @@
/* Other Shared Styles*/
h1 {
font-size: 2em;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
h2 {
font-size: 1.5em;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
h3 {
font-size: 1.17em;
- font-weight: bold;
+ font-family: var(--font-family-bold);
}
/* Stopgap solution until we remove hidden$ attributes. */
[hidden] {
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index 138d0ea..dffcaf9 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -27,13 +27,15 @@
'GrGerritAuth',
'GrLinkTextParser',
'GrPluginEndpoints',
+ 'GrPopupInterface',
'GrRangeNormalizer',
'GrReporting',
'GrReviewerUpdatesParser',
'GrThemeApi',
'moment',
'page',
- 'util'];
+ 'util',
+];
fs.readdir('./polygerrit-ui/temp/behaviors/', (err, data) => {
if (err) {
@@ -102,4 +104,4 @@
process.exit(1);
}
});
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 1df8d98..912b0ff 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -32,7 +32,6 @@
'gr-app_test.html',
'admin/gr-access-section/gr-access-section_test.html',
'admin/gr-admin-group-list/gr-admin-group-list_test.html',
- 'admin/gr-admin-project-list/gr-admin-project-list_test.html',
'admin/gr-admin-view/gr-admin-view_test.html',
'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
'admin/gr-create-change-dialog/gr-create-change-dialog_test.html',
@@ -48,6 +47,7 @@
'admin/gr-project-access/gr-project-access_test.html',
'admin/gr-project-commands/gr-project-commands_test.html',
'admin/gr-project-detail-list/gr-project-detail-list_test.html',
+ 'admin/gr-project-list/gr-project-list_test.html',
'admin/gr-rule-editor/gr-rule-editor_test.html',
'change-list/gr-change-list-item/gr-change-list-item_test.html',
'change-list/gr-change-list-view/gr-change-list-view_test.html',
@@ -103,6 +103,8 @@
'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
'plugins/gr-external-style/gr-external-style_test.html',
'plugins/gr-plugin-host/gr-plugin-host_test.html',
+ 'plugins/gr-popup-interface/gr-plugin-popup_test.html',
+ 'plugins/gr-popup-interface/gr-popup-interface_test.html',
'settings/gr-account-info/gr-account-info_test.html',
'settings/gr-change-table-editor/gr-change-table-editor_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',