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
This commit is contained in:
David Pursehouse
2017-04-28 12:22:36 +00:00
committed by Gerrit Code Review
11 changed files with 291 additions and 1 deletions

View File

@@ -61,6 +61,19 @@ request. They can then decide to remove the ignore star.
The ignore star is represented by the special star label 'ignore'. 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]]
== Query Stars == Query Stars

View File

@@ -2175,6 +2175,40 @@ Un-marks a change as ignored.
PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0 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]] [[edit-endpoints]]
== Change Edit Endpoints == Change Edit Endpoints

View File

@@ -94,6 +94,13 @@ public interface ChangeApi {
*/ */
void ignore(boolean ignore) throws RestApiException; 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. * Create a new change that reverts this change.
* *
@@ -495,5 +502,10 @@ public interface ChangeApi {
public void ignore(boolean ignore) { public void ignore(boolean ignore) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@Override
public void mute(boolean mute) {
throw new NotImplementedException();
}
} }
} }

View File

@@ -36,6 +36,7 @@ public class ChangeInfo {
public Timestamp updated; public Timestamp updated;
public Timestamp submitted; public Timestamp submitted;
public Boolean starred; public Boolean starred;
public Boolean muted;
public Collection<String> stars; public Collection<String> stars;
public Boolean reviewed; public Boolean reviewed;
public SubmitType submitType; public SubmitType submitType;

View File

@@ -136,6 +136,8 @@ public class ChangeInfo extends JavaScriptObject {
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/; 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 reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/; public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;

View File

@@ -147,6 +147,7 @@ public class StarredChangesUtil {
public static final String DEFAULT_LABEL = "star"; public static final String DEFAULT_LABEL = "star";
public static final String IGNORE_LABEL = "ignore"; public static final String IGNORE_LABEL = "ignore";
public static final String MUTE_LABEL = "mute";
public static final ImmutableSortedSet<String> DEFAULT_LABELS = public static final ImmutableSortedSet<String> DEFAULT_LABELS =
ImmutableSortedSet.of(DEFAULT_LABEL); ImmutableSortedSet.of(DEFAULT_LABEL);
@@ -355,6 +356,34 @@ public class StarredChangesUtil {
return byChange(changeId, IGNORE_LABEL).contains(accountId); 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 { private static StarRef readLabels(Repository repo, String refName) throws IOException {
Ref ref = repo.exactRef(refName); Ref ref = repo.exactRef(refName);
if (ref == null) { if (ref == null) {

View File

@@ -63,6 +63,7 @@ import com.google.gerrit.server.change.ListChangeComments;
import com.google.gerrit.server.change.ListChangeDrafts; import com.google.gerrit.server.change.ListChangeDrafts;
import com.google.gerrit.server.change.ListChangeRobotComments; import com.google.gerrit.server.change.ListChangeRobotComments;
import com.google.gerrit.server.change.Move; 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.PostHashtags;
import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.PublishDraftPatchSet; import com.google.gerrit.server.change.PublishDraftPatchSet;
@@ -77,6 +78,7 @@ import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.change.SubmittedTogether; import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestChangeReviewers; import com.google.gerrit.server.change.SuggestChangeReviewers;
import com.google.gerrit.server.change.Unignore; 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.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.update.UpdateException;
@@ -132,6 +134,8 @@ class ChangeApiImpl implements ChangeApi {
private final DeletePrivate deletePrivate; private final DeletePrivate deletePrivate;
private final Ignore ignore; private final Ignore ignore;
private final Unignore unignore; private final Unignore unignore;
private final Mute mute;
private final Unmute unmute;
@Inject @Inject
ChangeApiImpl( ChangeApiImpl(
@@ -171,6 +175,8 @@ class ChangeApiImpl implements ChangeApi {
DeletePrivate deletePrivate, DeletePrivate deletePrivate,
Ignore ignore, Ignore ignore,
Unignore unignore, Unignore unignore,
Mute mute,
Unmute unmute,
@Assisted ChangeResource change) { @Assisted ChangeResource change) {
this.changeApi = changeApi; this.changeApi = changeApi;
this.revert = revert; this.revert = revert;
@@ -208,6 +214,8 @@ class ChangeApiImpl implements ChangeApi {
this.deletePrivate = deletePrivate; this.deletePrivate = deletePrivate;
this.ignore = ignore; this.ignore = ignore;
this.unignore = unignore; this.unignore = unignore;
this.mute = mute;
this.unmute = unmute;
this.change = change; this.change = change;
} }
@@ -603,4 +611,13 @@ class ChangeApiImpl implements ChangeApi {
unignore.apply(change, new Unignore.Input()); 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());
}
}
} }

View File

@@ -514,6 +514,11 @@ public class ChangeJson {
if (user.isIdentifiedUser()) { if (user.isIdentifiedUser()) {
Collection<String> stars = cd.stars(user.getAccountId()); Collection<String> stars = cd.stars(user.getAccountId());
out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
out.muted =
stars.contains(
StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId())
? true
: null;
if (!stars.isEmpty()) { if (!stars.isEmpty()) {
out.stars = stars; out.stars = stars;
} }
@@ -521,7 +526,11 @@ public class ChangeJson {
if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) {
Account.Id accountId = user.getAccountId(); 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)); out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));

View File

@@ -89,6 +89,8 @@ public class Module extends RestApiModule {
delete(CHANGE_KIND, "private").to(DeletePrivate.class); delete(CHANGE_KIND, "private").to(DeletePrivate.class);
put(CHANGE_KIND, "ignore").to(Ignore.class); put(CHANGE_KIND, "ignore").to(Ignore.class);
put(CHANGE_KIND, "unignore").to(Unignore.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); post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class); get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);

View File

@@ -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;
}
}

View File

@@ -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;
}
}