Merge changes from topic 'check'

* changes:
  ChangeJson: Catch RuntimeException in fallback check handlers
  Support fixing corrupt changes in ConsistencyChecker
  Add API method for checking changes
  Check consistency with a ListChangesOption
  Embed consistency check results in ChangeInfo result
  ChangeData: Handle null PatchSet in isMergeable
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index db358f3..bd94789 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -300,6 +300,11 @@
 * `WEB_LINKS`: include the `web_links` field.
 --
 
+[[check]]
+--
+* `CHECK`: include potential problems with the change.
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -1143,7 +1148,12 @@
 --
 
 Performs consistency checks on the change, and returns a
-link:#check-result[CheckResult] entity.
+link:#change-info[ChangeInfo] entity with the `problems` field set to a
+list of link:#problem-info[ProblemInfo] entities.
+
+Depending on the type of problem, some fields not marked optional may be
+missing from the result. At least `id`, `project`, `branch`, and
+`_number` will be present.
 
 .Request
 ----
@@ -1158,26 +1168,80 @@
 
   )]}'
   {
-    "change": {
-      "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
-      "project": "myProject",
-      "branch": "master",
-      "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
-      "subject": "Implementing Feature X",
-      "status": "NEW",
-      "created": "2013-02-01 09:59:32.126000000",
-      "updated": "2013-02-21 11:16:36.775000000",
-      "mergeable": true,
-      "insertions": 34,
-      "deletions": 101,
-      "_sortkey": "0023412400000f7d",
-      "_number": 3965,
-      "owner": {
-        "name": "John Doe"
-      }
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 34,
+    "deletions": 101,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
     },
-    "messages": [
-      "Current patch set 1 not found"
+    "problems": [
+      {
+        "message": "Current patch set 1 not found"
+      }
+    ]
+  }
+----
+
+[[fix-change]]
+=== Fix change
+--
+'POST /changes/link:#change-id[\{change-id\}]/check'
+--
+
+Performs consistency checks on the change as with link:#check-change[GET
+/check], and additionally fixes any problems that can be fixed
+automatically. The returned field values reflect any fixes.
+
+Only the change owner, a project owner, or an administrator may fix changes.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "MERGED",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 34,
+    "deletions": 101,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    },
+    "problems": [
+      {
+        "message": "Current patch set 2 not found"
+      },
+      {
+        "message": "Patch set 1 (1eee2c9d8f352483781e772f35dc586a69ff5646) is merged into destination ref master (1eee2c9d8f352483781e772f35dc586a69ff5646), but change status is NEW",
+        "status": FIXED,
+        "outcome": "Marked change as merged"
+      }
     ]
   }
 ----
@@ -3259,6 +3323,9 @@
 |`_more_changes`      |optional, not set if `false`|
 Whether the query would deliver more results if not limited. +
 Only set on either the last or the first change that is returned.
+|`problems`           |optional|
+A list of link:#problem-info[ProblemInfo] entities describing potential
+problems with this change. Only set if link:#check[CHECK] is set.
 |==================================
 
 [[related-changes-info]]
@@ -3978,22 +4045,23 @@
 |`message`     ||New commit message.
 |===========================
 
-[[check-result]]
-=== CheckResult
-The `CheckResult` entity contains the results of a consistency check on
-a change.
+[[problem-info]]
+=== ProblemInfo
+The `ProblemInfo` entity contains a description of a potential consistency problem
+with a change. These are not related to the code review process, but rather
+indicate some inconsistency in Gerrit's database or repository metadata related
+to the enclosing change.
 
-[options="header",cols="1,6"]
+[options="header",cols="1,^1,5"]
 |===========================
-|Field Name|Description
-|`change`|
-link:#change-info[ChangeInfo] entity containing information about the change,
-as in link:#get-change[Get Change] with no options. Some fields not marked
-optional may be missing if a consistency check failed, but at least
-`id`, `project`, `branch`, and `_number` will be present.
-|`messages`|
-List of messages describing potential problems with the change. May be
-empty if no problems were found.
+|Field Name||Description
+|`message` ||Plaintext message describing the problem with the change.
+|`status`  |optional|
+The status of fixing the problem (`FIXED`, `FIX_FAILED`). Only set if a
+fix was attempted.
+|`outcome` |optional|
+If `status` is set, an additional plaintext message describing the
+outcome of the fix.
 |===========================
 
 GERRIT
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ae7e94b..ec21328 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -301,4 +301,17 @@
         .id(r.getChangeId())
         .topic()).isEqualTo("");
   }
+
+  @Test
+  public void check() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .get()
+        .problems).isNull();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .get(EnumSet.of(ListChangesOption.CHECK))
+        .problems).isEmpty();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
new file mode 100644
index 0000000..db45e4c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2014 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.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.reviewdb.client.Change;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+@NoHttpd
+public class CheckIT extends AbstractDaemonTest {
+  // Most types of tests belong in ConsistencyCheckerTest; these mostly just
+  // test paths outside of ConsistencyChecker, like API wiring.
+  @Test
+  public void currentPatchSetMissing() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change c = getChange(r);
+    db.patchSets().deleteKeys(Collections.singleton(c.currentPatchSetId()));
+
+    List<ProblemInfo> problems = gApi.changes()
+        .id(r.getChangeId())
+        .check()
+        .problems;
+    assertThat(problems).hasSize(1);
+    assertThat(problems.get(0).message)
+        .isEqualTo("Current patch set 1 not found");
+  }
+
+  @Test
+  public void fixReturnsUpdatedValue() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    Change c = getChange(r);
+    c.setStatus(Change.Status.NEW);
+    db.changes().update(Collections.singleton(c));
+
+    ChangeInfo info = gApi.changes()
+        .id(r.getChangeId())
+        .check();
+    assertThat(info.problems).hasSize(1);
+    assertThat(info.problems.get(0).status).isNull();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+    info = gApi.changes()
+        .id(r.getChangeId())
+        .check(new FixInput());
+    assertThat(info.problems).hasSize(1);
+    assertThat(info.problems.get(0).status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  private Change getChange(PushOneCommit.Result r) throws Exception {
+    return db.changes().get(new Change.Id(
+        gApi.changes().id(r.getChangeId()).get()._number));
+  }
+}
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 6dd6b06..78c95b9 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
@@ -81,9 +81,9 @@
 
   ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
 
-  /** {@code get} with {@link ListChangesOption} set to ALL. */
+  /** {@code get} with {@link ListChangesOption} set to all except CHECK. */
   ChangeInfo get() throws RestApiException;
-  /** {@code get} with {@link ListChangesOption} set to NONE. */
+  /** {@code get} with {@link ListChangesOption} set to none. */
   ChangeInfo info() throws RestApiException;
 
   /**
@@ -98,6 +98,9 @@
    */
   Set<String> getHashtags() throws RestApiException;
 
+  ChangeInfo check() throws RestApiException;
+  ChangeInfo check(FixInput fix) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -197,5 +200,15 @@
     public Set<String> getHashtags() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public ChangeInfo check() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo check(FixInput fix) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
similarity index 74%
copy from gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
index 66c1754..e87be82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
@@ -12,13 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.change;
+package com.google.gerrit.extensions.api.changes;
 
-import com.google.gerrit.extensions.common.ChangeInfo;
-
-import java.util.List;
-
-public class CheckResult {
-  public ChangeInfo change;
-  public List<String> messages;
+public class FixInput {
 }
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 5f57d18..8fd907e 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
@@ -16,6 +16,7 @@
 
 import java.sql.Timestamp;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 public class ChangeInfo {
@@ -50,4 +51,6 @@
   public String currentRevision;
   public Map<String, RevisionInfo> revisions;
   public Boolean _moreChanges;
+
+  public List<ProblemInfo> problems;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
index f9f8b62..dd3f075 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
@@ -52,7 +52,10 @@
   DOWNLOAD_COMMANDS(13),
 
   /** Include patch set weblinks. */
-  WEB_LINKS(14);
+  WEB_LINKS(14),
+
+  /** Include consistency check results. */
+  CHECK(15);
 
   private final int value;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
similarity index 74%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index 66c1754..369bcda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CheckResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.change;
+package com.google.gerrit.extensions.common;
 
-import com.google.gerrit.extensions.common.ChangeInfo;
+public class ProblemInfo {
+  public static enum Status {
+    FIXED, FIX_FAILED;
+  }
 
-import java.util.List;
-
-public class CheckResult {
-  public ChangeInfo change;
-  public List<String> messages;
+  public String message;
+  public Status status;
+  public String outcome;
 }
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 fcd6697..9dcf97b 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Check;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetTopic;
 import com.google.gerrit.server.change.PostHashtags;
@@ -65,6 +67,7 @@
   private final Provider<ChangeJson> changeJson;
   private final PostHashtags postHashtags;
   private final GetHashtags getHashtags;
+  private final Check check;
 
   @Inject
   ChangeApiImpl(Changes changeApi,
@@ -79,6 +82,7 @@
       Provider<ChangeJson> changeJson,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
+      Check check,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -92,6 +96,7 @@
     this.changeJson = changeJson;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
+    this.check = check;
     this.change = change;
   }
 
@@ -206,7 +211,7 @@
 
   @Override
   public ChangeInfo get() throws RestApiException {
-    return get(EnumSet.allOf(ListChangesOption.class));
+    return get(EnumSet.complementOf(EnumSet.of(ListChangesOption.CHECK)));
   }
 
   @Override
@@ -231,4 +236,22 @@
       throw new RestApiException("Cannot get hashtags", e);
     }
   }
+
+  @Override
+  public ChangeInfo check() throws RestApiException {
+    try {
+      return check.apply(change).value();
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot check change", e);
+    }
+  }
+
+  @Override
+  public ChangeInfo check(FixInput fix) throws RestApiException {
+    try {
+      return check.apply(change, fix).value();
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot check change", e);
+    }
+  }
 }
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 8b1fd5f..cdca93d 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
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.common.ListChangesOption.ALL_COMMITS;
 import static com.google.gerrit.extensions.common.ListChangesOption.ALL_FILES;
 import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.common.ListChangesOption.CHECK;
 import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_FILES;
@@ -55,6 +56,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
@@ -65,6 +67,7 @@
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
@@ -141,8 +144,10 @@
   private final EnumSet<ListChangesOption> options;
   private final ChangeMessagesUtil cmUtil;
   private final PatchLineCommentsUtil plcUtil;
+  private final Provider<ConsistencyChecker> checkerProvider;
 
   private AccountLoader accountLoader;
+  private FixInput fix;
 
   @Inject
   ChangeJson(
@@ -161,7 +166,8 @@
       Revisions revisions,
       WebLinks webLinks,
       ChangeMessagesUtil cmUtil,
-      PatchLineCommentsUtil plcUtil) {
+      PatchLineCommentsUtil plcUtil,
+      Provider<ConsistencyChecker> checkerProvider) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
@@ -178,6 +184,7 @@
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
     this.plcUtil = plcUtil;
+    this.checkerProvider = checkerProvider;
     options = EnumSet.noneOf(ListChangesOption.class);
   }
 
@@ -191,6 +198,11 @@
     return this;
   }
 
+  public ChangeJson fix(FixInput fix) {
+    this.fix = fix;
+    return this;
+  }
+
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
     return format(changeDataFactory.create(db.get(), rsrc.getControl()));
   }
@@ -200,7 +212,16 @@
   }
 
   public ChangeInfo format(Change.Id id) throws OrmException {
-    return format(changeDataFactory.create(db.get(), id));
+    Change c;
+    try {
+      c = db.get().changes().get(id);
+    } catch (OrmException e) {
+      if (!has(CHECK)) {
+        throw e;
+      }
+      return checkOnly(changeDataFactory.create(db.get(), id));
+    }
+    return format(changeDataFactory.create(db.get(), c));
   }
 
   public ChangeInfo format(ChangeData cd) throws OrmException {
@@ -209,14 +230,21 @@
 
   private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws OrmException {
-    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    Set<Change.Id> reviewed = Sets.newHashSet();
-    if (has(REVIEWED)) {
-      reviewed = loadReviewed(Collections.singleton(cd));
+    try {
+      accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+      Set<Change.Id> reviewed = Sets.newHashSet();
+      if (has(REVIEWED)) {
+        reviewed = loadReviewed(Collections.singleton(cd));
+      }
+      ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
+      accountLoader.fill();
+      return res;
+    } catch (OrmException | RuntimeException e) {
+      if (!has(CHECK)) {
+        throw e;
+      }
+      return checkOnly(cd);
     }
-    ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
-    accountLoader.fill();
-    return res;
   }
 
   public ChangeInfo format(RevisionResource rsrc) throws OrmException {
@@ -261,10 +289,14 @@
       if (i == null) {
         try {
           i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
-        } catch (OrmException e) {
-          log.warn(
-              "Omitting corrupt change " + cd.getId() + " from results", e);
-          continue;
+        } catch (OrmException | RuntimeException e) {
+          if (has(CHECK)) {
+            i = checkOnly(cd);
+          } else {
+            log.warn(
+                "Omitting corrupt change " + cd.getId() + " from results", e);
+            continue;
+          }
         }
         out.put(cd.getId(), i);
       }
@@ -273,11 +305,49 @@
     return info;
   }
 
+  private ChangeInfo checkOnly(ChangeData cd) {
+    ConsistencyChecker.Result result = checkerProvider.get().check(cd, fix);
+    ChangeInfo info;
+    Change c = result.change();
+    if (c != null) {
+      info = new ChangeInfo();
+      info.project = c.getProject().get();
+      info.branch = c.getDest().getShortName();
+      info.topic = c.getTopic();
+      info.changeId = c.getKey().get();
+      info.subject = c.getSubject();
+      info.status = c.getStatus().asChangeStatus();
+      info.owner = new AccountInfo(c.getOwner().get());
+      info.created = c.getCreatedOn();
+      info.updated = c.getLastUpdatedOn();
+      info._number = c.getId().get();
+      info.problems = result.problems();
+      finish(info);
+    } else {
+      info = new ChangeInfo();
+      info._number = result.id().get();
+      info.problems = result.problems();
+    }
+    return info;
+  }
+
   private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
       Optional<PatchSet.Id> limitToPsId) throws OrmException {
-    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
     ChangeInfo out = new ChangeInfo();
+
+    if (has(CHECK)) {
+      out.problems = checkerProvider.get().check(cd.change(), fix).problems();
+      // If any problems were fixed, the ChangeData needs to be reloaded.
+      for (ProblemInfo p : out.problems) {
+        if (p.status == ProblemInfo.Status.FIXED) {
+          cd = changeDataFactory.create(cd.db(), cd.getId());
+          break;
+        }
+      }
+    }
+
     Change in = cd.change();
+    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
@@ -355,6 +425,7 @@
         out.actions.put(descr.getId(), new ActionInfo(descr));
       }
     }
+
     return out;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index cb66231..210eeab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -14,63 +14,41 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.project.ChangeControl;
 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 Check implements RestReadView<ChangeResource> {
-  private static final Logger log = LoggerFactory.getLogger(Check.class);
-
-  private final Provider<ConsistencyChecker> checkerProvider;
+public class Check implements RestReadView<ChangeResource>,
+    RestModifyView<ChangeResource, FixInput> {
   private final ChangeJson json;
 
   @Inject
-  Check(Provider<ConsistencyChecker> checkerProvider,
-      ChangeJson json) {
-    this.checkerProvider = checkerProvider;
+  Check(ChangeJson json) {
     this.json = json;
+    json.addOption(ListChangesOption.CHECK);
   }
 
   @Override
-  public CheckResult apply(ChangeResource rsrc) {
-    CheckResult result = new CheckResult();
-    result.messages = checkerProvider.get().check(rsrc.getChange());
-    try {
-      result.change = json.format(rsrc);
-    } catch (OrmException e) {
-      // Even with no options there are a surprising number of dependencies in
-      // ChangeJson. Fall back to a very basic implementation with no
-      // dependencies if this fails.
-      String msg = "Error rendering final ChangeInfo";
-      log.warn(msg, e);
-      result.messages.add(msg);
-      result.change = basicChangeInfo(rsrc.getChange());
-    }
-    return result;
+  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
+    return GetChange.cache(json.format(rsrc));
   }
 
-  private static ChangeInfo basicChangeInfo(Change c) {
-    ChangeInfo info = new ChangeInfo();
-    info.project = c.getProject().get();
-    info.branch = c.getDest().getShortName();
-    info.topic = c.getTopic();
-    info.changeId = c.getKey().get();
-    info.subject = c.getSubject();
-    info.status = c.getStatus().asChangeStatus();
-    info.owner = new AccountInfo(c.getOwner().get());
-    info.created = c.getCreatedOn();
-    info.updated = c.getLastUpdatedOn();
-    info._number = c.getId().get();
-    ChangeJson.finish(info);
-    return info;
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
+      throws AuthException, OrmException {
+    ChangeControl ctl = rsrc.getControl();
+    if (!ctl.isOwner()
+        && !ctl.getProjectControl().isOwner()
+        && !ctl.getCurrentUser().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("Not owner");
+    }
+    return GetChange.cache(json.fix(input).format(rsrc));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index e5c2ad8..7197269 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -14,16 +14,23 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
 import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.extensions.common.ProblemInfo.Status;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -55,9 +62,28 @@
   private static final Logger log =
       LoggerFactory.getLogger(ConsistencyChecker.class);
 
+  @AutoValue
+  public static abstract class Result {
+    private static Result create(Change.Id id, List<ProblemInfo> problems) {
+      return new AutoValue_ConsistencyChecker_Result(id, null, problems);
+    }
+
+    private static Result create(Change c, List<ProblemInfo> problems) {
+      return new AutoValue_ConsistencyChecker_Result(c.getId(), c, problems);
+    }
+
+    public abstract Change.Id id();
+
+    @Nullable
+    public abstract Change change();
+
+    public abstract List<ProblemInfo> problems();
+  }
+
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
 
+  private FixInput fix;
   private Change change;
   private Repository repo;
   private RevWalk rw;
@@ -65,7 +91,7 @@
   private PatchSet currPs;
   private RevCommit currPsCommit;
 
-  private List<String> messages;
+  private List<ProblemInfo> problems;
 
   @Inject
   ConsistencyChecker(Provider<ReviewDb> db,
@@ -79,15 +105,34 @@
     change = null;
     repo = null;
     rw = null;
-    messages = new ArrayList<>();
+    problems = new ArrayList<>();
   }
 
-  public List<String> check(Change c) {
+  public Result check(ChangeData cd) {
+    return check(cd, null);
+  }
+
+  public Result check(ChangeData cd, @Nullable FixInput f) {
     reset();
+    try {
+      return check(cd.change(), f);
+    } catch (OrmException e) {
+      error("Error looking up change", e);
+      return Result.create(cd.getId(), problems);
+    }
+  }
+
+  public Result check(Change c) {
+    return check(c, null);
+  }
+
+  public Result check(Change c, @Nullable FixInput f) {
+    reset();
+    fix = f;
     change = c;
     try {
       checkImpl();
-      return messages;
+      return Result.create(c, problems);
     } finally {
       if (rw != null) {
         rw.release();
@@ -115,7 +160,7 @@
   private void checkOwner() {
     try {
       if (db.get().accounts().get(change.getOwner()) == null) {
-        messages.add("Missing change owner: " + change.getOwner());
+        problem("Missing change owner: " + change.getOwner());
       }
     } catch (OrmException e) {
       error("Failed to look up owner", e);
@@ -127,7 +172,7 @@
       PatchSet.Id psId = change.currentPatchSetId();
       currPs = db.get().patchSets().get(psId);
       if (currPs == null) {
-        messages.add(String.format("Current patch set %d not found", psId.get()));
+        problem(String.format("Current patch set %d not found", psId.get()));
       }
     } catch (OrmException e) {
       error("Failed to look up current patch set", e);
@@ -189,7 +234,7 @@
     for (Map.Entry<ObjectId, Collection<PatchSet>> e
         : bySha.asMap().entrySet()) {
       if (e.getValue().size() > 1) {
-        messages.add(String.format("Multiple patch sets pointing to %s: %s",
+        problem(String.format("Multiple patch sets pointing to %s: %s",
             e.getKey().name(),
             Collections2.transform(e.getValue(), toPsId)));
       }
@@ -204,11 +249,11 @@
     try {
       dest = repo.getRef(refName);
     } catch (IOException e) {
-      messages.add("Failed to look up destination ref: " + refName);
+      problem("Failed to look up destination ref: " + refName);
       return;
     }
     if (dest == null) {
-      messages.add("Destination ref not found (may be new branch): "
+      problem("Destination ref not found (may be new branch): "
           + change.getDest().get());
       return;
     }
@@ -221,38 +266,67 @@
     try {
       merged = rw.isMergedInto(currPsCommit, tip);
     } catch (IOException e) {
-      messages.add("Error checking whether patch set " + currPs.getId().get()
+      problem("Error checking whether patch set " + currPs.getId().get()
           + " is merged");
       return;
     }
     if (merged && change.getStatus() != Change.Status.MERGED) {
-      messages.add(String.format("Patch set %d (%s) is merged into destination"
-            + " ref %s (%s), but change status is %s", currPs.getId().get(),
-            currPsCommit.name(), refName, tip.name(), change.getStatus()));
-      // TODO(dborowitz): Just fix it.
+      ProblemInfo p = problem(String.format(
+          "Patch set %d (%s) is merged into destination ref %s (%s), but change"
+          + " status is %s", currPs.getId().get(), currPsCommit.name(),
+          refName, tip.name(), change.getStatus()));
+      if (fix != null) {
+        fixMerged(p);
+      }
     } else if (!merged && change.getStatus() == Change.Status.MERGED) {
-      messages.add(String.format("Patch set %d (%s) is not merged into"
+      problem(String.format("Patch set %d (%s) is not merged into"
             + " destination ref %s (%s), but change status is %s",
             currPs.getId().get(), currPsCommit.name(), refName, tip.name(),
             change.getStatus()));
     }
   }
 
+  private void fixMerged(ProblemInfo p) {
+    try {
+      change = db.get().changes().atomicUpdate(change.getId(),
+          new AtomicUpdate<Change>() {
+            @Override
+            public Change update(Change c) {
+              c.setStatus(Change.Status.MERGED);
+              return c;
+            }
+          });
+      p.status = Status.FIXED;
+      p.outcome = "Marked change as merged";
+    } catch (OrmException e) {
+      log.warn("Error marking " + change.getId() + "as merged", e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = "Error updating status to merged";
+    }
+  }
+
   private RevCommit parseCommit(ObjectId objId, String desc) {
     try {
       return rw.parseCommit(objId);
     } catch (MissingObjectException e) {
-      messages.add(String.format("Object missing: %s: %s", desc, objId.name()));
+      problem(String.format("Object missing: %s: %s", desc, objId.name()));
     } catch (IncorrectObjectTypeException e) {
-      messages.add(String.format("Not a commit: %s: %s", desc, objId.name()));
+      problem(String.format("Not a commit: %s: %s", desc, objId.name()));
     } catch (IOException e) {
-      messages.add(String.format("Failed to look up: %s: %s", desc, objId.name()));
+      problem(String.format("Failed to look up: %s: %s", desc, objId.name()));
     }
     return null;
   }
 
+  private ProblemInfo problem(String msg) {
+    ProblemInfo p = new ProblemInfo();
+    p.message = msg;
+    problems.add(p);
+    return p;
+  }
+
   private boolean error(String msg, Throwable t) {
-    messages.add(msg);
+    problem(msg);
     // TODO(dborowitz): Expose stack trace to administrators.
     log.warn("Error in consistency check of change " + change.getId(), t);
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
index d3bbb13..f3673c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
@@ -53,7 +53,7 @@
     return cache(json.format(rsrc));
   }
 
-  private Response<ChangeInfo> cache(ChangeInfo res) {
+  static Response<ChangeInfo> cache(ChangeInfo res) {
     return Response.ok(res)
         .caching(CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate());
   }
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 d2d3db3..4cf7692 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
@@ -53,6 +53,7 @@
     get(CHANGE_KIND, "in").to(IncludedIn.class);
     get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
     get(CHANGE_KIND, "check").to(Check.class);
+    post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND).to(DeleteDraftChange.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index d45a58e..0f1a7bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -594,6 +594,9 @@
         mergeable = true;
       } else {
         PatchSet ps = currentPatchSet();
+        if (ps == null) {
+          throw new OrmException("Missing patch set for mergeability check");
+        }
         Repository repo = null;
         try {
           repo = repoManager.openRepository(c.getProject());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
index 3104b69..51aa283 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -20,7 +20,11 @@
 import static com.google.gerrit.testutil.TestChanges.newPatchSet;
 import static java.util.Collections.singleton;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -40,6 +44,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.List;
+
 public class ConsistencyCheckerTest {
   private InMemoryDatabase schemaFactory;
   private ReviewDb db;
@@ -90,7 +96,7 @@
     PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
     db.patchSets().insert(singleton(ps2));
 
-    assertThat(checker.check(c)).isEmpty();
+    assertProblems(c);
   }
 
   @Test
@@ -110,7 +116,7 @@
     db.patchSets().insert(singleton(ps2));
 
     repo.branch(c.getDest().get()).update(commit2);
-    assertThat(checker.check(c)).isEmpty();
+    assertProblems(c);
   }
 
   @Test
@@ -122,7 +128,7 @@
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
     db.patchSets().insert(singleton(ps));
 
-    assertThat(checker.check(c)).containsExactly("Missing change owner: 2");
+    assertProblems(c, "Missing change owner: 2");
   }
 
   @Test
@@ -132,8 +138,7 @@
     PatchSet ps = newPatchSet(c.currentPatchSetId(),
         ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
     db.patchSets().insert(singleton(ps));
-    assertThat(checker.check(c))
-        .containsExactly("Destination repository not found: otherproject");
+    assertProblems(c, "Destination repository not found: otherproject");
   }
 
   @Test
@@ -153,7 +158,7 @@
     PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
     db.patchSets().insert(singleton(ps2));
 
-    assertThat(checker.check(c)).containsExactly(
+    assertProblems(c,
         "Invalid revision on patch set 1:"
         + " fooooooooooooooooooooooooooooooooooooooo");
   }
@@ -166,7 +171,7 @@
         ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
     db.patchSets().insert(singleton(ps));
 
-    assertThat(checker.check(c)).containsExactly(
+    assertProblems(c,
         "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
   }
 
@@ -174,8 +179,7 @@
   public void currentPatchSetMissing() throws Exception {
     Change c = newChange(project, userId);
     db.changes().insert(singleton(c));
-    assertThat(checker.check(c))
-      .containsExactly("Current patch set 1 not found");
+    assertProblems(c, "Current patch set 1 not found");
   }
 
   @Test
@@ -191,8 +195,8 @@
     PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit1, userId);
     db.patchSets().insert(singleton(ps2));
 
-    assertThat(checker.check(c)).containsExactly("Multiple patch sets pointing to "
-        + commit1.name() + ": [1, 2]");
+    assertProblems(c,
+        "Multiple patch sets pointing to " + commit1.name() + ": [1, 2]");
   }
 
   @Test
@@ -206,8 +210,7 @@
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
     db.patchSets().insert(singleton(ps));
 
-    assertThat(checker.check(c)).containsExactly(
-        "Destination ref not found (may be new branch): master");
+    assertProblems(c, "Destination ref not found (may be new branch): master");
   }
 
   @Test
@@ -220,7 +223,7 @@
     PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
     db.patchSets().insert(singleton(ps));
 
-    assertThat(checker.check(c)).containsExactly(
+    assertProblems(c,
         "Patch set 1 (" + commit.name() + ") is not merged into destination ref"
         + " master (" + tip.name() + "), but change status is MERGED");
   }
@@ -235,8 +238,42 @@
     db.patchSets().insert(singleton(ps));
     repo.branch(c.getDest().get()).update(commit);
 
-    assertThat(checker.check(c)).containsExactly(
+    assertProblems(c,
         "Patch set 1 (" + commit.name() + ") is merged into destination ref"
         + " master (" + commit.name() + "), but change status is NEW");
   }
+
+  @Test
+  public void newChangeIsMergedWithFix() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    db.patchSets().insert(singleton(ps));
+    repo.branch(c.getDest().get()).update(commit);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+        + " master (" + commit.name() + "), but change status is NEW");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Marked change as merged");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+    assertProblems(c);
+  }
+
+  private void assertProblems(Change c, String... expected) {
+    assertThat(Lists.transform(checker.check(c).problems(),
+          new Function<ProblemInfo, String>() {
+            @Override
+            public String apply(ProblemInfo in) {
+              return in.message;
+            }
+          })).containsExactly((Object[]) expected);
+  }
 }