Merge changes I4aa7ab9c,I5ba29aa5,Ie824acdb

* changes:
  SubmittedTogether: Also work for already submitted changes
  MergeOp: Record the change set
  Move SubmittedTogether assertion to AbstractDaemon
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8d97136..42fc81c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.Util.block;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
@@ -594,4 +597,17 @@
       .revision(1)
       .actions();
   }
+
+  protected void assertSubmittedTogether(String chId, String... expected)
+      throws Exception {
+    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+    assertThat(actual).hasSize(expected.length);
+    assertThat(Iterables.transform(actual,
+        new Function<ChangeInfo, String>() {
+      @Override
+      public String apply(ChangeInfo input) {
+        return input.changeId;
+      }
+    })).containsExactly((Object[])expected).inOrder();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 2af3c25..9aa4b34 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -49,8 +49,10 @@
     PushOneCommit.Result change = createChange();
     PushOneCommit.Result change2 = createChange();
 
-    approve(change.getChangeId());
-    submit(change2.getChangeId());
+    String id1 = change.getChangeId();
+    String id2 = change2.getChangeId();
+    approve(id1);
+    submit(id2);
 
     RevCommit head = getRemoteHead();
     assertThat(head.getId()).isEqualTo(change2.getCommitId());
@@ -59,6 +61,8 @@
     assertSubmitter(change2.getChangeId(), 1);
     assertPersonEquals(admin.getIdent(), head.getAuthorIdent());
     assertPersonEquals(admin.getIdent(), head.getCommitterIdent());
+    assertSubmittedTogether(id1, id1, id2);
+    assertSubmittedTogether(id2, id1, id2);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index 6ac34ce..3841c03 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -14,15 +14,11 @@
 
 package com.google.gerrit.acceptance.server.change;
 
-import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.ProjectConfig;
 
@@ -33,8 +29,6 @@
 import org.junit.Test;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
 
 public class SubmittedTogetherIT extends AbstractDaemonTest {
 
@@ -167,20 +161,6 @@
     assertSubmittedTogether(id2);
   }
 
-  private void assertSubmittedTogether(String chId, String... expected)
-      throws Exception {
-    List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
-    assertThat(actual).hasSize(expected.length);
-    assertThat(Arrays.asList(expected))
-        .containsExactlyElementsIn(
-            Iterables.transform(actual, new Function<ChangeInfo, String>() {
-              @Override
-              public String apply(ChangeInfo input) {
-                return input.changeId;
-              }
-            })).inOrder();
-  }
-
   private RevCommit getRemoteHead() throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index ae1a608..9612f71 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -228,11 +228,9 @@
           new TabChangeListCallback(Tab.SAME_TOPIC, info.project(), revision));
     } else {
       // TODO(sbeller): show only on latest revision
-      if (info.status().isOpen()) {
-        ChangeApi.change(info.legacyId().get()).view("submitted_together")
-            .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-                info.project(), revision));
-      }
+      ChangeApi.change(info.legacyId().get()).view("submitted_together")
+          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+              info.project(), revision));
     }
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 0701771..6aa6699 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -453,6 +453,13 @@
   @Column(id = 17, notNull = false)
   protected String originalSubject;
 
+  /**
+   * Unique id for the changes submitted together assigned during merging.
+   * Only set if the status is MERGED.
+   */
+  @Column(id = 18, notNull = false)
+  protected String submissionId;
+
   protected Change() {
   }
 
@@ -479,6 +486,7 @@
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
     originalSubject = other.originalSubject;
+    submissionId = other.submissionId;
     topic = other.topic;
   }
 
@@ -562,6 +570,14 @@
     }
   }
 
+  public String getSubmissionId() {
+    return submissionId;
+  }
+
+  public void setSubmissionId(String id) {
+    this.submissionId = id;
+  }
+
   public Status getStatus() {
     return Status.forCode(status);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index a4f9fef..dec56a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
 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.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -43,14 +48,17 @@
 
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final MergeSuperSet mergeSuperSet;
 
   @Inject
   SubmittedTogether(ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider,
+      Provider<InternalChangeQuery> queryProvider,
       MergeSuperSet mergeSuperSet) {
     this.json = json;
     this.dbProvider = dbProvider;
+    this.queryProvider = queryProvider;
     this.mergeSuperSet = mergeSuperSet;
   }
 
@@ -59,16 +67,32 @@
       throws AuthException, BadRequestException,
       ResourceConflictException, Exception {
     try {
-      ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(),
-          resource.getChange());
-      if (cs.size() > 1) {
-        return json.create(EnumSet.of(
-            ListChangesOption.CURRENT_REVISION,
-            ListChangesOption.CURRENT_COMMIT))
-          .format(cs.ids());
+      Change c = resource.getChange();
+      List<Change.Id> ids;
+      if (c.getStatus().isOpen()) {
+        ChangeSet cs = mergeSuperSet.completeChangeSet(dbProvider.get(), c);
+        ids = cs.ids().asList();
+      } else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
+        ids = Lists.newArrayList();
+        String subId = c.getSubmissionId();
+        if (subId.isEmpty()) {
+          ids = Collections.emptyList();
+        } else {
+          for (ChangeData cd : queryProvider.get().bySubmissionId(subId)) {
+            ids.add(cd.getId());
+          }
+        }
       } else {
-        return Collections.emptyList();
+        // ABANDONED
+        ids = Collections.emptyList();
       }
+      if (ids.size() <= 1) {
+        ids = Collections.emptyList();
+      }
+      return json.create(EnumSet.of(
+          ListChangesOption.CURRENT_REVISION,
+          ListChangesOption.CURRENT_COMMIT))
+        .format(ids);
     } catch (OrmException | IOException e) {
       log.error("Error on getting a ChangeSet", e);
       throw e;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 8ec8cbc..c5367ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -27,6 +27,8 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Table;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -91,6 +93,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -143,7 +147,19 @@
 
   private final Map<Change.Id, List<SubmitRecord>> records;
   private final Map<Change.Id, CodeReviewCommit> commits;
-  private String logPrefix;
+
+  private static final String MACHINE_ID;
+  static {
+    String id;
+    try {
+      id = InetAddress.getLocalHost().getHostAddress();
+    } catch (UnknownHostException e) {
+      id = "unknown";
+    }
+    MACHINE_ID = id;
+  }
+  private String staticSubmissionId;
+  private String submissionId;
 
   private ProjectState destProject;
   private ReviewDb db;
@@ -336,10 +352,19 @@
     }
   }
 
+  private void updateSubmissionId(Change change) {
+    Hasher h = Hashing.sha1().newHasher();
+    h.putLong(Thread.currentThread().getId())
+        .putUnencodedChars(MACHINE_ID);
+    staticSubmissionId = h.hash().toString().substring(0, 8);
+    submissionId = change.getId().get() + "-" + TimeUtil.nowMs() +
+        "-" + staticSubmissionId;
+  }
+
   public void merge(ReviewDb db, Change change, IdentifiedUser caller,
       boolean checkSubmitRules) throws NoSuchChangeException,
       OrmException, ResourceConflictException {
-    logPrefix = String.format("[%s]: ", String.valueOf(change.hashCode()));
+    updateSubmissionId(change);
     this.db = db;
     logDebug("Beginning integration of {}", change);
     try {
@@ -997,6 +1022,7 @@
       @Override
       public Change update(Change c) {
         c.setStatus(Change.Status.MERGED);
+        c.setSubmissionId(submissionId);
         if (!merged.equals(c.currentPatchSetId())) {
           // Uncool; the patch set changed after we merged it.
           // Go back to the patch set that was actually merged.
@@ -1253,28 +1279,28 @@
 
   private void logDebug(String msg, Object... args) {
     if (log.isDebugEnabled()) {
-      log.debug(logPrefix + msg, args);
+      log.debug("[" + submissionId + "]" + msg, args);
     }
   }
 
   private void logWarn(String msg, Throwable t) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg, t);
+      log.warn("[" + submissionId + "]" + msg, t);
     }
   }
 
   private void logWarn(String msg) {
     if (log.isWarnEnabled()) {
-      log.warn(logPrefix + msg);
+      log.warn("[" + submissionId + "]" + msg);
     }
   }
 
   private void logError(String msg, Throwable t) {
     if (log.isErrorEnabled()) {
       if (t != null) {
-        log.error(logPrefix + msg, t);
+        log.error("[" + submissionId + "]" + msg, t);
       } else {
-        log.error(logPrefix + msg);
+        log.error("[" + submissionId + "]" + msg);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index e711306..4fa5cd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -207,6 +207,21 @@
         }
       };
 
+  /** Submission id assigned by MergeOp. */
+  public static final FieldDef<ChangeData, String> SUBMISSIONID =
+      new FieldDef.Single<ChangeData, String>(
+          "submissionid", FieldType.EXACT, false) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getSubmissionId();
+        }
+      };
+
   /** Last update time since January 1, 1970. */
   public static final FieldDef<ChangeData, Timestamp> UPDATED =
       new FieldDef.Single<ChangeData, Timestamp>(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index a8a97a8..4789a14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -377,6 +377,42 @@
       ChangeField.AUTHOR,
       ChangeField.COMMITTER);
 
+  static final Schema<ChangeData> V25 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.SUBMISSIONID,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 31c35c2..bb11c05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -41,6 +41,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -194,6 +195,14 @@
     return query(commit(schema(indexes), id.name()));
   }
 
+  public List<ChangeData> bySubmissionId(String cs) throws OrmException {
+    if (cs.isEmpty()) {
+      return Collections.emptyList();
+    } else {
+      return query(new SubmissionIdPredicate(cs));
+    }
+  }
+
   public List<ChangeData> byProjectGroups(Project.NameKey project,
       Collection<String> groups) throws OrmException {
     List<GroupPredicate> groupPredicates = new ArrayList<>(groups.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
new file mode 100644
index 0000000..3b2dd94
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 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.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+class SubmissionIdPredicate extends IndexPredicate<ChangeData> {
+
+  SubmissionIdPredicate(String changeSet) {
+    super(ChangeField.SUBMISSIONID, changeSet);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    Change change = object.change();
+    if (change == null) {
+      return false;
+    }
+    if (change.getSubmissionId() == null) {
+      return false;
+    }
+    return getValue().equals(change.getSubmissionId());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 769329d..9022bfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_111> C = Schema_111.class;
+  public static final Class<Schema_112> C = Schema_112.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
new file mode 100644
index 0000000..6364c48
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_112.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2015 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_112 extends SchemaVersion {
+  @Inject
+  Schema_112(Provider<Schema_111> prior) {
+    super(prior);
+  }
+}