Merge "google-java-format: Add support for version 1.22.0"
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index fa30c87..b0e1b49 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2514,6 +2514,53 @@
   ]
 ----
 
+SortBy(sortby)::
+Sort the returned tags by one of the supported sort options: ref (default), creation_time.
++
+.Request
+----
+  GET /projects/work%2Fmy-project/tags?sortby=creation_time HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    }
+  ]
+----
+
 Substring(m)::
 Limit the results to those tags that match the specified substring.
 +
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index fa94e0d..0b1b6b0 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.common.BatchLabelInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.SubmitRequirementInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -76,6 +77,7 @@
     protected String substring;
     protected String regex;
     protected String nextPageToken;
+    protected ListTagSortOption sortBy = ListTagSortOption.REF;
 
     public abstract List<T> get() throws RestApiException;
 
@@ -94,6 +96,11 @@
       return this;
     }
 
+    public ListRefsRequest<T> withSortBy(ListTagSortOption sortBy) {
+      this.sortBy = sortBy;
+      return this;
+    }
+
     public ListRefsRequest<T> withNextPageToken(String token) {
       this.nextPageToken = token;
       return this;
@@ -121,6 +128,10 @@
       return descendingOrder;
     }
 
+    public ListTagSortOption getSortBy() {
+      return sortBy;
+    }
+
     public String getNextPageToken() {
       return nextPageToken;
     }
diff --git a/java/com/google/gerrit/extensions/common/ListTagSortOption.java b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
new file mode 100644
index 0000000..2140924
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/ListTagSortOption.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2024 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.extensions.common;
+
+public enum ListTagSortOption {
+  REF,
+  CREATION_TIME,
+}
diff --git a/java/com/google/gerrit/server/CmdLineParserModule.java b/java/com/google/gerrit/server/CmdLineParserModule.java
index be6b4cd8..c523a29 100644
--- a/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -18,12 +18,14 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.common.ListTagSortOption;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.args4j.AccountGroupIdHandler;
 import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.server.args4j.AccountIdHandler;
 import com.google.gerrit.server.args4j.ChangeIdHandler;
 import com.google.gerrit.server.args4j.InstantHandler;
+import com.google.gerrit.server.args4j.ListTagSortOptionHandler;
 import com.google.gerrit.server.args4j.ObjectIdHandler;
 import com.google.gerrit.server.args4j.PatchSetIdHandler;
 import com.google.gerrit.server.args4j.ProjectHandler;
@@ -54,6 +56,7 @@
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectState.class, ProjectHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
+    registerOptionHandler(ListTagSortOption.class, ListTagSortOptionHandler.class);
   }
 
   private <T> void registerOptionHandler(Class<T> type, Class<? extends OptionHandler<T>> impl) {
diff --git a/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java b/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java
new file mode 100644
index 0000000..9359ca1
--- /dev/null
+++ b/java/com/google/gerrit/server/args4j/ListTagSortOptionHandler.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 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.args4j;
+
+import static com.google.gerrit.util.cli.Localizable.localizable;
+
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ListTagSortOptionHandler extends OptionHandler<ListTagSortOption> {
+  @Inject
+  public ListTagSortOptionHandler(
+      @Assisted CmdLineParser parser,
+      @Assisted OptionDef option,
+      @Assisted Setter<ListTagSortOption> setter) {
+    super(parser, option, setter);
+  }
+
+  @Override
+  public int parseArguments(Parameters params) throws CmdLineException {
+    String param = params.getParameter(0);
+    try {
+      setter.addValue(ListTagSortOption.valueOf(param.toUpperCase()));
+      return 1;
+    } catch (IllegalArgumentException e) {
+      throw new CmdLineException(
+          owner, localizable("\"%s\" is not a valid sort option: %s"), param, e.getMessage());
+    }
+  }
+
+  @Override
+  public String getDefaultMetaVariable() {
+    return "SORT";
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 5eca1fc..24bbb24 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.gerrit.entities.RefNames.isConfigRef;
-import static java.util.Comparator.comparing;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
 import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -59,6 +59,7 @@
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
   private final WebLinks links;
+  private final TagSorter tagSorter;
 
   @Option(
       name = "--limit",
@@ -81,7 +82,7 @@
   @Option(
       name = "--descending",
       aliases = {"-d"},
-      usage = "sort the returned tags in descending order")
+      usage = "return the tags in descending order")
   public void setDescendingOrder(boolean descendingOrder) {
     this.descendingOrder = descendingOrder;
   }
@@ -104,18 +105,31 @@
     this.matchRegex = matchRegex;
   }
 
+  @Option(
+      name = "--sort-by",
+      aliases = {"-sortby"},
+      usage = "sort the tags")
+  private void setSortBy(ListTagSortOption sortBy) {
+    this.sortBy = sortBy;
+  }
+
   private int limit;
   private int start;
   private boolean descendingOrder;
   private String matchSubstring;
   private String matchRegex;
+  private ListTagSortOption sortBy = ListTagSortOption.REF;
 
   @Inject
   public ListTags(
-      GitRepositoryManager repoManager, PermissionBackend permissionBackend, WebLinks webLinks) {
+      GitRepositoryManager repoManager,
+      PermissionBackend permissionBackend,
+      WebLinks webLinks,
+      TagSorter tagSorter) {
     this.repoManager = repoManager;
     this.permissionBackend = permissionBackend;
     this.links = webLinks;
+    this.tagSorter = tagSorter;
   }
 
   public ListTags request(ListRefsRequest<TagInfo> request) {
@@ -124,6 +138,7 @@
     this.setDescendingOrder(request.getDescendingOrder());
     this.setMatchSubstring(request.getSubstring());
     this.setMatchRegex(request.getRegex());
+    this.setSortBy(request.getSortBy());
     return this;
   }
 
@@ -147,7 +162,7 @@
       }
     }
 
-    tags.sort(comparing(t -> t.ref));
+    tagSorter.sort(sortBy, tags, descendingOrder);
     if (descendingOrder) {
       Collections.reverse(tags);
     }
diff --git a/java/com/google/gerrit/server/restapi/project/TagSorter.java b/java/com/google/gerrit/server/restapi/project/TagSorter.java
new file mode 100644
index 0000000..4776ce1
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/TagSorter.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2024 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.restapi.project;
+
+import static java.util.Comparator.comparing;
+
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.inject.Inject;
+import java.sql.Timestamp;
+import java.util.Comparator;
+import java.util.List;
+
+public class TagSorter {
+  @Inject
+  public TagSorter() {}
+
+  /** Sort the tags by the given sort option, in place */
+  public void sort(ListTagSortOption sortBy, List<TagInfo> tags, boolean descendingOrder) {
+    switch (sortBy) {
+      case CREATION_TIME:
+        Comparator<Timestamp> nullsComparator =
+            descendingOrder
+                ? Comparator.nullsFirst(Comparator.naturalOrder())
+                : Comparator.nullsLast(Comparator.naturalOrder());
+        tags.sort(comparing(t -> t.created, nullsComparator));
+        break;
+      case REF:
+      default:
+        tags.sort(comparing(t -> t.ref));
+        break;
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index b69ca1c..e2809b0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.entities.AccessSection;
@@ -35,6 +36,7 @@
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
+import com.google.gerrit.extensions.common.ListTagSortOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -120,6 +122,7 @@
   }
 
   @Test
+  @UseClockStep
   public void listTags() throws Exception {
     createTags();
 
@@ -160,6 +163,19 @@
     // with descending order
     result = getTags().withDescendingOrder(true).get();
     assertTagList(FluentIterable.from(Lists.reverse(testTags)), result);
+
+    // with sortBy creation time
+    result = getTags().withSortBy(ListTagSortOption.CREATION_TIME).get();
+    assertTagList(FluentIterable.from(Lists.reverse(testTags)), result);
+
+    // with sortBy, descending order and limit
+    result =
+        getTags()
+            .withDescendingOrder(true)
+            .withLimit(2)
+            .withSortBy(ListTagSortOption.CREATION_TIME)
+            .get();
+    assertTagList(FluentIterable.from(ImmutableList.of("tag-A", "tag-B")), result);
   }
 
   @Test
@@ -481,9 +497,10 @@
     TagInput input = new TagInput();
     input.revision = revision;
 
-    for (String tagname : testTags) {
+    // Creating the tags in reverse order to allow testing the sortBy option
+    for (String tagname : Lists.reverse(testTags)) {
+      input.message = tagname; // This updates the 'created' time of the tag
       TagInfo result = tag(tagname).create(input).get();
-      assertThat(result.revision).isEqualTo(input.revision);
       assertThat(result.ref).isEqualTo(R_TAGS + tagname);
     }
   }
diff --git a/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java b/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java
new file mode 100644
index 0000000..9a926a2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/restapi/project/TagSorterTest.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2024 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.restapi.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.extensions.api.projects.TagInfo;
+import com.google.gerrit.extensions.common.ListTagSortOption;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TagSorterTest {
+  private static final String revision = "dfdd715e31db256dfba48239f83f9b8da4bc243f";
+  private static final boolean canDelete = true;
+  private static final List<WebLinkInfo> webLinks = new ArrayList<>();
+  private static final TagSorter tagSorter = new TagSorter();
+  private List<TagInfo> tags;
+
+  @Before
+  public void initializeTags() {
+    tags = createTags();
+  }
+
+  @Test
+  public void testSortTagsByRef() {
+    tagSorter.sort(ListTagSortOption.REF, tags, false);
+
+    assertThat(tags.get(0).ref).isEqualTo("refs/tags/v1.0");
+    assertThat(tags.get(1).ref).isEqualTo("refs/tags/v2.0");
+    assertThat(tags.get(2).ref).isEqualTo("refs/tags/v3.0");
+    assertThat(tags.get(3).ref).isEqualTo("refs/tags/v4.0");
+  }
+
+  @Test
+  public void testSortTagsByCreationTime() {
+    tagSorter.sort(ListTagSortOption.CREATION_TIME, tags, false);
+
+    assertThat(tags.get(0).ref).isEqualTo("refs/tags/v2.0");
+    assertThat(tags.get(1).ref).isEqualTo("refs/tags/v3.0");
+    assertThat(tags.get(2).ref).isEqualTo("refs/tags/v1.0");
+    assertThat(tags.get(3).ref).isEqualTo("refs/tags/v4.0");
+  }
+
+  @Test
+  public void testSortTagsByCreationTimeDescendingOrder() {
+    tagSorter.sort(ListTagSortOption.CREATION_TIME, tags, true);
+
+    assertThat(tags.get(0).ref).isEqualTo("refs/tags/v4.0");
+    assertThat(tags.get(1).ref).isEqualTo("refs/tags/v2.0");
+    assertThat(tags.get(2).ref).isEqualTo("refs/tags/v3.0");
+    assertThat(tags.get(3).ref).isEqualTo("refs/tags/v1.0");
+  }
+
+  private List<TagInfo> createTags() {
+    Instant t1 = Instant.now();
+    Instant t2 = t1.minusSeconds(10);
+    Instant t3 = t1.minusSeconds(1);
+
+    List<TagInfo> tags = new ArrayList<>();
+    tags.add(new TagInfo("refs/tags/v1.0", revision, canDelete, webLinks, t1));
+    tags.add(new TagInfo("refs/tags/v2.0", revision, canDelete, webLinks, t2));
+    tags.add(new TagInfo("refs/tags/v3.0", revision, canDelete, webLinks, t3));
+    tags.add(new TagInfo("refs/tags/v4.0", revision, canDelete, webLinks, (Instant) null));
+
+    return tags;
+  }
+}
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index dded0c6..37a17ba 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -146,6 +146,7 @@
   GENERATE_SUGGESTION_ENABLED = 'generate_suggestion_enabled',
   // User disabled generating suggestions
   GENERATE_SUGGESTION_DISABLED = 'generate_suggestion_disabled',
+  GENERATE_SUGGESTION_EDITED = 'generate_suggestion_edited',
   START_REVIEW = 'start-review',
   CODE_REVIEW_APPROVAL = 'code-review-approval',
   FILE_LIST_DIFF_COLLAPSED = 'file-list-diff-collapsed',
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 16a783c..48d7b1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -232,6 +232,9 @@
   generatedSuggestionId?: string;
 
   @state()
+  addedGeneratedSuggestion?: string;
+
+  @state()
   suggestionsProvider?: SuggestionsProvider;
 
   @state()
@@ -1149,9 +1152,10 @@
 
   private handleAddGeneratedSuggestion(code: string) {
     const addNewLine = this.messageText.length !== 0;
-    this.messageText += `${
+    this.addedGeneratedSuggestion = `${
       addNewLine ? '\n' : ''
     }${USER_SUGGESTION_START_PATTERN}${code}${'\n```'}`;
+    this.messageText += this.addedGeneratedSuggestion;
   }
 
   private generateSuggestEdit() {
@@ -1665,6 +1669,7 @@
     } else {
       // No need to make a backend call when nothing has changed.
       while (this.somethingToSave()) {
+        this.trackGeneratedSuggestionEdit();
         this.comment = await this.rawSave({showToast: true});
         if (isError(this.comment)) return;
       }
@@ -1746,6 +1751,19 @@
     );
     this.closeDeleteCommentModal();
   }
+
+  private trackGeneratedSuggestionEdit() {
+    const wasGeneratedSuggestionEdited =
+      this.addedGeneratedSuggestion &&
+      !this.messageText.includes(this.addedGeneratedSuggestion);
+    if (wasGeneratedSuggestionEdited) {
+      this.reporting.reportInteraction(Interaction.GENERATE_SUGGESTION_EDITED, {
+        uuid: this.generatedSuggestionId,
+        commentId: this.comment?.id ?? '',
+      });
+      this.addedGeneratedSuggestion = undefined;
+    }
+  }
 }
 
 declare global {