Merge "Add some logging for spurious diff closing issue"
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 556f42f..c8d93f8 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -37,8 +37,8 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.time.TimeUtil;
-import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
@@ -67,8 +67,9 @@
  *       refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain
  *       in Git and not get deleted. These refs point to an empty tree. We delete such refs.
  *   <li>Inspecting all draft-comment refs. Check for each draft if there exists a published comment
- *       with the same UUID. For now this runs in logging-only mode and does not remove these zombie
- *       drafts.
+ *       with the same UUID. These comments are called zombie drafts. If the program is run in
+ *       {@link #dryRun} mode, the zombie draft IDs will only be logged for tracking, otherwise they
+ *       will also be deleted.
  * </uL>
  */
 public class DeleteZombieCommentsRefs {
@@ -81,6 +82,15 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final int cleanupPercentage;
+
+  /**
+   * Run the logic in dry run mode only. That is, detected zombie drafts will be logged only but not
+   * deleted. Creators of this class can use {@link Factory#create(int, boolean)} to specify the dry
+   * run mode. If {@link Factory#create(int)} is used, the dry run mode will be set to its default:
+   * true.
+   */
+  private final boolean dryRun;
+
   private final Consumer<String> uiConsumer;
   @Nullable private final DraftCommentNotes.Factory draftNotesFactory;
   @Nullable private final ChangeNotes.Factory changeNotesFactory;
@@ -90,9 +100,11 @@
 
   public interface Factory {
     DeleteZombieCommentsRefs create(int cleanupPercentage);
+
+    DeleteZombieCommentsRefs create(int cleanupPercentage, boolean dryRun);
   }
 
-  @Inject
+  @AssistedInject
   public DeleteZombieCommentsRefs(
       AllUsersName allUsers,
       GitRepositoryManager repoManager,
@@ -106,6 +118,31 @@
         allUsers,
         repoManager,
         cleanupPercentage,
+        /* dryRun= */ true,
+        (msg) -> {},
+        changeNotesFactory,
+        draftNotesFactory,
+        commentsUtil,
+        changeUpdateFactory,
+        userFactory);
+  }
+
+  @AssistedInject
+  public DeleteZombieCommentsRefs(
+      AllUsersName allUsers,
+      GitRepositoryManager repoManager,
+      ChangeNotes.Factory changeNotesFactory,
+      DraftCommentNotes.Factory draftNotesFactory,
+      CommentsUtil commentsUtil,
+      ChangeUpdate.Factory changeUpdateFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      @Assisted Integer cleanupPercentage,
+      @Assisted boolean dryRun) {
+    this(
+        allUsers,
+        repoManager,
+        cleanupPercentage,
+        dryRun,
         (msg) -> {},
         changeNotesFactory,
         draftNotesFactory,
@@ -119,13 +156,24 @@
       GitRepositoryManager repoManager,
       Integer cleanupPercentage,
       Consumer<String> uiConsumer) {
-    this(allUsers, repoManager, cleanupPercentage, uiConsumer, null, null, null, null, null);
+    this(
+        allUsers,
+        repoManager,
+        cleanupPercentage,
+        /* dryRun= */ false,
+        uiConsumer,
+        null,
+        null,
+        null,
+        null,
+        null);
   }
 
   private DeleteZombieCommentsRefs(
       AllUsersName allUsers,
       GitRepositoryManager repoManager,
       Integer cleanupPercentage,
+      boolean dryRun,
       Consumer<String> uiConsumer,
       @Nullable ChangeNotes.Factory changeNotesFactory,
       @Nullable DraftCommentNotes.Factory draftNotesFactory,
@@ -135,6 +183,7 @@
     this.allUsers = allUsers;
     this.repoManager = repoManager;
     this.cleanupPercentage = (cleanupPercentage == null) ? 100 : cleanupPercentage;
+    this.dryRun = dryRun;
     this.uiConsumer = uiConsumer;
     this.draftNotesFactory = draftNotesFactory;
     this.changeNotesFactory = changeNotesFactory;
@@ -168,6 +217,12 @@
               .collect(toImmutableList());
       logInfo(String.format("Number of zombie refs to be cleaned = %d", zombieRefs.size()));
 
+      if (dryRun) {
+        logInfo(
+            "Running in dry run mode. Skipping deletion of draft refs pointing to an empty tree.");
+        return;
+      }
+
       long zombieRefsCnt = zombieRefs.size();
       long deletedRefsCnt = 0;
       long startTime = System.currentTimeMillis();
@@ -241,7 +296,7 @@
                       "Draft comment with uuid '%s' of change %s, account %s, written on %s,"
                           + " is a zombie draft that is already published.",
                       zombieDraft.key.uuid, changeId, accountId, zombieDraft.writtenOn));
-          if (!zombieDrafts.isEmpty()) {
+          if (!zombieDrafts.isEmpty() && !dryRun) {
             deleteZombieComments(accountId, notes, zombieDrafts);
           }
           numZombies += zombieDrafts.size();
@@ -284,6 +339,9 @@
         changeUpdateFactory.create(changeNotes, userFactory.create(accountId), TimeUtil.now());
     draftsToDelete.forEach(c -> changeUpdate.deleteComment(c));
     changeUpdate.commit();
+    logger.atInfo().log(
+        "Deleted zombie draft comments with UUIDs %s",
+        draftsToDelete.stream().map(d -> d.key.uuid).collect(Collectors.toList()));
   }
 
   /**
diff --git a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
index 25f01e3..1eef944 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
@@ -30,10 +30,12 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.server.notedb.ChangeNoteJson;
 import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.gson.JsonParser;
 import com.google.inject.Inject;
 import java.util.List;
 import org.apache.commons.lang3.reflect.TypeLiteral;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Ref;
@@ -42,13 +44,35 @@
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.Before;
 import org.junit.Test;
 
 /** Test for {@link com.google.gerrit.server.notedb.DeleteZombieCommentsRefs}. */
 public class DeleteZombieDraftIT extends AbstractDaemonTest {
+  private static final String TEST_PARAMETER_MARKER = "test_only_parameter";
 
   @Inject private DeleteZombieCommentsRefs.Factory deleteZombieDraftsFactory;
   @Inject private ChangeNoteJson changeNoteJson;
+  private boolean dryRun;
+
+  @ConfigSuite.Default
+  public static Config dryRunMode() {
+    Config config = new Config();
+    config.setBoolean(TEST_PARAMETER_MARKER, null, "dryRun", true);
+    return config;
+  }
+
+  @ConfigSuite.Config
+  public static Config deleteMode() {
+    Config config = new Config();
+    config.setBoolean(TEST_PARAMETER_MARKER, null, "dryRun", false);
+    return config;
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    dryRun = baseConfig.getBoolean(TEST_PARAMETER_MARKER, "dryRun", true);
+  }
 
   @Test
   public void draftRefWithOneZombie() throws Exception {
@@ -73,9 +97,13 @@
 
     // Run the cleanup logic. The zombie draft is cleared. The published comment is untouched.
     DeleteZombieCommentsRefs worker =
-        deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100);
+        deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
     assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
-    assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).isEmpty();
+    if (dryRun) {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).hasSize(1);
+    } else {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), revId)).isEmpty();
+    }
     assertNumPublishedComments(changeId, 1);
   }
 
@@ -105,17 +133,25 @@
 
     // Run the zombie cleanup logic. Zombie draft ref for PS2 will be removed.
     DeleteZombieCommentsRefs worker =
-        deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100);
+        deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100, dryRun);
     assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(1);
     assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
-    assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
+    if (dryRun) {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
+    } else {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
+    }
     assertNumPublishedComments(changeId, 1);
 
     // Re-run the worker: nothing happens.
-    assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(0);
+    assertThat(worker.deleteDraftCommentsThatAreAlsoPublished()).isEqualTo(dryRun ? 1 : 0);
     assertNumDrafts(changeId, 1);
     assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r1.getCommit().name())).hasSize(1);
-    assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
+    if (dryRun) {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).hasSize(1);
+    } else {
+      assertThat(getDraftsByParsingDraftRef(draftRef.getName(), r2.getCommit().name())).isEmpty();
+    }
     assertNumPublishedComments(changeId, 1);
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index cc8d6c2..ff5bcff 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -120,9 +120,6 @@
           margin-top: var(--spacing-m);
           padding: var(--spacing-m) var(--spacing-xl) 0;
         }
-        .section.description > .sectionContent {
-          white-space: pre-wrap;
-        }
       `,
     ];
   }
@@ -180,7 +177,12 @@
       <div class="sectionIcon">
         <iron-icon icon="gr-icons:description"></iron-icon>
       </div>
-      <div class="sectionContent">${description}</div>
+      <div class="sectionContent">
+        <gr-formatted-text
+          noTrailingMargin
+          .content=${description}
+        ></gr-formatted-text>
+      </div>
     </div>`;
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 7670ee80..2ed4bc7 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -207,7 +207,9 @@
           <div class="sectionIcon">
             <iron-icon icon="gr-icons:description"> </iron-icon>
           </div>
-          <div class="sectionContent">Test Description</div>
+          <div class="sectionContent">
+            <gr-formatted-text notrailingmargin=""></gr-formatted-text>
+          </div>
         </div>
         <div class="button">
           <gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
index aa923b6..af58802 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
@@ -87,7 +87,12 @@
       <div class="sectionIcon">
         <iron-icon icon="gr-icons:description"></iron-icon>
       </div>
-      <div class="sectionContent">${description}</div>
+      <div class="sectionContent">
+        <gr-formatted-text
+          noTrailingMargin
+          .content=${description}
+        ></gr-formatted-text>
+      </div>
     </div>`;
   }
 }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index c982e2b..314b96b 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -99,6 +99,9 @@
     row.setAttribute('right-type', rightLine.type);
     // TabIndex makes screen reader read a row when navigating with j/k
     row.tabIndex = -1;
+    // This is small hack to enable screen reader to read a row
+    row.setAttribute('role', 'button');
+    row.setAttribute('aria-roledescription', 'Code line');
 
     row.appendChild(this.createBlameCell(leftLine.beforeNumber));