Merge "Add an interface to allow executing pending draft updates together"
diff --git a/Documentation/images/generated-suggested-edit-added.png b/Documentation/images/generated-suggested-edit-added.png
new file mode 100644
index 0000000..37300c3
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-added.png
Binary files differ
diff --git a/Documentation/images/generated-suggested-edit-preview.png b/Documentation/images/generated-suggested-edit-preview.png
new file mode 100644
index 0000000..9ca82f1
--- /dev/null
+++ b/Documentation/images/generated-suggested-edit-preview.png
Binary files differ
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index a560b84..7dab1ad 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -415,6 +415,13 @@
 * `permissions/ref_filter/skip_filter_count`: Rate of ref filter operations
   where we skip full evaluation because the user can read all refs
 
+=== Validation
+
+* `validation/file_count`: Track number of files per change during commit
+  validation, if it exceeds the FILE_COUNT_WARNING_THRESHOLD threshold.
+** `file_count`: number of files in the patchset
+** `host_repo`: host and repository of the change in the format 'host/repo'
+
 === Reviewer Suggestion
 
 * `reviewer_suggestion/query_accounts`: Latency for querying accounts for
diff --git a/Documentation/user-suggest-edits.txt b/Documentation/user-suggest-edits.txt
index 9c67358..3b2c83a 100644
--- a/Documentation/user-suggest-edits.txt
+++ b/Documentation/user-suggest-edits.txt
@@ -36,6 +36,21 @@
 You can use copy to clipboard button to copy suggestion to clipboard and then you can paste it
 in your editor.
 
+== Generate Suggestion
+
+Following UI needs to be activated by a plugin that implements SuggestionsProvider. Gerrit is providing just UI.
+
+** When a user types a comment, Gerrit queries a plugin for a code snippet. When there is a snippet, the user can see a preview of snippet under comment.
+
+image::images/generated-suggested-edit-preview.png["Generate Suggested Edit", align="center", width=400]
+
+** A user needs to click on "ADD SUGGESTION TO COMMENT" button if they want to use this suggestion. Otherwise the suggestion is never used.
+
+image::images/generated-suggested-edit-added.png["Added Generated Suggested Edit", align="center", width=400]
+
+** By clicking on "ADD SUGGESTION TO COMMENT" button, the suggestion is added to end of comment. The user can then edit the suggestion, if needed.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 6adaae2..45618e5 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -36,6 +36,10 @@
 import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -115,6 +119,7 @@
     private final DiffOperations diffOperations;
     private final Config config;
     private final ChangeUtil changeUtil;
+    private final MetricMaker metricMaker;
 
     @Inject
     Factory(
@@ -131,7 +136,8 @@
         ProjectCache projectCache,
         ProjectConfig.Factory projectConfigFactory,
         DiffOperations diffOperations,
-        ChangeUtil changeUtil) {
+        ChangeUtil changeUtil,
+        MetricMaker metricMaker) {
       this.gerritIdent = gerritIdent;
       this.urlFormatter = urlFormatter;
       this.config = config;
@@ -146,6 +152,7 @@
       this.projectConfigFactory = projectConfigFactory;
       this.diffOperations = diffOperations;
       this.changeUtil = changeUtil;
+      this.metricMaker = metricMaker;
     }
 
     public CommitValidators forReceiveCommits(
@@ -166,7 +173,7 @@
           .add(new ProjectStateValidationListener(projectState))
           .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
-          .add(new FileCountValidator(config, urlFormatter.get(), diffOperations))
+          .add(new FileCountValidator(config, urlFormatter.get(), diffOperations, metricMaker))
           .add(new CommitterUploaderValidator(user, perm, urlFormatter.get()))
           .add(new SignedOffByValidator(user, perm, projectState))
           .add(
@@ -198,7 +205,7 @@
           .add(new ProjectStateValidationListener(projectState))
           .add(new AmendedGerritMergeCommitValidationListener(perm, gerritIdent))
           .add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
-          .add(new FileCountValidator(config, urlFormatter.get(), diffOperations))
+          .add(new FileCountValidator(config, urlFormatter.get(), diffOperations, metricMaker))
           .add(new SignedOffByValidator(user, perm, projectState))
           .add(
               new ChangeIdValidator(
@@ -445,10 +452,25 @@
     private final int maxFileCount;
     private final UrlFormatter urlFormatter;
     private final DiffOperations diffOperations;
+    private final Counter2<Integer, String> metricCountManyFilesPerChange;
 
-    FileCountValidator(Config config, UrlFormatter urlFormatter, DiffOperations diffOperations) {
+    FileCountValidator(
+        Config config,
+        UrlFormatter urlFormatter,
+        DiffOperations diffOperations,
+        MetricMaker metricMaker) {
       this.urlFormatter = urlFormatter;
       this.diffOperations = diffOperations;
+      this.metricCountManyFilesPerChange =
+          metricMaker.newCounter(
+              "validation/file_count",
+              new Description("Count commits with many files per change."),
+              Field.ofInteger("file_count", (meta, value) -> {})
+                  .description("number of files in the patchset")
+                  .build(),
+              Field.ofString("host_repo", (meta, value) -> {})
+                  .description("host and repository of the change in the format 'host/repo'")
+                  .build());
       maxFileCount = config.getInt("change", null, "maxFiles", 100_000);
     }
 
@@ -482,6 +504,9 @@
           logger.atWarning().log(
               "Warning: Change with %d files on host %s, project %s, ref %s",
               changedFiles, host, project, refName);
+
+          this.metricCountManyFilesPerChange.increment(
+              Math.toIntExact(changedFiles), String.format("%s/%s", host, project));
         }
       } catch (DiffNotAvailableException e) {
         // This happens e.g. for cherrypicks.
diff --git a/java/com/google/gerrit/server/patch/DiffFileSizeValidator.java b/java/com/google/gerrit/server/patch/DiffFileSizeValidator.java
index 4001ab4..6964d63 100644
--- a/java/com/google/gerrit/server/patch/DiffFileSizeValidator.java
+++ b/java/com/google/gerrit/server/patch/DiffFileSizeValidator.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Patch.PatchType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.patch.filediff.FileDiffOutput;
@@ -43,8 +44,14 @@
 
   @Override
   public void validate(FileDiffOutput fileDiff) throws LargeObjectException {
-    if (maxFileSize <= 0) {
+    if (maxFileSize <= 0
+        || (fileDiff.patchType().isPresent()
+            && fileDiff.patchType().get().equals(PatchType.BINARY))) {
       // Do not apply limits if the config is not set.
+      // Also do not check file size for binary files. For modified binary files, JGit skips the
+      // diff and returns no edits. On the API layer, we only set the DiffInfo.ContentEntry.skip
+      // parameter to the number of lines in the file and the front-end displays a "Difference in
+      // binary files" in the diff view.
       return;
     }
     if (fileDiff.size() > maxFileSize) {
diff --git a/javatests/com/google/gerrit/server/patch/DiffValidatorsTest.java b/javatests/com/google/gerrit/server/patch/DiffValidatorsTest.java
index 9cc19f5..9f697fd 100644
--- a/javatests/com/google/gerrit/server/patch/DiffValidatorsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffValidatorsTest.java
@@ -74,13 +74,25 @@
     diffValidators.validate(fileDiff);
   }
 
+  @Test
+  public void binaryFileSizeExceeded_notCheckedForFileSize() throws Exception {
+    diffFileSizeValidator.setMaxFileSize(1000);
+    int largeSize = 100000000;
+    FileDiffOutput fileDiff = createFakeFileDiffOutput(largeSize, PatchType.BINARY);
+    diffValidators.validate(fileDiff);
+  }
+
   private FileDiffOutput createFakeFileDiffOutput(int largeSize) {
+    return createFakeFileDiffOutput(largeSize, PatchType.UNIFIED);
+  }
+
+  private FileDiffOutput createFakeFileDiffOutput(int largeSize, PatchType patchType) {
     return FileDiffOutput.builder()
         .oldCommitId(ObjectId.zeroId())
         .newCommitId(ObjectId.zeroId())
         .comparisonType(ComparisonType.againstRoot())
         .changeType(ChangeType.ADDED)
-        .patchType(Optional.of(PatchType.UNIFIED))
+        .patchType(Optional.of(patchType))
         .oldPath(Optional.empty())
         .newPath(Optional.of("f.txt"))
         .oldMode(Optional.empty())
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 1e91345..5dea952 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -76,6 +76,9 @@
 import {formStyles} from '../../../styles/form-styles';
 import {Interaction} from '../../../constants/reporting';
 import {Suggestion} from '../../../api/suggestions';
+import {when} from 'lit/directives/when.js';
+import {getDocUrl} from '../../../utils/url-util';
+import {configModelToken} from '../../../models/config/config-model';
 
 // visible for testing
 export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
@@ -211,7 +214,10 @@
   generatedSuggestion?: Suggestion;
 
   @state()
-  generatedReplacementId?: string;
+  generatedSuggestionId?: string;
+
+  @state()
+  suggestionLoading = false;
 
   @property({type: Boolean, attribute: 'show-patchset'})
   showPatchset = false;
@@ -231,6 +237,8 @@
   @state()
   commentedText?: string;
 
+  @state() private docsBaseUrl = '';
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly reporting = getAppContext().reportingService;
@@ -243,6 +251,8 @@
 
   private readonly getPluginLoader = resolve(this, pluginLoaderToken);
 
+  private readonly getConfigModel = resolve(this, configModelToken);
+
   private readonly flagsService = getAppContext().flagsService;
 
   private readonly shortcuts = new ShortcutController(this);
@@ -338,6 +348,11 @@
         this.autoSave();
       }
     );
+    subscribe(
+      this,
+      () => this.getConfigModel().docsBaseUrl$,
+      docsBaseUrl => (this.docsBaseUrl = docsBaseUrl)
+    );
     if (this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT)) {
       subscribe(
         this,
@@ -550,6 +565,16 @@
           color: var(--selected-foreground);
           margin-right: var(--spacing-xl);
         }
+        /* The basics of .loadingSpin are defined in shared styles. */
+        .loadingSpin {
+          width: calc(var(--line-height-normal) - 2px);
+          height: calc(var(--line-height-normal) - 2px);
+          display: inline-block;
+          vertical-align: top;
+          position: relative;
+          /* Making up for the 2px reduced height above. */
+          top: 1px;
+        }
       `,
     ];
   }
@@ -949,7 +974,7 @@
     return html`<gr-suggestion-diff-preview
       .showAddSuggestionButton=${true}
       .suggestion=${this.generatedSuggestion?.replacement}
-      .uuid=${this.generatedReplacementId}
+      .uuid=${this.generatedSuggestionId}
     ></gr-suggestion-diff-preview>`;
   }
 
@@ -979,8 +1004,26 @@
               );
             }}
           />
-          Generate Suggestion${numberOfSuggestions}
+          Generate Suggestion
+          ${when(
+            this.suggestionLoading,
+            () => html`<span class="loadingSpin"></span>`,
+            () => html`${numberOfSuggestions}`
+          )}
         </label>
+        <a
+          href=${getDocUrl(
+            this.docsBaseUrl,
+            'user-suggest-edits.html#_generate_suggestion'
+          )}
+          target="_blank"
+          rel="noopener noreferrer"
+        >
+          <gr-icon
+            icon="help"
+            title="About Generated Suggested Edits"
+          ></gr-icon>
+        </a>
       </div>
     `;
   }
@@ -1005,10 +1048,11 @@
       this.messageText.length === 0
     )
       return;
-    this.generatedReplacementId = uuid();
+    this.generatedSuggestionId = uuid();
     this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_REQUEST, {
-      uuid: this.generatedReplacementId,
+      uuid: this.generatedSuggestionId,
     });
+    this.suggestionLoading = true;
     const suggestionResponse = await suggestionsPlugins[0].provider.suggestCode(
       {
         prompt: this.messageText,
@@ -1019,11 +1063,12 @@
         lineNumber: this.comment.line,
       }
     );
+    this.suggestionLoading = false;
     // TODO(milutin): The suggestionResponse can contain multiple suggestion
     // options. We pick the first one for now. In future we shouldn't ignore
     // other suggestions.
     this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_RESPONSE, {
-      uuid: this.generatedReplacementId,
+      uuid: this.generatedSuggestionId,
       response: suggestionResponse.responseCode,
       numSuggestions: suggestionResponse.suggestions.length,
       hasNewRange: suggestionResponse.suggestions?.[0]?.newRange !== undefined,
@@ -1130,7 +1175,7 @@
     if (
       changed.has('changeNum') ||
       changed.has('comment') ||
-      changed.has('generatedReplacement')
+      changed.has('generatedSuggestion')
     ) {
       if (
         !this.changeNum ||
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeader.soy b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
index 12b68b6..708d046 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeader.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeader.soy
@@ -17,7 +17,7 @@
 {namespace com.google.gerrit.server.mail.template.ChangeHeader}
 
 {template ChangeHeader kind="text"}
-  {@param attentionSet: list<string>|null}
+  {@param? attentionSet: list<string>|null}
   {if $attentionSet and length($attentionSet) > 0}
     Attention is currently required from:{sp}
     {for $attentionSetUser, $index in $attentionSet}
diff --git a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
index e17e021..d687ce9 100644
--- a/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
+++ b/resources/com/google/gerrit/server/mail/ChangeHeaderHtml.soy
@@ -18,7 +18,7 @@
 {namespace com.google.gerrit.server.mail.template.ChangeHeaderHtml}
 
 {template ChangeHeaderHtml}
-  {@param attentionSet: list<string>|null}
+  {@param? attentionSet: list<string>|null}
   {if $attentionSet and length($attentionSet) > 0}
     <p> Attention is currently required from:{sp}
     {for $attentionSetUser, $index in $attentionSet}