Merge "Introduce sequential success commit messages"
diff --git a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
index 1241585..914f402 100644
--- a/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
+++ b/java/com/google/gerrit/server/git/DefaultChangeReportFormatter.java
@@ -26,6 +26,7 @@
   private static final int SUBJECT_MAX_LENGTH = 80;
   private static final String SUBJECT_CROP_APPENDIX = "...";
   private static final int SUBJECT_CROP_RANGE = 10;
+  private static final String NEW_CHANGE_INDICATOR = " [NEW]";
 
   private final UrlFormatter urlFormatter;
 
@@ -36,7 +37,7 @@
 
   @Override
   public String newChange(ChangeReportFormatter.Input input) {
-    return formatChangeUrl(input);
+    return formatChangeUrl(input) + NEW_CHANGE_INDICATOR;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 02d70b4..39b34df 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -31,7 +31,6 @@
 import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Comparator.comparingInt;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
@@ -175,14 +174,17 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -693,15 +695,51 @@
 
   /** Appends messages for successful change creation/updates. */
   private void queueSuccessMessages(List<CreateRequest> newChanges) {
-    List<CreateRequest> created =
-        newChanges.stream().filter(r -> r.change != null).collect(toList());
-    List<ReplaceRequest> updated =
+    // adjacency list for commit => parent
+    Map<String, String> adjList = new HashMap<>();
+    for (CreateRequest cr : newChanges) {
+      String parent = cr.commit.getParentCount() == 0 ? null : cr.commit.getParent(0).name();
+      adjList.put(cr.commit.name(), parent);
+    }
+    for (ReplaceRequest rr : replaceByChange.values()) {
+      String parent = null;
+      if (rr.revCommit != null) {
+        parent = rr.revCommit.getParentCount() == 0 ? null : rr.revCommit.getParent(0).name();
+      }
+      adjList.put(rr.newCommitId.name(), parent);
+    }
+
+    // get commits that are not parents
+    Set<String> leafs = new TreeSet<>(adjList.keySet());
+    leafs.removeAll(adjList.values());
+    // go backwards from the last commit to its parent(s)
+    Set<String> ordered = new LinkedHashSet<>();
+    for (String leaf : leafs) {
+      if (ordered.contains(leaf)) {
+        continue;
+      }
+      while (leaf != null) {
+        if (!ordered.contains(leaf)) {
+          ordered.add(leaf);
+        }
+        leaf = adjList.get(leaf);
+      }
+    }
+    // reverse the order to start with earliest commit
+    List<String> orderedCommits = new ArrayList<>(ordered);
+    Collections.reverse(orderedCommits);
+
+    Map<String, CreateRequest> created =
+        newChanges
+            .stream()
+            .filter(r -> r.change != null)
+            .collect(Collectors.toMap(r -> r.commit.name(), r -> r));
+    Map<String, ReplaceRequest> updated =
         replaceByChange
             .values()
             .stream()
             .filter(r -> r.inputCommand.getResult() == OK)
-            .sorted(comparingInt(r -> r.notes.getChangeId().get()))
-            .collect(toList());
+            .collect(Collectors.toMap(r -> r.newCommitId.name(), r -> r));
 
     if (created.isEmpty() && updated.isEmpty()) {
       return;
@@ -709,63 +747,68 @@
 
     addMessage("");
     addMessage("SUCCESS");
+    addMessage("");
 
-    if (!created.isEmpty()) {
-      addMessage("");
-      addMessage("New Changes:");
-      for (CreateRequest c : created) {
-        addMessage(
-            changeFormatter.newChange(
-                ChangeReportFormatter.Input.builder().setChange(c.change).build()));
-      }
-    }
-
+    boolean edit = false;
+    Boolean isPrivate = null;
+    Boolean wip = null;
     if (!updated.isEmpty()) {
-      addMessage("");
-      addMessage("Updated Changes:");
-      boolean edit = magicBranch != null && (magicBranch.edit || magicBranch.draft);
-      for (ReplaceRequest u : updated) {
-        String subject;
-        Change change = u.notes.getChange();
-        if (edit) {
-          try {
-            subject = receivePack.getRevWalk().parseCommit(u.newCommitId).getShortMessage();
-          } catch (IOException e) {
-            // Log and fall back to original change subject
-            logger.atWarning().withCause(e).log("failed to get subject for edit patch set");
-            subject = change.getSubject();
-          }
-        } else {
-          subject = u.info.getSubject();
+      edit = magicBranch != null && (magicBranch.edit || magicBranch.draft);
+      if (magicBranch != null) {
+        if (magicBranch.isPrivate) {
+          isPrivate = true;
+        } else if (magicBranch.removePrivate) {
+          isPrivate = false;
         }
-
-        boolean isPrivate = change.isPrivate();
-        boolean wip = change.isWorkInProgress();
-        if (magicBranch != null) {
-          if (magicBranch.isPrivate) {
-            isPrivate = true;
-          } else if (magicBranch.removePrivate) {
-            isPrivate = false;
-          }
-          if (magicBranch.workInProgress) {
-            wip = true;
-          } else if (magicBranch.ready) {
-            wip = false;
-          }
+        if (magicBranch.workInProgress) {
+          wip = true;
+        } else if (magicBranch.ready) {
+          wip = false;
         }
-
-        ChangeReportFormatter.Input input =
-            ChangeReportFormatter.Input.builder()
-                .setChange(change)
-                .setSubject(subject)
-                .setIsEdit(edit)
-                .setIsPrivate(isPrivate)
-                .setIsWorkInProgress(wip)
-                .build();
-        addMessage(changeFormatter.changeUpdated(input));
       }
-      addMessage("");
     }
+
+    for (String commit : orderedCommits) {
+      if (created.get(commit) != null) {
+        addCreatedMessage(created.get(commit));
+      } else if (updated.get(commit) != null) {
+        addReplacedMessage(updated.get(commit), edit, isPrivate, wip);
+      }
+    }
+    addMessage("");
+  }
+
+  private void addCreatedMessage(CreateRequest c) {
+    addMessage(
+        changeFormatter.newChange(
+            ChangeReportFormatter.Input.builder().setChange(c.change).build()));
+  }
+
+  private void addReplacedMessage(ReplaceRequest u, boolean edit, Boolean isPrivate, Boolean wip) {
+    String subject;
+    if (edit) {
+      subject =
+          u.revCommit == null ? u.notes.getChange().getSubject() : u.revCommit.getShortMessage();
+    } else {
+      subject = u.info.getSubject();
+    }
+
+    if (isPrivate == null) {
+      isPrivate = u.notes.getChange().isPrivate();
+    }
+    if (wip == null) {
+      wip = u.notes.getChange().isWorkInProgress();
+    }
+
+    ChangeReportFormatter.Input input =
+        ChangeReportFormatter.Input.builder()
+            .setChange(u.notes.getChange())
+            .setSubject(subject)
+            .setIsEdit(edit)
+            .setIsPrivate(isPrivate)
+            .setIsWorkInProgress(wip)
+            .build();
+    addMessage(changeFormatter.changeUpdated(input));
   }
 
   private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
@@ -2556,6 +2599,7 @@
     final ObjectId newCommitId;
     final ReceiveCommand inputCommand;
     final boolean checkMergedInto;
+    RevCommit revCommit;
     ChangeNotes notes;
     BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet.Id psId;
@@ -2573,6 +2617,11 @@
       this.inputCommand = requireNonNull(cmd);
       this.checkMergedInto = checkMergedInto;
 
+      try {
+        revCommit = receivePack.getRevWalk().parseCommit(newCommitId);
+      } catch (IOException e) {
+        revCommit = null;
+      }
       revisions = HashBiMap.create();
       for (Ref ref : refs(toChange)) {
         try {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 143a4a4..3e26cab 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -136,6 +136,7 @@
 
   @Inject private RequestScopeOperations requestScopeOperations;
 
+  private static String NEW_CHANGE_INDICATOR = " [NEW]";
   private LabelType patchSetLock;
 
   @BeforeClass
@@ -376,7 +377,7 @@
     r1.assertOkStatus();
     r1.assertChange(Change.Status.NEW, null);
     r1.assertMessage(
-        "New changes:\n  " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n");
+        url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
 
     testRepo.reset(initialHead);
     String newMsg = r1.getCommit().getShortMessage() + " v2";
@@ -396,18 +397,17 @@
     r2.assertMessage(
         "success\n"
             + "\n"
-            + "New changes:\n"
-            + "  "
-            + url
-            + id2
-            + " another commit\n"
-            + "\n"
-            + "Updated changes:\n"
             + "  "
             + url
             + id1
             + " "
             + newMsg
+            + "\n"
+            + "  "
+            + url
+            + id2
+            + " another commit"
+            + NEW_CHANGE_INDICATOR
             + "\n");
   }
 
@@ -898,8 +898,7 @@
     assertThat(edit).isPresent();
     EditInfo editInfo = edit.get();
     r.assertMessage(
-        "Updated Changes:\n  "
-            + canonicalWebUrl.get()
+        canonicalWebUrl.get()
             + "c/"
             + project.get()
             + "/+/"
@@ -2412,6 +2411,74 @@
         .contains("warning: " + amended.abbreviate(7).name() + ": no files changed, was rebased");
   }
 
+  @Test
+  public void sequentialCommitMessages() throws Exception {
+    String url = canonicalWebUrl.get() + "c/" + project.get() + "/+/";
+    ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
+
+    PushOneCommit.Result r1 = pushTo("refs/for/master");
+    Change.Id id1 = r1.getChange().getId();
+    r1.assertOkStatus();
+    r1.assertChange(Change.Status.NEW, null);
+    r1.assertMessage(
+        url + id1 + " " + r1.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    PushOneCommit.Result r2 = pushTo("refs/for/master");
+    Change.Id id2 = r2.getChange().getId();
+    r2.assertOkStatus();
+    r2.assertChange(Change.Status.NEW, null);
+    r2.assertMessage(
+        url + id2 + " " + r2.getCommit().getShortMessage() + NEW_CHANGE_INDICATOR + "\n");
+
+    testRepo.reset(initialHead);
+
+    // rearrange the commit so that change no. 2 is the parent of change no. 1
+    String r1Message = "Position 2";
+    String r2Message = "Position 1";
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r2Message)
+        .insertChangeId(r2.getChangeId().substring(1))
+        .create();
+    testRepo
+        .branch("HEAD")
+        .commit()
+        .message(r1Message)
+        .insertChangeId(r1.getChangeId().substring(1))
+        .create();
+
+    PushOneCommit.Result r3 =
+        pushFactory
+            .create(admin.getIdent(), testRepo, "another commit", "b.txt", "bbb")
+            .to("refs/for/master");
+    Change.Id id3 = r3.getChange().getId();
+    r3.assertOkStatus();
+    r3.assertChange(Change.Status.NEW, null);
+    // should display commit r2, r1, r3 in that order.
+    r3.assertMessage(
+        "success\n"
+            + "\n"
+            + "  "
+            + url
+            + id2
+            + " "
+            + r2Message
+            + "\n"
+            + "  "
+            + url
+            + id1
+            + " "
+            + r1Message
+            + "\n"
+            + "  "
+            + url
+            + id3
+            + " another commit"
+            + NEW_CHANGE_INDICATOR
+            + "\n");
+  }
+
   private DraftInput newDraft(String path, int line, String message) {
     DraftInput d = new DraftInput();
     d.path = path;