Merge changes Iab3de63e,I83085033 * changes: Add REST API endpoints to mark a change as muted/unmuted New mute-label allows for temporarily unhighlighting changes in dashboard
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt index 553ac5b..1fb871a 100644 --- a/Documentation/dev-stars.txt +++ b/Documentation/dev-stars.txt
@@ -61,6 +61,19 @@ The ignore star is represented by the special star label 'ignore'. +[[mute-star]] +== Mute Star + +If the "mute/<patchset_id>"-star is set by a user, and <patchset_id> +matches the current patch set, the change is always reported as "reviewed" +in the ChangeInfo. + +This allows users to "de-highlight" changes in a dashboard until a new +patchset has been uploaded. + +The ChangeInfo muted-field will show if the change is currently in a +mute state. + [[query-stars]] == Query Stars
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 5cddf9c..3c3699c 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -2175,6 +2175,40 @@ PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0 ---- +[[mute]] +=== Mute +-- +'PUT /changes/link:#change-id[\{change-id\}]/mute' +-- + +Marks a change as muted. + +This allows users to "de-highlight" changes in their dashboard until a new +patch set is uploaded. + +This differs from the link:#ignore[ignore] endpoint, which will mute +emails and hide the change from dashboard completely until it is +link:#unignore[unignored] again. + + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0 +---- + +[[unmute]] +=== Unmute +-- +'PUT /changes/link:#change-id[\{change-id\}]/unmute' +-- + +Unmutes a change. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0 +---- + [[edit-endpoints]] == Change Edit Endpoints
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index 2d086d3..83d36b1 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -95,6 +95,13 @@ void ignore(boolean ignore) throws RestApiException; /** + * Mute or un-mute this change. + * + * @param mute mute the change if true + */ + void mute(boolean mute) throws RestApiException; + + /** * Create a new change that reverts this change. * * @see Changes#id(int) @@ -495,5 +502,10 @@ public void ignore(boolean ignore) { throw new NotImplementedException(); } + + @Override + public void mute(boolean mute) { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java index 2cb8384..49e4a05 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -36,6 +36,7 @@ public Timestamp updated; public Timestamp submitted; public Boolean starred; + public Boolean muted; public Collection<String> stars; public Boolean reviewed; public SubmitType submitType;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java index 3cac62c..abcfe31 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -136,6 +136,8 @@ public final native boolean starred() /*-{ return this.starred ? true : false; }-*/; + public final native boolean muted() /*-{ return this.muted ? true : false; }-*/; + public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/; public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java index 80f84bd..cbaae1e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -147,6 +147,7 @@ public static final String DEFAULT_LABEL = "star"; public static final String IGNORE_LABEL = "ignore"; + public static final String MUTE_LABEL = "mute"; public static final ImmutableSortedSet<String> DEFAULT_LABELS = ImmutableSortedSet.of(DEFAULT_LABEL); @@ -355,6 +356,34 @@ return byChange(changeId, IGNORE_LABEL).contains(accountId); } + private static String getMuteLabel(Change change) { + return MUTE_LABEL + "/" + change.currentPatchSetId().get(); + } + + public void mute(Account.Id accountId, Project.NameKey project, Change change) + throws OrmException { + star( + accountId, + project, + change.getId(), + ImmutableSet.of(getMuteLabel(change)), + ImmutableSet.of()); + } + + public void unmute(Account.Id accountId, Project.NameKey project, Change change) + throws OrmException { + star( + accountId, + project, + change.getId(), + ImmutableSet.of(), + ImmutableSet.of(getMuteLabel(change))); + } + + public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException { + return byChange(change.getId(), getMuteLabel(change)).contains(accountId); + } + private static StarRef readLabels(Repository repo, String refName) throws IOException { Ref ref = repo.exactRef(refName); if (ref == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index edca48f..c6fc67e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -63,6 +63,7 @@ import com.google.gerrit.server.change.ListChangeDrafts; import com.google.gerrit.server.change.ListChangeRobotComments; import com.google.gerrit.server.change.Move; +import com.google.gerrit.server.change.Mute; import com.google.gerrit.server.change.PostHashtags; import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.change.PublishDraftPatchSet; @@ -77,6 +78,7 @@ import com.google.gerrit.server.change.SubmittedTogether; import com.google.gerrit.server.change.SuggestChangeReviewers; import com.google.gerrit.server.change.Unignore; +import com.google.gerrit.server.change.Unmute; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.update.UpdateException; @@ -132,6 +134,8 @@ private final DeletePrivate deletePrivate; private final Ignore ignore; private final Unignore unignore; + private final Mute mute; + private final Unmute unmute; @Inject ChangeApiImpl( @@ -171,6 +175,8 @@ DeletePrivate deletePrivate, Ignore ignore, Unignore unignore, + Mute mute, + Unmute unmute, @Assisted ChangeResource change) { this.changeApi = changeApi; this.revert = revert; @@ -208,6 +214,8 @@ this.deletePrivate = deletePrivate; this.ignore = ignore; this.unignore = unignore; + this.mute = mute; + this.unmute = unmute; this.change = change; } @@ -603,4 +611,13 @@ unignore.apply(change, new Unignore.Input()); } } + + @Override + public void mute(boolean mute) throws RestApiException { + if (mute) { + this.mute.apply(change, new Mute.Input()); + } else { + unmute.apply(change, new Unmute.Input()); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java index 4724ea1..4dc7d36 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -514,6 +514,11 @@ if (user.isIdentifiedUser()) { Collection<String> stars = cd.stars(user.getAccountId()); out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; + out.muted = + stars.contains( + StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId()) + ? true + : null; if (!stars.isEmpty()) { out.stars = stars; } @@ -521,7 +526,11 @@ if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { Account.Id accountId = user.getAccountId(); - out.reviewed = cd.reviewedBy().contains(accountId) ? true : null; + if (out.muted != null) { + out.reviewed = true; + } else { + out.reviewed = cd.reviewedBy().contains(accountId) ? true : null; + } } out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index f78f8a8b..5aee90c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -89,6 +89,8 @@ delete(CHANGE_KIND, "private").to(DeletePrivate.class); put(CHANGE_KIND, "ignore").to(Ignore.class); put(CHANGE_KIND, "unignore").to(Unignore.class); + put(CHANGE_KIND, "mute").to(Mute.class); + put(CHANGE_KIND, "unmute").to(Unmute.class); post(CHANGE_KIND, "reviewers").to(PostReviewers.class); get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java new file mode 100644 index 0000000..d14fec8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Mute.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Mute(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Mute") + .setTitle("Mute the change to unhighlight it in the dashboard") + .setVisible(!rsrc.isUserOwner() && isMuteable(rsrc.getChange())); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || isMuted(rsrc.getChange())) { + // early exit for own changes and already muted changes + return Response.ok(""); + } + stars.mute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange()); + } catch (OrmException e) { + throw new RestApiException("failed to mute change", e); + } + return Response.ok(""); + } + + private boolean isMuted(Change change) { + try { + return stars.isMutedBy(change, self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check muted star", e); + } + return false; + } + + private boolean isMuteable(Change change) { + try { + return !isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java new file mode 100644 index 0000000..49b41cb --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
@@ -0,0 +1,86 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.change; + +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Unmute + implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Unmute.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Unmute(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Unmute") + .setTitle("Unmute the change") + .setVisible(!rsrc.isUserOwner() && isUnMuteable(rsrc.getChange())); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || !isMuted(rsrc.getChange())) { + // early exit for own changes and not muted changes + return Response.ok(""); + } + stars.unmute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange()); + } catch (OrmException e) { + throw new RestApiException("failed to unmute change", e); + } + return Response.ok(""); + } + + private boolean isMuted(Change change) { + try { + return stars.isMutedBy(change, self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check muted star", e); + } + return false; + } + + private boolean isUnMuteable(Change change) { + try { + return isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}