Support /COMMIT_MSG for change edit REST endpoints

The change edit REST endpoints to get and set file contents failed with
"409 Conflict: Invalid path: /COMMIT_MSG" if the magic file
"/COMMIT_MSG" is used to update the commit message.

This is what is currently used by PolyGerrit if you have a change edit,
open the diff via for the commit message and then try to edit and save
the file content.

Updating the commit message of change edits from the change screen
works, because that is using a dedicated REST endpoint to update the
commit message.

This fix makes the change edit REST endpoints aware of the magic file
"/COMMIT_MSG" by making them forward the request to the dedicated REST
endpoints to get/set the commit message. Alternatively PolyGerrit could
be changed to directly use these REST endpoints when the commit message
is edited.

Bug: Issue 11706
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I5c2ef8dbabe72e04ed61606e7b923d877efa3a23
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index cabb30d..d487da0 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
 import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.Input;
@@ -36,6 +39,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.WebLinks;
@@ -120,7 +124,7 @@
     @Override
     public Response<?> apply(ChangeResource resource, IdString id, Put.Input input)
         throws AuthException, ResourceConflictException, IOException, OrmException,
-            PermissionBackendException {
+            PermissionBackendException, BadRequestException {
       putEdit.apply(resource, id.get(), input.content);
       return Response.none();
     }
@@ -277,23 +281,34 @@
 
     private final ChangeEditModifier editModifier;
     private final GitRepositoryManager repositoryManager;
+    private final EditMessage editMessage;
 
     @Inject
-    Put(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) {
+    Put(
+        ChangeEditModifier editModifier,
+        GitRepositoryManager repositoryManager,
+        EditMessage editMessage) {
       this.editModifier = editModifier;
       this.repositoryManager = repositoryManager;
+      this.editMessage = editMessage;
     }
 
     @Override
     public Response<?> apply(ChangeEditResource rsrc, Input input)
         throws AuthException, ResourceConflictException, IOException, OrmException,
-            PermissionBackendException {
+            PermissionBackendException, BadRequestException {
       return apply(rsrc.getChangeResource(), rsrc.getPath(), input.content);
     }
 
     public Response<?> apply(ChangeResource rsrc, String path, RawInput newContent)
         throws ResourceConflictException, AuthException, IOException, OrmException,
-            PermissionBackendException {
+            PermissionBackendException, BadRequestException {
+      if (Patch.COMMIT_MSG.equals(path)) {
+        EditMessage.Input editCommitMessageInput = new EditMessage.Input();
+        editCommitMessageInput.message =
+            new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
+        return Response.ok(editMessage.apply(rsrc, editCommitMessageInput));
+      }
       if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
         throw new ResourceConflictException("Invalid path: " + path);
       }
@@ -347,6 +362,7 @@
   public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
     private final ProjectCache projectCache;
+    private final GetMessage getMessage;
 
     @Option(
         name = "--base",
@@ -355,14 +371,20 @@
     private boolean base;
 
     @Inject
-    Get(FileContentUtil fileContentUtil, ProjectCache projectCache) {
+    Get(FileContentUtil fileContentUtil, ProjectCache projectCache, GetMessage getMessage) {
       this.fileContentUtil = fileContentUtil;
       this.projectCache = projectCache;
+      this.getMessage = getMessage;
     }
 
     @Override
-    public Response<BinaryResult> apply(ChangeEditResource rsrc) throws IOException {
+    public Response<BinaryResult> apply(ChangeEditResource rsrc)
+        throws AuthException, IOException, OrmException {
       try {
+        if (Patch.COMMIT_MSG.equals(rsrc.getPath())) {
+          return Response.ok(getMessage.apply(rsrc.getChangeResource()));
+        }
+
         ChangeEdit edit = rsrc.getChangeEdit();
         return Response.ok(
             fileContentUtil.getContent(
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 47f4a8f..7a67fba 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -321,6 +322,17 @@
   }
 
   @Test
+  public void updateCommitMessageByEditingMagicCommitMsgFile() throws Exception {
+    createEmptyEditFor(changeId);
+    gApi.changes()
+        .id(changeId)
+        .edit()
+        .modifyFile(Patch.COMMIT_MSG, RawInputUtil.create("Foo Bar".getBytes(UTF_8)));
+    assertThat(getEdit(changeId)).isPresent();
+    ensureSameBytes(getFileContentOfEdit(changeId, Patch.COMMIT_MSG), "Foo Bar\n".getBytes(UTF_8));
+  }
+
+  @Test
   @TestProjectInput(createEmptyCommit = false)
   public void updateRootCommitMessage() throws Exception {
     // Re-clone empty repo; TestRepository doesn't let us reset to unborn head.