Merge "Revert "Provide in-dashboard help for creating changes""
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 72a9c21..37af110 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -90,6 +90,16 @@
eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
created.
+== Change Deleted
+
+Sent when a change has been deleted.
+
+type:: "change-deleted"
+
+change:: link:json.html#change[change attribute]
+
+deleter:: link:json.html#account[account attribute]
+
=== Change Merged
Sent when a change has been merged into the git repository.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index cc5386f..4456484 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -186,9 +186,10 @@
+
Controls whether server-side signed push validation is required on the
project. Only has an effect if signed push validation is enabled on the
-server, and link:#receive.enableSignedPush is set on the project. See
-the link:config-gerrit.html#receive.enableSignedPush[global
-configuration] for details.
+server, and link:#receive.enableSignedPush[`receive.enableSignedPush`] is
+set on the project. See the
+link:config-gerrit.html#receive.enableSignedPush[global configuration]
+for details.
+
Default is `INHERIT`, which means that this property is inherited from
the parent project.
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index e28a9c4..de5f278 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1098,10 +1098,7 @@
.Response
----
HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
- )]}'
ok
----
@@ -1805,6 +1802,69 @@
]
----
+[delete-draft-comments]
+=== Delete Draft Comments
+--
+'POST /accounts/link:#account-id[\{account-id\}]/drafts:delete'
+--
+
+Deletes some or all of a user's draft comments. The set of comments to delete is
+specified as a link:#delete-draft-comments-input[DeleteDraftCommentsInput]
+entity. An empty input entity deletes all comments.
+
+Only drafts belonging to the caller may be deleted.
+
+.Request
+----
+ POST /accounts/self/drafts.delete HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "query": "is:abandoned"
+ }
+----
+
+As a response, a list of
+link:#deleted-draft-comment-info[DeletedDraftCommentInfo] entities is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "change": {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Implementing Feature X",
+ "status": "ABANDONED",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "insertions": 34,
+ "deletions": 101,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ },
+ "deleted": [
+ {
+ "id": "TvcXrmjM",
+ "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "line": 23,
+ "message": "[nit] trailing whitespace",
+ "updated": "2013-02-26 15:40:43.986000000"
+ }
+ ]
+ }
+ ]
+----
+
[[sign-contributor-agreement]]
=== Sign Contributor Agreement
--
@@ -2316,6 +2376,37 @@
|`name` |The name of the agreement.
|=================================
+[[delete-draft-comments-input]]
+=== DeleteDraftCommentsInput
+The `DeleteDraftCommentsInput` entity contains information specifying a set of
+draft comments that should be deleted.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name ||Description
+|`query` |optional|
+A link:user-search.html[change query] limiting results to changes matching this
+query; `has:draft` is implied and not necessary to list explicitly. If not set,
+matches all changes with drafts.
+|=================================
+
+[[deleted-draft-comment-info]]
+=== DeletedDraftCommentInfo
+The `DeletedDraftCommentInfo` entity contains information about draft comments
+that were deleted.
+
+[options="header",cols="1,6"]
+|=================================
+|Field Name |Description
+|`change` |
+link:rest-api-changes.html#change-info[ChangeInfo] entity describing the change
+on which one or more comments was deleted. Populated with only the
+link:rest-api-changes.html#skip_mergeable[SKIP_MERGEABLE] option.
+|`deleted` |
+List of link:rest-api-changes.html#comment-info[CommentInfo] entities for each
+comment that was deleted.
+|=================================
+
[[diff-preferences-info]]
=== DiffPreferencesInfo
The `DiffPreferencesInfo` entity contains information about the diff
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 38e1b60..c8d052e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -126,25 +126,7 @@
if (size == 0) {
return Resources.C.notAvailable();
}
- int p = Math.abs(saturatedCast(delta * 100 / size));
- return p + "%";
- }
-
- /**
- * Returns the {@code int} nearest in value to {@code value}.
- *
- * @param value any {@code long} value
- * @return the same value cast to {@code int} if it is in the range of the {@code int} type,
- * {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if it is too
- * small
- */
- private static int saturatedCast(long value) {
- if (value > Integer.MAX_VALUE) {
- return Integer.MAX_VALUE;
- }
- if (value < Integer.MIN_VALUE) {
- return Integer.MIN_VALUE;
- }
- return (int) value;
+ long percentage = Math.abs(Math.round(delta * 100.0 / size));
+ return percentage + "%";
}
}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index 1416797..69492a9 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
@@ -150,6 +151,10 @@
accounts.values().removeIf(a -> ids.contains(a.id));
}
+ public ImmutableList<TestAccount> getAll() {
+ return ImmutableList.copyOf(accounts.values());
+ }
+
private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
InternalGroupUpdate groupUpdate =
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 5654c35..1af71b8 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.events.ChangeDeletedEvent;
import com.google.gerrit.server.events.ChangeMergedEvent;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.events.RefEvent;
@@ -69,6 +70,8 @@
public void onEvent(Event e) {
if (e instanceof ReviewerDeletedEvent) {
recordedEvents.put(ReviewerDeletedEvent.TYPE, (ReviewerDeletedEvent) e);
+ } else if (e instanceof ChangeDeletedEvent) {
+ recordedEvents.put(ChangeDeletedEvent.TYPE, (ChangeDeletedEvent) e);
} else if (e instanceof RefEvent) {
RefEvent event = (RefEvent) e;
String key =
@@ -138,6 +141,21 @@
return events;
}
+ private ImmutableList<ChangeDeletedEvent> getChangeDeletedEvents(int expectedSize) {
+ String key = ChangeDeletedEvent.TYPE;
+ if (expectedSize == 0) {
+ assertThat(recordedEvents).doesNotContainKey(key);
+ return ImmutableList.of();
+ }
+ assertThat(recordedEvents).containsKey(key);
+ ImmutableList<ChangeDeletedEvent> events =
+ FluentIterable.from(recordedEvents.get(key))
+ .transform(ChangeDeletedEvent.class::cast)
+ .toList();
+ assertThat(events).hasSize(expectedSize);
+ return events;
+ }
+
public void assertNoRefUpdatedEvents(String project, String branch) throws Exception {
getRefUpdatedEvents(project, branch, 0);
}
@@ -197,6 +215,18 @@
}
}
+ public void assertChangeDeletedEvents(String... expected) {
+ ImmutableList<ChangeDeletedEvent> events = getChangeDeletedEvents(expected.length / 2);
+ int i = 0;
+ for (ChangeDeletedEvent event : events) {
+ String id = event.change.get().id;
+ assertThat(id).isEqualTo(expected[i]);
+ String reviewer = event.deleter.get().email;
+ assertThat(reviewer).isEqualTo(expected[i + 1]);
+ i += 2;
+ }
+ }
+
public void close() {
eventListenerRegistration.remove();
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 6adc978..b7fce5f 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -109,6 +109,9 @@
void deleteExternalIds(List<String> externalIds) throws RestApiException;
+ List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
+ throws RestApiException;
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -301,5 +304,11 @@
public void deleteExternalIds(List<String> externalIds) throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/DeleteDraftCommentsInput.java b/java/com/google/gerrit/extensions/api/accounts/DeleteDraftCommentsInput.java
new file mode 100644
index 0000000..113f3d4
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/accounts/DeleteDraftCommentsInput.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class DeleteDraftCommentsInput {
+ /**
+ * Delete comments only on changes that match this query.
+ *
+ * <p>If null or empty, delete comments on all changes.
+ */
+ @DefaultInput public String query;
+
+ public DeleteDraftCommentsInput() {
+ this(null);
+ }
+
+ public DeleteDraftCommentsInput(@Nullable String query) {
+ this.query = query;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/accounts/DeletedDraftCommentInfo.java b/java/com/google/gerrit/extensions/api/accounts/DeletedDraftCommentInfo.java
new file mode 100644
index 0000000..c15d5bc
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/accounts/DeletedDraftCommentInfo.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import java.util.List;
+
+public class DeletedDraftCommentInfo {
+ public ChangeInfo change;
+ public List<CommentInfo> deleted;
+}
diff --git a/java/com/google/gerrit/extensions/events/ChangeDeletedListener.java b/java/com/google/gerrit/extensions/events/ChangeDeletedListener.java
new file mode 100644
index 0000000..70014f3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/ChangeDeletedListener.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a Change is deleted. */
+@ExtensionPoint
+public interface ChangeDeletedListener {
+ interface Event extends ChangeEvent {}
+
+ void onChangeDeleted(Event event);
+}
diff --git a/java/com/google/gerrit/httpd/WebModule.java b/java/com/google/gerrit/httpd/WebModule.java
index 538d605..d115f43 100644
--- a/java/com/google/gerrit/httpd/WebModule.java
+++ b/java/com/google/gerrit/httpd/WebModule.java
@@ -56,7 +56,9 @@
installAuthModule();
if (options.enableMasterFeatures()) {
install(new UrlModule(options, authConfig));
- install(new UiRpcModule());
+ if (options.enableGwtUi()) {
+ install(new UiRpcModule());
+ }
}
install(new GerritRequestModule());
install(new GitOverHttpServlet.Module(options.enableMasterFeatures()));
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 9ef5753..15e21fe 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -20,6 +20,8 @@
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.AgreementInput;
+import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
+import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
import com.google.gerrit.extensions.api.accounts.EmailApi;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
@@ -51,6 +53,7 @@
import com.google.gerrit.server.restapi.account.AddSshKey;
import com.google.gerrit.server.restapi.account.CreateEmail;
import com.google.gerrit.server.restapi.account.DeleteActive;
+import com.google.gerrit.server.restapi.account.DeleteDraftComments;
import com.google.gerrit.server.restapi.account.DeleteEmail;
import com.google.gerrit.server.restapi.account.DeleteExternalIds;
import com.google.gerrit.server.restapi.account.DeleteSshKey;
@@ -125,6 +128,7 @@
private final Index index;
private final GetExternalIds getExternalIds;
private final DeleteExternalIds deleteExternalIds;
+ private final DeleteDraftComments deleteDraftComments;
private final PutStatus putStatus;
private final GetGroups getGroups;
private final EmailApiImpl.Factory emailApi;
@@ -165,6 +169,7 @@
Index index,
GetExternalIds getExternalIds,
DeleteExternalIds deleteExternalIds,
+ DeleteDraftComments deleteDraftComments,
PutStatus putStatus,
GetGroups getGroups,
EmailApiImpl.Factory emailApi,
@@ -204,6 +209,7 @@
this.index = index;
this.getExternalIds = getExternalIds;
this.deleteExternalIds = deleteExternalIds;
+ this.deleteDraftComments = deleteDraftComments;
this.putStatus = putStatus;
this.getGroups = getGroups;
this.emailApi = emailApi;
@@ -561,4 +567,14 @@
throw asRestApiException("Cannot delete external IDs", e);
}
}
+
+ @Override
+ public List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
+ throws RestApiException {
+ try {
+ return deleteDraftComments.apply(account, input);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot delete draft comments", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 0761d2e..4c043b6 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -33,6 +33,7 @@
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
+import com.google.gerrit.extensions.events.ChangeDeletedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
@@ -311,6 +312,7 @@
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), AssigneeChangedListener.class);
DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
+ DynamicSet.setOf(binder(), ChangeDeletedListener.class);
DynamicSet.setOf(binder(), CommentAddedListener.class);
DynamicSet.setOf(binder(), HashtagsEditedListener.class);
DynamicSet.setOf(binder(), ChangeMergedListener.class);
diff --git a/java/com/google/gerrit/server/events/ChangeDeletedEvent.java b/java/com/google/gerrit/server/events/ChangeDeletedEvent.java
new file mode 100644
index 0000000..63142fd
--- /dev/null
+++ b/java/com/google/gerrit/server/events/ChangeDeletedEvent.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.common.base.Supplier;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.data.AccountAttribute;
+
+public class ChangeDeletedEvent extends ChangeEvent {
+ public static final String TYPE = "change-deleted";
+ public Supplier<AccountAttribute> deleter;
+
+ public ChangeDeletedEvent(Change change) {
+ super(TYPE, change);
+ }
+}
diff --git a/java/com/google/gerrit/server/events/EventTypes.java b/java/com/google/gerrit/server/events/EventTypes.java
index cd2b464..5498ec8 100644
--- a/java/com/google/gerrit/server/events/EventTypes.java
+++ b/java/com/google/gerrit/server/events/EventTypes.java
@@ -24,6 +24,7 @@
static {
register(AssigneeChangedEvent.TYPE, AssigneeChangedEvent.class);
register(ChangeAbandonedEvent.TYPE, ChangeAbandonedEvent.class);
+ register(ChangeDeletedEvent.TYPE, ChangeDeletedEvent.class);
register(ChangeMergedEvent.TYPE, ChangeMergedEvent.class);
register(ChangeRestoredEvent.TYPE, ChangeRestoredEvent.class);
register(CommentAddedEvent.TYPE, CommentAddedEvent.class);
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 367a38b..97a3f39 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
+import com.google.gerrit.extensions.events.ChangeDeletedListener;
import com.google.gerrit.extensions.events.ChangeMergedListener;
import com.google.gerrit.extensions.events.ChangeRestoredListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
@@ -75,6 +76,7 @@
public class StreamEventsApiListener
implements AssigneeChangedListener,
ChangeAbandonedListener,
+ ChangeDeletedListener,
ChangeMergedListener,
ChangeRestoredListener,
WorkInProgressStateChangedListener,
@@ -95,6 +97,7 @@
protected void configure() {
DynamicSet.bind(binder(), AssigneeChangedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeAbandonedListener.class).to(StreamEventsApiListener.class);
+ DynamicSet.bind(binder(), ChangeDeletedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeMergedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), ChangeRestoredListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), CommentAddedListener.class).to(StreamEventsApiListener.class);
@@ -522,4 +525,20 @@
logger.atSevere().withCause(e).log("Failed to dispatch event");
}
}
+
+ @Override
+ public void onChangeDeleted(ChangeDeletedListener.Event ev) {
+ try {
+ ChangeNotes notes = getNotes(ev.getChange());
+ Change change = notes.getChange();
+ ChangeDeletedEvent event = new ChangeDeletedEvent(change);
+
+ event.change = changeAttributeSupplier(change, notes);
+ event.deleter = accountAttributeSupplier(ev.getWho());
+
+ dispatcher.get().postEvent(change, event);
+ } catch (OrmException | PermissionBackendException e) {
+ logger.atSevere().withCause(e).log("Failed to dispatch event");
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
new file mode 100644
index 0000000..dd1dc07
--- /dev/null
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.ChangeDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+
+@Singleton
+public class ChangeDeleted {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final DynamicSet<ChangeDeletedListener> listeners;
+ private final EventUtil util;
+
+ @Inject
+ ChangeDeleted(DynamicSet<ChangeDeletedListener> listeners, EventUtil util) {
+ this.listeners = listeners;
+ this.util = util;
+ }
+
+ public void fire(Change change, AccountState deleter, Timestamp when) {
+ if (!listeners.iterator().hasNext()) {
+ return;
+ }
+ try {
+ Event event = new Event(util.changeInfo(change), util.accountInfo(deleter), when);
+ for (ChangeDeletedListener l : listeners) {
+ try {
+ l.onChangeDeleted(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
+ } catch (OrmException e) {
+ logger.atSevere().withCause(e).log("Couldn't fire event");
+ }
+ }
+
+ private static class Event extends AbstractChangeEvent implements ChangeDeletedListener.Event {
+ Event(ChangeInfo change, AccountInfo deleter, Timestamp when) {
+ super(change, deleter, when, NotifyHandling.ALL);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 4a4ea37..46bf45f 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -98,23 +98,6 @@
state = ps;
}
- ProjectControl forUser(CurrentUser who) {
- ProjectControl r =
- new ProjectControl(
- uploadGroups,
- receiveGroups,
- permissionFilter,
- changeControlFactory,
- permissionBackend,
- refFilterFactory,
- identifiedUserFactory,
- who,
- state);
- // Not per-user, and reusing saves lookup time.
- r.allSections = allSections;
- return r;
- }
-
ForProject asForProject() {
return new ForProjectImpl();
}
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 762dfbe..b324e77 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -77,14 +77,6 @@
return projectControl.getUser();
}
- RefControl forUser(CurrentUser who) {
- ProjectControl newCtl = projectControl.forUser(who);
- if (relevant.isUserSpecific()) {
- return newCtl.controlForRef(refName);
- }
- return new RefControl(identifiedUserFactory, newCtl, refName, relevant);
- }
-
/** Is this user a ref owner? */
boolean isOwner() {
if (owner == null) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 1813211..7fb0989 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -1089,14 +1089,15 @@
public boolean isReviewedBy(Account.Id accountId) throws OrmException {
Collection<String> stars = stars(accountId);
- if (stars.contains(
- StarredChangesUtil.REVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
- return true;
- }
+ PatchSet ps = currentPatchSet();
+ if (ps != null) {
+ if (stars.contains(StarredChangesUtil.REVIEWED_LABEL + "/" + ps.getPatchSetId())) {
+ return true;
+ }
- if (stars.contains(
- StarredChangesUtil.UNREVIEWED_LABEL + "/" + currentPatchSet().getPatchSetId())) {
- return false;
+ if (stars.contains(StarredChangesUtil.UNREVIEWED_LABEL + "/" + ps.getPatchSetId())) {
+ return false;
+ }
}
return reviewedBy().contains(accountId);
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
new file mode 100644
index 0000000..26d6cf4
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -0,0 +1,211 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.account;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
+import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.HasDraftByPredicate;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.restapi.change.CommentJson;
+import com.google.gerrit.server.restapi.change.CommentJson.CommentFormatter;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdate.Factory;
+import com.google.gerrit.server.update.BatchUpdateListener;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Singleton
+public class DeleteDraftComments
+ implements RestModifyView<AccountResource, DeleteDraftCommentsInput> {
+
+ private final Provider<ReviewDb> db;
+ private final Provider<CurrentUser> userProvider;
+ private final BatchUpdate.Factory batchUpdateFactory;
+ private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final ChangeData.Factory changeDataFactory;
+ private final ChangeJson.Factory changeJsonFactory;
+ private final Provider<CommentJson> commentJsonProvider;
+ private final CommentsUtil commentsUtil;
+ private final PatchSetUtil psUtil;
+ private final PatchListCache patchListCache;
+
+ @Inject
+ DeleteDraftComments(
+ Provider<ReviewDb> db,
+ Provider<CurrentUser> userProvider,
+ Factory batchUpdateFactory,
+ Provider<ChangeQueryBuilder> queryBuilderProvider,
+ Provider<InternalChangeQuery> queryProvider,
+ ChangeData.Factory changeDataFactory,
+ ChangeJson.Factory changeJsonFactory,
+ Provider<CommentJson> commentJsonProvider,
+ CommentsUtil commentsUtil,
+ PatchSetUtil psUtil,
+ PatchListCache patchListCache) {
+ this.db = db;
+ this.userProvider = userProvider;
+ this.batchUpdateFactory = batchUpdateFactory;
+ this.queryBuilderProvider = queryBuilderProvider;
+ this.queryProvider = queryProvider;
+ this.changeDataFactory = changeDataFactory;
+ this.changeJsonFactory = changeJsonFactory;
+ this.commentJsonProvider = commentJsonProvider;
+ this.commentsUtil = commentsUtil;
+ this.psUtil = psUtil;
+ this.patchListCache = patchListCache;
+ }
+
+ @Override
+ public ImmutableList<DeletedDraftCommentInfo> apply(
+ AccountResource rsrc, DeleteDraftCommentsInput input)
+ throws RestApiException, OrmException, UpdateException {
+ CurrentUser user = userProvider.get();
+ if (!user.isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+ if (!rsrc.getUser().hasSameAccountId(user)) {
+ // Disallow even for admins or users with Modify Account. Drafts are not like preferences or
+ // other account info; there is no way even for admins to read or delete another user's drafts
+ // using the normal draft endpoints under the change resource, so disallow it here as well.
+ // (Admins may still call this endpoint with impersonation, but in that case it would pass the
+ // hasSameAccountId check.)
+ throw new AuthException("Cannot delete drafts of other user");
+ }
+
+ CommentFormatter commentFormatter = commentJsonProvider.get().newCommentFormatter();
+ Account.Id accountId = rsrc.getUser().getAccountId();
+ Timestamp now = TimeUtil.nowTs();
+ Map<Project.NameKey, BatchUpdate> updates = new LinkedHashMap<>();
+ List<Op> ops = new ArrayList<>();
+ for (ChangeData cd :
+ queryProvider
+ .get()
+ // Don't attempt to mutate any changes the user can't currently see.
+ .enforceVisibility(true)
+ .query(predicate(accountId, input))) {
+ BatchUpdate update =
+ updates.computeIfAbsent(
+ cd.project(), p -> batchUpdateFactory.create(db.get(), p, rsrc.getUser(), now));
+ Op op = new Op(commentFormatter, accountId);
+ update.addOp(cd.getId(), op);
+ ops.add(op);
+ }
+
+ // Currently there's no way to let some updates succeed even if others fail. Even if there were,
+ // all updates from this operation only happen in All-Users and thus are fully atomic, so
+ // allowing partial failure would have little value.
+ batchUpdateFactory.execute(updates.values(), BatchUpdateListener.NONE, false);
+
+ return ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList());
+ }
+
+ private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
+ throws BadRequestException {
+ Predicate<ChangeData> hasDraft = new HasDraftByPredicate(accountId);
+ if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) {
+ return hasDraft;
+ }
+ try {
+ return Predicate.and(hasDraft, queryBuilderProvider.get().parse(input.query));
+ } catch (QueryParseException e) {
+ throw new BadRequestException("Invalid query: " + e.getMessage(), e);
+ }
+ }
+
+ private class Op implements BatchUpdateOp {
+ private final CommentFormatter commentFormatter;
+ private final Account.Id accountId;
+ private DeletedDraftCommentInfo result;
+
+ Op(CommentFormatter commentFormatter, Id accountId) {
+ this.commentFormatter = commentFormatter;
+ this.accountId = accountId;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws OrmException, PatchListNotAvailableException, PermissionBackendException {
+ ImmutableList.Builder<CommentInfo> comments = ImmutableList.builder();
+ boolean dirty = false;
+ for (Comment c : commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), accountId)) {
+ dirty = true;
+ PatchSet.Id psId = new PatchSet.Id(ctx.getChange().getId(), c.key.patchSetId);
+ setCommentRevId(
+ c, patchListCache, ctx.getChange(), psUtil.get(ctx.getDb(), ctx.getNotes(), psId));
+ commentsUtil.deleteComments(ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c));
+ comments.add(commentFormatter.format(c));
+ }
+ if (dirty) {
+ result = new DeletedDraftCommentInfo();
+ result.change =
+ changeJsonFactory
+ .create(ListChangesOption.SKIP_MERGEABLE)
+ .format(changeDataFactory.create(ctx.getDb(), ctx.getNotes()));
+ result.deleted = comments.build();
+ }
+ return dirty;
+ }
+
+ @Nullable
+ DeletedDraftCommentInfo getResult() {
+ return result;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index 5c466bf..7889f6e 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -26,8 +26,8 @@
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
@@ -172,9 +172,9 @@
}
@Override
- public Response<String> apply(Capability resource) throws ResourceNotFoundException {
+ public BinaryResult apply(Capability resource) throws ResourceNotFoundException {
permissionBackend.checkUsesDefaultCapabilities();
- return Response.ok("ok");
+ return BinaryResult.create("ok\n");
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index be0924a..9b012f7 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -107,6 +107,8 @@
get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class);
+ post(ACCOUNT_KIND, "drafts:delete").to(DeleteDraftComments.class);
+
// The gpgkeys REST endpoints are bound via GpgApiModule.
factory(AccountsUpdate.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CommentJson.java b/java/com/google/gerrit/server/restapi/change/CommentJson.java
index a562592..7112bbf 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentJson.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentJson.java
@@ -41,7 +41,7 @@
import java.util.Map;
import java.util.TreeMap;
-class CommentJson {
+public class CommentJson {
private final AccountLoader.Factory accountLoaderFactory;
@@ -161,7 +161,7 @@
}
}
- class CommentFormatter extends BaseCommentFormatter<Comment, CommentInfo> {
+ public class CommentFormatter extends BaseCommentFormatter<Comment, CommentInfo> {
@Override
protected CommentInfo toInfo(Comment c, AccountLoader loader) {
CommentInfo ci = new CommentInfo();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
index 9658fb4..d41e504 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
@@ -26,6 +26,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.gerrit.server.extensions.events.ChangeDeleted;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.BatchUpdateReviewDb;
@@ -45,6 +46,7 @@
private final PatchSetUtil psUtil;
private final StarredChangesUtil starredChangesUtil;
private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
+ private final ChangeDeleted changeDeleted;
private Change.Id id;
@@ -52,10 +54,12 @@
DeleteChangeOp(
PatchSetUtil psUtil,
StarredChangesUtil starredChangesUtil,
- DynamicItem<AccountPatchReviewStore> accountPatchReviewStore) {
+ DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
+ ChangeDeleted changeDeleted) {
this.psUtil = psUtil;
this.starredChangesUtil = starredChangesUtil;
this.accountPatchReviewStore = accountPatchReviewStore;
+ this.changeDeleted = changeDeleted;
}
@Override
@@ -75,6 +79,7 @@
deleteChangeElementsFromDb(ctx, id);
ctx.deleteChange();
+ changeDeleted.fire(ctx.getChange(), ctx.getAccount(), ctx.getWhen());
return true;
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index de66b87..1eaadca 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -15,8 +15,10 @@
package com.google.gerrit.acceptance.api.accounts;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
import static com.google.gerrit.acceptance.GitUtil.fetch;
@@ -62,8 +64,11 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
+import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
@@ -73,6 +78,7 @@
import com.google.gerrit.extensions.common.AccountDetailInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.EmailInfo;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.GroupInfo;
@@ -92,6 +98,7 @@
import com.google.gerrit.gpg.testing.TestKey;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -2583,6 +2590,160 @@
assertThat(stalenessChecker.isStale(accountId)).isFalse();
}
+ @Test
+ public void deleteAllDraftComments() throws Exception {
+ try {
+ TestTimeUtil.resetWithClockStep(1, SECONDS);
+ Project.NameKey project2 = createProject("project2");
+ PushOneCommit.Result r1 = createChange();
+
+ TestRepository<?> tr2 = cloneProject(project2);
+ PushOneCommit.Result r2 =
+ createChange(
+ tr2,
+ "refs/heads/master",
+ "Change in project2",
+ PushOneCommit.FILE_NAME,
+ "content2",
+ null);
+
+ // Create 2 drafts each on both changes for user.
+ setApiUser(user);
+ createDraft(r1, PushOneCommit.FILE_NAME, "draft 1a");
+ createDraft(r1, PushOneCommit.FILE_NAME, "draft 1b");
+ createDraft(r2, PushOneCommit.FILE_NAME, "draft 2a");
+ createDraft(r2, PushOneCommit.FILE_NAME, "draft 2b");
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(2);
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(2);
+
+ // Create 1 draft on first change for admin.
+ setApiUser(admin);
+ createDraft(r1, PushOneCommit.FILE_NAME, "admin draft");
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
+
+ // Delete user's draft comments; leave admin's alone.
+ setApiUser(user);
+ List<DeletedDraftCommentInfo> result =
+ gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
+
+ // Results are ordered according to the change search, most recently updated first.
+ assertThat(result).hasSize(2);
+ DeletedDraftCommentInfo del2 = result.get(0);
+ assertThat(del2.change.changeId).isEqualTo(r2.getChangeId());
+ assertThat(del2.deleted.stream().map(c -> c.message)).containsExactly("draft 2a", "draft 2b");
+ DeletedDraftCommentInfo del1 = result.get(1);
+ assertThat(del1.change.changeId).isEqualTo(r1.getChangeId());
+ assertThat(del1.deleted.stream().map(c -> c.message)).containsExactly("draft 1a", "draft 1b");
+
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).isEmpty();
+
+ setApiUser(admin);
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
+ } finally {
+ cleanUpDrafts();
+ }
+ }
+
+ @Test
+ public void deleteDraftCommentsByQuery() throws Exception {
+ try {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+
+ createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
+ createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
+
+ List<DeletedDraftCommentInfo> result =
+ gApi.accounts()
+ .self()
+ .deleteDraftComments(new DeleteDraftCommentsInput("change:" + r1.getChangeId()));
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
+ assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
+
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
+ } finally {
+ cleanUpDrafts();
+ }
+ }
+
+ @Test
+ public void deleteOtherUsersDraftCommentsDisallowed() throws Exception {
+ try {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ createDraft(r, PushOneCommit.FILE_NAME, "draft");
+ setApiUser(admin);
+ try {
+ gApi.accounts().id(user.id.get()).deleteDraftComments(new DeleteDraftCommentsInput());
+ assert_().fail("expected AuthException");
+ } catch (AuthException e) {
+ assertThat(e).hasMessageThat().isEqualTo("Cannot delete drafts of other user");
+ }
+ } finally {
+ cleanUpDrafts();
+ }
+ }
+
+ @Test
+ public void deleteDraftCommentsSkipsInvisibleChanges() throws Exception {
+ try {
+ createBranch(new Branch.NameKey(project, "secret"));
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange("refs/for/secret");
+
+ setApiUser(user);
+ createDraft(r1, PushOneCommit.FILE_NAME, "draft a");
+ createDraft(r2, PushOneCommit.FILE_NAME, "draft b");
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).hasSize(1);
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
+
+ block(project, "refs/heads/secret", Permission.READ, REGISTERED_USERS);
+ List<DeletedDraftCommentInfo> result =
+ gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).change.changeId).isEqualTo(r1.getChangeId());
+ assertThat(result.get(0).deleted.stream().map(c -> c.message)).containsExactly("draft a");
+
+ removePermission(project, "refs/heads/secret", Permission.READ);
+ assertThat(gApi.changes().id(r1.getChangeId()).current().draftsAsList()).isEmpty();
+ // Draft still exists since change wasn't visible when drafts where deleted.
+ assertThat(gApi.changes().id(r2.getChangeId()).current().draftsAsList()).hasSize(1);
+ } finally {
+ cleanUpDrafts();
+ }
+ }
+
+ private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
+ DraftInput in = new DraftInput();
+ in.path = path;
+ in.line = 1;
+ in.message = message;
+ gApi.changes().id(r.getChangeId()).current().createDraft(in);
+ }
+
+ private void cleanUpDrafts() throws Exception {
+ for (TestAccount testAccount : accountCreator.getAll()) {
+ setApiUser(testAccount);
+ for (ChangeInfo changeInfo : gApi.changes().query("has:draft").get()) {
+ for (CommentInfo c :
+ gApi.changes()
+ .id(changeInfo.id)
+ .drafts()
+ .values()
+ .stream()
+ .flatMap(List::stream)
+ .collect(toImmutableList())) {
+ gApi.changes().id(changeInfo.id).revision(c.patchSet).draft(c.id).delete();
+ }
+ }
+ }
+ }
+
private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
return new Correspondence<GroupInfo, String>() {
@Override
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index b3a5e2d..88803ec 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1089,6 +1089,7 @@
String ref = new Change.Id(id).toRefPrefix() + "1";
eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
+ eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email);
} finally {
removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES);
removePermission(project, "refs/*", Permission.DELETE_CHANGES);
diff --git a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
index 1a401b0..b0adba7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
@@ -88,6 +88,7 @@
RestCall.put("/accounts/%s/agreements"),
RestCall.get("/accounts/%s/external.ids"),
RestCall.post("/accounts/%s/external.ids:delete"),
+ RestCall.post("/accounts/%s/drafts:delete"),
RestCall.get("/accounts/%s/oauthtoken"),
RestCall.get("/accounts/%s/capabilities"),
RestCall.get("/accounts/%s/capabilities/viewPlugins"),
diff --git a/plugins/hooks b/plugins/hooks
index cc74144..7185e5c 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit cc74144db755a18c5a63764a336b93ab3d1be1fe
+Subproject commit 7185e5ce46646e952071befd9cf8f4267560b51d
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 76d7f1e..461bfc4 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
@@ -606,6 +606,7 @@
</gr-overlay>
<gr-overlay id="uploadHelpOverlay" with-backdrop>
<gr-upload-help-dialog
+ target-branch="[[_change.branch]]"
on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog>
</gr-overlay>
<gr-overlay id="includedInOverlay" with-backdrop>
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 a710b0b..fa984c9 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
@@ -287,6 +287,7 @@
data-path$="[[file.__path]]" tabindex="-1">
<div class$="[[_computeClass('status', file.__path)]]"
tabindex="0"
+ title$="[[_computeFileStatusLabel(file.status)]]"
aria-label$="[[_computeFileStatusLabel(file.status)]]">
[[_computeFileStatus(file.status)]]
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 95615ea..f54e058 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
@@ -35,6 +35,7 @@
A: 'Added',
C: 'Copied',
D: 'Deleted',
+ M: 'Modified',
R: 'Renamed',
W: 'Rewritten',
U: 'Unchanged',
@@ -894,6 +895,12 @@
rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
},
+ /**
+ * Get a descriptive label for use in the status indicator's tooltip and
+ * ARIA label.
+ * @param {string} status
+ * @return {string}
+ */
_computeFileStatusLabel(status) {
const statusCode = this._computeFileStatus(status);
return FileStatus.hasOwnProperty(statusCode) ?
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 8e32ea3..88b5f66 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
@@ -797,6 +797,11 @@
assert.isTrue(tapSpy.lastCall.args[0].defaultPrevented);
});
+ test('_computeFileStatusLabel', () => {
+ assert.equal(element._computeFileStatusLabel('A'), 'Added');
+ assert.equal(element._computeFileStatusLabel('M'), 'Modified');
+ });
+
test('_handleFileListTap', () => {
element._filesByPath = {
'/COMMIT_MSG': {},
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index d796999..548116c 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -18,7 +18,7 @@
'use strict';
const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit';
- const PUSH_COMMAND = 'git push origin HEAD:refs/for/master';
+ const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/';
Polymer({
is: 'gr-upload-help-dialog',
@@ -30,6 +30,7 @@
*/
properties: {
+ targetBranch: String,
_commitCommand: {
type: String,
value: COMMIT_COMMAND,
@@ -37,8 +38,7 @@
},
_pushCommand: {
type: String,
- value: PUSH_COMMAND,
- readOnly: true,
+ computed: '_computePushCommand(targetBranch)',
},
},
@@ -46,5 +46,9 @@
e.preventDefault();
this.fire('close', null, {bubbles: false});
},
+
+ _computePushCommand(targetBranch) {
+ return PUSH_COMMAND_PREFIX + targetBranch;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
new file mode 100644
index 0000000..60fe3e6
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-upload-help-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-upload-help-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-upload-help-dialog></gr-upload-help-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-upload-help-dialog tests', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('constructs push command from branch', () => {
+ element.targetBranch = 'foo';
+ assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo');
+
+ element.targetBranch = 'master';
+ assert.equal(element._pushCommand,
+ 'git push origin HEAD:refs/for/master');
+ });
+ });
+</script>
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 96a160e..60bf5ca 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
@@ -353,7 +353,7 @@
};
/**
- * @param {number} line
+ * @param {GrDiffLine} line
* @param {string=} opt_side
* @return {!Object}
*/
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 3e9e796..6f61fb9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -28,6 +28,8 @@
UNIFIED: 'UNIFIED_DIFF',
};
+ const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
+
/**
* @param {Object} diff
* @return {boolean}
@@ -105,7 +107,10 @@
type: Boolean,
reflectToAttribute: true,
},
- noRenderOnPrefsChange: Boolean,
+ noRenderOnPrefsChange: {
+ type: Boolean,
+ value: false,
+ },
comments: Object,
lineWrapping: {
type: Boolean,
@@ -167,12 +172,19 @@
type: Object,
value: null,
},
+
+ _loadedWhitespaceLevel: String,
},
listeners: {
'draft-interaction': '_handleDraftInteraction',
},
+ observers: [
+ '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
+ ' noRenderOnPrefsChange)',
+ ],
+
ready() {
if (this._canReload()) {
this.reload();
@@ -189,10 +201,15 @@
reload() {
this._loading = true;
this._errorMessage = null;
+ const whitespaceLevel = this._getIgnoreWhitespace();
const diffRequest = this._getDiff()
.then(diff => {
+ this._loadedWhitespaceLevel = whitespaceLevel;
this._reportDiff(diff);
+ if (this._getIgnoreWhitespace() !== WHITESPACE_IGNORE_NONE) {
+ return this._translateChunksToIgnore(diff);
+ }
return diff;
})
.catch(e => {
@@ -321,6 +338,7 @@
this.patchRange.basePatchNum,
this.patchRange.patchNum,
this.path,
+ this._getIgnoreWhitespace(),
reject)
.then(resolve);
});
@@ -430,5 +448,63 @@
_handleDraftInteraction() {
this.$.reporting.recordDraftInteraction();
},
+
+ /**
+ * Take a diff that was loaded with a ignore-whitespace other than
+ * IGNORE_NONE, and convert delta chunks labeled as common into shared
+ * chunks.
+ * @param {!Object} diff
+ * @returns {!Object}
+ */
+ _translateChunksToIgnore(diff) {
+ const newDiff = Object.assign({}, diff);
+ const mergedContent = [];
+
+ // Was the last chunk visited a shared chunk?
+ let lastWasShared = false;
+
+ for (const chunk of diff.content) {
+ if (lastWasShared && chunk.common && chunk.b) {
+ // The last chunk was shared and this chunk should be ignored, so
+ // add its revision content to the previous chunk.
+ mergedContent[mergedContent.length - 1].ab.push(...chunk.b);
+ } else if (chunk.common && !chunk.b) {
+ // If the chunk should be ignored, but it doesn't have revision
+ // content, then drop it and continue without updating lastWasShared.
+ continue;
+ } else if (lastWasShared && chunk.ab) {
+ // Both the last chunk and the current chunk are shared. Merge this
+ // chunk's shared content into the previous shared content.
+ mergedContent[mergedContent.length - 1].ab.push(...chunk.ab);
+ } else if (!lastWasShared && chunk.common && chunk.b) {
+ // If the previous chunk was not shared, but this one should be
+ // ignored, then add it as a shared chunk.
+ mergedContent.push({ab: chunk.b});
+ } else {
+ // Otherwise add the chunk as is.
+ mergedContent.push(chunk);
+ }
+
+ lastWasShared = !!mergedContent[mergedContent.length - 1].ab;
+ }
+
+ newDiff.content = mergedContent;
+ return newDiff;
+ },
+
+ _getIgnoreWhitespace() {
+ if (!this.prefs || !this.prefs.ignore_whitespace) {
+ return WHITESPACE_IGNORE_NONE;
+ }
+ return this.prefs.ignore_whitespace;
+ },
+
+ _whitespaceChanged(preferredWhitespaceLevel, loadedWhitespaceLevel,
+ noRenderOnPrefsChange) {
+ if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
+ !noRenderOnPrefsChange) {
+ this.reload();
+ }
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index a05d44f..f83253e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -775,5 +775,89 @@
assert.isUndefined(reportStub.lastCall.args[1]);
});
});
+
+ suite('_translateChunksToIgnore', () => {
+ let content;
+
+ setup(() => {
+ content = [
+ {ab: ['one', 'two']},
+ {a: ['three'], b: ['different three']},
+ {b: ['four']},
+ {ab: ['five', 'six']},
+ {a: ['seven']},
+ {ab: ['eight', 'nine']},
+ ];
+ });
+
+ test('does nothing to unmarked diff', () => {
+ assert.deepEqual(element._translateChunksToIgnore({content}),
+ {content});
+ });
+
+ test('merges marked delta chunk', () => {
+ content[1].common = true;
+ assert.deepEqual(element._translateChunksToIgnore({content}), {
+ content: [
+ {ab: ['one', 'two', 'different three']},
+ {b: ['four']},
+ {ab: ['five', 'six']},
+ {a: ['seven']},
+ {ab: ['eight', 'nine']},
+ ],
+ });
+ });
+
+ test('merges marked addition chunk', () => {
+ content[2].common = true;
+ assert.deepEqual(element._translateChunksToIgnore({content}), {
+ content: [
+ {ab: ['one', 'two']},
+ {a: ['three'], b: ['different three']},
+ {ab: ['four', 'five', 'six']},
+ {a: ['seven']},
+ {ab: ['eight', 'nine']},
+ ],
+ });
+ });
+
+ test('merges multiple marked delta', () => {
+ content[1].common = true;
+ content[2].common = true;
+ assert.deepEqual(element._translateChunksToIgnore({content}), {
+ content: [
+ {ab: ['one', 'two', 'different three', 'four', 'five', 'six']},
+ {a: ['seven']},
+ {ab: ['eight', 'nine']},
+ ],
+ });
+ });
+
+ test('marked deletion chunks are omitted', () => {
+ content[4].common = true;
+ assert.deepEqual(element._translateChunksToIgnore({content}), {
+ content: [
+ {ab: ['one', 'two']},
+ {a: ['three'], b: ['different three']},
+ {b: ['four']},
+ {ab: ['five', 'six', 'eight', 'nine']},
+ ],
+ });
+ });
+
+ test('marked deltas can start shared chunks', () => {
+ content[0] = {a: ['one'], b: ['two'], common: true};
+ assert.deepEqual(element._translateChunksToIgnore({content}), {
+ content: [
+ {ab: ['two']},
+ {a: ['three'], b: ['different three']},
+ {b: ['four']},
+ {ab: ['five', 'six']},
+ {a: ['seven']},
+ {ab: ['eight', 'nine']},
+ ],
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index 375e598..78814d4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -60,7 +60,7 @@
align-items: center;
display: flex;
padding: .35em 1.5em;
- width: 20em;
+ width: 25em;
}
.pref:hover {
background-color: var(--hover-background-color);
@@ -149,6 +149,15 @@
type="checkbox"
on-tap="_handleAutomaticReviewTap">
</div>
+ <div class="pref">
+ <label for="ignoreWhitespace">Ignore Whitespace</label>
+ <select id="ignoreWhitespace" on-change="_handleIgnoreWhitespaceChange">
+ <option value="IGNORE_NONE">None</option>
+ <option value="IGNORE_TRAILING">Trailing</option>
+ <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+ <option value="IGNORE_ALL">All</option>
+ </select>
+ </div>
</div>
<div class="actions">
<gr-button id="cancelButton" link on-tap="_handleCancel">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 47a3c2d..8fc90b9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -67,6 +67,7 @@
this.$.lineWrappingInput.checked = prefs.line_wrapping;
this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting;
this.$.automaticReviewInput.checked = !prefs.manual_review;
+ this.$.ignoreWhitespace.value = prefs.ignore_whitespace;
},
_localPrefsChanged(changeRecord) {
@@ -79,6 +80,11 @@
this.set('_newPrefs.context', parseInt(selectEl.value, 10));
},
+ _handleIgnoreWhitespaceChange(e) {
+ const selectEl = Polymer.dom(e).rootTarget;
+ this.set('_newPrefs.ignore_whitespace', selectEl.value);
+ },
+
_handleShowTabsTap(e) {
this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked);
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index 978058f..44bb52a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -25,8 +25,10 @@
this.highlights = [];
}
+ /** @type {number|string} */
GrDiffLine.prototype.afterNumber = 0;
+ /** @type {number|string} */
GrDiffLine.prototype.beforeNumber = 0;
GrDiffLine.prototype.contextGroup = null;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index fa188d7..4f69513 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -37,6 +37,9 @@
cursor: pointer;
text-align: center;
}
+ .checkboxContainer input {
+ cursor: pointer;
+ }
.checkboxContainer:hover {
outline: 1px solid var(--border-color);
}
@@ -52,12 +55,12 @@
<tbody>
<tr>
<td>Number</td>
- <td
- class="checkboxContainer"
- on-tap="_handleTargetTap">
+ <td class="checkboxContainer"
+ on-tap="_handleCheckboxContainerTap">
<input
type="checkbox"
name="number"
+ on-tap="_handleNumberCheckboxTap"
checked$="[[showNumber]]">
</td>
</tr>
@@ -65,10 +68,11 @@
<tr>
<td>[[item]]</td>
<td class="checkboxContainer"
- on-tap="_handleTargetTap">
+ on-tap="_handleCheckboxContainerTap">
<input
type="checkbox"
name="[[item]]"
+ on-tap="_handleTargetTap"
checked$="[[!isColumnHidden(item, displayedColumns)]]">
</td>
</tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 7b74096..7d109633 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -35,40 +35,42 @@
Gerrit.ChangeTableBehavior,
],
- _getButtonText(isShown) {
- return isShown ? 'Hide' : 'Show';
- },
-
- _updateDisplayedColumns(displayedColumns, name, checked) {
- if (!checked) {
- return displayedColumns.filter(column => {
- return name.toLowerCase() !== column.toLowerCase();
- });
- } else {
- return displayedColumns.concat([name]);
- }
+ /**
+ * Get the list of enabled column names from whichever checkboxes are
+ * checked (excluding the number checkbox).
+ * @return {!Array<string>}
+ */
+ _getDisplayedColumns() {
+ return Polymer.dom(this.root)
+ .querySelectorAll('.checkboxContainer input:not([name=number])')
+ .filter(checkbox => checkbox.checked)
+ .map(checkbox => checkbox.name);
},
/**
- * Handles tap on either the checkbox itself or the surrounding table cell.
+ * Handle a tap on a checkbox container and relay the tap to the checkbox it
+ * contains.
+ */
+ _handleCheckboxContainerTap(e) {
+ const checkbox = Polymer.dom(e.target).querySelector('input');
+ if (!checkbox) { return; }
+ checkbox.click();
+ },
+
+ /**
+ * Handle a tap on the number checkbox and update the showNumber property
+ * accordingly.
+ */
+ _handleNumberCheckboxTap(e) {
+ this.showNumber = Polymer.dom(e).rootTarget.checked;
+ },
+
+ /**
+ * Handle a tap on a displayed column checkboxes (excluding number) and
+ * update the displayedColumns property accordingly.
*/
_handleTargetTap(e) {
- let checkbox = Polymer.dom(e.target).querySelector('input');
- if (checkbox) {
- checkbox.click();
- } else {
- // The target is the checkbox itself.
- checkbox = Polymer.dom(e).rootTarget;
- }
-
- if (checkbox.name === 'number') {
- this.showNumber = checkbox.checked;
- return;
- }
-
- this.set('displayedColumns',
- this._updateDisplayedColumns(
- this.displayedColumns, checkbox.name, checkbox.checked));
+ this.set('displayedColumns', this._getDisplayedColumns());
},
});
})();
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 587cc3b..32fab9d 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -53,6 +53,7 @@
];
element.set('displayedColumns', columns);
+ element.showNumber = false;
flushAsynchronousOperations();
});
@@ -108,66 +109,50 @@
displayedLength + 1);
});
- test('_handleTargetTap', () => {
- const checkbox = element.$$('table tr:nth-child(2) input');
- let originalDisplayedColumns = element.displayedColumns;
- const td = element.$$('table tr:nth-child(2) .checkboxContainer');
- const displayedColumnStub =
- sandbox.stub(element, '_updateDisplayedColumns');
-
- MockInteractions.tap(checkbox);
- assert.isTrue(displayedColumnStub.lastCall.calledWithExactly(
- originalDisplayedColumns,
- checkbox.name,
- checkbox.checked));
-
- originalDisplayedColumns = element.displayedColumns;
- MockInteractions.tap(td);
- assert.isTrue(displayedColumnStub.lastCall.calledWithExactly(
- originalDisplayedColumns,
- checkbox.name,
- checkbox.checked));
+ test('_getDisplayedColumns', () => {
+ assert.deepEqual(element._getDisplayedColumns(), columns);
+ MockInteractions.tap(
+ element.$$('.checkboxContainer input[name=Assignee]'));
+ assert.deepEqual(element._getDisplayedColumns(),
+ columns.filter(c => c !== 'Assignee'));
});
- test('_handleTargetTap on number', () => {
- element.showNumber = false;
- const checkbox = element.$$('table tr:nth-child(1) input');
- const displayedColumnStub =
- sandbox.stub(element, '_updateDisplayedColumns');
+ test('_handleCheckboxContainerTap relayes taps to checkboxes', () => {
+ sandbox.stub(element, '_handleNumberCheckboxTap');
+ sandbox.stub(element, '_handleTargetTap');
- MockInteractions.tap(checkbox);
- assert.isFalse(displayedColumnStub.called);
+ MockInteractions.tap(
+ element.$$('table tr:first-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
+ assert.isFalse(element._handleTargetTap.called);
+
+ MockInteractions.tap(
+ element.$$('table tr:last-of-type .checkboxContainer'));
+ assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
+ assert.isTrue(element._handleTargetTap.calledOnce);
+ });
+
+ test('_handleNumberCheckboxTap', () => {
+ sandbox.spy(element, '_handleNumberCheckboxTap');
+
+ MockInteractions
+ .tap(element.$$('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxTap.calledOnce);
assert.isTrue(element.showNumber);
- MockInteractions.tap(checkbox);
+ MockInteractions
+ .tap(element.$$('.checkboxContainer input[name=number]'));
+ assert.isTrue(element._handleNumberCheckboxTap.calledTwice);
assert.isFalse(element.showNumber);
});
- test('_updateDisplayedColumns', () => {
- let name = 'Subject';
- let checked = false;
- assert.deepEqual(element._updateDisplayedColumns(columns, name, checked),
- [
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- ]);
- name = 'Size';
- checked = true;
- assert.deepEqual(element._updateDisplayedColumns(columns, name, checked),
- [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ]);
+ test('_handleTargetTap', () => {
+ sandbox.spy(element, '_handleTargetTap');
+ assert.include(element.displayedColumns, 'Assignee');
+ MockInteractions
+ .tap(element.$$('.checkboxContainer input[name=Assignee]'));
+ assert.isTrue(element._handleTargetTap.calledOnce);
+ assert.notInclude(element.displayedColumns, 'Assignee');
});
});
</script>
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 18c1734..029ce88 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
@@ -348,6 +348,20 @@
on-change="_handleDiffSyntaxHighlightingChanged">
</span>
</section>
+ <section>
+ <div class="pref">
+ <span class="title">Ignore Whitespace</span>
+ <span class="value">
+ <gr-select bind-value="{{_diffPrefs.ignore_whitespace}}">
+ <select>
+ <option value="IGNORE_NONE">None</option>
+ <option value="IGNORE_TRAILING">Trailing</option>
+ <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option>
+ <option value="IGNORE_ALL">All</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
<gr-button
id="saveDiffPrefs"
on-tap="_handleSaveDiffPreferences"
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 af10b8371..c97c4c7 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
@@ -2134,13 +2134,16 @@
* index.
* @param {number|string} patchNum
* @param {string} path
+ * @param {string=} opt_whitespace the ignore-whitespace level for the diff
+ * algorithm.
* @param {function(?Response, string=)=} opt_errFn
*/
- getDiff(changeNum, basePatchNum, patchNum, path, opt_errFn) {
+ getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace,
+ opt_errFn) {
const params = {
context: 'ALL',
intraline: null,
- whitespace: 'IGNORE_NONE',
+ whitespace: opt_whitespace || 'IGNORE_NONE',
};
if (this.isMergeParent(basePatchNum)) {
params.parent = this.getParentIndex(basePatchNum);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index a489627..816700b 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -86,6 +86,7 @@
'change/gr-reply-dialog/gr-reply-dialog_test.html',
'change/gr-reviewer-list/gr-reviewer-list_test.html',
'change/gr-thread-list/gr-thread-list_test.html',
+ 'change/gr-upload-help-dialog/gr-upload-help-dialog_test.html',
'core/gr-account-dropdown/gr-account-dropdown_test.html',
'core/gr-error-dialog/gr-error-dialog_test.html',
'core/gr-error-manager/gr-error-manager_test.html',