Merge "Add hovercard-accounts to avatar stacks."
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2e175e6..32c05b0 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1672,6 +1672,16 @@
 +
 Default is 5 minutes.
 
+[[change.diff3ConflictView]]change.diff3ConflictView::
++
+Use the diff3 formatter for merge commits with conflicts. With diff3 when
+the conflicts are shown in the "Auto Merge" view, the base section from the
+common parents will be shown as well.
+This setting takes effect when generating the automerge, which happens on upload.
+Changing the setting leaves existing changes unaffected.
++
+Default is false.
+
 [[changeCleanup]]
 === Section changeCleanup
 
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 9e71df7..528be41 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -139,11 +139,16 @@
   )]}'
   {
     "some-project": {
-      "id": "some-project"
+      "id": "some-project",
+      _more_projects: true
     }
   }
 ----
 
+If the number of projects matching the query exceeds either the
+internal limit or a supplied `limit` query parameter, the last project
+object has a `_more_projects: true` JSON field set.
+
 
 [[suggest-projects]]
 Prefix(p)::
@@ -420,6 +425,10 @@
   GET /projects/?query=<query>&limit=25 HTTP/1.0
 ----
 
+If the number of projects matching the query exceeds either the
+internal limit or a supplied `limit` query parameter, the last project
+object has a `_more_projects: true` JSON field set.
+
 The `/projects/` URL also accepts a start integer in the `start`
 parameter. The results will skip `start` projects from project list.
 
@@ -4370,28 +4379,31 @@
 The `ProjectInfo` entity contains information about a project.
 
 [options="header",cols="1,^2,4"]
-|===========================
-|Field Name    ||Description
-|`id`          ||The URL encoded project name.
-|`name`        |
+|=============================
+|Field Name      ||Description
+|`id`            ||The URL encoded project name.
+|`name`          |
 not set if returned in a map where the project name is used as map key|
 The name of the project.
-|`parent`      |optional|
+|`parent`        |optional|
 The name of the parent project. +
 `?-<n>` if the parent project is not visible (`<n>` is a number which
 is increased for each non-visible project).
-|`description` |optional|The description of the project.
-|`state`       |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
-|`branches`    |optional|Map of branch names to HEAD revisions.
-|`labels`      |optional|
+|`description`   |optional|The description of the project.
+|`state`         |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
+|`branches`      |optional|Map of branch names to HEAD revisions.
+|`labels`        |optional|
 Map of label names to
 link:#label-type-info[LabelTypeInfo] entries.
 This field is filled for link:#create-project[Create Project] and
 link:#get-project[Get Project] calls.
-|`web_links`   |optional|
+|`web_links`     |optional|
 Links to the project in external sites as a list of
 link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
-|===========================
+|`_more_projects`|optional, not set if `false`|
+Whether the query would deliver more results if not limited. +
+Only set on the last project that is returned.
+|=============================
 
 [[project-input]]
 === ProjectInput
diff --git a/java/com/google/gerrit/extensions/common/ProjectInfo.java b/java/com/google/gerrit/extensions/common/ProjectInfo.java
index 46b2599..2b00710 100644
--- a/java/com/google/gerrit/extensions/common/ProjectInfo.java
+++ b/java/com/google/gerrit/extensions/common/ProjectInfo.java
@@ -27,4 +27,10 @@
   public Map<String, String> branches;
   public List<WebLinkInfo> webLinks;
   public Map<String, LabelTypeInfo> labels;
+
+  /**
+   * Whether the query would deliver more results if not limited. Only set on the last project that
+   * is returned as a query result.
+   */
+  public Boolean _moreProjects;
 }
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index a522a2f..73aec64 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -286,7 +286,6 @@
     return commit;
   }
 
-  @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
   public static ObjectId mergeWithConflicts(
       RevWalk rw,
       ObjectInserter ins,
@@ -297,6 +296,21 @@
       RevCommit theirs,
       Map<String, MergeResult<? extends Sequence>> mergeResults)
       throws IOException {
+    return mergeWithConflicts(rw, ins, dc, oursName, ours, theirsName, theirs, mergeResults, false);
+  }
+
+  @SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
+  public static ObjectId mergeWithConflicts(
+      RevWalk rw,
+      ObjectInserter ins,
+      DirCache dc,
+      String oursName,
+      RevCommit ours,
+      String theirsName,
+      RevCommit theirs,
+      Map<String, MergeResult<? extends Sequence>> mergeResults,
+      boolean diff3Format)
+      throws IOException {
     rw.parseBody(ours);
     rw.parseBody(theirs);
     String oursMsg = ours.getShortMessage();
@@ -324,7 +338,11 @@
       try {
         // TODO(dborowitz): Respect inCoreLimit here.
         buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
-        fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+        if (diff3Format) {
+          fmt.formatMergeDiff3(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+        } else {
+          fmt.formatMerge(buf, p, "BASE", oursNameFormatted, theirsNameFormatted, UTF_8);
+        }
         buf.close(); // Flush file and close for writes, but leave available for reading.
 
         try (InputStream in = buf.openInputStream()) {
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 0818f23..e27faf6 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -83,6 +83,10 @@
     return cfg.getBoolean("change", null, "cacheAutomerge", true);
   }
 
+  public static boolean diff3ConflictView(Config cfg) {
+    return cfg.getBoolean("change", null, "diff3ConflictView", false);
+  }
+
   private enum OperationType {
     CACHE_LOAD,
     IN_MEMORY_WRITE,
@@ -93,6 +97,7 @@
   private final Timer1<OperationType> latency;
   private final Provider<PersonIdent> gerritIdentProvider;
   private final boolean save;
+  private final boolean useDiff3;
   private final ThreeWayMergeStrategy configuredMergeStrategy;
 
   @Inject
@@ -117,6 +122,7 @@
                 .setUnit("milliseconds"),
             operationTypeField);
     this.save = cacheAutomerge(cfg);
+    this.useDiff3 = diff3ConflictView(cfg);
     this.gerritIdentProvider = gerritIdentProvider;
     this.configuredMergeStrategy = MergeUtil.getMergeStrategy(cfg);
   }
@@ -254,7 +260,8 @@
               merge.getParent(0),
               "BRANCH",
               merge.getParent(1),
-              m.getMergeResults());
+              m.getMergeResults(),
+              useDiff3);
     }
     logger.atFine().log("AutoMerge treeId=%s", treeId.name());
 
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
index 055648d..88aec1a 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
@@ -41,6 +41,8 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.index.project.ProjectField;
+import com.google.gerrit.index.project.ProjectIndexCollection;
 import com.google.gerrit.json.OutputFormat;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.WebLinks;
@@ -184,6 +186,7 @@
   private AccountGroup.UUID groupUuid;
   private final Provider<QueryProjects> queryProjectsProvider;
   private final boolean listProjectsFromIndex;
+  private final ProjectIndexCollection projectIndexes;
 
   @Inject
   protected ListProjectsImpl(
@@ -196,7 +199,8 @@
       ProjectNode.Factory projectNodeFactory,
       WebLinks webLinks,
       Provider<QueryProjects> queryProjectsProvider,
-      @GerritServerConfig Config config) {
+      @GerritServerConfig Config config,
+      ProjectIndexCollection projectIndexes) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.groupResolver = groupResolver;
@@ -207,6 +211,7 @@
     this.webLinks = webLinks;
     this.queryProjectsProvider = queryProjectsProvider;
     this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
+    this.projectIndexes = projectIndexes;
   }
 
   public List<String> getShowBranch() {
@@ -251,11 +256,15 @@
     return display(null);
   }
 
-  private Optional<String> expressAsProjectsQuery() {
+  private Optional<String> expressAsProjectsQuery() throws BadRequestException {
     return listProjectsFromIndex
             && !all
             && state != HIDDEN
-            && isNullOrEmpty(matchPrefix)
+            && (isNullOrEmpty(matchPrefix)
+                || projectIndexes
+                    .getSearchIndex()
+                    .getSchema()
+                    .hasField(ProjectField.PREFIX_NAME_SPEC))
             && isNullOrEmpty(matchRegex)
             && type == FilterType.ALL
             && showBranch.isEmpty()
@@ -264,12 +273,25 @@
         : Optional.empty();
   }
 
-  private String toQuery() {
+  private String toQuery() throws BadRequestException {
+    // QueryProjects supports specifying matchPrefix and matchSubstring at the same time, but to
+    // keep the behavior consistent regardless of whether 'gerrit.listProjectsFromIndex' is true or
+    // false, disallow specifying both at the same time here. This way
+    // 'gerrit.listProjectsFromIndex' can be troggled without breaking any caller.
+    if (matchPrefix != null) {
+      checkMatchOptions(matchSubstring == null);
+    } else if (matchSubstring != null) {
+      checkMatchOptions(matchPrefix == null);
+    }
+
     List<String> queries = new ArrayList<>();
 
     if (state != null) {
       queries.add(String.format("(state:%s)", state.name()));
     }
+    if (!isNullOrEmpty(matchPrefix)) {
+      queries.add(String.format("prefix:%s", matchPrefix));
+    }
     if (!isNullOrEmpty(matchSubstring)) {
       queries.add(String.format("substring:%s", matchSubstring));
     }
@@ -353,6 +375,7 @@
     Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
     PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
     final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
+    ProjectInfo lastInfo = null;
     try {
       Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
       while (projectStatesIt.hasNext()) {
@@ -384,10 +407,15 @@
           continue;
         }
         if (limit > 0 && ++found > limit) {
+          if (lastInfo != null) {
+            lastInfo._moreProjects = true;
+          }
           break;
         }
 
         ProjectInfo info = new ProjectInfo();
+        lastInfo = info;
+
         info.name = projectName.get();
         if (showTree && format.isJson()) {
           addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index ef64bad..dc7499d 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -129,6 +129,9 @@
       for (ProjectData pd : pds) {
         projectInfos.add(json.format(pd.getProject()));
       }
+      if (!projectInfos.isEmpty() && result.more()) {
+        projectInfos.get(projectInfos.size() - 1)._moreProjects = true;
+      }
       return projectInfos;
     } catch (QueryParseException e) {
       throw new BadRequestException(e.getMessage(), e);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index d7078d8..b1cc866 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -533,6 +533,23 @@
   }
 
   @Test
+  public void commitMessage_providedMessageWithCorrectChangeId() throws Exception {
+    initDestBranch();
+    String originalChangeId =
+        gApi.changes()
+            .create(new ChangeInput(project.get(), DESTINATION_BRANCH, "Default commit message"))
+            .info()
+            .changeId;
+    ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+    in.commitMessage = "custom commit message\n\nChange-Id: " + originalChangeId + "\n";
+
+    ChangeInfo result = gApi.changes().id(originalChangeId).applyPatch(in);
+
+    ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+    assertThat(info.revisions.get(info.currentRevision).commit.message).isEqualTo(in.commitMessage);
+  }
+
+  @Test
   public void commitMessage_providedMessageWithWrongChangeId() throws Exception {
     initDestBranch();
     String originalChangeId =
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 5a7d7b3..1df3f3e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -140,6 +140,33 @@
   }
 
   @Test
+  @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+  public void listProjectsSetsMoreProjectsIfLimited_indexEnabled() throws Exception {
+    testListProjectsSetsMoreProjectsIfLimited();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "false")
+  public void listProjectsSetsMoreProjectsIfLimited_indexDisabled() throws Exception {
+    testListProjectsSetsMoreProjectsIfLimited();
+  }
+
+  private void testListProjectsSetsMoreProjectsIfLimited() throws Exception {
+    for (int i = 0; i < 3; i++) {
+      projectOperations.newProject().name("prefix-" + i).create();
+    }
+
+    List<ProjectInfo> result = gApi.projects().list().withPrefix("prefix").get();
+    assertThat(Iterables.getLast(result)._moreProjects).isNull();
+
+    result = gApi.projects().list().withPrefix("prefix").withLimit(Integer.MAX_VALUE).get();
+    assertThat(Iterables.getLast(result)._moreProjects).isNull();
+
+    result = gApi.projects().list().withPrefix("prefix").withLimit(2).get();
+    assertThat(Iterables.getLast(result)._moreProjects).isTrue();
+  }
+
+  @Test
   public void listProjectsToOutputStream() throws Exception {
     int numInitialProjects = gApi.projects().list().get().size();
     int numTestProjects = 5;
@@ -199,7 +226,18 @@
   }
 
   @Test
-  public void listProjectsWithPrefix() throws Exception {
+  @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "true")
+  public void listProjectsWithPrefix_indexEnabled() throws Exception {
+    testListProjectsWithPrefix();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.listProjectsFromIndex", value = "false")
+  public void listProjectsWithPrefix_indexDisabled() throws Exception {
+    testListProjectsWithPrefix();
+  }
+
+  private void testListProjectsWithPrefix() throws Exception {
     Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
     Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
     projectOperations.newProject().name("other-prefix-project").create();
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 3377b92..47d485d 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -23,6 +23,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
@@ -360,8 +361,10 @@
     String query =
         "name:" + project1.name + " OR name:" + project2.name + " OR name:" + project3.name;
     List<ProjectInfo> result = assertQuery(query, project1, project2, project3);
+    assertThat(Iterables.getLast(result)._moreProjects).isNull();
 
-    assertQuery(newQuery(query).withLimit(2), result.subList(0, 2));
+    result = assertQuery(newQuery(query).withLimit(2), result.subList(0, 2));
+    assertThat(Iterables.getLast(result)._moreProjects).isTrue();
   }
 
   @Test
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index 02d5124..769b064 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -42,7 +42,7 @@
 import {Timing} from '../../../constants/reporting';
 import {changeModelToken} from '../../../models/change/change-model';
 
-interface FilePreview {
+export interface FilePreview {
   filepath: string;
   preview: DiffInfo;
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
new file mode 100644
index 0000000..974329a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-comment-model/gr-comment-model.ts
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {Observable} from 'rxjs';
+import {filter} from 'rxjs/operators';
+import {define} from '../../../models/dependency';
+import {Model} from '../../../models/model';
+import {isDefined} from '../../../types/types';
+import {select} from '../../../utils/observable-util';
+import {Comment} from '../../../types/common';
+
+export interface CommentState {
+  comment: Comment;
+  commentedText?: string;
+}
+
+export const commentModelToken = define<CommentModel>('diff-model');
+
+export class CommentModel extends Model<CommentState | undefined> {
+  readonly comment$: Observable<Comment> = select(
+    this.state$.pipe(filter(isDefined)),
+    commentState => commentState.comment
+  );
+
+  readonly commentedText$: Observable<string | undefined> = select(
+    this.state$.pipe(filter(isDefined)),
+    commentState => commentState.commentedText
+  );
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 30576c8..24777fb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -315,6 +315,8 @@
           font-size: var(--font-size-normal);
           font-weight: var(--font-weight-normal);
           line-height: var(--line-height-normal);
+        }
+        gr-diff#diff {
           /* Explicitly set the background color of the diff. We
            * cannot use the diff content type ab because of the skip chunk preceding
            * it, diff processor assumes the chunk of type skip/ab can be 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 69d7387..d27be9f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -17,7 +17,7 @@
 import {getAppContext} from '../../../services/app-context';
 import {css, html, LitElement, nothing, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators.js';
-import {resolve} from '../../../models/dependency';
+import {provide, resolve} from '../../../models/dependency';
 import {GrTextarea} from '../gr-textarea/gr-textarea';
 import {
   AccountDetailInfo,
@@ -68,6 +68,10 @@
 import {modalStyles} from '../../../styles/gr-modal-styles';
 import {KnownExperimentId} from '../../../services/flags/flags';
 import {pluginLoaderToken} from '../gr-js-api-interface/gr-plugin-loader';
+import {
+  CommentModel,
+  commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
 
 // visible for testing
 export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
@@ -226,6 +230,8 @@
 
   private readonly shortcuts = new ShortcutController(this);
 
+  private commentModel = new CommentModel(undefined);
+
   /**
    * This is triggered when the user types into the editing textarea. We then
    * debounce it and call autoSave().
@@ -246,6 +252,7 @@
 
   constructor() {
     super();
+    provide(this, commentModelToken, () => this.commentModel);
     // Allow the shortcuts to bubble up so that GrReplyDialog can respond to
     // them as well.
     this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEsc(), {
@@ -979,6 +986,22 @@
         whenVisible(this, () => this.textarea?.putCursorAtEnd());
       }
     }
+    if (changed.has('changeNum') || changed.has('comment')) {
+      if (
+        !this.flagsService.isEnabled(
+          KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
+        ) ||
+        !this.changeNum ||
+        !this.comment
+      )
+        return;
+      (async () => {
+        const commentedText = await this.getCommentedCode();
+        this.commentModel.updateState({
+          commentedText,
+        });
+      })();
+    }
   }
 
   override willUpdate(changed: PropertyValues) {
@@ -987,6 +1010,11 @@
       if (isDraft(this.comment) && isError(this.comment)) {
         this.edit();
       }
+      if (this.comment) {
+        this.commentModel.updateState({
+          comment: this.comment,
+        });
+      }
     }
     if (changed.has('editing')) {
       this.onEditingChanged();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 3f97ded..b6af123 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -187,7 +187,7 @@
   }
 
   protected override willUpdate(changedProperties: PropertyValues): void {
-    if (changedProperties.has('items') || changedProperties.has('value')) {
+    if (changedProperties.has('value')) {
       this.handleValueChange();
     }
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index becfbc2..88866d4 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -14,11 +14,15 @@
 import {getAppContext} from '../../../services/app-context';
 import './gr-formatted-text';
 import {GrFormattedText} from './gr-formatted-text';
-import {createConfig} from '../../../test/test-data-generators';
+import {createComment, createConfig} from '../../../test/test-data-generators';
 import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
 import {CommentLinks, EmailAddress} from '../../../api/rest-api';
 import {testResolver} from '../../../test/common-test-setup';
 import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
+import {
+  CommentModel,
+  commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
 
 suite('gr-formatted-text tests', () => {
   let element: GrFormattedText;
@@ -37,6 +41,7 @@
       testResolver(changeModelToken),
       getAppContext().restApiService
     );
+    const commentModel = new CommentModel({comment: createComment()});
     await setCommentLinks({
       customLinkRewrite: {
         match: '(LinkRewriteMe)',
@@ -54,9 +59,13 @@
     element = (
       await fixture(
         wrapInProvider(
-          html`<gr-formatted-text></gr-formatted-text>`,
-          configModelToken,
-          configModel
+          wrapInProvider(
+            html`<gr-formatted-text></gr-formatted-text>`,
+            configModelToken,
+            configModel
+          ),
+          commentModelToken,
+          commentModel
         )
       )
     ).querySelector('gr-formatted-text')!;
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
index 2b13b40..e484d6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix.ts
@@ -3,12 +3,34 @@
  * Copyright 2023 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
+import '../../../embed/diff/gr-diff/gr-diff';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-icon/gr-icon';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {css, html, LitElement, nothing} from 'lit';
-import {customElement} from 'lit/decorators.js';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {customElement, state} from 'lit/decorators.js';
+import {getAppContext} from '../../../services/app-context';
+import {Comment} from '../../../types/common';
 import {fire} from '../../../utils/event-util';
+import {anyLineTooLong} from '../../../utils/diff-util';
+import {
+  DiffLayer,
+  DiffPreferencesInfo,
+  DiffViewMode,
+  RenderPreferences,
+} from '../../../api/diff';
+import {when} from 'lit/directives/when.js';
+import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
+import {resolve} from '../../../models/dependency';
+import {highlightServiceToken} from '../../../services/highlight/highlight-service';
+import {NumericChangeId} from '../../../api/rest-api';
+import {changeModelToken} from '../../../models/change/change-model';
+import {subscribe} from '../../lit/subscription-controller';
+import {FilePreview} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
+import {userModelToken} from '../../../models/user/user-model';
+import {createUserFixSuggestion} from '../../../utils/comment-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {commentModelToken} from '../gr-comment-model/gr-comment-model';
 
 declare global {
   interface HTMLElementEventMap {
@@ -23,7 +45,78 @@
 }
 
 @customElement('gr-user-suggestion-fix')
-export class GrUserSuggetionFix extends LitElement {
+export class GrUserSuggestionsFix extends LitElement {
+  @state()
+  comment?: Comment;
+
+  @state()
+  commentedText?: string;
+
+  @state()
+  layers: DiffLayer[] = [];
+
+  @state()
+  previewLoaded = false;
+
+  @state()
+  changeNum?: NumericChangeId;
+
+  @state()
+  preview?: FilePreview;
+
+  @state()
+  diffPrefs?: DiffPreferencesInfo;
+
+  @state()
+  renderPrefs: RenderPreferences = {
+    disable_context_control_buttons: true,
+    show_file_comment_button: false,
+    hide_line_length_indicator: true,
+  };
+
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
+  private readonly restApiService = getAppContext().restApiService;
+
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  private readonly getCommentModel = resolve(this, commentModelToken);
+
+  private readonly flagsService = getAppContext().flagsService;
+
+  private readonly syntaxLayer = new GrSyntaxLayerWorker(
+    resolve(this, highlightServiceToken),
+    () => getAppContext().reportingService
+  );
+
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getChangeModel().changeNum$,
+      changeNum => (this.changeNum = changeNum)
+    );
+    subscribe(
+      this,
+      () => this.getUserModel().diffPreferences$,
+      diffPreferences => {
+        if (!diffPreferences) return;
+        this.diffPrefs = diffPreferences;
+        this.syntaxLayer.setEnabled(!!this.diffPrefs.syntax_highlighting);
+      }
+    );
+    subscribe(
+      this,
+      () => this.getCommentModel().comment$,
+      comment => (this.comment = comment)
+    );
+    subscribe(
+      this,
+      () => this.getCommentModel().commentedText$,
+      commentedText => (this.commentedText = commentedText)
+    );
+  }
+
   static override get styles() {
     return [
       css`
@@ -63,6 +156,19 @@
     ];
   }
 
+  override updated(changed: PropertyValues) {
+    if (changed.has('commentedText') || changed.has('comment')) {
+      if (
+        this.flagsService.isEnabled(
+          KnownExperimentId.DIFF_FOR_USER_SUGGESTED_EDIT
+        ) &&
+        !this.previewLoaded
+      ) {
+        this.fetchFixPreview();
+      }
+    }
+  }
+
   override render() {
     if (!this.textContent) return nothing;
     const code = this.textContent;
@@ -95,17 +201,79 @@
           </gr-button>
         </div>
       </div>
-      <code>${code}</code>`;
+      ${when(
+        this.previewLoaded,
+        () => this.renderDiff(),
+        () => html`<code>${code}</code>`
+      )} `;
   }
 
   handleShowFix() {
     if (!this.textContent) return;
     fire(this, 'open-user-suggest-preview', {code: this.textContent});
   }
+
+  private renderDiff() {
+    if (!this.preview) return;
+    const diff = this.preview.preview;
+    if (!anyLineTooLong(diff)) {
+      this.syntaxLayer.process(diff);
+    }
+    return html`<gr-diff
+      .prefs=${this.overridePartialDiffPrefs()}
+      .path=${this.preview.filepath}
+      .diff=${diff}
+      .layers=${this.layers}
+      .renderPrefs=${this.renderPrefs}
+      .viewMode=${DiffViewMode.UNIFIED}
+    ></gr-diff>`;
+  }
+
+  private async fetchFixPreview() {
+    if (
+      !this.changeNum ||
+      !this.comment?.patch_set ||
+      !this.textContent ||
+      !this.commentedText
+    )
+      return;
+
+    const fixSuggestions = createUserFixSuggestion(
+      this.comment,
+      this.commentedText,
+      this.textContent
+    );
+    const res = await this.restApiService.getFixPreview(
+      this.changeNum,
+      this.comment?.patch_set,
+      fixSuggestions[0].replacements
+    );
+    if (res) {
+      const currentPreviews = Object.keys(res).map(key => {
+        return {filepath: key, preview: res[key]};
+      });
+      if (currentPreviews.length > 0) {
+        this.preview = currentPreviews[0];
+        this.previewLoaded = true;
+      }
+    }
+
+    return res;
+  }
+
+  private overridePartialDiffPrefs() {
+    if (!this.diffPrefs) return undefined;
+    return {
+      ...this.diffPrefs,
+      context: 1,
+      line_length: Math.min(this.diffPrefs.line_length, 100),
+      line_wrapping: true,
+    };
+  }
 }
 
 declare global {
   interface HTMLElementTagNameMap {
-    'gr-user-suggestion-fix': GrUserSuggetionFix;
+    'gr-user-suggestion-fix': GrUserSuggestionsFix;
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
index 8b0a427..423fe62 100644
--- a/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-user-suggestion-fix/gr-user-suggestion-fix_test.ts
@@ -6,18 +6,28 @@
 import '../../../test/common-test-setup';
 import './gr-user-suggestion-fix';
 import {fixture, html, assert} from '@open-wc/testing';
-import {GrUserSuggetionFix} from './gr-user-suggestion-fix';
-import {getAppContext} from '../../../services/app-context';
+import {GrUserSuggestionsFix} from './gr-user-suggestion-fix';
+import {
+  CommentModel,
+  commentModelToken,
+} from '../gr-comment-model/gr-comment-model';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {createComment} from '../../../test/test-data-generators';
 
 suite('gr-user-suggestion-fix tests', () => {
-  let element: GrUserSuggetionFix;
+  let element: GrUserSuggestionsFix;
 
   setup(async () => {
-    const flagsService = getAppContext().flagsService;
-    sinon.stub(flagsService, 'isEnabled').returns(true);
-    element = await fixture<GrUserSuggetionFix>(html`
-      <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix>
-    `);
+    const commentModel = new CommentModel({comment: createComment()});
+    element = (
+      await fixture<GrUserSuggestionsFix>(
+        wrapInProvider(
+          html` <gr-user-suggestion-fix>Hello World</gr-user-suggestion-fix> `,
+          commentModelToken,
+          commentModel
+        )
+      )
+    ).querySelector<GrUserSuggestionsFix>('gr-user-suggestion-fix')!;
     await element.updateComplete;
   });
 
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 2104c4a..a8d4a3b 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,4 +19,5 @@
   PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
   PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
   ML_SUGGESTED_EDIT = 'UiFeature__ml_suggested_edit',
+  DIFF_FOR_USER_SUGGESTED_EDIT = 'UiFeature__diff_for_user_suggested_edit',
 }
diff --git a/polygerrit-ui/app/utils/message-util.ts b/polygerrit-ui/app/utils/message-util.ts
index c70ccb1..fffe612 100644
--- a/polygerrit-ui/app/utils/message-util.ts
+++ b/polygerrit-ui/app/utils/message-util.ts
@@ -7,7 +7,8 @@
 import {ChangeId, ChangeMessageInfo} from '../types/common';
 
 function getRevertChangeIdFromMessage(msg: ChangeMessageInfo): ChangeId {
-  const REVERT_REGEX = /^Created a revert of this change as (I[0-9a-f]{40})$/;
+  const REVERT_REGEX =
+    /^Created a revert of this change as .*?(I[0-9a-f]{40})$/;
   const changeId = msg.message.match(REVERT_REGEX)?.[1];
   if (!changeId) throw new Error('revert changeId not found');
   return changeId as ChangeId;
diff --git a/polygerrit-ui/app/utils/message-util_test.ts b/polygerrit-ui/app/utils/message-util_test.ts
index ca82818..64d765a 100644
--- a/polygerrit-ui/app/utils/message-util_test.ts
+++ b/polygerrit-ui/app/utils/message-util_test.ts
@@ -29,4 +29,24 @@
       'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
     ]);
   });
+
+  test('getRevertCreatedChangeIds with extra spam', () => {
+    const messages = [
+      {
+        ...createChangeMessage(),
+        message:
+          'Created a revert of this change as IIf02ca1cd494579d6bb92a157bf1819e3689cd6b1',
+        tag: MessageTag.TAG_REVERT as ReviewInputTag,
+      },
+      {
+        ...createChangeMessage(),
+        message: 'Created a revert of this change as abc',
+        tag: undefined,
+      },
+    ];
+
+    assert.deepEqual(getRevertCreatedChangeIds(messages), [
+      'If02ca1cd494579d6bb92a157bf1819e3689cd6b1' as ChangeId,
+    ]);
+  });
 });