Add /changes/{id}/revisions/{commit}/drafts

Draft comments can now be managed through the REST API endpoint:

  GET /changes/{id}/revisions/{commit}/drafts

  Returns all drafts on that commit by the caller, organized as an
  object mapping path name to a list of comment objects.

  DELETE /changes/{id}/revisions/{commit}/drafts/{id}

  Remove a draft comment.

  PUT /changes/{id}/revisions/{commit}/drafts/{id}

  Update the contents of a draft comment. This not only supports
  changing the text, but also moving the comment to a different
  line or to an entirely different file.

  PUT /changes/{id}/revisions/{commit}/drafts

  Create a new draft, with a new unique identifier returned.

Change-Id: I53eb11138fac4b29623885d01c4451f51aa5ff31
This commit is contained in:
Shawn O. Pearce
2012-11-18 16:25:03 -08:00
parent 5367b8bab5
commit 80a1c26722
11 changed files with 525 additions and 7 deletions

View File

@@ -67,8 +67,9 @@ public interface RestCollection<P extends RestResource, R extends RestResource>
*
* @return view to list the collection.
* @throws ResourceNotFoundException if the collection cannot be listed.
* @throws AuthException if the collection requires authentication.
*/
RestView<P> list() throws ResourceNotFoundException;
RestView<P> list() throws ResourceNotFoundException, AuthException;
/**
* Parse a path component into a resource handle.

View File

@@ -138,6 +138,10 @@ public final class PatchLineComment {
return lineNbr;
}
public void setLine(int line) {
lineNbr = line;
}
public Account.Id getAuthor() {
return author;
}

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2012 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.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.PutDraft.Input;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collections;
class CreateDraft implements RestModifyView<RevisionResource, Input> {
private final Provider<ReviewDb> db;
@Inject
CreateDraft(Provider<ReviewDb> db) {
this.db = db;
}
@Override
public Class<Input> inputType() {
return Input.class;
}
@Override
public Object apply(RevisionResource rsrc, Input in) throws AuthException,
BadRequestException, ResourceConflictException, Exception {
if (Strings.isNullOrEmpty(in.path)) {
throw new BadRequestException("path must be non-empty");
} else if (in.message == null || in.message.trim().isEmpty()) {
throw new BadRequestException("message must be non-empty");
} else if (in.line != null && in.line <= 0) {
throw new BadRequestException("line must be > 0");
}
PatchLineComment c = new PatchLineComment(
new PatchLineComment.Key(
new Patch.Key(rsrc.getPatchSet().getId(), in.path),
ChangeUtil.messageUUID(db.get())),
in.line != null ? in.line : 0,
rsrc.getAuthorId(),
null);
c.setStatus(Status.DRAFT);
c.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
c.setMessage(in.message.trim());
db.get().patchComments().insert(Collections.singleton(c));
return new GetDraft.Comment(c);
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2012 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.RestModifyView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.DeleteDraft.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collections;
class DeleteDraft implements RestModifyView<DraftResource, Input> {
static class Input {
}
private final Provider<ReviewDb> db;
@Inject
DeleteDraft(Provider<ReviewDb> db) {
this.db = db;
}
@Override
public Class<Input> inputType() {
return Input.class;
}
@Override
public Object apply(DraftResource rsrc, Input input) throws OrmException {
db.get().patchComments().delete(Collections.singleton(rsrc.getComment()));
return new Object();
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (C) 2012 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.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
public class DraftResource implements RestResource {
public static final TypeLiteral<RestView<DraftResource>> DRAFT_KIND =
new TypeLiteral<RestView<DraftResource>>() {};
private final RevisionResource rev;
private final PatchLineComment comment;
DraftResource(RevisionResource rev, PatchLineComment c) {
this.rev = rev;
this.comment = c;
}
public ChangeControl getControl() {
return rev.getControl();
}
public Change getChange() {
return getControl().getChange();
}
public PatchSet getPatchSet() {
return rev.getPatchSet();
}
PatchLineComment getComment() {
return comment;
}
String getId() {
return comment.getKey().get();
}
Account.Id getAuthorId() {
return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
}
}

View File

@@ -0,0 +1,83 @@
// Copyright (C) 2012 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.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
class Drafts implements ChildCollection<RevisionResource, DraftResource> {
private final DynamicMap<RestView<DraftResource>> views;
private final Provider<CurrentUser> user;
private final Provider<ListDrafts> list;
private final Provider<ReviewDb> dbProvider;
@Inject
Drafts(DynamicMap<RestView<DraftResource>> views,
Provider<CurrentUser> user,
Provider<ListDrafts> list,
Provider<ReviewDb> dbProvider) {
this.views = views;
this.user = user;
this.list = list;
this.dbProvider = dbProvider;
}
@Override
public DynamicMap<RestView<DraftResource>> views() {
return views;
}
@Override
public RestView<RevisionResource> list() throws AuthException {
checkIdentifiedUser();
return list.get();
}
@Override
public DraftResource parse(RevisionResource rev, String id)
throws ResourceNotFoundException, UnsupportedEncodingException,
OrmException, AuthException {
checkIdentifiedUser();
String uuid = URLDecoder.decode(id, "UTF-8");
for (PatchLineComment c : dbProvider.get().patchComments()
.draftByPatchSetAuthor(
rev.getPatchSet().getId(),
rev.getAuthorId())) {
if (uuid.equals(c.getKey().get())) {
return new DraftResource(rev, c);
}
}
throw new ResourceNotFoundException(id);
}
private void checkIdentifiedUser() throws AuthException {
if (!(user.get() instanceof IdentifiedUser)) {
throw new AuthException("drafts only available to authenticated users");
}
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (C) 2012 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.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.Timestamp;
class GetDraft implements RestReadView<DraftResource> {
@Override
public Object apply(DraftResource rsrc) throws AuthException,
BadRequestException, ResourceConflictException, Exception {
return new Comment(rsrc.getComment());
}
static enum Side {
PARENT, REVISION;
}
static class Comment {
final String kind = "gerritcodereview#comment";
String id;
String path;
Side side;
Integer line;
String message;
Timestamp updated;
Comment(PatchLineComment c) {
try {
id = URLEncoder.encode(c.getKey().get(), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding not supported", e);
}
path = c.getKey().getParentKey().getFileName();
if (c.getSide() == 0) {
side = Side.PARENT;
}
if (c.getLine() > 0) {
line = c.getLine();
}
message = Strings.emptyToNull(c.getMessage());
updated = c.getWrittenOn();
}
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (C) 2012 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 static com.google.common.base.Objects.firstNonNull;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.GetDraft.Comment;
import com.google.gerrit.server.change.GetDraft.Side;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
class ListDrafts implements RestReadView<RevisionResource> {
private final Provider<ReviewDb> db;
@Inject
ListDrafts(Provider<ReviewDb> db) {
this.db = db;
}
@Override
public Object apply(RevisionResource rsrc) throws AuthException,
BadRequestException, ResourceConflictException, Exception {
Map<String, List<Comment>> out = Maps.newTreeMap();
for (PatchLineComment c : db.get().patchComments()
.draftByPatchSetAuthor(
rsrc.getPatchSet().getId(),
rsrc.getAuthorId())) {
Comment o = new Comment(c);
List<Comment> list = out.get(o.path);
if (list == null) {
list = Lists.newArrayList();
out.put(o.path, list);
}
list.add(o);
}
for (List<Comment> list : out.values()) {
Collections.sort(list, new Comparator<Comment>() {
@Override
public int compare(Comment a, Comment b) {
int c = firstNonNull(a.side, Side.REVISION).ordinal()
- firstNonNull(b.side, Side.REVISION).ordinal();
if (c == 0) {
c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
}
if (c == 0) {
c = a.id.compareTo(b.id);
}
return c;
}
});
}
return out;
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
@@ -26,6 +27,7 @@ public class Module extends RestApiModule {
@Override
protected void configure() {
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), DRAFT_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
@@ -41,6 +43,12 @@ public class Module extends RestApiModule {
child(CHANGE_KIND, "revisions").to(Revisions.class);
post(REVISION_KIND, "review").to(PostReview.class);
child(REVISION_KIND, "drafts").to(Drafts.class);
put(REVISION_KIND, "drafts").to(CreateDraft.class);
get(DRAFT_KIND).to(GetDraft.class);
put(DRAFT_KIND).to(PutDraft.class);
delete(DRAFT_KIND).to(DeleteDraft.class);
install(new FactoryModule() {
@Override
protected void configure() {

View File

@@ -82,13 +82,9 @@ class PostReview implements RestModifyView<RevisionResource, Input> {
DELETE, PUBLISH, KEEP;
}
static enum Side {
PARENT, REVISION;
}
static class Comment {
String id;
Side side;
GetDraft.Side side;
int line;
String message;
}
@@ -273,7 +269,7 @@ class PostReview implements RestModifyView<RevisionResource, Input> {
}
e.setStatus(PatchLineComment.Status.PUBLISHED);
e.setWrittenOn(timestamp);
e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
e.setSide(c.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
e.setMessage(c.message);
(create ? ins : upd).add(e);
}

View File

@@ -0,0 +1,103 @@
// Copyright (C) 2012 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.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.GetDraft.Side;
import com.google.gerrit.server.change.PutDraft.Input;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.Timestamp;
import java.util.Collections;
class PutDraft implements RestModifyView<DraftResource, Input> {
static class Input {
String kind;
String id;
String path;
Side side;
Integer line;
Timestamp updated; // Accepted but ignored.
@DefaultInput
String message;
}
private final Provider<ReviewDb> db;
private final Provider<DeleteDraft> delete;
@Inject
PutDraft(Provider<ReviewDb> db, Provider<DeleteDraft> delete) {
this.db = db;
this.delete = delete;
}
@Override
public Class<Input> inputType() {
return Input.class;
}
@Override
public Object apply(DraftResource rsrc, Input in)
throws AuthException, BadRequestException, ResourceConflictException,
Exception {
if (in == null || in.message == null || in.message.trim().isEmpty()) {
return delete.get().apply(rsrc, null);
} else if (in.kind != null && !"gerritcodereview#comment".equals(in.kind)) {
throw new BadRequestException("expected kind gerritcodereview#comment");
} else if (in.line != null && in.line < 0) {
throw new BadRequestException("line must be >= 0");
}
PatchLineComment c = rsrc.getComment();
if (in.path != null
&& !in.path.equals(c.getKey().getParentKey().getFileName())) {
// Updating the path alters the primary key, which isn't possible.
// Delete then recreate the comment instead of an update.
db.get().patchComments().delete(Collections.singleton(c));
c = update(new PatchLineComment(
new PatchLineComment.Key(
new Patch.Key(rsrc.getPatchSet().getId(), in.path),
c.getKey().get()),
c.getLine(),
rsrc.getAuthorId(),
c.getParentUuid()), in);
db.get().patchComments().insert(Collections.singleton(c));
} else {
db.get().patchComments().update(Collections.singleton(update(c, in)));
}
return new GetDraft.Comment(c);
}
private PatchLineComment update(PatchLineComment e, Input in) {
if (in.side != null) {
e.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
}
if (in.line != null) {
e.setLine(in.line);
}
e.setMessage(in.message.trim());
e.updated();
return e;
}
}