Merge "Highlight gr-change-list-item on hover"
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 2c42d74..9aa0a3b 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -773,6 +773,12 @@
and `Edit Config` buttons on the project screen, and the `Follow-Up`
button on the change screen).
+- [[publish-comments-on-push]]`Publish Draft Comments When a Change Is Updated by Push`:
++
+Whether to publish any outstanding draft comments by default when pushing
+updates to open changes. This preference just sets the default; the behavior can
+still be overridden using a link:user-upload.html#publish-comments[push option].
+
- [[use-flash]]`Use Flash Clipboard Widget`:
+
Whether the Flash clipboard widget should be used. If enabled and the Flash
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 13fca66..fbfd5e4 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1248,6 +1248,7 @@
"size_bar_in_change_table": true,
"review_category_strategy": "ABBREV",
"mute_common_path_prefixes": true,
+ "publish_comments_on_push": true,
"default_base_for_merges": "FIRST_PARENT",
"my": [
{
@@ -1361,6 +1362,7 @@
"size_bar_in_change_table": true,
"review_category_strategy": "NAME",
"diff_view": "SIDE_BY_SIDE",
+ "publish_comments_on_push": true,
"mute_common_path_prefixes": true,
"my": [
{
@@ -2643,6 +2645,9 @@
The base which should be pre-selected in the 'Diff Against' drop-down
list when the change screen is opened for a merge commit.
Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
+|`publish_comments_on_push` |not set if `false`|
+Whether to link:user-upload.html#publish-comments[publish draft comments] on
+push by default.
|============================================
[[preferences-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index fd35a29..49dbf30 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1065,6 +1065,7 @@
"size_bar_in_change_table": true,
"review_category_strategy": "NONE",
"mute_common_path_prefixes": true,
+ "publish_comments_on_push": true,
"my": [
{
"url": "#/dashboard/self",
@@ -1147,6 +1148,7 @@
"size_bar_in_change_table": true,
"review_category_strategy": "NONE",
"mute_common_path_prefixes": true,
+ "publish_comments_on_push": true,
"my": [
{
"url": "#/dashboard/self",
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 259540e..deec660 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -262,6 +262,21 @@
git push refs parameter does not allow spaces. Use the '_' character instead,
it will then be applied as "This is a rebase on master".
+[[publish-comments]]
+==== Publish Draft Comments
+
+If you have draft comments on the change(s) that are updated by the push, the
+`publish-comments` option will cause them to be published:
+
+----
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%publish-comments
+----
+
+The default for this option can be set as a
+link:intro-user.html#publish-comments-on-push[user preference]. If the
+preference is set so the default behavior is to publish, this can be overridden
+with the `no-publish-comments` (or `np`) option.
+
[[review_labels]]
==== Review Labels
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 4f4b6c6..2ce0b3b 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
@@ -15,21 +15,25 @@
package com.google.gerrit.acceptance.git;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.assertPushRejected;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.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.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.toList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -37,16 +41,20 @@
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
@@ -61,12 +69,14 @@
import com.google.gerrit.testutil.TestTimeUtil;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.regex.Pattern;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
@@ -75,6 +85,7 @@
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -111,6 +122,14 @@
grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
}
+ @After
+ public void tearDown() throws Exception {
+ setApiUser(admin);
+ GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+ prefs.publishCommentsOnPush = false;
+ gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+ }
+
protected void selectProtocol(Protocol p) throws Exception {
String url;
switch (p) {
@@ -1469,6 +1488,193 @@
assertThat(info.status).isEqualTo(ChangeStatus.NEW);
}
+ @Test
+ public void publishCommentsOnPushPublishesDraftsOnAllRevisions() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String rev1 = r.getCommit().name();
+ CommentInfo c1 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment1"));
+ CommentInfo c2 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment2"));
+
+ r = amendChange(r.getChangeId());
+ String rev2 = r.getCommit().name();
+ CommentInfo c3 = addDraft(r.getChangeId(), rev2, newDraft(FILE_NAME, 1, "comment3"));
+
+ assertThat(getPublishedComments(r.getChangeId())).isEmpty();
+
+ gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+ sender.clear();
+ amendChange(r.getChangeId(), "refs/for/master%publish-comments");
+
+ Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
+ assertThat(comments.stream().map(c -> c.id)).containsExactly(c1.id, c2.id, c3.id);
+ assertThat(comments.stream().map(c -> c.message))
+ .containsExactly("comment1", "comment2", "comment3");
+ assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)");
+
+ List<String> messages =
+ sender
+ .getMessages()
+ .stream()
+ .map(m -> m.body())
+ .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1))
+ .collect(toList());
+ assertThat(messages).hasSize(2);
+
+ assertThat(messages.get(0)).contains("Gerrit-MessageType: newpatchset");
+ assertThat(messages.get(0)).contains("I'd like you to reexamine a change");
+ assertThat(messages.get(0)).doesNotContain("Uploaded patch set 3");
+
+ assertThat(messages.get(1)).contains("Gerrit-MessageType: comment");
+ assertThat(messages.get(1))
+ .containsMatch(
+ Pattern.compile(
+ // A little weird that the comment email contains this text, but it's actually
+ // what's in the ChangeMessage. Really we should fuse the emails into one, but until
+ // then, this test documents the current behavior.
+ "Uploaded patch set 3\\.\n"
+ + "\n"
+ + "\\(3 comments\\)\\n.*"
+ + "PS1, Line 1:.*"
+ + "comment1\\n.*"
+ + "PS1, Line 1:.*"
+ + "comment2\\n.*"
+ + "PS2, Line 1:.*"
+ + "comment3\\n",
+ Pattern.DOTALL));
+ }
+
+ @Test
+ public void publishCommentsOnPushWithMessage() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String rev = r.getCommit().name();
+ addDraft(r.getChangeId(), rev, newDraft(FILE_NAME, 1, "comment1"));
+
+ r = amendChange(r.getChangeId(), "refs/for/master%publish-comments,m=The_message");
+
+ Collection<CommentInfo> comments = getPublishedComments(r.getChangeId());
+ assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1");
+ assertThat(getLastMessage(r.getChangeId()))
+ .isEqualTo("Uploaded patch set 2.\n\n(1 comment)\n\nThe message");
+ }
+
+ @Test
+ public void publishCommentsOnPushPublishesDraftsOnMultipleChanges() throws Exception {
+ ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+ List<RevCommit> commits = createChanges(2, "refs/for/master");
+ String id1 = byCommit(commits.get(0)).change().getKey().get();
+ String id2 = byCommit(commits.get(1)).change().getKey().get();
+ CommentInfo c1 = addDraft(id1, commits.get(0).name(), newDraft(FILE_NAME, 1, "comment1"));
+ CommentInfo c2 = addDraft(id2, commits.get(1).name(), newDraft(FILE_NAME, 1, "comment2"));
+
+ assertThat(getPublishedComments(id1)).isEmpty();
+ assertThat(getPublishedComments(id2)).isEmpty();
+
+ amendChanges(initialHead, commits, "refs/for/master%publish-comments");
+
+ Collection<CommentInfo> cs1 = getPublishedComments(id1);
+ assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1");
+ assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id);
+ assertThat(getLastMessage(id1))
+ .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)");
+
+ Collection<CommentInfo> cs2 = getPublishedComments(id2);
+ assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2");
+ assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
+ assertThat(getLastMessage(id2))
+ .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)");
+ }
+
+ @Test
+ public void publishCommentsOnPushOnlyPublishesDraftsOnUpdatedChanges() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ String id1 = r1.getChangeId();
+ String id2 = r2.getChangeId();
+ addDraft(id1, r1.getCommit().name(), newDraft(FILE_NAME, 1, "comment1"));
+ CommentInfo c2 = addDraft(id2, r2.getCommit().name(), newDraft(FILE_NAME, 1, "comment2"));
+
+ assertThat(getPublishedComments(id1)).isEmpty();
+ assertThat(getPublishedComments(id2)).isEmpty();
+
+ r2 = amendChange(id2, "refs/for/master%publish-comments");
+
+ assertThat(getPublishedComments(id1)).isEmpty();
+ assertThat(gApi.changes().id(id1).drafts()).hasSize(1);
+
+ Collection<CommentInfo> cs2 = getPublishedComments(id2);
+ assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2");
+ assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id);
+
+ assertThat(getLastMessage(id1)).doesNotMatch("[Cc]omment");
+ assertThat(getLastMessage(id2)).isEqualTo("Uploaded patch set 2.\n\n(1 comment)");
+ }
+
+ @Test
+ public void publishCommentsOnPushWithPreference() throws Exception {
+ PushOneCommit.Result r = createChange();
+ addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1"));
+ r = amendChange(r.getChangeId());
+
+ assertThat(getPublishedComments(r.getChangeId())).isEmpty();
+
+ GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+ prefs.publishCommentsOnPush = true;
+ gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+
+ r = amendChange(r.getChangeId());
+ assertThat(getPublishedComments(r.getChangeId()).stream().map(c -> c.message))
+ .containsExactly("comment1");
+ }
+
+ @Test
+ public void publishCommentsOnPushOverridingPreference() throws Exception {
+ PushOneCommit.Result r = createChange();
+ addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1"));
+
+ GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+ prefs.publishCommentsOnPush = true;
+ gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+
+ r = amendChange(r.getChangeId(), "refs/for/master%no-publish-comments");
+
+ assertThat(getPublishedComments(r.getChangeId())).isEmpty();
+ }
+
+ private DraftInput newDraft(String path, int line, String message) {
+ DraftInput d = new DraftInput();
+ d.path = path;
+ d.side = Side.REVISION;
+ d.line = line;
+ d.message = message;
+ d.unresolved = true;
+ return d;
+ }
+
+ private CommentInfo addDraft(String changeId, String revId, DraftInput in) throws Exception {
+ return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
+ }
+
+ private Collection<CommentInfo> getPublishedComments(String changeId) throws Exception {
+ return gApi.changes()
+ .id(changeId)
+ .comments()
+ .values()
+ .stream()
+ .flatMap(cs -> cs.stream())
+ .collect(toList());
+ }
+
+ private String getLastMessage(String changeId) throws Exception {
+ return Streams.findLast(
+ gApi.changes()
+ .id(changeId)
+ .get(EnumSet.of(ListChangesOption.MESSAGES))
+ .messages
+ .stream()
+ .map(m -> m.message))
+ .get();
+ }
+
private void assertThatUserIsOnlyReviewer(ChangeInfo ci, TestAccount reviewer) {
assertThat(ci.reviewers).isNotNull();
assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 36f8452..552d7a4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -163,7 +163,7 @@
systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
"refs/for/" + newBranch.get());
exception.expect(AuthException.class);
- exception.expectMessage("Move not permitted");
+ exception.expectMessage("move not permitted");
move(r.getChangeId(), newBranch.get());
}
@@ -179,7 +179,7 @@
r.getChange().change().getDest().get());
setApiUser(user);
exception.expect(AuthException.class);
- exception.expectMessage("Move not permitted");
+ exception.expectMessage("move not permitted");
move(r.getChangeId(), newBranch.get());
}
@@ -223,7 +223,7 @@
revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
exception.expect(AuthException.class);
- exception.expectMessage("Move not permitted");
+ exception.expectMessage("move not permitted");
move(r.getChangeId(), newBranch.get());
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index bf08ac9..067c80a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -117,7 +117,7 @@
private void assertDeleteForbidden() throws Exception {
exception.expect(AuthException.class);
- exception.expectMessage("Cannot delete branch");
+ exception.expectMessage("delete not permitted");
branch().delete();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
index c9d7446..c0c2e93 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -112,7 +112,7 @@
private void assertDeleteForbidden() throws Exception {
exception.expect(AuthException.class);
- exception.expectMessage("Cannot delete tag");
+ exception.expectMessage("delete not permitted");
tag().delete();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index f191681..6d2bf16 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -243,7 +243,7 @@
TagInput input = new TagInput();
input.ref = "test";
exception.expect(AuthException.class);
- exception.expectMessage("Cannot create tag \"" + R_TAGS + "test\"");
+ exception.expectMessage("create not permitted");
tag(input.ref).create(input);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index d628268..2b29054 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -32,6 +32,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
@@ -83,6 +84,7 @@
import org.junit.Before;
import org.junit.Test;
+@NoHttpd
public class NoteDbPrimaryIT extends AbstractDaemonTest {
@ConfigSuite.Default
public static Config defaultConfig() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 7192ff9..9dcba5e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -157,6 +157,7 @@
public EmailStrategy emailStrategy;
public EmailFormat emailFormat;
public DefaultBase defaultBaseForMerges;
+ public Boolean publishCommentsOnPush;
public boolean isShowInfoInReviewCategory() {
return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
@@ -225,6 +226,7 @@
p.muteCommonPathPrefixes = true;
p.signedOffBy = false;
p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
+ p.publishCommentsOnPush = false;
return p;
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
index 1d4cda7..0b4f459 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
@@ -22,4 +22,12 @@
public AuthException(String msg) {
super(msg);
}
+
+ /**
+ * @param msg message to return to the client.
+ * @param cause cause of this exception.
+ */
+ public AuthException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
index 23e1a93..1dcb284 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -148,6 +148,9 @@
private native String defaultBaseForMergesRaw() /*-{ return this.default_base_for_merges }-*/;
+ public final native boolean
+ publishCommentsOnPush() /*-{ return this.publish_comments_on_push || false }-*/;
+
public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/;
public final native void changesPerPage(int n) /*-{ this.changes_per_page = n }-*/;
@@ -224,6 +227,9 @@
private native void defaultBaseForMergesRaw(String b) /*-{ this.default_base_for_merges = b }-*/;
+ public final native void publishCommentsOnPush(
+ boolean p) /*-{ this.publish_comments_on_push = p }-*/;
+
public final void setMyMenus(List<TopMenuItem> myMenus) {
initMy();
for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 4a3a5f8..314871e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -67,6 +67,8 @@
String signedOffBy();
+ String publishCommentsOnPush();
+
String myMenu();
String myMenuInfo();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 9d87365..d31abdb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -38,6 +38,7 @@
showLegacycidInChangeTable = Show Change Number In Changes Table
muteCommonPathPrefixes = Mute Common Path Prefixes In File List
signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
+publishCommentsOnPush = Publish Draft Comments When a Change Is Updated by Push
myMenu = My Menu
myMenuInfo = \
Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 2edc137..9be15ff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -55,6 +55,7 @@
private CheckBox legacycidInChangeTable;
private CheckBox muteCommonPathPrefixes;
private CheckBox signedOffBy;
+ private CheckBox publishCommentsOnPush;
private ListBox maximumPageSize;
private ListBox dateFormat;
private ListBox timeFormat;
@@ -161,9 +162,10 @@
legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
signedOffBy = new CheckBox(Util.C.signedOffBy());
+ publishCommentsOnPush = new CheckBox(Util.C.publishCommentsOnPush());
boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
- final Grid formGrid = new Grid(14 + (flashClippy ? 1 : 0), 2);
+ final Grid formGrid = new Grid(15 + (flashClippy ? 1 : 0), 2);
int row = 0;
@@ -223,6 +225,10 @@
formGrid.setWidget(row, fieldIdx, signedOffBy);
row++;
+ formGrid.setText(row, labelIdx, "");
+ formGrid.setWidget(row, fieldIdx, publishCommentsOnPush);
+ row++;
+
if (flashClippy) {
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, useFlashClipboard);
@@ -257,6 +263,7 @@
e.listenTo(legacycidInChangeTable);
e.listenTo(muteCommonPathPrefixes);
e.listenTo(signedOffBy);
+ e.listenTo(publishCommentsOnPush);
e.listenTo(diffView);
e.listenTo(reviewCategoryStrategy);
e.listenTo(emailStrategy);
@@ -295,6 +302,7 @@
legacycidInChangeTable.setEnabled(on);
muteCommonPathPrefixes.setEnabled(on);
signedOffBy.setEnabled(on);
+ publishCommentsOnPush.setEnabled(on);
reviewCategoryStrategy.setEnabled(on);
diffView.setEnabled(on);
emailStrategy.setEnabled(on);
@@ -320,6 +328,7 @@
legacycidInChangeTable.setValue(p.legacycidInChangeTable());
muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
signedOffBy.setValue(p.signedOffBy());
+ publishCommentsOnPush.setValue(p.publishCommentsOnPush());
setListBox(
reviewCategoryStrategy,
GeneralPreferencesInfo.ReviewCategoryStrategy.NONE,
@@ -412,6 +421,7 @@
p.legacycidInChangeTable(legacycidInChangeTable.getValue());
p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
p.signedOffBy(signedOffBy.getValue());
+ p.publishCommentsOnPush(publishCommentsOnPush.getValue());
p.reviewCategoryStrategy(
getListBox(
reviewCategoryStrategy, ReviewCategoryStrategy.NONE, ReviewCategoryStrategy.values()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
index 77eff90..aee97fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.edit.tree.TreeModification;
import com.google.gerrit.server.fixes.FixReplacementInterpreter;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
@@ -61,7 +62,7 @@
@Override
public Response<EditInfo> apply(FixResource fixResource, Void nothing)
throws AuthException, OrmException, ResourceConflictException, IOException,
- ResourceNotFoundException {
+ ResourceNotFoundException, PermissionBackendException {
RevisionResource revisionResource = fixResource.getRevisionResource();
Project.NameKey project = revisionResource.getProject();
ProjectState projectState = revisionResource.getControl().getProjectControl().getProjectState();
@@ -75,8 +76,7 @@
ChangeEdit changeEdit =
changeEditModifier.combineWithModifiedPatchSetTree(
repository, revisionResource.getControl(), patchSet, treeModifications);
- EditInfo editInfo = changeEditJson.toEditInfo(changeEdit, false);
- return Response.ok(editInfo);
+ return Response.ok(changeEditJson.toEditInfo(changeEdit, false));
} catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index c1db891..929268b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -46,6 +46,7 @@
import com.google.gerrit.server.edit.UnchangedCommitMessageException;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
@@ -154,7 +155,8 @@
@Override
public Response<?> apply(ChangeResource resource, Put.Input input)
- throws AuthException, ResourceConflictException, IOException, OrmException {
+ throws AuthException, ResourceConflictException, IOException, OrmException,
+ PermissionBackendException {
putEdit.apply(resource.getControl(), path, input.content);
return Response.none();
}
@@ -178,7 +180,8 @@
@Override
public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in)
- throws IOException, AuthException, ResourceConflictException, OrmException {
+ throws IOException, AuthException, ResourceConflictException, OrmException,
+ PermissionBackendException {
return deleteContent.apply(rsrc.getControl(), path);
}
}
@@ -268,7 +271,8 @@
@Override
public Response<?> apply(ChangeResource resource, Post.Input input)
- throws AuthException, IOException, ResourceConflictException, OrmException {
+ throws AuthException, IOException, ResourceConflictException, OrmException,
+ PermissionBackendException {
Project.NameKey project = resource.getProject();
try (Repository repository = repositoryManager.openRepository(project)) {
ChangeControl changeControl = resource.getControl();
@@ -314,12 +318,14 @@
@Override
public Response<?> apply(ChangeEditResource rsrc, Input input)
- throws AuthException, ResourceConflictException, IOException, OrmException {
+ throws AuthException, ResourceConflictException, IOException, OrmException,
+ PermissionBackendException {
return apply(rsrc.getControl(), rsrc.getPath(), input.content);
}
public Response<?> apply(ChangeControl changeControl, String path, RawInput newContent)
- throws ResourceConflictException, AuthException, IOException, OrmException {
+ throws ResourceConflictException, AuthException, IOException, OrmException,
+ PermissionBackendException {
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
throw new ResourceConflictException("Invalid path: " + path);
}
@@ -356,12 +362,14 @@
@Override
public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input)
- throws AuthException, ResourceConflictException, OrmException, IOException {
+ throws AuthException, ResourceConflictException, OrmException, IOException,
+ PermissionBackendException {
return apply(rsrc.getControl(), rsrc.getPath());
}
public Response<?> apply(ChangeControl changeControl, String filePath)
- throws AuthException, IOException, OrmException, ResourceConflictException {
+ throws AuthException, IOException, OrmException, ResourceConflictException,
+ PermissionBackendException {
Project.NameKey project = changeControl.getChange().getProject();
try (Repository repository = repositoryManager.openRepository(project)) {
editModifier.deleteFile(repository, changeControl, filePath);
@@ -456,7 +464,7 @@
@Override
public Object apply(ChangeResource rsrc, Input input)
throws AuthException, IOException, BadRequestException, ResourceConflictException,
- OrmException {
+ OrmException, PermissionBackendException {
if (input == null || Strings.isNullOrEmpty(input.message)) {
throw new BadRequestException("commit message must be provided");
}
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 02c91ac..b53bdd9 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
@@ -22,7 +22,6 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -41,6 +40,8 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectControl;
@@ -102,27 +103,18 @@
@Override
protected Response<ChangeInfo> applyImpl(
- BatchUpdate.Factory updateFactory, ChangeResource req, MergePatchSetInput in)
+ BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in)
throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
- UpdateException {
- if (in.merge == null) {
- throw new BadRequestException("merge field is required");
- }
+ UpdateException, PermissionBackendException {
+ rsrc.permissions().database(db).check(ChangePermission.ADD_PATCH_SET);
MergeInput merge = in.merge;
- if (Strings.isNullOrEmpty(merge.source)) {
+ if (merge == null || Strings.isNullOrEmpty(merge.source)) {
throw new BadRequestException("merge.source must be non-empty");
}
- ChangeControl ctl = req.getControl();
- if (!ctl.isVisible(db.get())) {
- throw new InvalidChangeOperationException("Base change not found: " + req.getId());
- }
+ ChangeControl ctl = rsrc.getControl();
PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
- if (!ctl.canAddPatchSet(db.get())) {
- throw new AuthException("cannot add patch set");
- }
-
ProjectControl projectControl = ctl.getProjectControl();
Change change = ctl.getChange();
Project.NameKey project = change.getProject();
@@ -139,11 +131,9 @@
}
RevCommit currentPsCommit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
-
Timestamp now = TimeUtil.nowTs();
IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
-
RevCommit newCommit =
createMergeCommit(
in,
@@ -166,7 +156,8 @@
psInserter
.setMessage("Uploaded patch set " + nextPsId.get() + ".")
.setDraft(ps.isDraft())
- .setNotify(NotifyHandling.NONE));
+ .setNotify(NotifyHandling.NONE)
+ .setCheckAddPatchSetPermission(false));
bu.execute();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
index b0b222b..d358184 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
@@ -27,9 +27,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -54,9 +52,6 @@
private final PermissionBackend permissionBackend;
private final BatchUpdate.Factory batchUpdateFactory;
private final CommentsUtil commentsUtil;
- private final PatchSetUtil psUtil;
- private final BatchUpdate.Factory updateFactory;
- private final PatchListCache patchListCache;
private final Provider<CommentJson> commentJson;
private final ChangeNotes.Factory notesFactory;
@@ -67,9 +62,6 @@
PermissionBackend permissionBackend,
BatchUpdate.Factory batchUpdateFactory,
CommentsUtil commentsUtil,
- PatchSetUtil psUtil,
- BatchUpdate.Factory updateFactory,
- PatchListCache patchListCache,
Provider<CommentJson> commentJson,
ChangeNotes.Factory notesFactory) {
this.userProvider = userProvider;
@@ -77,9 +69,6 @@
this.permissionBackend = permissionBackend;
this.batchUpdateFactory = batchUpdateFactory;
this.commentsUtil = commentsUtil;
- this.psUtil = psUtil;
- this.updateFactory = updateFactory;
- this.patchListCache = patchListCache;
this.commentJson = commentJson;
this.notesFactory = notesFactory;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 1a0d118..040b6de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -258,8 +258,7 @@
if (patchSet == null) {
throw new PatchListNotAvailableException(
String.format(
- "patch set %s of change %s not found",
- old.get(), change.getId().get()));
+ "patch set %s of change %s not found", old.get(), change.getId().get()));
}
PatchList oldList = patchListCache.get(change, patchSet);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index 98b79d1..ffc0dc4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -33,10 +33,14 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.project.ChangeControl;
+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.permissions.RefPermission;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -57,6 +61,7 @@
@Singleton
public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> {
+ private final PermissionBackend permissionBackend;
private final Provider<ReviewDb> dbProvider;
private final ChangeJson.Factory json;
private final GitRepositoryManager repoManager;
@@ -66,6 +71,7 @@
@Inject
Move(
+ PermissionBackend permissionBackend,
Provider<ReviewDb> dbProvider,
ChangeJson.Factory json,
GitRepositoryManager repoManager,
@@ -74,6 +80,7 @@
RetryHelper retryHelper,
PatchSetUtil psUtil) {
super(retryHelper);
+ this.permissionBackend = permissionBackend;
this.dbProvider = dbProvider;
this.json = json;
this.repoManager = repoManager;
@@ -84,22 +91,40 @@
@Override
protected ChangeInfo applyImpl(
- BatchUpdate.Factory updateFactory, ChangeResource req, MoveInput input)
- throws RestApiException, OrmException, UpdateException {
- ChangeControl control = req.getControl();
+ BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input)
+ throws RestApiException, OrmException, UpdateException, PermissionBackendException {
+ Change change = rsrc.getChange();
+ Project.NameKey project = rsrc.getProject();
+ IdentifiedUser caller = rsrc.getUser();
input.destinationBranch = RefNames.fullName(input.destinationBranch);
- if (!control.canMoveTo(input.destinationBranch, dbProvider.get())) {
- throw new AuthException("Move not permitted");
+
+ if (change.getStatus().isClosed()) {
+ throw new ResourceConflictException("Change is " + ChangeUtil.status(change));
+ }
+
+ Branch.NameKey newDest = new Branch.NameKey(project, input.destinationBranch);
+ if (change.getDest().equals(newDest)) {
+ throw new ResourceConflictException("Change is already destined for the specified branch");
+ }
+
+ // Move requires abandoning this change, and creating a new change.
+ try {
+ rsrc.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+ permissionBackend
+ .user(caller)
+ .database(dbProvider)
+ .ref(newDest)
+ .check(RefPermission.CREATE_CHANGE);
+ } catch (AuthException denied) {
+ throw new AuthException("move not permitted", denied);
}
try (BatchUpdate u =
- updateFactory.create(
- dbProvider.get(), req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
- u.addOp(req.getChange().getId(), new Op(input));
+ updateFactory.create(dbProvider.get(), project, caller, TimeUtil.nowTs())) {
+ u.addOp(change.getId(), new Op(input));
u.execute();
}
-
- return json.noOptions().format(req.getChange());
+ return json.noOptions().format(project, rsrc.getId());
}
private class Op implements BatchUpdateOp {
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 5933b19..581f2ba 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
@@ -44,6 +44,9 @@
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.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.ssh.NoSshInfo;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -69,6 +72,7 @@
}
// Injected fields.
+ private final PermissionBackend permissionBackend;
private final PatchSetInfoFactory patchSetInfoFactory;
private final CommitValidators.Factory commitValidatorsFactory;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
@@ -108,6 +112,7 @@
@Inject
public PatchSetInserter(
+ PermissionBackend permissionBackend,
ApprovalsUtil approvalsUtil,
ApprovalCopier approvalCopier,
ChangeMessagesUtil cmUtil,
@@ -119,6 +124,7 @@
@Assisted ChangeControl ctl,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
+ this.permissionBackend = permissionBackend;
this.approvalsUtil = approvalsUtil;
this.approvalCopier = approvalCopier;
this.cmUtil = cmUtil;
@@ -206,7 +212,8 @@
@Override
public void updateRepo(RepoContext ctx)
- throws AuthException, ResourceConflictException, IOException, OrmException {
+ throws AuthException, ResourceConflictException, IOException, OrmException,
+ PermissionBackendException {
validate(ctx);
ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName());
}
@@ -301,9 +308,13 @@
}
private void validate(RepoContext ctx)
- throws AuthException, ResourceConflictException, IOException, OrmException {
- if (checkAddPatchSetPermission && !origCtl.canAddPatchSet(ctx.getDb())) {
- throw new AuthException("cannot add patch set");
+ throws AuthException, ResourceConflictException, IOException, PermissionBackendException {
+ if (checkAddPatchSetPermission) {
+ permissionBackend
+ .user(ctx.getUser())
+ .database(ctx.getDb())
+ .change(origCtl.getNotes())
+ .check(ChangePermission.ADD_PATCH_SET);
}
if (!validate) {
return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
index 7b673dd..9e840c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -28,6 +28,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.edit.ChangeEditModifier;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -82,7 +83,8 @@
@Override
public Response<?> apply(ChangeResource rsrc, Rebase.Input in)
- throws AuthException, ResourceConflictException, IOException, OrmException {
+ throws AuthException, ResourceConflictException, IOException, OrmException,
+ PermissionBackendException {
Project.NameKey project = rsrc.getProject();
try (Repository repository = repositoryManager.openRepository(project)) {
editModifier.rebaseEdit(repository, rsrc.getControl());
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 16e3a08..50c03fe 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
@@ -25,6 +25,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -133,7 +134,7 @@
@Override
public void updateRepo(RepoContext ctx)
throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
- OrmException, NoSuchChangeException {
+ OrmException, NoSuchChangeException, PermissionBackendException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevId oldRev = originalPatchSet.getRevision();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 75da89d..8d3cbae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -37,6 +37,9 @@
import com.google.gerrit.server.edit.tree.TreeCreator;
import com.google.gerrit.server.edit.tree.TreeModification;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.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.InvalidChangeOperationException;
import com.google.gwtorm.server.OrmException;
@@ -78,6 +81,7 @@
private final ChangeIndexer indexer;
private final Provider<ReviewDb> reviewDb;
private final Provider<CurrentUser> currentUser;
+ private final PermissionBackend permissionBackend;
private final ChangeEditUtil changeEditUtil;
private final PatchSetUtil patchSetUtil;
@@ -87,11 +91,13 @@
ChangeIndexer indexer,
Provider<ReviewDb> reviewDb,
Provider<CurrentUser> currentUser,
+ PermissionBackend permissionBackend,
ChangeEditUtil changeEditUtil,
PatchSetUtil patchSetUtil) {
this.indexer = indexer;
this.reviewDb = reviewDb;
this.currentUser = currentUser;
+ this.permissionBackend = permissionBackend;
this.tz = gerritIdent.getTimeZone();
this.changeEditUtil = changeEditUtil;
this.patchSetUtil = patchSetUtil;
@@ -105,10 +111,12 @@
* be created
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws InvalidChangeOperationException if a change edit already existed for the change
+ * @throws PermissionBackendException
*/
public void createEdit(Repository repository, ChangeControl changeControl)
- throws AuthException, IOException, InvalidChangeOperationException, OrmException {
- ensureAuthenticatedAndPermitted(changeControl);
+ throws AuthException, IOException, InvalidChangeOperationException, OrmException,
+ PermissionBackendException {
+ assertCanEdit(changeControl);
Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl);
if (changeEdit.isPresent()) {
@@ -132,11 +140,12 @@
* change, the change edit is already based on the latest patch set, or the change represents
* the root commit
* @throws MergeConflictException if rebase fails due to merge conflicts
+ * @throws PermissionBackendException
*/
public void rebaseEdit(Repository repository, ChangeControl changeControl)
throws AuthException, InvalidChangeOperationException, IOException, OrmException,
- MergeConflictException {
- ensureAuthenticatedAndPermitted(changeControl);
+ MergeConflictException, PermissionBackendException {
+ assertCanEdit(changeControl);
Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
if (!optionalChangeEdit.isPresent()) {
@@ -195,11 +204,13 @@
* @param newCommitMessage the new commit message
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws UnchangedCommitMessageException if the commit message is the same as before
+ * @throws PermissionBackendException
*/
public void modifyMessage(
Repository repository, ChangeControl changeControl, String newCommitMessage)
- throws AuthException, IOException, UnchangedCommitMessageException, OrmException {
- ensureAuthenticatedAndPermitted(changeControl);
+ throws AuthException, IOException, UnchangedCommitMessageException, OrmException,
+ PermissionBackendException {
+ assertCanEdit(changeControl);
newCommitMessage = getWellFormedCommitMessage(newCommitMessage);
Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
@@ -236,10 +247,12 @@
* @param newContent the new file content
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws InvalidChangeOperationException if the file already had the specified content
+ * @throws PermissionBackendException
*/
public void modifyFile(
Repository repository, ChangeControl changeControl, String filePath, RawInput newContent)
- throws AuthException, InvalidChangeOperationException, IOException, OrmException {
+ throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+ PermissionBackendException {
modifyTree(repository, changeControl, new ChangeFileContentModification(filePath, newContent));
}
@@ -253,9 +266,11 @@
* @param file path of the file which should be deleted
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws InvalidChangeOperationException if the file does not exist
+ * @throws PermissionBackendException
*/
public void deleteFile(Repository repository, ChangeControl changeControl, String file)
- throws AuthException, InvalidChangeOperationException, IOException, OrmException {
+ throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+ PermissionBackendException {
modifyTree(repository, changeControl, new DeleteFileModification(file));
}
@@ -271,13 +286,15 @@
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws InvalidChangeOperationException if the file was already renamed to the specified new
* name
+ * @throws PermissionBackendException
*/
public void renameFile(
Repository repository,
ChangeControl changeControl,
String currentFilePath,
String newFilePath)
- throws AuthException, InvalidChangeOperationException, IOException, OrmException {
+ throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+ PermissionBackendException {
modifyTree(repository, changeControl, new RenameFileModification(currentFilePath, newFilePath));
}
@@ -292,16 +309,19 @@
* @param file the path of the file which should be restored
* @throws AuthException if the user isn't authenticated or not allowed to use change edits
* @throws InvalidChangeOperationException if the file was already restored
+ * @throws PermissionBackendException
*/
public void restoreFile(Repository repository, ChangeControl changeControl, String file)
- throws AuthException, InvalidChangeOperationException, IOException, OrmException {
+ throws AuthException, InvalidChangeOperationException, IOException, OrmException,
+ PermissionBackendException {
modifyTree(repository, changeControl, new RestoreFileModification(file));
}
private void modifyTree(
Repository repository, ChangeControl changeControl, TreeModification treeModification)
- throws AuthException, IOException, OrmException, InvalidChangeOperationException {
- ensureAuthenticatedAndPermitted(changeControl);
+ throws AuthException, IOException, OrmException, InvalidChangeOperationException,
+ PermissionBackendException {
+ assertCanEdit(changeControl);
Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl);
@@ -345,8 +365,8 @@
PatchSet patchSet,
List<TreeModification> treeModifications)
throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
- OrmException {
- ensureAuthenticatedAndPermitted(changeControl);
+ OrmException, PermissionBackendException {
+ assertCanEdit(changeControl);
Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
ensureAllowedPatchSet(changeControl, optionalChangeEdit, patchSet);
@@ -375,21 +395,19 @@
return createEdit(repository, changeControl, patchSet, newEditCommit, nowTimestamp);
}
- private void ensureAuthenticatedAndPermitted(ChangeControl changeControl)
- throws AuthException, OrmException {
- ensureAuthenticated();
- ensurePermitted(changeControl);
- }
-
- private void ensureAuthenticated() throws AuthException {
+ private void assertCanEdit(ChangeControl changeControl)
+ throws AuthException, PermissionBackendException {
if (!currentUser.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
- }
-
- private void ensurePermitted(ChangeControl changeControl) throws OrmException, AuthException {
- if (!changeControl.canAddPatchSet(reviewDb.get())) {
- throw new AuthException("Not allowed to edit a change.");
+ try {
+ permissionBackend
+ .user(currentUser)
+ .database(reviewDb)
+ .change(changeControl.getNotes())
+ .check(ChangePermission.ADD_PATCH_SET);
+ } catch (AuthException denied) {
+ throw new AuthException("edit not permitted", denied);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index bb07884..ac504c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
@@ -104,6 +105,7 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -1119,24 +1121,30 @@
return false;
}
- private void parseDelete(ReceiveCommand cmd) {
+ private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException {
logDebug("Deleting {}", cmd);
- RefControl ctl = projectControl.controlForRef(cmd.getRefName());
- if (ctl.getRefName().startsWith(REFS_CHANGES)) {
- errors.put(Error.DELETE_CHANGES, ctl.getRefName());
+ if (cmd.getRefName().startsWith(REFS_CHANGES)) {
+ errors.put(Error.DELETE_CHANGES, cmd.getRefName());
reject(cmd, "cannot delete changes");
- } else if (ctl.canDelete()) {
+ } else if (canDelete(cmd)) {
if (!validRefOperation(cmd)) {
return;
}
actualCommands.add(cmd);
+ } else if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) {
+ reject(cmd, "cannot delete project configuration");
} else {
- if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) {
- reject(cmd, "cannot delete project configuration");
- } else {
- errors.put(Error.DELETE, ctl.getRefName());
- reject(cmd, "cannot delete references");
- }
+ errors.put(Error.DELETE, cmd.getRefName());
+ reject(cmd, "cannot delete references");
+ }
+ }
+
+ private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException {
+ try {
+ permissions.ref(cmd.getRefName()).check(RefPermission.DELETE);
+ return true;
+ } catch (AuthException e) {
+ return false;
}
}
@@ -1187,6 +1195,7 @@
final ReceiveCommand cmd;
final LabelTypes labelTypes;
final NotesMigration notesMigration;
+ private final boolean defaultPublishComments;
Branch.NameKey dest;
RefControl ctl;
Set<Account.Id> reviewer = Sets.newLinkedHashSet();
@@ -1235,6 +1244,16 @@
@Option(name = "--merged", usage = "create single change for a merged commit")
boolean merged;
+ @Option(name = "--publish-comments", usage = "publish all draft comments on updated changes")
+ private boolean publishComments;
+
+ @Option(
+ name = "--no-publish-comments",
+ aliases = {"--np"},
+ usage = "do not publish draft comments"
+ )
+ private boolean noPublishComments;
+
@Option(
name = "--notify",
usage =
@@ -1318,11 +1337,17 @@
//TODO(dpursehouse): validate hashtags
}
- MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes, NotesMigration notesMigration) {
+ MagicBranchInput(
+ IdentifiedUser user,
+ ReceiveCommand cmd,
+ LabelTypes labelTypes,
+ NotesMigration notesMigration) {
this.cmd = cmd;
this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
this.labelTypes = labelTypes;
this.notesMigration = notesMigration;
+ this.defaultPublishComments =
+ firstNonNull(user.getAccount().getGeneralPreferencesInfo().publishCommentsOnPush, false);
}
MailRecipients getMailRecipients() {
@@ -1338,6 +1363,15 @@
return accountsToNotify;
}
+ boolean shouldPublishComments() {
+ if (publishComments) {
+ return true;
+ } else if (noPublishComments) {
+ return false;
+ }
+ return defaultPublishComments;
+ }
+
String parse(
CmdLineParser clp,
Repository repo,
@@ -1407,7 +1441,7 @@
}
logDebug("Found magic branch {}", cmd.getRefName());
- magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration);
+ magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration);
magicBranch.reviewer.addAll(reviewersFromCommandLine);
magicBranch.cc.addAll(ccFromCommandLine);
@@ -1450,7 +1484,8 @@
magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
magicBranch.ctl = projectControl.controlForRef(ref);
- if (!magicBranch.ctl.canWrite()) {
+ if (projectControl.getProject().getState()
+ != com.google.gerrit.extensions.client.ProjectState.ACTIVE) {
reject(cmd, "project is read only");
return;
}
@@ -1484,6 +1519,11 @@
reject(cmd, "the options 'wip' and 'ready' are mutually exclusive");
return;
}
+ if (magicBranch.publishComments && magicBranch.noPublishComments) {
+ reject(
+ cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive");
+ return;
+ }
if (magicBranch.draft && magicBranch.submit) {
reject(cmd, "cannot submit draft");
@@ -2219,7 +2259,7 @@
req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
}
}
- } catch (IOException err) {
+ } catch (IOException | PermissionBackendException err) {
logError(
String.format(
"Cannot read repository before replacement for project %s", project.getName()),
@@ -2260,7 +2300,6 @@
final ReceiveCommand inputCommand;
final boolean checkMergedInto;
ChangeNotes notes;
- ChangeControl changeCtl;
BiMap<RevCommit, PatchSet.Id> revisions;
PatchSet.Id psId;
ReceiveCommand prev;
@@ -2308,8 +2347,10 @@
* @return whether the new commit is valid
* @throws IOException
* @throws OrmException
+ * @throws PermissionBackendException
*/
- boolean validate(boolean autoClose) throws IOException, OrmException {
+ boolean validate(boolean autoClose)
+ throws IOException, OrmException, PermissionBackendException {
if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
return false;
} else if (notes == null) {
@@ -2317,7 +2358,8 @@
return false;
}
- priorPatchSet = notes.getChange().currentPatchSetId();
+ Change change = notes.getChange();
+ priorPatchSet = change.currentPatchSetId();
if (!revisions.containsValue(priorPatchSet)) {
reject(inputCommand, "change " + ontoChange + " missing revisions");
return false;
@@ -2325,16 +2367,18 @@
RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId);
RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-
- changeCtl = projectControl.controlFor(notes);
- if (!changeCtl.canAddPatchSet(db)) {
+ try {
+ permissions.change(notes).database(db).check(ChangePermission.ADD_PATCH_SET);
+ } catch (AuthException no) {
String locked = ".";
- if (changeCtl.isPatchSetLocked(db)) {
+ if (projectControl.controlFor(notes).isPatchSetLocked(db)) {
locked = ". Change is patch set locked.";
}
reject(inputCommand, "cannot add patch set to " + ontoChange + locked);
return false;
- } else if (notes.getChange().getStatus().isClosed()) {
+ }
+
+ if (change.getStatus().isClosed()) {
reject(inputCommand, "change " + ontoChange + " closed");
return false;
} else if (revisions.containsKey(newCommit)) {
@@ -2359,6 +2403,7 @@
}
}
+ ChangeControl changeCtl = projectControl.controlFor(notes);
if (!validCommit(rp.getRevWalk(), changeCtl.getRefControl(), inputCommand, newCommit)) {
return false;
}
@@ -2410,7 +2455,7 @@
Optional<ChangeEdit> edit = null;
try {
- edit = editUtil.byChange(changeCtl);
+ edit = editUtil.byChange(projectControl.controlFor(notes));
} catch (AuthException | IOException e) {
logError("Cannot retrieve edit", e);
return false;
@@ -2837,7 +2882,7 @@
bu.execute();
} catch (RestApiException e) {
logError("Can't insert patchset", e);
- } catch (IOException | OrmException | UpdateException e) {
+ } catch (IOException | OrmException | UpdateException | PermissionBackendException e) {
logError("Can't scan for changes to close", e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index 0cee090..382af51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
+import static com.google.gerrit.server.ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
@@ -22,6 +23,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -30,20 +32,24 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.server.ApprovalCopier;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.EmailReviewComments;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
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.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
@@ -100,6 +106,8 @@
private final ChangeData.Factory changeDataFactory;
private final ChangeKindCache changeKindCache;
private final ChangeMessagesUtil cmUtil;
+ private final CommentsUtil commentsUtil;
+ private final EmailReviewComments.Factory emailCommentsFactory;
private final ExecutorService sendEmailExecutor;
private final RevisionCreated revisionCreated;
private final CommentAdded commentAdded;
@@ -123,10 +131,11 @@
private final MailRecipients recipients = new MailRecipients();
private RevCommit commit;
private ReceiveCommand cmd;
- private Change change;
+ private ChangeNotes notes;
private PatchSet newPatchSet;
private ChangeKind changeKind;
private ChangeMessage msg;
+ private List<Comment> comments = ImmutableList.of();
private String rejectMessage;
private MergedByPushOp mergedByPushOp;
private RequestScopePropagator requestScopePropagator;
@@ -140,6 +149,8 @@
ChangeData.Factory changeDataFactory,
ChangeKindCache changeKindCache,
ChangeMessagesUtil cmUtil,
+ CommentsUtil commentsUtil,
+ EmailReviewComments.Factory emailCommentsFactory,
RevisionCreated revisionCreated,
CommentAdded commentAdded,
MergedByPushOp.Factory mergedByPushOpFactory,
@@ -164,6 +175,8 @@
this.changeDataFactory = changeDataFactory;
this.changeKindCache = changeKindCache;
this.cmUtil = cmUtil;
+ this.commentsUtil = commentsUtil;
+ this.emailCommentsFactory = emailCommentsFactory;
this.revisionCreated = revisionCreated;
this.commentAdded = commentAdded;
this.mergedByPushOpFactory = mergedByPushOpFactory;
@@ -211,13 +224,14 @@
@Override
public boolean updateChange(ChangeContext ctx)
throws RestApiException, OrmException, IOException {
- change = ctx.getChange();
+ notes = ctx.getNotes();
+ Change change = notes.getChange();
if (change == null || change.getStatus().isClosed()) {
rejectMessage = CHANGE_IS_CLOSED;
return false;
}
if (groups.isEmpty()) {
- PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+ PatchSet prevPs = psUtil.current(ctx.getDb(), notes);
groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of();
}
@@ -233,7 +247,7 @@
approvals.putAll(magicBranch.labels);
Set<String> hashtags = magicBranch.hashtags;
if (hashtags != null && !hashtags.isEmpty()) {
- hashtags.addAll(ctx.getNotes().getHashtags());
+ hashtags.addAll(notes.getHashtags());
update.setHashtags(hashtags);
}
if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
@@ -253,6 +267,9 @@
change.setWorkInProgress(true);
update.setWorkInProgress(true);
}
+ if (shouldPublishComments()) {
+ comments = publishComments(ctx);
+ }
}
boolean draft = magicBranch != null && magicBranch.draft;
@@ -305,6 +322,20 @@
recipients.add(oldRecipients);
+ msg = createChangeMessage(ctx, reviewMessage);
+ cmUtil.addChangeMessage(ctx.getDb(), update, msg);
+
+ if (mergedByPushOp == null) {
+ resetChange(ctx);
+ } else {
+ mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
+ }
+
+ return true;
+ }
+
+ private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage)
+ throws OrmException {
String approvalMessage =
ApprovalsUtil.renderMessageWithApprovals(
patchSetId.get(), approvals, scanLabels(ctx, approvals));
@@ -315,26 +346,21 @@
} else {
message.append('.');
}
+ if (comments.size() == 1) {
+ message.append("\n\n(1 comment)");
+ } else if (comments.size() > 1) {
+ message.append(String.format("\n\n(%d comments)", comments.size()));
+ }
if (!Strings.isNullOrEmpty(reviewMessage)) {
- message.append("\n").append(reviewMessage);
+ message.append("\n\n").append(reviewMessage);
}
boolean workInProgress = magicBranch != null && magicBranch.workInProgress;
- msg =
- ChangeMessagesUtil.newMessage(
- patchSetId,
- ctx.getUser(),
- ctx.getWhen(),
- message.toString(),
- ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
- cmUtil.addChangeMessage(ctx.getDb(), update, msg);
-
- if (mergedByPushOp == null) {
- resetChange(ctx);
- } else {
- mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx);
- }
-
- return true;
+ return ChangeMessagesUtil.newMessage(
+ patchSetId,
+ ctx.getUser(),
+ ctx.getWhen(),
+ message.toString(),
+ ChangeMessagesUtil.uploadedPatchSetTag(workInProgress));
}
private String changeKindMessage(ChangeKind changeKind) {
@@ -396,50 +422,45 @@
}
}
+ private List<Comment> publishComments(ChangeContext ctx) throws OrmException {
+ List<Comment> comments =
+ commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), ctx.getUser().getAccountId());
+ commentsUtil.publish(ctx, patchSetId, comments, TAG_UPLOADED_PATCH_SET);
+ return comments;
+ }
+
@Override
public void postUpdate(Context ctx) throws Exception {
if (changeKind != ChangeKind.TRIVIAL_REBASE) {
- Runnable sender =
- new Runnable() {
- @Override
- public void run() {
- try {
- ReplacePatchSetSender cm =
- replacePatchSetFactory.create(
- projectControl.getProject().getNameKey(), change.getId());
- cm.setFrom(ctx.getAccountId());
- cm.setPatchSet(newPatchSet, info);
- cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
- if (magicBranch != null) {
- cm.setNotify(magicBranch.notify);
- cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
- }
- cm.addReviewers(recipients.getReviewers());
- cm.addExtraCC(recipients.getCcOnly());
- cm.send();
- } catch (Exception e) {
- log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
- }
- }
-
- @Override
- public String toString() {
- return "send-email newpatchset";
- }
- };
-
+ // TODO(dborowitz): Merge email templates so we only have to send one.
+ Runnable e = new ReplaceEmailTask(ctx);
if (requestScopePropagator != null) {
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError =
- sendEmailExecutor.submit(requestScopePropagator.wrap(sender));
+ Future<?> possiblyIgnoredError = sendEmailExecutor.submit(requestScopePropagator.wrap(e));
} else {
- sender.run();
+ e.run();
}
}
NotifyHandling notify =
magicBranch != null && magicBranch.notify != null ? magicBranch.notify : NotifyHandling.ALL;
- revisionCreated.fire(change, newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
+
+ if (shouldPublishComments()) {
+ emailCommentsFactory
+ .create(
+ notify,
+ magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(),
+ notes,
+ newPatchSet,
+ ctx.getUser().asIdentifiedUser(),
+ msg,
+ comments,
+ msg.getMessage(),
+ ImmutableList.of()) // TODO(dborowitz): Include labels.
+ .sendAsync();
+ }
+
+ revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify);
try {
fireCommentAddedEvent(ctx);
} catch (Exception e) {
@@ -450,6 +471,40 @@
}
}
+ private class ReplaceEmailTask implements Runnable {
+ private final Context ctx;
+
+ private ReplaceEmailTask(Context ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void run() {
+ try {
+ ReplacePatchSetSender cm =
+ replacePatchSetFactory.create(
+ projectControl.getProject().getNameKey(), notes.getChangeId());
+ cm.setFrom(ctx.getAccount().getId());
+ cm.setPatchSet(newPatchSet, info);
+ cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
+ if (magicBranch != null) {
+ cm.setNotify(magicBranch.notify);
+ cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
+ }
+ cm.addReviewers(recipients.getReviewers());
+ cm.addExtraCC(recipients.getCcOnly());
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "send-email newpatchset";
+ }
+ }
+
private void fireCommentAddedEvent(Context ctx) throws OrmException {
if (approvals.isEmpty()) {
return;
@@ -461,7 +516,7 @@
* show a transition from an oldValue of 0 to the new value.
*/
ChangeControl changeControl =
- changeControlFactory.controlFor(ctx.getDb(), change, ctx.getUser());
+ changeControlFactory.controlFor(ctx.getDb(), notes.getChange(), ctx.getUser());
List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
Map<String, Short> allApprovals = new HashMap<>();
Map<String, Short> oldApprovals = new HashMap<>();
@@ -477,7 +532,13 @@
}
commentAdded.fire(
- change, newPatchSet, ctx.getAccount(), null, allApprovals, oldApprovals, ctx.getWhen());
+ notes.getChange(),
+ newPatchSet,
+ ctx.getAccount(),
+ null,
+ allApprovals,
+ oldApprovals,
+ ctx.getWhen());
}
public PatchSet getPatchSet() {
@@ -485,7 +546,7 @@
}
public Change getChange() {
- return change;
+ return notes.getChange();
}
public String getRejectMessage() {
@@ -520,4 +581,8 @@
return null;
}
}
+
+ private boolean shouldPublishComments() {
+ return magicBranch != null && magicBranch.shouldPublishComments();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index a156a90..40bb6c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.git.MergeIdenticalTreeException;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.RebaseSorter;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.update.ChangeContext;
@@ -114,7 +115,7 @@
@Override
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, InvalidChangeOperationException, RestApiException, IOException,
- OrmException {
+ OrmException, PermissionBackendException {
if (args.mergeUtil.canFastForward(
args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
if (!rebaseAlways) {
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 156f106..9aa8d27 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
@@ -219,6 +219,20 @@
/** @return instance scoped for {@code ref} in this project. */
public abstract ForRef ref(String ref);
+ /** @return instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeData cd) {
+ try {
+ return ref(cd.change().getDest().get()).change(cd);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ /** @return instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeNotes notes) {
+ return ref(notes.getChange().getDest().get()).change(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/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 16c63c8..2cb8d96 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
@@ -252,11 +252,6 @@
&& !isPatchSetLocked(db);
}
- /** Can this user change the destination branch of this change to the new ref? */
- public boolean canMoveTo(String ref, ReviewDb db) throws OrmException {
- return getProjectControl().controlForRef(ref).canUpload() && canAbandon(db);
- }
-
/** Can this user publish this draft change or any draft patch set of this change? */
public boolean canPublish(final ReviewDb db) throws OrmException {
return (isOwner() || getRefControl().canPublishDrafts()) && isVisible(db);
@@ -330,7 +325,7 @@
}
/** Can this user add a patch set to this change? */
- public boolean canAddPatchSet(ReviewDb db) throws OrmException {
+ private boolean canAddPatchSet(ReviewDb db) throws OrmException {
if (!getRefControl().canUpload()
|| isPatchSetLocked(db)
|| !isPatchVisible(patchSetUtil.current(db, notes), db)) {
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 5919ba1..0598d75 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
@@ -26,6 +26,8 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -50,6 +52,7 @@
}
private final Provider<IdentifiedUser> identifiedUser;
+ private final PermissionBackend permissionBackend;
private final GitRepositoryManager repoManager;
private final Provider<ReviewDb> db;
private final GitReferenceUpdated referenceUpdated;
@@ -59,12 +62,14 @@
@Inject
CreateBranch(
Provider<IdentifiedUser> identifiedUser,
+ PermissionBackend permissionBackend,
GitRepositoryManager repoManager,
Provider<ReviewDb> db,
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refHelperFactory,
@Assisted String ref) {
this.identifiedUser = identifiedUser;
+ this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
this.db = db;
this.referenceUpdated = referenceUpdated;
@@ -170,7 +175,10 @@
BranchInfo info = new BranchInfo();
info.ref = ref;
info.revision = revid.getName();
- info.canDelete = refControl.canDelete() ? true : null;
+ info.canDelete =
+ permissionBackend.user(identifiedUser).ref(name).testOrFalse(RefPermission.DELETE)
+ ? true
+ : null;
return info;
} catch (IOException err) {
log.error("Cannot create branch \"" + name + "\"", err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
index f674d17..6b1c0e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -31,6 +31,9 @@
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.RefUtil.InvalidRevisionException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -56,6 +59,7 @@
CreateTag create(String ref);
}
+ private final PermissionBackend permissionBackend;
private final Provider<IdentifiedUser> identifiedUser;
private final GitRepositoryManager repoManager;
private final TagCache tagCache;
@@ -64,11 +68,13 @@
@Inject
CreateTag(
+ PermissionBackend permissionBackend,
Provider<IdentifiedUser> identifiedUser,
GitRepositoryManager repoManager,
TagCache tagCache,
GitReferenceUpdated referenceUpdated,
@Assisted String ref) {
+ this.permissionBackend = permissionBackend;
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
this.tagCache = tagCache;
@@ -78,7 +84,7 @@
@Override
public TagInfo apply(ProjectResource resource, TagInput input)
- throws RestApiException, IOException {
+ throws RestApiException, IOException, PermissionBackendException {
if (input == null) {
input = new TagInput();
}
@@ -92,6 +98,9 @@
ref = RefUtil.normalizeTagRef(ref);
RefControl refControl = resource.getControl().controlForRef(ref);
+ PermissionBackend.ForRef perm =
+ permissionBackend.user(identifiedUser).project(resource.getNameKey()).ref(ref);
+
try (Repository repo = repoManager.openRepository(resource.getNameKey())) {
ObjectId revid = RefUtil.parseBaseRevision(repo, resource.getNameKey(), input.revision);
RevWalk rw = RefUtil.verifyConnected(repo, revid);
@@ -103,8 +112,8 @@
throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
} else if (isAnnotated && !refControl.canPerform(Permission.CREATE_TAG)) {
throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
- } else if (!refControl.canPerform(Permission.CREATE)) {
- throw new AuthException("Cannot create tag \"" + ref + "\"");
+ } else {
+ perm.check(RefPermission.CREATE);
}
if (repo.getRefDatabase().exactRef(ref) != null) {
throw new ResourceConflictException("tag \"" + ref + "\" already exists");
@@ -134,7 +143,7 @@
result.getObjectId(),
identifiedUser.get().getAccount());
try (RevWalk w = new RevWalk(repo)) {
- return ListTags.createTagInfo(result, w, refControl);
+ return ListTags.createTagInfo(perm, result, w);
}
}
} catch (InvalidRevisionException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 4601bfe..2fad19b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.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.gerrit.server.project.DeleteBranch.Input;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
@@ -33,19 +36,25 @@
private final Provider<InternalChangeQuery> queryProvider;
private final DeleteRef.Factory deleteRefFactory;
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
@Inject
- DeleteBranch(Provider<InternalChangeQuery> queryProvider, DeleteRef.Factory deleteRefFactory) {
+ DeleteBranch(
+ Provider<InternalChangeQuery> queryProvider,
+ DeleteRef.Factory deleteRefFactory,
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend) {
this.queryProvider = queryProvider;
this.deleteRefFactory = deleteRefFactory;
+ this.user = user;
+ this.permissionBackend = permissionBackend;
}
@Override
public Response<?> apply(BranchResource rsrc, Input input)
- throws RestApiException, OrmException, IOException {
- if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) {
- throw new AuthException("Cannot delete branch");
- }
+ throws RestApiException, OrmException, IOException, PermissionBackendException {
+ permissionBackend.user(user).ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index f5e55b1..4b45a41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -35,12 +36,10 @@
@Override
public Response<?> apply(ProjectResource project, DeleteBranchesInput input)
- throws OrmException, IOException, RestApiException {
-
+ throws OrmException, IOException, RestApiException, PermissionBackendException {
if (input == null || input.branches == null || input.branches.isEmpty()) {
throw new BadRequestException("branches must be specified");
}
-
deleteRefFactory.create(project).refs(input.branches).delete();
return Response.none();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
index 78a78c2..9b6538c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
@@ -19,11 +19,15 @@
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -52,6 +56,7 @@
private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
private final Provider<IdentifiedUser> identifiedUser;
+ private final PermissionBackend permissionBackend;
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated referenceUpdated;
private final RefValidationHelper refDeletionValidator;
@@ -67,12 +72,14 @@
@Inject
DeleteRef(
Provider<IdentifiedUser> identifiedUser,
+ PermissionBackend permissionBackend,
GitRepositoryManager repoManager,
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refDeletionValidatorFactory,
Provider<InternalChangeQuery> queryProvider,
@Assisted ProjectResource resource) {
this.identifiedUser = identifiedUser;
+ this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
this.referenceUpdated = referenceUpdated;
this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE);
@@ -96,7 +103,8 @@
return this;
}
- public void delete() throws OrmException, IOException, ResourceConflictException {
+ public void delete()
+ throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
if (!refsToDelete.isEmpty()) {
try (Repository r = repoManager.openRepository(resource.getNameKey())) {
if (refsToDelete.size() == 1) {
@@ -168,7 +176,7 @@
}
private void deleteMultipleRefs(Repository r)
- throws OrmException, IOException, ResourceConflictException {
+ throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
batchUpdate.setAtomic(false);
List<String> refs =
@@ -198,7 +206,7 @@
}
private ReceiveCommand createDeleteCommand(ProjectResource project, Repository r, String refName)
- throws OrmException, IOException, ResourceConflictException {
+ throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
Ref ref = r.getRefDatabase().getRef(refName);
ReceiveCommand command;
if (ref == null) {
@@ -210,7 +218,13 @@
}
command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
- if (!project.getControl().controlForRef(refName).canDelete()) {
+ try {
+ permissionBackend
+ .user(identifiedUser)
+ .project(project.getNameKey())
+ .ref(refName)
+ .check(RefPermission.DELETE);
+ } catch (AuthException denied) {
command.setResult(
Result.REJECTED_OTHER_REASON,
"it doesn't exist or you do not have permission to delete it");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java
index f26d40f..a05fa2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java
@@ -14,36 +14,46 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.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.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class DeleteTag implements RestModifyView<TagResource, DeleteTag.Input> {
- private final DeleteRef.Factory deleteRefFactory;
-
public static class Input {}
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> user;
+ private final DeleteRef.Factory deleteRefFactory;
+
@Inject
- DeleteTag(DeleteRef.Factory deleteRefFactory) {
+ DeleteTag(
+ PermissionBackend permissionBackend,
+ Provider<CurrentUser> user,
+ DeleteRef.Factory deleteRefFactory) {
+ this.permissionBackend = permissionBackend;
+ this.user = user;
this.deleteRefFactory = deleteRefFactory;
}
@Override
public Response<?> apply(TagResource resource, Input input)
- throws OrmException, RestApiException, IOException {
+ throws OrmException, RestApiException, IOException, PermissionBackendException {
String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref);
- RefControl refControl = resource.getControl().controlForRef(tag);
-
- if (!refControl.canDelete()) {
- throw new AuthException("Cannot delete tag");
- }
-
+ permissionBackend
+ .user(user)
+ .project(resource.getNameKey())
+ .ref(tag)
+ .check(RefPermission.DELETE);
deleteRefFactory.create(resource).ref(tag).delete();
return Response.none();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java
index 75cf03f..c020351 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -37,12 +38,10 @@
@Override
public Response<?> apply(ProjectResource project, DeleteTagsInput input)
- throws OrmException, RestApiException, IOException {
-
+ throws OrmException, RestApiException, IOException, PermissionBackendException {
if (input == null || input.tags == null || input.tags.isEmpty()) {
throw new BadRequestException("tags must be specified");
}
-
deleteRefFactory.create(project).refs(input.tags).prefix(R_TAGS).delete();
return Response.none();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 09a6b86..ecb3ea6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -26,10 +26,14 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,6 +50,8 @@
public class ListBranches implements RestReadView<ProjectResource> {
private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> user;
private final DynamicMap<RestView<BranchResource>> branchViews;
private final UiActions uiActions;
private final WebLinks webLinks;
@@ -98,10 +104,14 @@
@Inject
public ListBranches(
GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ Provider<CurrentUser> user,
DynamicMap<RestView<BranchResource>> branchViews,
UiActions uiActions,
WebLinks webLinks) {
this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
+ this.user = user;
this.branchViews = branchViews;
this.uiActions = uiActions;
this.webLinks = webLinks;
@@ -140,6 +150,8 @@
}
}
+ ProjectControl pctl = rsrc.getControl();
+ PermissionBackend.ForProject perm = permissionBackend.user(user).project(rsrc.getNameKey());
List<BranchInfo> branches = new ArrayList<>(refs.size());
for (Ref ref : refs) {
if (ref.isSymbolic()) {
@@ -147,7 +159,7 @@
// showing the resolved value, show the name it references.
//
String target = ref.getTarget().getName();
- RefControl targetRefControl = rsrc.getControl().controlForRef(target);
+ RefControl targetRefControl = pctl.controlForRef(target);
if (!targetRefControl.isVisible()) {
continue;
}
@@ -161,14 +173,13 @@
branches.add(b);
if (!Constants.HEAD.equals(ref.getName())) {
- b.canDelete = targetRefControl.canDelete() ? true : null;
+ b.canDelete = perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE) ? true : null;
}
continue;
}
- RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
- if (refControl.isVisible()) {
- branches.add(createBranchInfo(ref, refControl, targets));
+ if (pctl.controlForRef(ref.getName()).isVisible()) {
+ branches.add(createBranchInfo(perm.ref(ref.getName()), ref, pctl, targets));
}
}
Collections.sort(branches, new BranchComparator());
@@ -194,13 +205,15 @@
}
}
- private BranchInfo createBranchInfo(Ref ref, RefControl refControl, Set<String> targets) {
+ private BranchInfo createBranchInfo(
+ PermissionBackend.ForRef perm, Ref ref, ProjectControl pctl, Set<String> targets) {
BranchInfo info = new BranchInfo();
info.ref = ref.getName();
info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
- info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete() ? true : null;
+ info.canDelete =
+ !targets.contains(ref.getName()) && perm.testOrFalse(RefPermission.DELETE) ? true : null;
- BranchResource rsrc = new BranchResource(refControl.getProjectControl(), info);
+ BranchResource rsrc = new BranchResource(pctl, info);
for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
if (info.actions == null) {
info.actions = new TreeMap<>();
@@ -208,9 +221,7 @@
info.actions.put(d.getId(), new ActionInfo(d));
}
- List<WebLinkInfo> links =
- webLinks.getBranchLinks(
- refControl.getProjectControl().getProject().getName(), ref.getName());
+ List<WebLinkInfo> links = webLinks.getBranchLinks(pctl.getProject().getName(), ref.getName());
info.webLinks = links.isEmpty() ? null : links;
return info;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 7f1ee60..7cbea47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -24,11 +24,14 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.RefPermission;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
@@ -50,6 +53,8 @@
public class ListTags implements RestReadView<ProjectResource> {
private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> user;
private final Provider<ReviewDb> dbProvider;
private final TagCache tagCache;
private final ChangeNotes.Factory changeNotesFactory;
@@ -103,11 +108,15 @@
@Inject
public ListTags(
GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ Provider<CurrentUser> user,
Provider<ReviewDb> dbProvider,
TagCache tagCache,
ChangeNotes.Factory changeNotesFactory,
@Nullable SearchingChangeCacheImpl changeCache) {
this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
+ this.user = user;
this.dbProvider = dbProvider;
this.tagCache = tagCache;
this.changeNotesFactory = changeNotesFactory;
@@ -119,13 +128,14 @@
throws IOException, ResourceNotFoundException, BadRequestException {
List<TagInfo> tags = new ArrayList<>();
+ PermissionBackend.ForProject perm = permissionBackend.user(user).project(resource.getNameKey());
try (Repository repo = getRepository(resource.getNameKey());
RevWalk rw = new RevWalk(repo)) {
- ProjectControl control = resource.getControl();
+ ProjectControl pctl = resource.getControl();
Map<String, Ref> all =
- visibleTags(control, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
+ visibleTags(pctl, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
for (Ref ref : all.values()) {
- tags.add(createTagInfo(ref, rw, control.controlForRef(ref.getName())));
+ tags.add(createTagInfo(perm.ref(ref.getName()), ref, rw));
}
}
@@ -158,15 +168,22 @@
ProjectControl control = resource.getControl();
if (ref != null
&& !visibleTags(control, repo, ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
- return createTagInfo(ref, rw, control.controlForRef(ref.getName()));
+ return createTagInfo(
+ permissionBackend
+ .user(control.getUser())
+ .project(resource.getNameKey())
+ .ref(ref.getName()),
+ ref,
+ rw);
}
}
throw new ResourceNotFoundException(id);
}
- public static TagInfo createTagInfo(Ref ref, RevWalk rw, RefControl control)
+ public static TagInfo createTagInfo(PermissionBackend.ForRef perm, Ref ref, RevWalk rw)
throws MissingObjectException, IOException {
RevObject object = rw.parseAny(ref.getObjectId());
+ boolean canDelete = perm.testOrFalse(RefPermission.DELETE);
if (object instanceof RevTag) {
// Annotated or signed tag
RevTag tag = (RevTag) object;
@@ -177,10 +194,10 @@
tag.getObject().getName(),
tag.getFullMessage().trim(),
tagger != null ? CommonConverters.toGitPerson(tag.getTaggerIdent()) : null,
- control.canDelete());
+ canDelete);
}
// Lightweight tag
- return new TagInfo(ref.getName(), ref.getObjectId().getName(), control.canDelete());
+ return new TagInfo(ref.getName(), ref.getObjectId().getName(), canDelete);
}
private Repository getRepository(Project.NameKey project)
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 6291fb0..8773bad 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
@@ -14,6 +14,8 @@
package com.google.gerrit.server.project;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
@@ -46,6 +48,8 @@
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -533,6 +537,31 @@
}
@Override
+ public ForChange change(ChangeData cd) {
+ try {
+ checkProject(cd.change());
+ return super.change(cd);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ @Override
+ public ForChange change(ChangeNotes notes) {
+ checkProject(notes.getChange());
+ return super.change(notes);
+ }
+
+ private void checkProject(Change change) {
+ Project.NameKey project = getProject().getNameKey();
+ checkArgument(
+ project.equals(change.getProject()),
+ "expected change in project %s, not %s",
+ project,
+ change.getProject());
+ }
+
+ @Override
public void check(ProjectPermission perm) throws AuthException, PermissionBackendException {
if (!can(perm)) {
throw new AuthException(perm.describeForException() + " not permitted");
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 812872c..bf53ab1 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
@@ -22,6 +22,7 @@
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
@@ -116,7 +117,9 @@
/** Can this user see this reference exists? */
public boolean isVisible() {
if (isVisible == null) {
- isVisible = (getUser().isInternalUser() || canPerform(Permission.READ)) && canRead();
+ isVisible =
+ (getUser().isInternalUser() || canPerform(Permission.READ))
+ && isProjectStatePermittingRead();
}
return isVisible;
}
@@ -155,7 +158,7 @@
*/
public boolean canUpload() {
return projectControl.controlForRef("refs/for/" + getRefName()).canPerform(Permission.PUSH)
- && canWrite();
+ && isProjectStatePermittingWrite();
}
/** @return true if this user can add a new patch set to this ref */
@@ -163,7 +166,7 @@
return projectControl
.controlForRef("refs/for/" + getRefName())
.canPerform(Permission.ADD_PATCH_SET)
- && canWrite();
+ && isProjectStatePermittingWrite();
}
/** @return true if this user can submit merge patch sets to this ref */
@@ -171,12 +174,12 @@
return projectControl
.controlForRef("refs/for/" + getRefName())
.canPerform(Permission.PUSH_MERGE)
- && canWrite();
+ && isProjectStatePermittingWrite();
}
/** @return true if this user can rebase changes on this ref */
boolean canRebase() {
- return canPerform(Permission.REBASE) && canWrite();
+ return canPerform(Permission.REBASE) && isProjectStatePermittingWrite();
}
/** @return true if this user can submit patch sets to this ref */
@@ -189,7 +192,7 @@
// granting of powers beyond submitting to the configuration.
return projectControl.isOwner();
}
- return canPerform(Permission.SUBMIT, isChangeOwner) && canWrite();
+ return canPerform(Permission.SUBMIT, isChangeOwner) && isProjectStatePermittingWrite();
}
/** @return true if the user can update the reference as a fast-forward. */
@@ -209,12 +212,12 @@
return false;
}
}
- return canPerform(Permission.PUSH) && canWrite();
+ return canPerform(Permission.PUSH) && isProjectStatePermittingWrite();
}
/** @return true if the user can rewind (force push) the reference. */
private boolean canForceUpdate() {
- if (!canWrite()) {
+ if (!isProjectStatePermittingWrite()) {
return false;
}
@@ -237,16 +240,18 @@
}
}
- public boolean canWrite() {
+ private boolean isProjectStatePermittingWrite() {
return getProjectControl().getProject().getState().equals(ProjectState.ACTIVE);
}
- public boolean canRead() {
- return getProjectControl().getProject().getState().equals(ProjectState.READ_ONLY) || canWrite();
+ private boolean isProjectStatePermittingRead() {
+ return getProjectControl().getProject().getState().equals(ProjectState.READ_ONLY)
+ || isProjectStatePermittingWrite();
}
private boolean canPushWithForce() {
- if (!canWrite() || (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner())) {
+ if (!isProjectStatePermittingWrite()
+ || (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner())) {
// Pushing requires being at least project owner, in addition to push.
// Pushing configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
@@ -266,7 +271,7 @@
* @return {@code true} if the user specified can create a new Git ref
*/
public boolean canCreate(ReviewDb db, Repository repo, RevObject object) {
- if (!canWrite()) {
+ if (!isProjectStatePermittingWrite()) {
return false;
}
@@ -358,8 +363,8 @@
*
* @return {@code true} if the user specified can delete a Git ref.
*/
- public boolean canDelete() {
- if (!canWrite() || (RefNames.REFS_CONFIG.equals(refName))) {
+ private boolean canDelete() {
+ if (!isProjectStatePermittingWrite() || (RefNames.REFS_CONFIG.equals(refName))) {
// Never allow removal of the refs/meta/config branch.
// Deleting the branch would destroy all Gerrit specific
// metadata about the project, including its access rules.
@@ -683,10 +688,13 @@
@Override
public ForChange change(ChangeNotes notes) {
+ Project.NameKey project = getProjectControl().getProject().getNameKey();
Change change = notes.getChange();
checkArgument(
- getProjectControl().getProject().getNameKey().equals(change.getProject()),
- "mismatched project");
+ project.equals(change.getProject()),
+ "expected change in project %s, not %s",
+ project,
+ change.getProject());
return getProjectControl().controlFor(notes).asForChange(null, db);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 611a1a4..39e9241 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -118,7 +118,7 @@
private static final Pattern DEF_CHANGE =
Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
- private static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
+ static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
// NOTE: As new search operations are added, please keep the
// SearchSuggestOracle up to date.
@@ -993,12 +993,16 @@
private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
throws QueryParseException, OrmException {
+ Predicate<ChangeData> byState =
+ reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
+ if (byState == Predicate.<ChangeData>any()) {
+ return Predicate.any();
+ }
if (args.getSchema().hasField(ChangeField.WIP)) {
return Predicate.and(
- Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
- reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField));
+ Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)), byState);
}
- return reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
+ return byState;
}
@Operator
@@ -1162,12 +1166,18 @@
// Adapt the capacity of this list when adding more default predicates.
List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
try {
- predicates.add(ownerDefaultField(query));
+ Predicate<ChangeData> p = ownerDefaultField(query);
+ if (p != Predicate.<ChangeData>any()) {
+ predicates.add(p);
+ }
} catch (OrmException | QueryParseException e) {
// Skip.
}
try {
- predicates.add(reviewerDefaultField(query));
+ Predicate<ChangeData> p = reviewerDefaultField(query);
+ if (p != Predicate.<ChangeData>any()) {
+ predicates.add(p);
+ }
} catch (OrmException | QueryParseException e) {
// Skip.
}
@@ -1266,7 +1276,10 @@
return Predicate.or(reviewerPredicate, reviewerByEmailPredicate);
} else if (reviewerPredicate != null) {
return reviewerPredicate;
+ } else if (reviewerByEmailPredicate != null) {
+ return reviewerByEmailPredicate;
+ } else {
+ return Predicate.any();
}
- return reviewerByEmailPredicate;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
index 4830fe1..456a01a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
@@ -23,4 +23,4 @@
Schema_150(Provider<Schema_149> prior) {
super(prior);
}
-}
\ No newline at end of file
+}
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 0b01362..ac421f0 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
@@ -67,6 +67,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.change.ChangeInserter;
@@ -88,6 +89,8 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.testutil.ConfigSuite;
@@ -138,6 +141,7 @@
return cfg;
}
+ @Inject protected AccountCache accountCache;
@Inject protected AccountManager accountManager;
@Inject protected AllUsersName allUsersName;
@Inject protected BatchUpdate.Factory updateFactory;
@@ -151,6 +155,7 @@
@Inject protected InMemoryRepositoryManager repoManager;
@Inject protected InternalChangeQuery internalChangeQuery;
@Inject protected ChangeNotes.Factory notesFactory;
+ @Inject protected OneOffRequestContext oneOffRequestContext;
@Inject protected PatchSetInserter.Factory patchSetFactory;
@Inject protected PatchSetUtil psUtil;
@Inject protected ChangeControl.GenericFactory changeControlFactory;
@@ -2001,6 +2006,14 @@
assertQuery("starredby:me", change2, change1);
}
+ @Test
+ public void defaultFieldWithManyUsers() throws Exception {
+ for (int i = 0; i < ChangeQueryBuilder.MAX_ACCOUNTS_PER_DEFAULT_FIELD * 2; i++) {
+ createAccount("user" + i, "User " + i, "user" + i + "@example.com", true);
+ }
+ assertQuery("us");
+ }
+
protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
return newChange(repo, null, null, null, null);
}
@@ -2204,4 +2217,21 @@
Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment));
gApi.changes().id(changeId).current().review(input);
}
+
+ private Account.Id createAccount(String username, String fullName, String email, boolean active)
+ throws Exception {
+ try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+ Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId();
+ if (email != null) {
+ accountManager.link(id, AuthRequest.forEmail(email));
+ }
+ Account a = db.accounts().get(id);
+ a.setFullName(fullName);
+ a.setPreferredEmail(email);
+ a.setActive(active);
+ db.accounts().update(ImmutableList.of(a));
+ accountCache.evict(id);
+ return id;
+ }
+ }
}
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 3fd774d..ce444c5 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -137,3 +137,12 @@
simple style issues.
If you modify JS inside of `<script>` tags, like for test suites, you may have
to supply the `--ext .html` flag.
+
+Some useful commands:
+
+* To run ESLint on the whole app, less some dependency code:
+`eslint --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app`
+* To run ESLint on just the subdirectory you modified:
+`eslint --ext .html,.js polygerrit-ui/app/$YOUR_DIR_HERE`
+* To run the linter on all of your local changes:
+`git diff --name-only master | xargs eslint --ext .html,.js`
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
index 6b35328..3e9e19e 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -15,4 +15,6 @@
-->
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html">
+<script src="../../scripts/rootElement.js"></script>
+
<script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 8eb63a4..ad3f2c2 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -75,7 +75,7 @@
// Set visibility to hidden before appending to the DOM so that
// calculations can be made based on the element’s size.
tooltip.style.visibility = 'hidden';
- Polymer.dom(document.body).appendChild(tooltip);
+ Gerrit.getRootElement().appendChild(tooltip);
this._positionTooltip(tooltip);
tooltip.style.visibility = null;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 425ea76..a18cfaa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -99,6 +99,7 @@
'n ]': '_handleNKey',
'o enter': '_handleEnterKey',
'p [': '_handlePKey',
+ 'shift+r': '_handleRKey',
},
attached: function() {
@@ -241,6 +242,15 @@
this.fire('previous-page');
},
+ _handleRKey: function(e) {
+ if (this.shouldSuppressKeyboardShortcut(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ window.location.reload();
+ },
+
_changeURLForIndex: function(index) {
var changeEls = this._getListItems();
if (index < changeEls.length && changeEls[index]) {
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 7408e04..4918b03 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
@@ -205,7 +205,7 @@
<template is="dom-if" if="[[change.topic]]">
<gr-linked-chip
text="[[change.topic]]"
- href="[[_computeTopicHref(change.topic)]]"
+ href="[[_computeTopicURL(change.topic)]]"
removable="[[!_topicReadOnly]]"
on-remove="_handleTopicRemoved"></gr-linked-chip>
</template>
@@ -255,7 +255,7 @@
<section class="labelStatus">
<span class="title">Label Status</span>
<span class="value">
- <div hidden$="[[!change.work_in_progress]]">
+ <div hidden$="[[!_isWip]]">
Work in progress
</div>
<div hidden$="[[!_showMissingLabels(change.labels)]]">
@@ -268,7 +268,7 @@
</template>
</ul>
</div>
- <div hidden$="[[_showMissingRequirements(change.labels, change.work_in_progress)]]">
+ <div hidden$="[[_showMissingRequirements(change.labels, _isWip)]]">
Ready to submit
</div>
</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 9046d1b..4527104 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -44,6 +44,10 @@
},
_assignee: Array,
+ _isWip: {
+ type: Boolean,
+ computed: '_computeIsWip(change)',
+ },
},
behaviors: [
@@ -264,15 +268,19 @@
' status:' + this.encodeURL(status, false);
},
- _computeTopicHref: function(topic) {
- var encodedTopic = encodeURIComponent('\"' + topic + '\"');
+ _computeTopicURL: function(topic) {
return this.getBaseUrl() + '/q/topic:' +
- encodeURIComponent(encodedTopic) + '+(status:open OR status:merged)';
+ this.encodeURL('"' + topic + '"', false) +
+ '+(status:open OR status:merged)';
},
_handleTopicRemoved: function() {
this.set(['change', 'topic'], '');
this.$.restAPI.setChangeTopic(this.change._number, null);
},
+
+ _computeIsWip(change) {
+ return !!change.work_in_progress;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 0ac8531..2840c1f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -153,6 +153,19 @@
assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
});
+ test('determines whether to show "Ready to Submit" label', function() {
+ var showMissingSpy = sandbox.spy(element, '_showMissingRequirements');
+ element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {
+ test: {
+ all: [{_account_id: 1, name: 'bojack', value: 1}],
+ default_value: 0,
+ values: [],
+ },
+ }};
+ flushAsynchronousOperations();
+ assert.isTrue(showMissingSpy.called);
+ });
+
suite('Topic removal', function() {
var change;
setup(function() {
@@ -282,7 +295,7 @@
});
test('topic href has quotes', function() {
- var hrefArr = element._computeTopicHref('test')
+ var hrefArr = element._computeTopicURL('test')
.split('%2522'); // Double-escaped quote.
assert.equal(hrefArr[1], 'test');
});
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 776ace2..2d57392 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
@@ -19,6 +19,7 @@
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<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="../../diff/gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -78,6 +79,9 @@
font-size: 1.2em;
font-weight: bold;
}
+ .prefsButton {
+ float: right;
+ }
gr-change-star {
margin-right: .25em;
vertical-align: -.425em;
@@ -193,6 +197,9 @@
height: 0;
margin-bottom: 1em;
}
+ #diffPrefsContainer {
+ margin: auto 0 auto auto;
+ }
.patchInfo-header-wrapper {
width: 100%;
}
@@ -461,9 +468,17 @@
read-only="[[_descriptionReadOnly]]"
on-changed="_handleDescriptionChanged"></gr-editable-label>
</span>
+ <span id="diffPrefsContainer"
+ hidden$="[[_computePrefsButtonHidden(_diffPrefs, _loggedIn)]]"
+ hidden>
+ <gr-button link
+ class="prefsButton desktop"
+ on-tap="_handlePrefsTap">Diff Preferences</gr-button>
+ </span>
</div>
</div>
<gr-file-list id="fileList"
+ diff-prefs="{{_diffPrefs}}"
change="[[_change]]"
change-num="[[_changeNum]]"
patch-range="{{_patchRange}}"
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 f140296..ea835e7 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
@@ -73,6 +73,7 @@
type: Object,
value: function() { return document.body; },
},
+ _diffPrefs: Object,
_numFilesShown: {
type: Number,
observer: '_numFilesShownChanged',
@@ -190,6 +191,7 @@
'u': '_handleUKey',
'x': '_handleXKey',
'z': '_handleZKey',
+ ',': '_handleCommaKey',
},
attached: function() {
@@ -225,6 +227,10 @@
}
},
+ _computePrefsButtonHidden: function(prefs, loggedIn) {
+ return !loggedIn || !prefs;
+ },
+
_handleEditCommitMessage: function(e) {
this._editingCommitMessage = true;
this.$.commitMessageEditor.focusTextarea();
@@ -273,6 +279,11 @@
return false;
},
+ _handlePrefsTap: function(e) {
+ e.preventDefault();
+ this.$.fileList.openDiffPrefs();
+ },
+
_handleCommentSave: function(e) {
if (!e.target.comment.__draft) { return; }
@@ -800,6 +811,14 @@
this.$.messageList.handleExpandCollapse(false);
},
+ _handleCommaKey: function(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this.$.fileList.openDiffPrefs();
+ },
+
_determinePageBack: function() {
// Default backPage to '/' if user came to change view page
// via an email link, etc.
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 06fb387..9e42408 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
@@ -98,6 +98,8 @@
test('A toggles overlay when logged in', function(done) {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown')
+ .returns(Promise.resolve(true));
element._change = {labels: {}};
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(function() {
@@ -157,6 +159,45 @@
MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
assert.isTrue(stub.called);
});
+
+ test(', should open diff preferences', function() {
+ var stub = sandbox.stub(element.$.fileList.$.diffPreferences, 'open');
+ MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+ assert.isTrue(stub.called);
+ });
+ });
+
+ test('Diff preferences hidden when no prefs or logged out',
+ function() {
+ element._loggedIn = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = false;
+ element._diffPrefs = {'font_size': '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('prefsButton opens gr-diff-preferences', function() {
+ var handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+ var overlayOpenStub = sandbox.stub(element.$.fileList,
+ 'openDiffPrefs');
+ var prefsButton = Polymer.dom(element.root).querySelectorAll(
+ '.prefsButton')[0];
+
+ MockInteractions.tap(prefsButton);
+
+ assert.isTrue(handlePrefsTapSpy.called);
+ assert.isTrue(overlayOpenStub.called);
});
test('_computeDescriptionReadOnly', function() {
@@ -987,6 +1028,8 @@
suite('reply dialog tests', function() {
setup(function() {
sandbox.stub(element.$.replyDialog, '_draftChanged');
+ sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown',
+ function() { return Promise.resolve(true); });
element._change = {labels: {}};
});
@@ -1035,6 +1078,11 @@
});
suite('commit message expand/collapse', function() {
+ setup(function() {
+ sandbox.stub(element, 'fetchIsLatestKnown',
+ function() { return Promise.resolve(false); });
+ });
+
test('commitCollapseToggle hidden for short commit message', function() {
element._latestCommitMessage = '';
assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
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 6f8568c..af412db 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
@@ -323,14 +323,13 @@
no-auto-render
inline-index=[[index]]
hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- project="[[change.project]]"
- commit="[[change.current_revision]]"
change-num="[[changeNum]]"
patch-range="[[patchRange]]"
path="[[file.__path]]"
- prefs="[[_diffPrefs]]"
+ prefs="[[diffPrefs]]"
project-config="[[projectConfig]]"
on-line-selected="_onLineSelected"
+ no-render-on-prefs-change
view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
</template>
</template>
@@ -384,6 +383,10 @@
[[_computeShowAllText(_files)]]
</gr-button><!--
--></gr-tooltip-content>
+ <gr-diff-preferences
+ id="diffPreferences"
+ prefs="{{diffPrefs}}"
+ local-prefs="{{_localPrefs}}"></gr-diff-preferences>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-storage id="storage"></gr-storage>
<gr-diff-cursor id="diffCursor"></gr-diff-cursor>
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 705a8f1..4647d81 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
@@ -54,6 +54,7 @@
diffViewMode: {
type: String,
notify: true,
+ observer: '_updateDiffPreferences',
},
_files: {
type: Array,
@@ -69,7 +70,11 @@
value: function() { return []; },
},
_diffAgainst: String,
- _diffPrefs: Object,
+ diffPrefs: {
+ type: Object,
+ notify: true,
+ observer: '_updateDiffPreferences',
+ },
_userPrefs: Object,
_localPrefs: Object,
_showInlineDiffs: Boolean,
@@ -158,7 +163,7 @@
this._localPrefs = this.$.storage.getPreferences();
promises.push(this._getDiffPreferences().then(function(prefs) {
- this._diffPrefs = prefs;
+ this.diffPrefs = prefs;
}.bind(this)));
promises.push(this._getPreferences().then(function(prefs) {
@@ -173,6 +178,10 @@
return Polymer.dom(this.root).querySelectorAll('gr-diff');
},
+ openDiffPrefs: function() {
+ this.$.diffPreferences.open();
+ },
+
_calculatePatchChange: function(files) {
var filesNoCommitMsg = files.filter(function(files) {
return files.__path !== '/COMMIT_MSG';
@@ -232,6 +241,19 @@
this._patchRangeStr(patchRange), true));
},
+ _updateDiffPreferences: function() {
+ if (!this.diffs.length) { return; }
+ // Re-render all expanded diffs sequentially.
+ var timerName = 'Update ' + this._expandedFilePaths.length +
+ ' diffs with new prefs';
+ this._renderInOrder(this._expandedFilePaths, this.diffs,
+ this._expandedFilePaths.length)
+ .then(function() {
+ this.$.reporting.timeEnd(timerName);
+ this.$.diffCursor.handleDiffUpdate();
+ }.bind(this));
+ },
+
_forEachDiff: function(fn) {
var diffs = this.diffs;
for (var i = 0; i < diffs.length; i++) {
@@ -519,6 +541,9 @@
e.preventDefault();
if (this._showInlineDiffs) {
this._openCursorFile();
+ } else if (this._userPrefs && this._userPrefs.expand_inline_diffs) {
+ if (this.$.fileCursor.index === -1) { return; }
+ this._togglePathExpandedByIndex(this.$.fileCursor.index);
} else {
this._openSelectedFile();
}
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 a34463b..0fbd367 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
@@ -381,42 +381,70 @@
}
});
- test('_handleEnterKey navigates', function() {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- sandbox.stub(element, 'modifierPressed').returns(false);
- var expandStub = sandbox.stub(element, '_openCursorFile');
- var navStub = sandbox.stub(element, '_openSelectedFile');
- var e = new CustomEvent('fake-keyboard-event');
- sinon.stub(e, 'preventDefault');
- element._showInlineDiffs = false;
- element._handleEnterKey(e);
- assert.isTrue(e.preventDefault.called);
- assert.isTrue(navStub.called);
- assert.isFalse(expandStub.called);
- });
+ suite('_handleEnterKey', function() {
+ var interact;
- test('_handleEnterKey expands', function() {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- sandbox.stub(element, 'modifierPressed').returns(false);
- var expandStub = sandbox.stub(element, '_openCursorFile');
- var navStub = sandbox.stub(element, '_openSelectedFile');
- var e = new CustomEvent('fake-keyboard-event');
- sinon.stub(e, 'preventDefault');
- element._showInlineDiffs = true;
- element._handleEnterKey(e);
- assert.isTrue(e.preventDefault.called);
- assert.isFalse(navStub.called);
- assert.isTrue(expandStub.called);
- });
+ setup(function() {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut')
+ .returns(false);
+ sandbox.stub(element, 'modifierPressed').returns(false);
+ var openCursorStub = sandbox.stub(element, '_openCursorFile');
+ var openSelectedStub = sandbox.stub(element, '_openSelectedFile');
+ var expandStub = sandbox.stub(element, '_togglePathExpanded');
- test('_handleEnterKey noop when anchor focused', function() {
- sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
- sandbox.stub(element, 'modifierPressed').returns(false);
- var e = new CustomEvent('fake-keyboard-event',
- {detail: {keyboardEvent: {target: document.createElement('a')}}});
- sinon.stub(e, 'preventDefault');
- element._handleEnterKey(e);
- assert.isFalse(e.preventDefault.called);
+ interact = function(opt_payload) {
+ openCursorStub.reset();
+ openSelectedStub.reset();
+ expandStub.reset();
+
+ var e = new CustomEvent('fake-keyboard-event', opt_payload);
+ sinon.stub(e, 'preventDefault');
+ element._handleEnterKey(e);
+ assert.isTrue(e.preventDefault.called);
+ var result = {};
+ if (openCursorStub.called) {
+ result.opened_cursor = true;
+ }
+ if (openSelectedStub.called) {
+ result.opened_selected = true;
+ }
+ if (expandStub.called) {
+ result.expanded = true;
+ }
+ return result;
+ };
+ });
+
+ test('open from selected file', function() {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ });
+
+ test('open from diff cursor', function() {
+ element._showInlineDiffs = true;
+ assert.deepEqual(interact(), {opened_cursor: true});
+
+ // "Show diffs" mode overrides userPrefs.expand_inline_diffs
+ element._userPrefs = {expand_inline_diffs: true};
+ assert.deepEqual(interact(), {opened_cursor: true});
+ });
+
+ test('expand when user prefers', function() {
+ element._showInlineDiffs = false;
+ assert.deepEqual(interact(), {opened_selected: true});
+ element._userPrefs = {};
+ assert.deepEqual(interact(), {opened_selected: true});
+ element._userPrefs.expand_inline_diffs = true;
+ assert.deepEqual(interact(), {expanded: true});
+ });
+
+ test('noop when anchor focused', function() {
+ var e = new CustomEvent('fake-keyboard-event',
+ {detail: {keyboardEvent: {target: document.createElement('a')}}});
+ sinon.stub(e, 'preventDefault');
+ element._handleEnterKey(e);
+ assert.isFalse(e.preventDefault.called);
+ });
});
});
@@ -699,6 +727,7 @@
basePatchNum: 'PARENT',
patchNum: '2',
};
+ sandbox.spy(element, '_updateDiffPreferences');
element.$.fileCursor.setCursorAtIndex(0);
flushAsynchronousOperations();
@@ -714,6 +743,7 @@
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', function() {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 79a1f34..dc40fdd 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -14,21 +14,21 @@
(function() {
'use strict';
- var STORAGE_DEBOUNCE_INTERVAL_MS = 400;
+ const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
- var FocusTarget = {
+ const FocusTarget = {
ANY: 'any',
BODY: 'body',
CCS: 'cc',
REVIEWERS: 'reviewers',
};
- var ReviewerTypes = {
+ const ReviewerTypes = {
REVIEWER: 'REVIEWER',
CC: 'CC',
};
- var LatestPatchState = {
+ const LatestPatchState = {
LATEST: 'latest',
CHECKING: 'checking',
NOT_LATEST: 'not-latest',
@@ -86,7 +86,7 @@
diffDrafts: Object,
filterReviewerSuggestion: {
type: Function,
- value: function() {
+ value() {
return this._filterReviewerSuggestion.bind(this);
},
},
@@ -138,7 +138,7 @@
},
},
- FocusTarget: FocusTarget,
+ FocusTarget,
behaviors: [
Gerrit.BaseUrlBehavior,
@@ -148,7 +148,7 @@
],
keyBindings: {
- 'esc': '_handleEscKey',
+ esc: '_handleEscKey',
},
observers: [
@@ -157,23 +157,23 @@
'_reviewersChanged(_reviewers.splices)',
],
- attached: function() {
- this._getAccount().then(function(account) {
+ attached() {
+ this._getAccount().then(account => {
this._account = account || {};
- }.bind(this));
+ });
},
- ready: function() {
+ ready() {
this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
},
- open: function(opt_focusTarget) {
+ open(opt_focusTarget) {
this.knownLatestState = LatestPatchState.CHECKING;
this.fetchIsLatestKnown(this.change, this.$.restAPI)
- .then(function(isUpToDate) {
+ .then(isUpToDate => {
this.knownLatestState = isUpToDate ?
LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
- }.bind(this));
+ });
this._focusOn(opt_focusTarget);
if (!this.draft || !this.draft.length) {
@@ -181,54 +181,54 @@
}
},
- focus: function() {
+ focus() {
this._focusOn(FocusTarget.ANY);
},
- getFocusStops: function() {
+ getFocusStops() {
return {
start: this.$.reviewers.focusStart,
end: this.$.cancelButton,
};
},
- setLabelValue: function(label, value) {
- var selectorEl =
+ setLabelValue(label, value) {
+ const selectorEl =
this.$.labelScores.$$('iron-selector[data-label="' + label + '"]');
// The selector may not be present if it’s not at the latest patch set.
if (!selectorEl) { return; }
- var item = selectorEl.$$('gr-button[data-value="' + value + '"]');
+ const item = selectorEl.$$('gr-button[data-value="' + value + '"]');
if (!item) { return; }
selectorEl.selectIndex(selectorEl.indexOf(item));
},
- _handleEscKey: function(e) {
+ _handleEscKey(e) {
this.cancel();
},
- _ccsChanged: function(splices) {
+ _ccsChanged(splices) {
if (splices && splices.indexSplices) {
this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
}
},
- _reviewersChanged: function(splices) {
+ _reviewersChanged(splices) {
if (splices && splices.indexSplices) {
this._processReviewerChange(splices.indexSplices,
ReviewerTypes.REVIEWER);
}
},
- _processReviewerChange: function(indexSplices, type) {
- indexSplices.forEach(function(splice) {
- splice.removed.forEach(function(account) {
+ _processReviewerChange(indexSplices, type) {
+ for (const splice of indexSplices) {
+ for (const account of splice.removed) {
if (!this._reviewersPendingRemove[type]) {
console.err('Invalid type ' + type + ' for reviewer.');
return;
}
this._reviewersPendingRemove[type].push(account);
- }.bind(this));
- }.bind(this));
+ }
+ }
},
/**
@@ -239,15 +239,14 @@
* @param {Object} opt_accountIdsTransferred map of account IDs that must
* not be removed, because they have been readded in another state.
*/
- _purgeReviewersPendingRemove: function(isCancel,
- opt_accountIdsTransferred) {
- var reviewerArr;
- var keep = opt_accountIdsTransferred || {};
- for (var type in this._reviewersPendingRemove) {
+ _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
+ let reviewerArr;
+ const keep = opt_accountIdsTransferred || {};
+ for (const type in this._reviewersPendingRemove) {
if (this._reviewersPendingRemove.hasOwnProperty(type)) {
if (!isCancel) {
reviewerArr = this._reviewersPendingRemove[type];
- for (var i = 0; i < reviewerArr.length; i++) {
+ for (let i = 0; i < reviewerArr.length; i++) {
if (!keep[reviewerArr[i]._account_id]) {
this._removeAccount(reviewerArr[i], type);
}
@@ -265,76 +264,76 @@
* @param {Object} account
* @param {ReviewerTypes} type
*/
- _removeAccount: function(account, type) {
+ _removeAccount(account, type) {
if (account._pendingAdd) { return; }
return this.$.restAPI.removeChangeReviewer(this.change._number,
- account._account_id).then(function(response) {
- if (!response.ok) { return response; }
+ account._account_id).then(response => {
+ if (!response.ok) { return response; }
- var reviewers = this.change.reviewers[type] || [];
- for (var i = 0; i < reviewers.length; i++) {
- if (reviewers[i]._account_id == account._account_id) {
- this.splice(['change', 'reviewers', type], i, 1);
- break;
- }
- }
- }.bind(this));
+ const reviewers = this.change.reviewers[type] || [];
+ for (let i = 0; i < reviewers.length; i++) {
+ if (reviewers[i]._account_id == account._account_id) {
+ this.splice(['change', 'reviewers', type], i, 1);
+ break;
+ }
+ }
+ });
},
- _mapReviewer: function(reviewer) {
- var reviewerId;
- var confirmed;
+ _mapReviewer(reviewer) {
+ let reviewerId;
+ let confirmed;
if (reviewer.account) {
reviewerId = reviewer.account._account_id || reviewer.account.email;
} else if (reviewer.group) {
reviewerId = reviewer.group.id;
confirmed = reviewer.group.confirmed;
}
- return {reviewer: reviewerId, confirmed: confirmed};
+ return {reviewer: reviewerId, confirmed};
},
- send: function(includeComments) {
+ send(includeComments) {
if (this.knownLatestState === 'not-latest') {
this.fire('show-alert',
{message: 'Cannot reply to non-latest patch.'});
return;
}
- var labels = this.$.labelScores.getLabelValues();
+ const labels = this.$.labelScores.getLabelValues();
- var obj = {
+ const obj = {
drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
- labels: labels,
+ labels,
};
if (this.draft != null) {
obj.message = this.draft;
}
- var accountAdditions = {};
- obj.reviewers = this.$.reviewers.additions().map(function(reviewer) {
+ const accountAdditions = {};
+ obj.reviewers = this.$.reviewers.additions().map(reviewer => {
if (reviewer.account) {
accountAdditions[reviewer.account._account_id] = true;
}
return this._mapReviewer(reviewer);
- }.bind(this));
- var ccsEl = this.$$('#ccs');
+ });
+ const ccsEl = this.$$('#ccs');
if (ccsEl) {
- ccsEl.additions().forEach(function(reviewer) {
+ for (let reviewer of ccsEl.additions()) {
if (reviewer.account) {
accountAdditions[reviewer.account._account_id] = true;
}
reviewer = this._mapReviewer(reviewer);
reviewer.state = 'CC';
obj.reviewers.push(reviewer);
- }.bind(this));
+ }
}
this.disabled = true;
- var errFn = this._handle400Error.bind(this);
- return this._saveReview(obj, errFn).then(function(response) {
+ const errFn = this._handle400Error.bind(this);
+ return this._saveReview(obj, errFn).then(response => {
if (!response || !response.ok) {
return response;
}
@@ -343,29 +342,29 @@
this._includeComments = true;
this.fire('send', null, {bubbles: false});
return accountAdditions;
- }.bind(this)).catch(function(err) {
+ }).catch(err => {
this.disabled = false;
throw err;
- }.bind(this));
+ });
},
- _focusOn: function(section) {
+ _focusOn(section) {
if (section === FocusTarget.ANY) {
section = this._chooseFocusTarget();
}
if (section === FocusTarget.BODY) {
- var textarea = this.$.textarea;
+ const textarea = this.$.textarea;
textarea.async(textarea.textarea.focus.bind(textarea.textarea));
} else if (section === FocusTarget.REVIEWERS) {
- var reviewerEntry = this.$.reviewers.focusStart;
+ const reviewerEntry = this.$.reviewers.focusStart;
reviewerEntry.async(reviewerEntry.focus);
} else if (section === FocusTarget.CCS) {
- var ccEntry = this.$$('#ccs').focusStart;
+ const ccEntry = this.$$('#ccs').focusStart;
ccEntry.async(ccEntry.focus);
}
},
- _chooseFocusTarget: function() {
+ _chooseFocusTarget() {
// If we are the owner and the reviewers field is empty, focus on that.
if (this._account && this.change && this.change.owner &&
this._account._account_id === this.change.owner._account_id &&
@@ -377,7 +376,7 @@
return FocusTarget.BODY;
},
- _handle400Error: function(response) {
+ _handle400Error(response) {
// A call to _saveReview could fail with a server error if erroneous
// reviewers were requested. This is signalled with a 400 Bad Request
// status. The default gr-rest-api-interface error handling would
@@ -393,80 +392,83 @@
if (response.status !== 400) {
// This is all restAPI does when there is no custom error handling.
- this.fire('server-error', {response: response});
+ this.fire('server-error', {response});
return response;
}
// Process the response body, format a better error message, and fire
// an event for gr-event-manager to display.
- var jsonPromise = this.$.restAPI.getResponseObject(response);
- return jsonPromise.then(function(result) {
- var errors = [];
- ['reviewers', 'ccs'].forEach(function(state) {
- for (var input in result[state]) {
- var reviewer = result[state][input];
- if (!!reviewer.error) {
+ const jsonPromise = this.$.restAPI.getResponseObject(response);
+ return jsonPromise.then(result => {
+ const errors = [];
+ for (const state of ['reviewers', 'ccs']) {
+ for (const reviewer of result[state]) {
+ if (reviewer.error) {
errors.push(reviewer.error);
}
}
- });
+ }
response = {
ok: false,
status: response.status,
- text: function() { return Promise.resolve(errors.join(', ')); },
+ text() { return Promise.resolve(errors.join(', ')); },
};
- this.fire('server-error', {response: response});
- }.bind(this));
+ this.fire('server-error', {response});
+ });
},
- _computeHideDraftList: function(drafts) {
+ _computeHideDraftList(drafts) {
return Object.keys(drafts || {}).length == 0;
},
- _computeDraftsTitle: function(drafts) {
- var total = 0;
- for (var file in drafts) {
- total += drafts[file].length;
+ _computeDraftsTitle(drafts) {
+ let total = 0;
+ for (const file in drafts) {
+ if (drafts.hasOwnProperty(file)) {
+ total += drafts[file].length;
+ }
}
if (total == 0) { return ''; }
if (total == 1) { return '1 Draft'; }
if (total > 1) { return total + ' Drafts'; }
},
- _computeMessagePlaceholder: function(canBeStarted) {
+ _computeMessagePlaceholder(canBeStarted) {
return canBeStarted ?
'Add a note for your reviewers...' :
'Say something nice...';
},
- _changeUpdated: function(changeRecord, owner, serverConfig) {
+ _changeUpdated(changeRecord, owner, serverConfig) {
this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig);
},
- _rebuildReviewerArrays: function(change, owner, serverConfig) {
+ _rebuildReviewerArrays(change, owner, serverConfig) {
this._owner = owner;
- var reviewers = [];
- var ccs = [];
+ let reviewers = [];
+ const ccs = [];
- for (var key in change) {
- if (key !== 'REVIEWER' && key !== 'CC') {
- console.warn('unexpected reviewer state:', key);
- continue;
+ for (const key in change) {
+ if (change.hasOwnProperty(key)) {
+ if (key !== 'REVIEWER' && key !== 'CC') {
+ console.warn('unexpected reviewer state:', key);
+ continue;
+ }
+ for (const entry of change[key]) {
+ if (entry._account_id === owner._account_id) {
+ return;
+ }
+ switch (key) {
+ case 'REVIEWER':
+ reviewers.push(entry);
+ break;
+ case 'CC':
+ ccs.push(entry);
+ break;
+ }
+ }
}
- change[key].forEach(function(entry) {
- if (entry._account_id === owner._account_id) {
- return;
- }
- switch (key) {
- case 'REVIEWER':
- reviewers.push(entry);
- break;
- case 'CC':
- ccs.push(entry);
- break;
- }
- });
}
if (serverConfig.note_db_enabled) {
@@ -478,12 +480,12 @@
this._reviewers = reviewers;
},
- _accountOrGroupKey: function(entry) {
+ _accountOrGroupKey(entry) {
return entry.id || entry._account_id;
},
- _filterReviewerSuggestion: function(suggestion) {
- var entry;
+ _filterReviewerSuggestion(suggestion) {
+ let entry;
if (suggestion.account) {
entry = suggestion.account;
} else if (suggestion.group) {
@@ -496,8 +498,8 @@
return false;
}
- var key = this._accountOrGroupKey(entry);
- var finder = function(entry) {
+ const key = this._accountOrGroupKey(entry);
+ const finder = function(entry) {
return this._accountOrGroupKey(entry) === key;
}.bind(this);
@@ -505,56 +507,56 @@
this._ccs.find(finder) === undefined;
},
- _getAccount: function() {
+ _getAccount() {
return this.$.restAPI.getAccount();
},
- _cancelTapHandler: function(e) {
+ _cancelTapHandler(e) {
e.preventDefault();
this.cancel();
},
- cancel: function() {
+ cancel() {
this.fire('cancel', null, {bubbles: false});
this._purgeReviewersPendingRemove(true);
this._rebuildReviewerArrays(this.change.reviewers, this._owner,
this.serverConfig);
},
- _saveTapHandler: function(e) {
+ _saveTapHandler(e) {
e.preventDefault();
- this.send(this._includeComments).then(function(keepReviewers) {
+ this.send(this._includeComments).then(keepReviewers => {
this._purgeReviewersPendingRemove(false, keepReviewers);
- }.bind(this));
+ });
},
- _sendTapHandler: function(e) {
+ _sendTapHandler(e) {
e.preventDefault();
if (this.canBeStarted) {
this._startReview()
- .then(function() {
- return this.send(this._includeComments);
- }.bind(this))
- .then(this._purgeReviewersPendingRemove.bind(this));
+ .then(() => {
+ return this.send(this._includeComments);
+ })
+ .then(this._purgeReviewersPendingRemove.bind(this));
return;
}
this.send(this._includeComments)
- .then(this._purgeReviewersPendingRemove.bind(this));
+ .then(this._purgeReviewersPendingRemove.bind(this));
},
- _saveReview: function(review, opt_errFn) {
+ _saveReview(review, opt_errFn) {
return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
review, opt_errFn);
},
- _startReview: function() {
+ _startReview() {
if (!this.canBeStarted) {
return Promise.resolve();
}
return this.$.restAPI.startReview(this.change._number);
},
- _reviewerPendingConfirmationUpdated: function(reviewer) {
+ _reviewerPendingConfirmationUpdated(reviewer) {
if (reviewer === null) {
this.$.reviewerConfirmationOverlay.close();
} else {
@@ -564,7 +566,7 @@
}
},
- _confirmPendingReviewer: function() {
+ _confirmPendingReviewer() {
if (this._ccPendingConfirmation) {
this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group);
this._focusOn(FocusTarget.CCS);
@@ -574,16 +576,16 @@
}
},
- _cancelPendingReviewer: function() {
+ _cancelPendingReviewer() {
this._ccPendingConfirmation = null;
this._reviewerPendingConfirmation = null;
- var target =
+ const target =
this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
this._focusOn(target);
},
- _getStorageLocation: function() {
+ _getStorageLocation() {
// Tests trigger this method without setting change.
if (!this.change) { return {}; }
return {
@@ -593,13 +595,13 @@
};
},
- _loadStoredDraft: function() {
- var draft = this.$.storage.getDraftComment(this._getStorageLocation());
+ _loadStoredDraft() {
+ const draft = this.$.storage.getDraftComment(this._getStorageLocation());
return draft ? draft.message : '';
},
- _draftChanged: function(newDraft, oldDraft) {
- this.debounce('store', function() {
+ _draftChanged(newDraft, oldDraft) {
+ this.debounce('store', () => {
if (!newDraft.length && oldDraft) {
// If the draft has been modified to be empty, then erase the storage
// entry.
@@ -611,24 +613,24 @@
}, STORAGE_DEBOUNCE_INTERVAL_MS);
},
- _handleHeightChanged: function(e) {
+ _handleHeightChanged(e) {
// If the textarea resizes, we need to re-fit the overlay.
- this.debounce('autogrow', function() {
+ this.debounce('autogrow', () => {
this.fire('autogrow');
});
},
- _isState: function(knownLatestState, value) {
+ _isState(knownLatestState, value) {
return knownLatestState === value;
},
- _reload: function() {
+ _reload() {
// Load the current change without any patch range.
location.href = this.getBaseUrl() + '/c/' + this.change._number;
},
- _computeSendButtonLabel: function(canBeStarted) {
- return canBeStarted ? "Start review" : "Send";
+ _computeSendButtonLabel(canBeStarted) {
+ return canBeStarted ? 'Start review' : 'Send';
},
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 55a2c9c..ef0d9dc 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -33,36 +33,36 @@
</test-fixture>
<script>
- suite('gr-reply-dialog tests', function() {
- var element;
- var changeNum;
- var patchNum;
+ suite('gr-reply-dialog tests', () => {
+ let element;
+ let changeNum;
+ let patchNum;
- var sandbox;
- var getDraftCommentStub;
- var setDraftCommentStub;
- var eraseDraftCommentStub;
+ let sandbox;
+ let getDraftCommentStub;
+ let setDraftCommentStub;
+ let eraseDraftCommentStub;
- var lastId = 0;
- var makeAccount = function() { return {_account_id: lastId++}; };
- var makeGroup = function() { return {id: lastId++}; };
+ let lastId = 0;
+ const makeAccount = function() { return {_account_id: lastId++}; };
+ const makeGroup = function() { return {id: lastId++}; };
- setup(function() {
+ setup(() => {
sandbox = sinon.sandbox.create();
changeNum = 42;
patchNum = 1;
stub('gr-rest-api-interface', {
- getConfig: function() { return Promise.resolve({}); },
- getAccount: function() { return Promise.resolve({}); },
+ getConfig() { return Promise.resolve({}); },
+ getAccount() { return Promise.resolve({}); },
});
element = fixture('basic');
element.change = {
_number: changeNum,
labels: {
- Verified: {
+ 'Verified': {
values: {
'-1': 'Fails',
' 0': 'No score',
@@ -89,7 +89,7 @@
' 0',
'+1',
],
- Verified: [
+ 'Verified': [
'-1',
' 0',
'+1',
@@ -103,26 +103,25 @@
'eraseDraftComment');
sandbox.stub(element, 'fetchIsLatestKnown',
- function() { return Promise.resolve(true); });
+ () => { return Promise.resolve(true); });
// Allow the elements created by dom-repeat to be stamped.
flushAsynchronousOperations();
});
- teardown(function() {
+ teardown(() => {
sandbox.restore();
});
- test('default to publishing drafts with reply', function(done) {
+ test('default to publishing drafts with reply', done => {
// Async tick is needed because iron-selector content is distributed and
// distributed content requires an observer to be set up.
// Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(function() {
- flush(function() {
+ flush(() => {
+ flush(() => {
element.draft = 'I wholeheartedly disapprove';
- var saveReviewStub = sandbox.stub(element, '_saveReview',
- function(review) {
+ sandbox.stub(element, '_saveReview', review => {
assert.deepEqual(review, {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {},
@@ -136,26 +135,25 @@
// This is needed on non-Blink engines most likely due to the ways in
// which the dom-repeat elements are stamped.
- flush(function() {
+ flush(() => {
MockInteractions.tap(element.$$('.send'));
});
});
});
});
- test('keep drafts with reply', function(done) {
+ test('keep drafts with reply', done => {
MockInteractions.tap(element.$$('#includeComments'));
assert.equal(element._includeComments, false);
// Async tick is needed because iron-selector content is distributed and
// distributed content requires an observer to be set up.
// Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
- flush(function() {
- flush(function() {
+ flush(() => {
+ flush(() => {
element.draft = 'I wholeheartedly disapprove';
- var saveReviewStub = sandbox.stub(element, '_saveReview',
- function(review) {
+ sandbox.stub(element, '_saveReview', review => {
assert.deepEqual(review, {
drafts: 'KEEP',
labels: {},
@@ -169,17 +167,16 @@
// This is needed on non-Blink engines most likely due to the ways in
// which the dom-repeat elements are stamped.
- flush(function() {
+ flush(() => {
MockInteractions.tap(element.$$('.send'));
});
});
});
});
- test('label picker', function(done) {
+ test('label picker', done => {
element.draft = 'I wholeheartedly disapprove';
- var saveReviewStub = sandbox.stub(element, '_saveReview',
- function(review) {
+ sandbox.stub(element, '_saveReview', review => {
assert.deepEqual(review, {
drafts: 'PUBLISH_ALL_REVISIONS',
labels: {
@@ -192,14 +189,14 @@
return Promise.resolve({ok: true});
});
- sandbox.stub(element.$.labelScores, 'getLabelValues', function() {
+ sandbox.stub(element.$.labelScores, 'getLabelValues', () => {
return {
'Code-Review': -1,
'Verified': -1,
};
});
- element.addEventListener('send', function() {
+ element.addEventListener('send', () => {
assert.isFalse(element.disabled,
'Element should be enabled when done sending reply.');
assert.equal(element.draft.length, 0);
@@ -208,21 +205,21 @@
// This is needed on non-Blink engines most likely due to the ways in
// which the dom-repeat elements are stamped.
- flush(function() {
+ flush(() => {
MockInteractions.tap(element.$$('.send'));
assert.isTrue(element.disabled);
});
});
- test('setlabelValue', function() {
+ test('setlabelValue', () => {
element._account = {_account_id: 1};
flushAsynchronousOperations();
- var label = 'Verified';
- var value = '+1';
+ const label = 'Verified';
+ const value = '+1';
element.setLabelValue(label, value);
flushAsynchronousOperations();
- var labels = element.$.labelScores.getLabelValues();
- assert.deepEqual(labels, {'Verified': 1});
+ const labels = element.$.labelScores.getLabelValues();
+ assert.deepEqual(labels, {Verified: 1});
});
function getActiveElement() {
@@ -235,7 +232,7 @@
}
function overlayObserver(mode) {
- return new Promise(function(resolve) {
+ return new Promise(resolve => {
function listener() {
element.removeEventListener('iron-overlay-' + mode, listener);
resolve();
@@ -245,9 +242,9 @@
}
function testConfirmationDialog(done, cc) {
- var yesButton =
+ const yesButton =
element.$$('.reviewerConfirmationButtons gr-button:first-child');
- var noButton =
+ const noButton =
element.$$('.reviewerConfirmationButtons gr-button:last-child');
element.serverConfig = {note_db_enabled: true};
@@ -257,19 +254,19 @@
assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
// Cause the confirmation dialog to display.
- var observer = overlayObserver('opened');
- var group = {
+ let observer = overlayObserver('opened');
+ const group = {
id: 'id',
name: 'name',
};
if (cc) {
element._ccPendingConfirmation = {
- group: group,
+ group,
count: 10,
};
} else {
element._reviewerPendingConfirmation = {
- group: group,
+ group,
count: 10,
};
}
@@ -285,16 +282,16 @@
element._pendingConfirmationDetails);
}
- observer.then(function() {
+ observer.then(() => {
assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
observer = overlayObserver('closed');
- var expected = 'Group name has 10 members';
+ const expected = 'Group name has 10 members';
assert.notEqual(
element.$.reviewerConfirmationOverlay.innerText.indexOf(expected),
-1);
MockInteractions.tap(noButton); // close the overlay
return observer;
- }).then(function() {
+ }).then(() => {
assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
// We should be focused on account entry input.
@@ -308,24 +305,24 @@
observer = overlayObserver('opened');
if (cc) {
element._ccPendingConfirmation = {
- group: group,
+ group,
count: 10,
};
} else {
element._reviewerPendingConfirmation = {
- group: group,
+ group,
count: 10,
};
}
return observer;
- }).then(function() {
+ }).then(() => {
assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
observer = overlayObserver('closed');
MockInteractions.tap(yesButton); // Confirm the group.
return observer;
- }).then(function() {
+ }).then(() => {
assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
- var additions = cc ?
+ const additions = cc ?
element.$$('#ccs').additions() :
element.$.reviewers.additions();
assert.deepEqual(
@@ -345,41 +342,41 @@
// We should be focused on account entry input.
assert.equal(getActiveElement().id, 'input');
}).then(done);
- };
+ }
- test('cc confirmation', function(done) {
+ test('cc confirmation', done => {
testConfirmationDialog(done, true);
});
- test('reviewer confirmation', function(done) {
+ test('reviewer confirmation', done => {
testConfirmationDialog(done, false);
});
- test('_getStorageLocation', function() {
- var actual = element._getStorageLocation();
+ test('_getStorageLocation', () => {
+ const actual = element._getStorageLocation();
assert.equal(actual.changeNum, changeNum);
assert.equal(actual.patchNum, patchNum);
assert.equal(actual.path, '@change');
});
- test('gets draft from storage on open', function() {
- var storedDraft = 'hello world';
+ test('gets draft from storage on open', () => {
+ const storedDraft = 'hello world';
getDraftCommentStub.returns({message: storedDraft});
element.open();
assert.isTrue(getDraftCommentStub.called);
assert.equal(element.draft, storedDraft);
});
- test('blank if no stored draft', function() {
+ test('blank if no stored draft', () => {
getDraftCommentStub.returns(null);
element.open();
assert.isTrue(getDraftCommentStub.called);
assert.equal(element.draft, '');
});
- test('updates stored draft on edits', function() {
- var firstEdit = 'hello';
- var location = element._getStorageLocation();
+ test('updates stored draft on edits', () => {
+ const firstEdit = 'hello';
+ const location = element._getStorageLocation();
element.draft = firstEdit;
element.flushDebouncer('store');
@@ -392,32 +389,32 @@
assert.isTrue(eraseDraftCommentStub.calledWith(location));
});
- test('400 converts to human-readable server-error', function(done) {
- sandbox.stub(window, 'fetch', function() {
- var text = '....{"reviewers":{"id1":{"error":"first error"}},' +
+ test('400 converts to human-readable server-error', done => {
+ sandbox.stub(window, 'fetch', () => {
+ const text = '....{"reviewers":{"id1":{"error":"first error"}},' +
'"ccs":{"id2":{"error":"second error"}}}';
return Promise.resolve({
ok: false,
status: 400,
- text: function() { return Promise.resolve(text); },
+ text() { return Promise.resolve(text); },
});
});
- element.addEventListener('server-error', function(event) {
+ element.addEventListener('server-error', event => {
if (event.target !== element) {
return;
}
- event.detail.response.text().then(function(body) {
+ event.detail.response.text().then(body => {
assert.equal(body, 'first error, second error');
});
});
// Async tick is needed because iron-selector content is distributed and
// distributed content requires an observer to be set up.
- flush(function() { element.send().then(done); });
+ flush(() => { element.send().then(done); });
});
- test('ccs are displayed if NoteDb is enabled', function() {
+ test('ccs are displayed if NoteDb is enabled', () => {
function hasCc() {
flushAsynchronousOperations();
return !!element.$$('#ccs');
@@ -430,12 +427,12 @@
assert.isTrue(hasCc());
});
- test('filterReviewerSuggestion', function() {
- var owner = makeAccount();
- var reviewer1 = makeAccount();
- var reviewer2 = makeGroup();
- var cc1 = makeAccount();
- var cc2 = makeGroup();
+ test('filterReviewerSuggestion', () => {
+ const owner = makeAccount();
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeGroup();
+ const cc1 = makeAccount();
+ const cc2 = makeGroup();
element._owner = owner;
element._reviewers = [reviewer1, reviewer2];
@@ -457,7 +454,7 @@
assert.isFalse(element._filterReviewerSuggestion({group: cc2}));
});
- test('_chooseFocusTarget', function() {
+ test('_chooseFocusTarget', () => {
element._account = null;
assert.strictEqual(
element._chooseFocusTarget(), element.FocusTarget.BODY);
@@ -484,15 +481,14 @@
element._chooseFocusTarget(), element.FocusTarget.BODY);
});
- test('only send labels that have changed', function(done) {
- flush(function() {
- var saveReviewStub = sandbox.stub(element, '_saveReview',
- function(review) {
+ test('only send labels that have changed', done => {
+ flush(() => {
+ sandbox.stub(element, '_saveReview', review => {
assert.deepEqual(review.labels, {Verified: -1});
return Promise.resolve({ok: true});
});
- element.addEventListener('send', function() {
+ element.addEventListener('send', () => {
done();
});
// Without wrapping this test in flush(), the below two calls to
@@ -507,16 +503,16 @@
});
});
- test('do not display tooltips on touch devices', function() {
+ test('do not display tooltips on touch devices', () => {
element._account = {_account_id: 1};
element.set(['change', 'labels', 'Verified', 'all'],
[{_account_id: 1, value: -1}]);
element.labels = {
- Verified: {
+ 'Verified': {
values: {
'-1': 'Fails',
' 0': 'No score',
- '+1': 'Verified'
+ '+1': 'Verified',
},
default_value: 0,
},
@@ -526,7 +522,7 @@
'-1': 'I would prefer that you didn\'t submit this',
' 0': 'No score',
'+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved'
+ '+2': 'Looks good to me, approved',
},
default_value: 0,
},
@@ -534,7 +530,7 @@
flushAsynchronousOperations();
- var verifiedBtn = element.$$('gr-label-scores').$$(
+ const verifiedBtn = element.$$('gr-label-scores').$$(
'iron-selector[data-label="Verified"] > ' +
'gr-button[data-value="-1"]');
@@ -553,8 +549,8 @@
assert.isNotOk(verifiedBtn._tooltip);
});
- test('_processReviewerChange', function() {
- var mockIndexSplices = function(toRemove) {
+ test('_processReviewerChange', () => {
+ const mockIndexSplices = function(toRemove) {
return [{
removed: [toRemove],
}];
@@ -565,16 +561,16 @@
assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1);
});
- test('_purgeReviewersPendingRemove', function() {
- var removeStub = sandbox.stub(element, '_removeAccount');
- var mock = function() {
+ test('_purgeReviewersPendingRemove', () => {
+ const removeStub = sandbox.stub(element, '_removeAccount');
+ const mock = function() {
element._reviewersPendingRemove = {
test: [makeAccount()],
test2: [makeAccount(), makeAccount()],
};
};
- var checkObjEmpty = function(obj) {
- for (var prop in obj) {
+ const checkObjEmpty = function(obj) {
+ for (const prop in obj) {
if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; }
}
return true;
@@ -590,48 +586,46 @@
assert.isTrue(checkObjEmpty(element._reviewersPendingRemove));
});
- test('_removeAccount', function(done) {
+ test('_removeAccount', done => {
sandbox.stub(element.$.restAPI, 'removeChangeReviewer')
.returns(Promise.resolve({ok: true}));
- var arr = [makeAccount(), makeAccount()];
+ const arr = [makeAccount(), makeAccount()];
element.change.reviewers = {
REVIEWER: arr.slice(),
};
- element._removeAccount(arr[1], 'REVIEWER').then(function() {
+ element._removeAccount(arr[1], 'REVIEWER').then(() => {
assert.equal(element.change.reviewers.REVIEWER.length, 1);
assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1));
done();
});
});
- test('migrate reviewers between states', function(done) {
+ test('migrate reviewers between states', done => {
element.serverConfig = {note_db_enabled: true};
element._reviewersPendingRemove = {
CC: [],
REVIEWER: [],
};
flushAsynchronousOperations();
- var reviewers = element.$.reviewers;
- var ccs = element.$$('#ccs');
- var reviewer1 = makeAccount();
- var reviewer2 = makeAccount();
- var cc1 = makeAccount();
- var cc2 = makeAccount();
+ const reviewers = element.$.reviewers;
+ const ccs = element.$$('#ccs');
+ const reviewer1 = makeAccount();
+ const reviewer2 = makeAccount();
+ const cc1 = makeAccount();
+ const cc2 = makeAccount();
element._reviewers = [reviewer1, reviewer2];
element._ccs = [cc1, cc2];
- var mutations = [];
+ const mutations = [];
- var saveReviewStub = sandbox.stub(element, '_saveReview',
- function(review) {
- mutations.push.apply(mutations, review.reviewers);
+ sandbox.stub(element, '_saveReview', review => {
+ mutations.push(...review.reviewers);
return Promise.resolve({ok: true});
});
- var removeAccountStub = sandbox.stub(element, '_removeAccount',
- function(account, type) {
- mutations.push({state: 'REMOVED', account: account});
+ sandbox.stub(element, '_removeAccount', (account, type) => {
+ mutations.push({state: 'REMOVED', account});
return Promise.resolve();
});
@@ -645,8 +639,8 @@
// (Currently not possible in UI, but this is a good consistency check).
reviewers.$.entry.fire('add', {value: {account: cc2}});
ccs.$.entry.fire('add', {value: {account: reviewer2}});
- var mapReviewer = function(reviewer, opt_state) {
- var result = {reviewer: reviewer._account_id, confirmed: undefined};
+ const mapReviewer = function(reviewer, opt_state) {
+ const result = {reviewer: reviewer._account_id, confirmed: undefined};
if (opt_state) {
result.state = opt_state;
}
@@ -656,20 +650,20 @@
// Send and purge and verify moves without deletions.
element.send()
.then(element._purgeReviewersPendingRemove.bind(element))
- .then(function() {
- assert.deepEqual(
- mutations, [
- mapReviewer(cc1),
- mapReviewer(cc2),
- mapReviewer(reviewer1, 'CC'),
- mapReviewer(reviewer2, 'CC'),
- ]);
- done();
- });
+ .then(() => {
+ assert.deepEqual(
+ mutations, [
+ mapReviewer(cc1),
+ mapReviewer(cc2),
+ mapReviewer(reviewer1, 'CC'),
+ mapReviewer(reviewer2, 'CC'),
+ ]);
+ done();
+ });
});
- test('emits cancel on esc key', function() {
- var cancelHandler = sandbox.spy();
+ test('emits cancel on esc key', () => {
+ const cancelHandler = sandbox.spy();
element.addEventListener('cancel', cancelHandler);
MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
flushAsynchronousOperations();
@@ -677,7 +671,7 @@
assert.isTrue(cancelHandler.called);
});
- test('_computeMessagePlaceholder', function() {
+ test('_computeMessagePlaceholder', () => {
assert.equal(
element._computeMessagePlaceholder(false),
'Say something nice...');
@@ -686,7 +680,7 @@
'Add a note for your reviewers...');
});
- test('_computeSendButtonLabel', function() {
+ test('_computeSendButtonLabel', () => {
assert.equal(
element._computeSendButtonLabel(false),
'Send');
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index ab5ecc2..1ccb30b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,13 +14,13 @@
(function() {
'use strict';
- var HIDE_ALERT_TIMEOUT_MS = 5000;
- var CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
- var STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
- var SIGN_IN_WIDTH_PX = 690;
- var SIGN_IN_HEIGHT_PX = 500;
- var TOO_MANY_FILES = 'too many files to find conflicts';
- var AUTHENTICATION_REQUIRED = 'Authentication required\n';
+ const HIDE_ALERT_TIMEOUT_MS = 5000;
+ const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
+ const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
+ const SIGN_IN_WIDTH_PX = 690;
+ const SIGN_IN_HEIGHT_PX = 500;
+ const TOO_MANY_FILES = 'too many files to find conflicts';
+ const AUTHENTICATION_REQUIRED = 'Authentication required\n';
Polymer({
is: 'gr-error-manager',
@@ -48,11 +48,11 @@
*/
_lastCredentialCheck: {
type: Number,
- value: function() { return Date.now(); },
- }
+ value() { return Date.now(); },
+ },
},
- attached: function() {
+ attached() {
this.listen(document, 'server-error', '_handleServerError');
this.listen(document, 'network-error', '_handleNetworkError');
this.listen(document, 'show-alert', '_handleShowAlert');
@@ -60,7 +60,7 @@
this.listen(document, 'show-auth-required', '_handleAuthRequired');
},
- detached: function() {
+ detached() {
this._clearHideAlertHandle();
this.unlisten(document, 'server-error', '_handleServerError');
this.unlisten(document, 'network-error', '_handleNetworkError');
@@ -68,21 +68,21 @@
this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
},
- _shouldSuppressError: function(msg) {
- return msg.indexOf(TOO_MANY_FILES) > -1;
+ _shouldSuppressError(msg) {
+ return msg.includes(TOO_MANY_FILES);
},
- _handleAuthRequired: function() {
+ _handleAuthRequired() {
this._showAuthErrorAlert(
'Log in is required to perform that action.', 'Log in.');
},
- _handleServerError: function(e) {
+ _handleServerError(e) {
Promise.all([
- e.detail.response.text(), this._getLoggedIn()
- ]).then(function(values) {
- var text = values[0];
- var loggedIn = values[1];
+ e.detail.response.text(), this._getLoggedIn(),
+ ]).then(values => {
+ const text = values[0];
+ const loggedIn = values[1];
if (e.detail.response.status === 403 &&
loggedIn &&
text === AUTHENTICATION_REQUIRED) {
@@ -92,24 +92,24 @@
} else if (!this._shouldSuppressError(text)) {
this._showAlert('Server error: ' + text);
}
- }.bind(this));
+ });
},
- _handleShowAlert: function(e) {
+ _handleShowAlert(e) {
this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
e.detail.dismissOnNavigation);
},
- _handleNetworkError: function(e) {
+ _handleNetworkError(e) {
this._showAlert('Server unavailable');
console.error(e.detail.error.message);
},
- _getLoggedIn: function() {
+ _getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
- _showAlert: function(text, opt_actionText, opt_actionCallback,
+ _showAlert(text, opt_actionText, opt_actionCallback,
dismissOnNavigation) {
if (this._alertElement) { return; }
@@ -121,12 +121,12 @@
this._hideAlertHandle =
this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
}
- var el = this._createToastAlert();
+ const el = this._createToastAlert();
el.show(text, opt_actionText, opt_actionCallback);
this._alertElement = el;
},
- _hideAlert: function() {
+ _hideAlert() {
if (!this._alertElement) { return; }
this._alertElement.hide();
@@ -136,14 +136,14 @@
this.unlisten(document, 'location-change', '_hideAlert');
},
- _clearHideAlertHandle: function() {
+ _clearHideAlertHandle() {
if (this._hideAlertHandle != null) {
this.cancelAsync(this._hideAlertHandle);
this._hideAlertHandle = null;
}
},
- _showAuthErrorAlert: function(errorText, actionText) {
+ _showAuthErrorAlert(errorText, actionText) {
// TODO(viktard): close alert if it's not for auth error.
if (this._alertElement) { return; }
@@ -158,13 +158,13 @@
}
},
- _createToastAlert: function() {
- var el = document.createElement('gr-alert');
+ _createToastAlert() {
+ const el = document.createElement('gr-alert');
el.toast = true;
return el;
},
- _handleVisibilityChange: function() {
+ _handleVisibilityChange() {
// Ignore when the page is transitioning to hidden (or hidden is
// undefined).
if (document.hidden !== false) { return; }
@@ -172,7 +172,7 @@
// If not currently refreshing credentials and the credentials are old,
// request them to confirm their validity or (display an auth toast if it
// fails).
- var timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
+ const timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
if (!this._refreshingCredentials &&
this.knownAccountId !== undefined &&
timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
@@ -181,18 +181,17 @@
}
},
- _requestCheckLoggedIn: function() {
+ _requestCheckLoggedIn() {
this.debounce(
- 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+ 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
},
- _checkSignedIn: function() {
- this.$.restAPI.checkCredentials().then(function(account) {
- var isLoggedIn = !!account;
+ _checkSignedIn() {
+ this.$.restAPI.checkCredentials().then(account => {
+ const isLoggedIn = !!account;
this._lastCredentialCheck = Date.now();
if (this._refreshingCredentials) {
if (isLoggedIn) {
-
// If the credentials were refreshed but the account is different
// then reload the page completely.
if (account._account_id !== this.knownAccountId) {
@@ -205,17 +204,19 @@
this._requestCheckLoggedIn();
}
}
- }.bind(this));
+ });
},
- _reloadPage: function() {
+ _reloadPage() {
window.location.reload();
},
- _createLoginPopup: function() {
- var left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
- var top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
- var options = [
+ _createLoginPopup() {
+ const left = window.screenLeft +
+ (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
+ const top = window.screenTop +
+ (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
+ const options = [
'width=' + SIGN_IN_WIDTH_PX,
'height=' + SIGN_IN_HEIGHT_PX,
'left=' + left,
@@ -226,14 +227,14 @@
this.listen(window, 'focus', '_handleWindowFocus');
},
- _handleCredentialRefreshed: function() {
+ _handleCredentialRefreshed() {
this.unlisten(window, 'focus', '_handleWindowFocus');
this._refreshingCredentials = false;
this._hideAlert();
this._showAlert('Credentials refreshed.');
},
- _handleWindowFocus: function() {
+ _handleWindowFocus() {
this.flushDebouncer('checkLoggedIn');
},
});
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 83d4a7b..ba10f09 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -33,69 +33,69 @@
</test-fixture>
<script>
- suite('gr-error-manager tests', function() {
- var element;
- var sandbox;
+ suite('gr-error-manager tests', () => {
+ let element;
+ let sandbox;
- setup(function() {
+ setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
- getLoggedIn: function() { return Promise.resolve(true); },
+ getLoggedIn() { return Promise.resolve(true); },
});
element = fixture('basic');
});
- teardown(function() {
+ teardown(() => {
sandbox.restore();
});
- test('does not show auth error on 403 by default', function(done) {
- var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
- var responseText = Promise.resolve('server says no.');
+ test('does not show auth error on 403 by default', done => {
+ const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+ const responseText = Promise.resolve('server says no.');
element.fire('server-error',
- {response: {status: 403, text: function() { return responseText; }}}
+ {response: {status: 403, text() { return responseText; }}}
);
Promise.all([
element.$.restAPI.getLoggedIn.lastCall.returnValue,
responseText,
- ]).then(function() {
- assert.isFalse(showAuthErrorStub.calledOnce);
- done();
+ ]).then(() => {
+ assert.isFalse(showAuthErrorStub.calledOnce);
+ done();
});
});
- test('shows auth error on 403 and Authentication required', function(done) {
- var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
- var responseText = Promise.resolve('Authentication required\n');
+ test('shows auth error on 403 and Authentication required', done => {
+ const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+ const responseText = Promise.resolve('Authentication required\n');
element.fire('server-error',
- {response: {status: 403, text: function() { return responseText; }}}
+ {response: {status: 403, text() { return responseText; }}}
);
Promise.all([
element.$.restAPI.getLoggedIn.lastCall.returnValue,
responseText,
- ]).then(function() {
+ ]).then(() => {
assert.isTrue(showAuthErrorStub.calledOnce);
done();
});
});
- test('show logged in error', function() {
+ test('show logged in error', () => {
sandbox.stub(element, '_showAuthErrorAlert');
element.fire('show-auth-required');
assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
'Log in is required to perform that action.', 'Log in.'));
});
- test('show normal server error', function(done) {
- var showAlertStub = sandbox.stub(element, '_showAlert');
- var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); });
+ test('show normal server error', done => {
+ const showAlertStub = sandbox.stub(element, '_showAlert');
+ const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); });
element.fire('server-error', {response: {status: 500, text: textSpy}});
assert.isTrue(textSpy.called);
Promise.all([
element.$.restAPI.getLoggedIn.lastCall.returnValue,
textSpy.lastCall.returnValue,
- ]).then(function() {
+ ]).then(() => {
assert.isTrue(showAlertStub.calledOnce);
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
'Server error: ZOMG'));
@@ -103,9 +103,9 @@
});
});
- test('suppress TOO_MANY_FILES error', function(done) {
- var showAlertStub = sandbox.stub(element, '_showAlert');
- var textSpy = sandbox.spy(function() {
+ test('suppress TOO_MANY_FILES error', done => {
+ const showAlertStub = sandbox.stub(element, '_showAlert');
+ const textSpy = sandbox.spy(() => {
return Promise.resolve('too many files to find conflicts');
});
element.fire('server-error', {response: {status: 500, text: textSpy}});
@@ -114,17 +114,17 @@
Promise.all([
element.$.restAPI.getLoggedIn.lastCall.returnValue,
textSpy.lastCall.returnValue,
- ]).then(function() {
+ ]).then(() => {
assert.isFalse(showAlertStub.called);
done();
});
});
- test('show network error', function(done) {
- var consoleErrorStub = sandbox.stub(console, 'error');
- var showAlertStub = sandbox.stub(element, '_showAlert');
+ test('show network error', done => {
+ const consoleErrorStub = sandbox.stub(console, 'error');
+ const showAlertStub = sandbox.stub(element, '_showAlert');
element.fire('network-error', {error: new Error('ZOMG')});
- flush(function() {
+ flush(() => {
assert.isTrue(showAlertStub.calledOnce);
assert.isTrue(showAlertStub.lastCall.calledWithExactly(
'Server unavailable'));
@@ -134,21 +134,21 @@
});
});
- test('show auth refresh toast', function(done) {
- var refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials',
- function() { return Promise.resolve(true); });
- var toastSpy = sandbox.spy(element, '_createToastAlert');
- var windowOpen = sandbox.stub(window, 'open');
- var responseText = Promise.resolve('Authentication required\n');
+ test('show auth refresh toast', done => {
+ const refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials',
+ () => { return Promise.resolve(true); });
+ const toastSpy = sandbox.spy(element, '_createToastAlert');
+ const windowOpen = sandbox.stub(window, 'open');
+ const responseText = Promise.resolve('Authentication required\n');
element.fire('server-error',
- {response: {status: 403, text: function() { return responseText; }}}
+ {response: {status: 403, text() { return responseText; }}}
);
Promise.all([
element.$.restAPI.getLoggedIn.lastCall.returnValue,
responseText,
- ]).then(function() {
+ ]).then(() => {
assert.isTrue(toastSpy.called);
- var toast = toastSpy.lastCall.returnValue;
+ let toast = toastSpy.lastCall.returnValue;
assert.isOk(toast);
assert.include(
Polymer.dom(toast.root).textContent, 'Auth error');
@@ -163,12 +163,12 @@
assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
-1);
- var hideToastSpy = sandbox.spy(toast, 'hide');
+ const hideToastSpy = sandbox.spy(toast, 'hide');
element._handleWindowFocus();
assert.isTrue(refreshStub.called);
element.flushDebouncer('checkLoggedIn');
- flush(function() {
+ flush(() => {
assert.isTrue(refreshStub.called);
assert.isTrue(hideToastSpy.called);
@@ -182,18 +182,18 @@
});
});
- test('show alert', function() {
- var alertObj = {message: 'foo'}
+ test('show alert', () => {
+ const alertObj = {message: 'foo'};
sandbox.stub(element, '_showAlert');
- element.fire('show-alert', {message: 'foo'});
+ element.fire('show-alert', alertObj);
assert.isTrue(element._showAlert.calledOnce);
assert.equal(element._showAlert.lastCall.args[0], 'foo');
assert.isNotOk(element._showAlert.lastCall.args[1]);
assert.isNotOk(element._showAlert.lastCall.args[2]);
});
- test('checks stale credentials on visibility change', function() {
- var refreshStub = sandbox.stub(element.$.restAPI,
+ test('checks stale credentials on visibility change', () => {
+ const refreshStub = sandbox.stub(element.$.restAPI,
'checkCredentials');
sandbox.stub(Date, 'now').returns(999999);
element._lastCredentialCheck = 0;
@@ -211,19 +211,19 @@
assert.equal(element._lastCredentialCheck, 999999);
});
- test('refresh loop continues on credential fail', function(done) {
- var accountPromise = Promise.resolve(null);
+ test('refresh loop continues on credential fail', done => {
+ const accountPromise = Promise.resolve(null);
sandbox.stub(element.$.restAPI, 'checkCredentials')
.returns(accountPromise);
- var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
- var handleRefreshStub = sandbox.stub(element,
+ const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
'_handleCredentialRefreshed');
- var reloadStub = sandbox.stub(element, '_reloadPage');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
element._refreshingCredentials = true;
element._checkSignedIn();
- accountPromise.then(function() {
+ accountPromise.then(() => {
assert.isTrue(requestCheckStub.called);
assert.isFalse(handleRefreshStub.called);
assert.isFalse(reloadStub.called);
@@ -231,20 +231,20 @@
});
});
- test('refreshes with same credentials', function(done) {
- var accountPromise = Promise.resolve({_account_id: 1234});
+ test('refreshes with same credentials', done => {
+ const accountPromise = Promise.resolve({_account_id: 1234});
sandbox.stub(element.$.restAPI, 'checkCredentials')
.returns(accountPromise);
- var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
- var handleRefreshStub = sandbox.stub(element,
+ const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
'_handleCredentialRefreshed');
- var reloadStub = sandbox.stub(element, '_reloadPage');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
element.knownAccountId = 1234;
element._refreshingCredentials = true;
element._checkSignedIn();
- accountPromise.then(function() {
+ accountPromise.then(() => {
assert.isFalse(requestCheckStub.called);
assert.isTrue(handleRefreshStub.called);
assert.isFalse(reloadStub.called);
@@ -252,20 +252,20 @@
});
});
- test('reloads when refreshed credentials differ', function(done) {
- var accountPromise = Promise.resolve({_account_id: 1234});
+ test('reloads when refreshed credentials differ', done => {
+ const accountPromise = Promise.resolve({_account_id: 1234});
sandbox.stub(element.$.restAPI, 'checkCredentials')
.returns(accountPromise);
- var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
- var handleRefreshStub = sandbox.stub(element,
+ const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+ const handleRefreshStub = sandbox.stub(element,
'_handleCredentialRefreshed');
- var reloadStub = sandbox.stub(element, '_reloadPage');
+ const reloadStub = sandbox.stub(element, '_reloadPage');
element.knownAccountId = 4321; // Different from 1234
element._refreshingCredentials = true;
element._checkSignedIn();
- accountPromise.then(function() {
+ accountPromise.then(() => {
assert.isFalse(requestCheckStub.called);
assert.isFalse(handleRefreshStub.called);
assert.isTrue(reloadStub.called);
@@ -273,9 +273,9 @@
});
});
- test('dismissOnNavigation respected', function() {
- var asyncStub = sandbox.stub(element, 'async');
- var hideSpy = sandbox.spy(element, '_hideAlert');
+ test('dismissOnNavigation respected', () => {
+ const asyncStub = sandbox.stub(element, 'async');
+ const hideSpy = sandbox.spy(element, '_hideAlert');
// No async call when dismissOnNavigation supplied.
element._showAlert('test', null, null, true);
assert.isFalse(asyncStub.called);
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 9a3a267..b3180e9 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
@@ -162,6 +162,13 @@
</td>
<td>Show selected change</td>
</tr>
+ <tr>
+ <td>
+ <span class="key modifier">Shift</span>
+ <span class="key">r</span>
+ </td>
+ <td>Refresh list of changes</td>
+ </tr>
</tbody>
<!-- Dashboard -->
<tbody hidden$="[[!_computeInView(view, 'gr-dashboard-view')]]" hidden>
@@ -183,6 +190,13 @@
</td>
<td>Show selected change</td>
</tr>
+ <tr>
+ <td>
+ <span class="key modifier">Shift</span>
+ <span class="key">r</span>
+ </td>
+ <td>Refresh list of changes</td>
+ </tr>
</tbody>
<!-- Change View -->
<tbody hidden$="[[!_computeInView(view, 'gr-change-view')]]" hidden>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 4edf1be..ee08e75 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -15,7 +15,7 @@
'use strict';
// Possible static search options for auto complete.
- var SEARCH_OPERATORS = [
+ const SEARCH_OPERATORS = [
'added',
'age',
'age:1week', // Give an example age
@@ -81,12 +81,12 @@
'tr',
];
- var SELF_EXPRESSION = 'self';
- var ME_EXPRESSION = 'me';
+ const SELF_EXPRESSION = 'self';
+ const ME_EXPRESSION = 'me';
- var MAX_AUTOCOMPLETE_RESULTS = 10;
+ const MAX_AUTOCOMPLETE_RESULTS = 10;
- var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
+ const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
Polymer({
is: 'gr-search-bar',
@@ -113,22 +113,22 @@
},
keyEventTarget: {
type: Object,
- value: function() { return document.body; },
+ value() { return document.body; },
},
query: {
type: Function,
- value: function() {
+ value() {
return this._getSearchSuggestions.bind(this);
},
},
_inputVal: String,
},
- _valueChanged: function(value) {
+ _valueChanged(value) {
this._inputVal = value;
},
- _handleInputCommit: function(e) {
+ _handleInputCommit(e) {
this._preventDefaultAndNavigateToInputVal(e);
},
@@ -140,9 +140,9 @@
*
* @param {!Event} e
*/
- _preventDefaultAndNavigateToInputVal: function(e) {
+ _preventDefaultAndNavigateToInputVal(e) {
e.preventDefault();
- var target = Polymer.dom(e).rootTarget;
+ const target = Polymer.dom(e).rootTarget;
// If the target is the #searchInput or has a sub-input component, that
// is what holds the focus as opposed to the target from the DOM event.
if (target.$.input) {
@@ -164,22 +164,21 @@
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
*/
- _fetchAccounts: function(predicate, expression) {
+ _fetchAccounts(predicate, expression) {
if (expression.length === 0) { return Promise.resolve([]); }
return this.$.restAPI.getSuggestedAccounts(
expression,
MAX_AUTOCOMPLETE_RESULTS)
- .then(function(accounts) {
+ .then(accounts => {
if (!accounts) { return []; }
- return accounts.map(function(acct) {
- return predicate + ':"' + acct.name + ' <' + acct.email + '>"';
- });
- }).then(function(accounts) {
+ return accounts.map(acct =>
+ predicate + ':"' + acct.name + ' <' + acct.email + '>"');
+ }).then(accounts => {
// When the expression supplied is a beginning substring of 'self',
// add it as an autocomplete option.
- if (SELF_EXPRESSION.indexOf(expression) === 0) {
+ if (SELF_EXPRESSION.startsWith(expression)) {
return accounts.concat([predicate + ':' + SELF_EXPRESSION]);
- } else if (ME_EXPRESSION.indexOf(expression) === 0) {
+ } else if (ME_EXPRESSION.startsWith(expression)) {
return accounts.concat([predicate + ':' + ME_EXPRESSION]);
} else {
return accounts;
@@ -196,15 +195,15 @@
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
*/
- _fetchGroups: function(predicate, expression) {
+ _fetchGroups(predicate, expression) {
if (expression.length === 0) { return Promise.resolve([]); }
return this.$.restAPI.getSuggestedGroups(
expression,
MAX_AUTOCOMPLETE_RESULTS)
- .then(function(groups) {
+ .then(groups => {
if (!groups) { return []; }
- var keys = Object.keys(groups);
- return keys.map(function(key) { return predicate + ':' + key; });
+ const keys = Object.keys(groups);
+ return keys.map(key => predicate + ':' + key);
});
},
@@ -217,14 +216,14 @@
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
*/
- _fetchProjects: function(predicate, expression) {
+ _fetchProjects(predicate, expression) {
return this.$.restAPI.getSuggestedProjects(
expression,
MAX_AUTOCOMPLETE_RESULTS)
- .then(function(projects) {
+ .then(projects => {
if (!projects) { return []; }
- var keys = Object.keys(projects);
- return keys.map(function(key) { return predicate + ':' + key; });
+ const keys = Object.keys(projects);
+ return keys.map(key => predicate + ':' + key);
});
},
@@ -235,11 +234,11 @@
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
*/
- _fetchSuggestions: function(input) {
+ _fetchSuggestions(input) {
// Split the input on colon to get a two part predicate/expression.
- var splitInput = input.split(':');
- var predicate = splitInput[0];
- var expression = splitInput[1] || '';
+ const splitInput = input.split(':');
+ const predicate = splitInput[0];
+ const expression = splitInput[1] || '';
// Switch on the predicate to determine what to autocomplete.
switch (predicate) {
case 'ownerin':
@@ -265,9 +264,7 @@
default:
return Promise.resolve(SEARCH_OPERATORS
- .filter(function(operator) {
- return operator.indexOf(input) !== -1;
- }));
+ .filter(operator => operator.includes(input)));
}
},
@@ -277,19 +274,19 @@
* @return {!Promise} This returns a promise that resolves to an array of
* strings.
*/
- _getSearchSuggestions: function(input) {
+ _getSearchSuggestions(input) {
// Allow spaces within quoted terms.
- var tokens = input.match(TOKENIZE_REGEX);
- var trimmedInput = tokens[tokens.length - 1].toLowerCase();
+ const tokens = input.match(TOKENIZE_REGEX);
+ const trimmedInput = tokens[tokens.length - 1].toLowerCase();
return this._fetchSuggestions(trimmedInput)
- .then(function(operators) {
+ .then(operators => {
if (!operators || !operators.length) { return []; }
return operators
// Prioritize results that start with the input.
- .sort(function(a, b) {
- var aContains = a.toLowerCase().indexOf(trimmedInput);
- var bContains = b.toLowerCase().indexOf(trimmedInput);
+ .sort((a, b) => {
+ const aContains = a.toLowerCase().indexOf(trimmedInput);
+ const bContains = b.toLowerCase().indexOf(trimmedInput);
if (aContains === bContains) {
return a.localeCompare(b);
}
@@ -304,7 +301,7 @@
// Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
.slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
// Map to an object to play nice with gr-autocomplete.
- .map(function(operator) {
+ .map(operator => {
return {
name: operator,
value: operator,
@@ -313,7 +310,7 @@
});
},
- _handleForwardSlashKey: function(e) {
+ _handleForwardSlashKey(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 05ceb34..96240cd 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -35,26 +35,26 @@
</test-fixture>
<script>
- suite('gr-search-bar tests', function() {
- var element;
+ suite('gr-search-bar tests', () => {
+ let element;
- setup(function() {
+ setup(() => {
element = fixture('basic');
});
- test('value is propagated to _inputVal', function() {
+ test('value is propagated to _inputVal', () => {
element.value = 'foo';
assert.equal(element._inputVal, 'foo');
});
- function getActiveElement() {
+ getActiveElement = () => {
return document.activeElement.shadowRoot ?
document.activeElement.shadowRoot.activeElement :
document.activeElement;
}
- test('tap on search button triggers nav', function(done) {
- sinon.stub(page, 'show', function() {
+ test('tap on search button triggers nav', done => {
+ sinon.stub(page, 'show', () => {
page.show.restore();
assert.notEqual(getActiveElement(), element.$.searchInput);
assert.notEqual(getActiveElement(), element.$.searchButton);
@@ -64,8 +64,8 @@
MockInteractions.tap(element.$.searchButton);
});
- test('enter in search input triggers nav', function(done) {
- sinon.stub(page, 'show', function() {
+ test('enter in search input triggers nav', done => {
+ sinon.stub(page, 'show', () => {
page.show.restore();
assert.notEqual(getActiveElement(), element.$.searchInput);
assert.notEqual(getActiveElement(), element.$.searchButton);
@@ -76,8 +76,8 @@
null, 'enter');
});
- test('search query should be double-escaped', function() {
- var showStub = sinon.stub(page, 'show');
+ test('search query should be double-escaped', () => {
+ const showStub = sinon.stub(page, 'show');
element.$.searchInput.text = 'fate/stay';
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
null, 'enter');
@@ -85,9 +85,9 @@
showStub.restore();
});
- test('input blurred after commit', function() {
- var showStub = sinon.stub(page, 'show');
- var blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
+ test('input blurred after commit', () => {
+ const showStub = sinon.stub(page, 'show');
+ const blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
element.$.searchInput.text = 'fate/stay';
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
null, 'enter');
@@ -96,97 +96,97 @@
blurSpy.restore();
});
- test('empty search query does not trigger nav', function() {
- var showSpy = sinon.spy(page, 'show');
+ test('empty search query does not trigger nav', () => {
+ const showSpy = sinon.spy(page, 'show');
element.value = '';
MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
null, 'enter');
assert.isFalse(showSpy.called);
});
- test('keyboard shortcuts', function() {
- var focusSpy = sinon.spy(element.$.searchInput, 'focus');
- var selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll');
+ test('keyboard shortcuts', () => {
+ const focusSpy = sinon.spy(element.$.searchInput, 'focus');
+ const selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll');
MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
assert.isTrue(focusSpy.called);
assert.isTrue(selectAllSpy.called);
});
- suite('_getSearchSuggestions', function() {
- setup(function() {
- sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() {
- return Promise.resolve([
+ suite('_getSearchSuggestions', () => {
+ setup(() => {
+ sinon.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+ Promise.resolve([
{
name: 'fred',
email: 'fred@goog.co',
},
- ]);
- });
- sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() {
- return Promise.resolve({
+ ])
+ );
+ sinon.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+ Promise.resolve({
Polygerrit: 0,
gerrit: 0,
gerrittest: 0,
- });
- });
- sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() {
- return Promise.resolve({
+ })
+ );
+ sinon.stub(element.$.restAPI, 'getSuggestedProjects', () =>
+ Promise.resolve({
Polygerrit: 0,
- });
- });
+ })
+ );
});
- teardown(function() {
+ teardown(() => {
element.$.restAPI.getSuggestedAccounts.restore();
element.$.restAPI.getSuggestedGroups.restore();
element.$.restAPI.getSuggestedProjects.restore();
});
- test('Autocompletes accounts', function(done) {
- element._getSearchSuggestions('owner:fr').then(function(s) {
+ test('Autocompletes accounts', done => {
+ element._getSearchSuggestions('owner:fr').then(s => {
assert.equal(s[0].value, 'owner:"fred <fred@goog.co>"');
done();
});
});
- test('Inserts self as option when valid', function(done) {
- element._getSearchSuggestions('owner:s').then(function(s) {
+ test('Inserts self as option when valid', done => {
+ element._getSearchSuggestions('owner:s').then(s => {
assert.equal(s[0].value, 'owner:self');
- }).then(function() {
- element._getSearchSuggestions('owner:selfs').then(function(s) {
+ }).then(() => {
+ element._getSearchSuggestions('owner:selfs').then(s => {
assert.notEqual(s[0].value, 'owner:self');
done();
});
});
});
- test('Inserts me as option when valid', function(done) {
- element._getSearchSuggestions('owner:m').then(function(s) {
+ test('Inserts me as option when valid', done => {
+ element._getSearchSuggestions('owner:m').then(s => {
assert.equal(s[0].value, 'owner:me');
- }).then(function() {
- element._getSearchSuggestions('owner:meme').then(function(s) {
+ }).then(() => {
+ element._getSearchSuggestions('owner:meme').then(s => {
assert.notEqual(s[0].value, 'owner:me');
done();
});
});
});
- test('Autocompletes groups', function(done) {
- element._getSearchSuggestions('ownerin:pol').then(function(s) {
+ test('Autocompletes groups', done => {
+ element._getSearchSuggestions('ownerin:pol').then(s => {
assert.equal(s[0].value, 'ownerin:Polygerrit');
done();
});
});
- test('Autocompletes projects', function(done) {
- element._getSearchSuggestions('project:pol').then(function(s) {
+ test('Autocompletes projects', done => {
+ element._getSearchSuggestions('project:pol').then(s => {
assert.equal(s[0].value, 'project:Polygerrit');
done();
});
});
- test('Autocompletes simple searches', function(done) {
- element._getSearchSuggestions('is:o').then(function(s) {
+ test('Autocompletes simple searches', done => {
+ element._getSearchSuggestions('is:o').then(s => {
assert.equal(s[0].name, 'is:open');
assert.equal(s[0].value, 'is:open');
assert.equal(s[1].name, 'is:owner');
@@ -195,26 +195,25 @@
});
});
- test('Does not autocomplete with no match', function(done) {
- element._getSearchSuggestions('asdasdasdasd').then(function(s) {
+ test('Does not autocomplete with no match', done => {
+ element._getSearchSuggestions('asdasdasdasd').then(s => {
assert.equal(s.length, 0);
done();
});
});
- test('Autocomplete doesnt override exact matches to input',
- function(done) {
- element._getSearchSuggestions('ownerin:gerrit').then(function(s) {
- assert.equal(s[0].value, 'ownerin:gerrit');
- done();
- });
- });
+ test('Autocomplete doesnt override exact matches to input', done => {
+ element._getSearchSuggestions('ownerin:gerrit').then(s => {
+ assert.equal(s[0].value, 'ownerin:gerrit');
+ done();
+ });
+ });
- test('Autocomplete respects spaces', function(done) {
- element._getSearchSuggestions('is:ope').then(function(s) {
+ test('Autocomplete respects spaces', done => {
+ element._getSearchSuggestions('is:ope').then(s => {
assert.equal(s[0].name, 'is:open');
assert.equal(s[0].value, 'is:open');
- element._getSearchSuggestions('is:ope ').then(function(s) {
+ element._getSearchSuggestions('is:ope ').then(s => {
assert.equal(s.length, 0);
done();
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 214454a..c407829 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -405,7 +405,6 @@
td.classList.add(line.type);
var html = this._escapeHTML(text);
html = this._addTabWrappers(html, this._prefs.tab_size);
-
if (!this._prefs.line_wrapping &&
this._textLength(text, this._prefs.tab_size) >
this._prefs.line_length) {
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 a3ff3d1..76d2a69 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
@@ -264,7 +264,8 @@
<option value="SIDE_BY_SIDE">Side By Side</option>
<option value="UNIFIED_DIFF">Unified</option>
</select>
- <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
+ <span id="diffPrefsContainer"
+ hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
<span class="preferences desktop">
<span
hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
@@ -289,8 +290,6 @@
</div>
<gr-diff
id="diff"
- project="[[_change.project]]"
- commit="[[_change.current_revision]]"
is-image-diff="{{_isImageDiff}}"
files-weblinks="{{_filesWeblinks}}"
change-num="[[_changeNum]]"
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 166a3d3..663cd3b 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
@@ -252,6 +252,38 @@
'Should navigate to /c/42/1');
});
+ test('Diff preferences hidden when no prefs or logged out',
+ function() {
+ element._loggedIn = false;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = false;
+ element._prefs = {'font_size': '12'};
+ flushAsynchronousOperations();
+ assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+ element._loggedIn = true;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.diffPrefsContainer.hidden);
+ });
+
+ test('prefsButton opens gr-diff-preferences', function() {
+ var handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+ var overlayOpenStub = sandbox.stub(element.$.diffPreferences,
+ 'open');
+ var prefsButton = Polymer.dom(element.root).querySelector('.prefsButton');
+
+ MockInteractions.tap(prefsButton);
+
+ assert.isTrue(handlePrefsTapSpy.called);
+ assert.isTrue(overlayOpenStub.called);
+ });
+
test('go up to change via kb without change loaded', function() {
element._changeNum = '42';
element._patchRange = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index cd1931a..ba402f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -54,8 +54,6 @@
type: Object,
observer: '_projectConfigChanged',
},
- project: String,
- commit: String,
displayLine: {
type: Boolean,
value: false,
@@ -74,6 +72,7 @@
type: Boolean,
reflectToAttribute: true,
},
+ noRenderOnPrefsChange: Boolean,
_loggedIn: {
type: Boolean,
value: false,
@@ -438,7 +437,7 @@
this.updateStyles();
- if (this._diff && this._comments) {
+ if (this._diff && this._comments && !this.noRenderOnPrefsChange) {
this._renderDiffTable();
}
},
@@ -572,8 +571,8 @@
},
_getImages: function() {
- return this.$.restAPI.getImagesForDiff(this.project, this.commit,
- this.changeNum, this._diff, this.patchRange);
+ return this.$.restAPI.getImagesForDiff(this.changeNum, this._diff,
+ this.patchRange);
},
_projectConfigChanged: function(projectConfig) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 5868f77..f765b26 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -327,9 +327,6 @@
stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo',
function() { return Promise.resolve(mockCommit); }));
stubs.push(sandbox.stub(element.$.restAPI,
- 'getCommitFileContents',
- function() { return Promise.resolve(mockFile1); }));
- stubs.push(sandbox.stub(element.$.restAPI,
'getChangeFileContents',
function(changeId, patchNum, path, opt_parentIndex) {
return Promise.resolve(opt_parentIndex === 1 ? mockFile1 :
@@ -840,6 +837,59 @@
});
});
+ suite('change in preferences', function() {
+ setup(function() {
+ element._diff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ diff_header: [],
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [{skip: 66}],
+ };
+ element._comments = {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+ ],
+ right: [
+ {id: 'c1', __commentSide: 'right'},
+ {id: 'c2', __commentSide: 'right'},
+ {id: 'd1', __draft: true, __commentSide: 'right'},
+ {id: 'd2', __draft: true, __commentSide: 'right'},
+ ],
+ };
+ });
+
+ test('change in preferences re-renders diff', function() {
+ sandbox.stub(element, '_renderDiffTable');
+ element.prefs = {};
+ element.prefs = {time_format: 'HHMM_12'};
+ assert.isTrue(element._renderDiffTable.called);
+ });
+
+ test('change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange', function() {
+ sandbox.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = {};
+ element.prefs = {time_format: 'HHMM_12'};
+ assert.isFalse(element._renderDiffTable.called);
+ });
+ });
+
suite('handle comment-update', function() {
setup(function() {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 6f09701..87a4687 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -43,7 +43,7 @@
<style>
:host {
display: flex;
- min-height: 100vh;
+ min-height: 100%;
flex-direction: column;
}
gr-main-header,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 35b5ab1..dbe3423 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -53,7 +53,9 @@
plugin: {js_resource_paths: []},
});
},
+ getPreferences: function() { return Promise.resolve({my: []}); },
getVersion: function() { return Promise.resolve(42); },
+ probePath: function() { return Promise.resolve(42); },
});
element = fixture('basic');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index f485dd6..de3134b 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -205,6 +205,16 @@
on-change="_handleExpandInlineDiffsChanged">
</span>
</section>
+ <section>
+ <span class="title">Publish comments on push</span>
+ <span class="value">
+ <input
+ id="publishCommentsOnPush"
+ type="checkbox"
+ checked$="[[_localPrefs.publish_comments_on_push]]"
+ on-change="_handlePublishCommentsOnPushChanged">
+ </span>
+ </section>
<gr-button
id="savePrefs"
on-tap="_handleSavePreferences"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 4647a2d..6c26550 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -21,6 +21,7 @@
'email_strategy',
'diff_view',
'expand_inline_diffs',
+ 'publish_comments_on_push',
'email_format',
];
@@ -256,6 +257,11 @@
this.$.expandInlineDiffs.checked);
},
+ _handlePublishCommentsOnPushChanged: function() {
+ this.set('_localPrefs.publish_comments_on_push',
+ this.$.publishCommentsOnPush.checked);
+ },
+
_handleMenuChanged: function() {
if (this._isLoading()) { return; }
this._menuChanged = true;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index cb471d4..33e6bd5 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -171,6 +171,8 @@
.firstElementChild.bindValue, preferences.diff_view);
assert.equal(valueOf('Expand inline diffs', 'preferences')
.firstElementChild.checked, false);
+ assert.equal(valueOf('Publish comments on push', 'preferences')
+ .firstElementChild.checked, false);
assert.isFalse(element._prefsChanged);
assert.isFalse(element._menuChanged);
@@ -205,6 +207,29 @@
});
});
+ test('publish comments on push', function(done) {
+ var publishCommentsOnPush =
+ valueOf('Publish comments on push', 'preferences').firstElementChild;
+ MockInteractions.tap(publishCommentsOnPush);
+
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences: function(prefs) {
+ assert.equal(prefs.publish_comments_on_push, true);
+ return Promise.resolve();
+ }
+ });
+
+ // Save the change.
+ element._handleSavePreferences().then(function() {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
test('diff preferences', function(done) {
// Rendered with the expected preferences selected.
assert.equal(valueOf('Context', 'diffPreferences')
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 10eadf9..71cdec4 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -17,6 +17,8 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-button/gr-button.html">
+<script src="../../../scripts/rootElement.js"></script>
+
<dom-module id="gr-alert">
<template>
<style>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index ee7fe97..a2daeb3 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -60,14 +60,14 @@
this.actionText = opt_actionText;
this._hideActionButton = !opt_actionText;
this._actionCallback = opt_actionCallback;
- document.body.appendChild(this);
+ Gerrit.getRootElement().appendChild(this);
this._setShown(true);
},
hide: function() {
this._setShown(false);
if (this._hasZeroTransitionDuration()) {
- document.body.removeChild(this);
+ Gerrit.getRootElement().removeChild(this);
}
},
@@ -81,7 +81,7 @@
_handleTransitionEnd: function(e) {
if (this.shown) { return; }
- document.body.removeChild(this);
+ Gerrit.getRootElement().removeChild(this);
},
_handleActionTap: function(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index ad79a2c..ae42c2a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -984,15 +984,7 @@
'/content' + parent);
},
- getCommitFileContents: function(projectName, commit, path) {
- return this._fetchB64File(
- '/projects/' + encodeURIComponent(projectName) +
- '/commits/' + encodeURIComponent(commit) +
- '/files/' + encodeURIComponent(path) +
- '/content');
- },
-
- getImagesForDiff: function(project, commit, changeNum, diff, patchRange) {
+ getImagesForDiff: function(changeNum, diff, patchRange) {
var promiseA;
var promiseB;
@@ -1000,15 +992,7 @@
if (patchRange.basePatchNum === 'PARENT') {
// Note: we only attempt to get the image from the first parent.
promiseA = this.getChangeFileContents(changeNum, patchRange.patchNum,
- diff.meta_a.name, 1)
- .catch(function(result) {
- // If getting the parent-indexed version of the image fails, it
- // may be because the API has not been rolled out. Fall back to
- // getting the file from the commit using the slow API.
- // NOTE(wyatta): Remove this when the rollout is complete.
- return this._getImageFromCommit(project, commit,
- diff.meta_a.name);
- }.bind(this));
+ diff.meta_a.name, 1);
} else {
promiseA = this.getChangeFileContents(changeNum,
patchRange.basePatchNum, diff.meta_a.name);
@@ -1043,19 +1027,6 @@
}.bind(this));
},
- /**
- * Remove when parent-indexed file requests are completely rolled out.
- */
- _getImageFromCommit: function(project, commit, path) {
- return this.getCommitInfo(project, commit).then(function(info) {
- if (info.parents.length !== 1) {
- return Promise.reject('Change commit has multiple parents.');
- }
- var parent = info.parents[0].commit;
- return this.getCommitFileContents(project, parent, path);
- }.bind(this));
- },
-
setChangeTopic: function(changeNum, topic) {
return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) +
'/topic', {topic: topic});
diff --git a/polygerrit-ui/app/scripts/rootElement.js b/polygerrit-ui/app/scripts/rootElement.js
new file mode 100644
index 0000000..1a07edf
--- /dev/null
+++ b/polygerrit-ui/app/scripts/rootElement.js
@@ -0,0 +1,20 @@
+// 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) {
+ window.Gerrit = window.Gerrit || {};
+ if (window.Gerrit.hasOwnProperty('getRootElement')) { return; }
+
+ window.Gerrit.getRootElement = () => document.body;
+})(window);
\ No newline at end of file