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
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java
index e874a52..56ff555 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java
@@ -67,8 +67,9 @@
    *
    * @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.
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index f28d597..24f7bba 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -138,6 +138,10 @@
     return lineNbr;
   }
 
+  public void setLine(int line) {
+    lineNbr = line;
+  }
+
   public Account.Id getAuthor() {
     return author;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
new file mode 100644
index 0000000..01e7e3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -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);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
new file mode 100644
index 0000000..af9b846
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
@@ -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();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
new file mode 100644
index 0000000..bcd8902
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
@@ -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();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
new file mode 100644
index 0000000..83959a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
@@ -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");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
new file mode 100644
index 0000000..fbfe1d5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
@@ -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();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
new file mode 100644
index 0000000..208f271
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -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;
+  }
+}
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 f0e7e46..279e925 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
@@ -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 @@
   @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 @@
     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() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 59fe465..1735afc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -82,13 +82,9 @@
     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 @@
         }
         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);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
new file mode 100644
index 0000000..38b4da1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -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;
+  }
+}