Fix starred changes clash after repo import from another site

Introduce `virtualId` to change data and initiate it with the
`legacy_id_str` from the index so that changes list screen (which is
populated from the index only) could be properly updated with star bit.

Ensure that `changeServerId` is loaded from notes when notes are
available in the `ChangeData`.

Finally use the `virtualId` in all calls to get/set starred changes so
that the refs clash is avoided. As a result the starred changes ref
gets modified to the following form:

  refs/starred-changes/NN/<virtual-id>/<account-id>

where `virtual-id` is `server-id` encoded `change-id` for imported
changes and `change-id` for existing/newly created ones.

Issue 331016670: to be addressed as the follow-up - toggle star from
changes list screen doesn't work for imported changes - the problem is
that UI uses change.id to obtain the project first which clashes with
existing changes. It works fine on the single change screen.

Bug: Issue 325309573
Release-Notes: Starred changes clash when importing changes from another instance
Forward-Compatible: checked
Change-Id: I85c946efcab5d20dd25682cb758bb9601c93b1d2
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 40ae2ec..d381b68 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -99,6 +99,7 @@
   public Boolean containsGitConflicts;
 
   public Integer _number;
+  public Integer _virtualIdNumber;
 
   public AccountInfo owner;
 
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index eaae40f..a47b87e 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -112,7 +112,7 @@
   private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
 
   static Term idTerm(ChangeData cd) {
-    return idTerm(cd.getVirtualId());
+    return idTerm(cd.virtualId());
   }
 
   static Term idTerm(Change.Id id) {
@@ -541,7 +541,13 @@
     IndexableField cb = Iterables.getFirst(doc.get(CHANGE_FIELD), null);
     if (cb != null) {
       BytesRef proto = cb.binaryValue();
-      cd = changeDataFactory.create(parseProtoFrom(proto, ChangeProtoConverter.INSTANCE));
+      // pass the id field value (which is the change virtual id for the imported changes) when
+      // available
+      IndexableField f = Iterables.getFirst(doc.get(idFieldName), null);
+      cd =
+          changeDataFactory.create(
+              parseProtoFrom(proto, ChangeProtoConverter.INSTANCE),
+              f != null ? Change.id(Integer.valueOf(f.stringValue())) : null);
     } else {
       IndexableField f = Iterables.getFirst(doc.get(idFieldName), null);
 
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index c845415..0f4e8f3 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -183,22 +183,22 @@
     this.queryProvider = queryProvider;
   }
 
-  public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
+  public NavigableSet<String> getLabels(Account.Id accountId, Change.Id virtualId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
+      return readLabels(repo, RefNames.refsStarredChanges(virtualId, accountId)).labels();
     } catch (IOException e) {
       throw new StorageException(
           String.format(
               "Reading stars from change %d for account %d failed",
-              changeId.get(), accountId.get()),
+              virtualId.get(), accountId.get()),
           e);
     }
   }
 
-  public void star(Account.Id accountId, Project.NameKey project, Change.Id changeId, Operation op)
+  public void star(Account.Id accountId, Project.NameKey project, Change.Id virtualId, Operation op)
       throws IllegalLabelException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      String refName = RefNames.refsStarredChanges(changeId, accountId);
+      String refName = RefNames.refsStarredChanges(virtualId, accountId);
       StarRef old = readLabels(repo, refName);
 
       NavigableSet<String> labels = new TreeSet<>(old.labels());
@@ -218,19 +218,19 @@
       }
     } catch (IOException e) {
       throw new StorageException(
-          String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
+          String.format("Star change %d for account %d failed", virtualId.get(), accountId.get()),
           e);
     }
   }
 
   /**
-   * Returns a subset of change IDs among the input {@code changeIds} list that are starred by the
+   * Returns a subset of change IDs among the input {@code virtualIds} list that are starred by the
    * {@code caller} user.
    */
   public Set<Change.Id> areStarred(
-      Repository allUsersRepo, List<Change.Id> changeIds, Account.Id caller) {
+      Repository allUsersRepo, List<Change.Id> virtualIds, Account.Id caller) {
     List<String> starRefs =
-        changeIds.stream()
+        virtualIds.stream()
             .map(c -> RefNames.refsStarredChanges(c, caller))
             .collect(Collectors.toList());
     try {
@@ -241,7 +241,7 @@
     } catch (IOException e) {
       logger.atWarning().withCause(e).log(
           "Failed getting starred changes for account %d within changes: %s",
-          caller.get(), Joiner.on(", ").join(changeIds));
+          caller.get(), Joiner.on(", ").join(virtualIds));
       return ImmutableSet.of();
     }
   }
@@ -252,18 +252,18 @@
    * <p>Intended for use only when we're about to delete a change. For that reason, the change is
    * not reindexed.
    *
-   * @param changeId change ID.
+   * @param virtualId change ID.
    * @throws IOException if an error occurred.
    */
-  public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
+  public void unstarAllForChangeDeletion(Change.Id virtualId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
       batchUpdate.setAllowNonFastForwards(true);
       batchUpdate.setRefLogIdent(serverIdent.get());
-      batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
-      for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
-        String refName = RefNames.refsStarredChanges(changeId, accountId);
+      batchUpdate.setRefLogMessage("Unstar change " + virtualId.get(), true);
+      for (Account.Id accountId : byChangeFromIndex(virtualId).keySet()) {
+        String refName = RefNames.refsStarredChanges(virtualId, accountId);
         Ref ref = repo.getRefDatabase().exactRef(refName);
         if (ref != null) {
           batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
@@ -275,7 +275,7 @@
           String message =
               String.format(
                   "Unstar change %d failed, ref %s could not be deleted: %s",
-                  changeId.get(), command.getRefName(), command.getResult());
+                  virtualId.get(), command.getRefName(), command.getResult());
           if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
             throw new LockFailureException(message, batchUpdate);
           }
@@ -285,21 +285,21 @@
     }
   }
 
-  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
+  public ImmutableMap<Account.Id, StarRef> byChange(Change.Id virtualId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
-      for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
+      for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(virtualId))) {
         Integer id = Ints.tryParse(refPart);
         if (id == null) {
           continue;
         }
         Account.Id accountId = Account.id(id);
-        builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
+        builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(virtualId, accountId)));
       }
       return builder.build();
     } catch (IOException e) {
       throw new StorageException(
-          String.format("Get accounts that starred change %d failed", changeId.get()), e);
+          String.format("Get accounts that starred change %d failed", virtualId.get()), e);
     }
   }
 
@@ -332,14 +332,14 @@
     }
   }
 
-  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
+  public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id virtualId) {
     List<ChangeData> changeData =
         queryProvider
             .get()
             .setRequestedFields(ChangeField.ID, ChangeField.STAR)
-            .byLegacyChangeId(changeId);
+            .byLegacyChangeId(virtualId);
     if (changeData.size() != 1) {
-      throw new NoSuchChangeException(changeId);
+      throw new NoSuchChangeException(virtualId);
     }
     return changeData.get(0).stars();
   }
@@ -351,14 +351,14 @@
         .collect(toSet());
   }
 
-  public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
+  public ObjectId getObjectId(Account.Id accountId, Change.Id virtualId) {
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
+      Ref ref = repo.exactRef(RefNames.refsStarredChanges(virtualId, accountId));
       return ref != null ? ref.getObjectId() : ObjectId.zeroId();
     } catch (IOException e) {
       logger.atSevere().withCause(e).log(
           "Getting star object ID for account %d on change %d failed",
-          accountId.get(), changeId.get());
+          accountId.get(), virtualId.get());
       return ObjectId.zeroId();
     }
   }
diff --git a/java/com/google/gerrit/server/account/AccountResource.java b/java/com/google/gerrit/server/account/AccountResource.java
index 9629809..14b363b 100644
--- a/java/com/google/gerrit/server/account/AccountResource.java
+++ b/java/com/google/gerrit/server/account/AccountResource.java
@@ -99,6 +99,10 @@
     public Change getChange() {
       return change.getChange();
     }
+
+    public Change.Id getVirtualId() {
+      return change.getVirtualId();
+    }
   }
 
   public static class Star implements RestResource {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index c7e0799..928c8f8 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -486,6 +486,9 @@
       if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
         ChangeData.ensureReviewedByLoadedForOpenChanges(all);
       }
+      if (has(STAR) && userProvider.get().isIdentifiedUser()) {
+        ChangeData.ensureChangeServerId(all);
+      }
       ChangeData.ensureCurrentApprovalsLoaded(all);
     } else {
       for (ChangeData cd : all) {
@@ -770,6 +773,8 @@
               .collect(toList());
     }
 
+    out._virtualIdNumber = cd.virtualId().get();
+
     return out;
   }
 
@@ -960,14 +965,15 @@
     // repository only once
     try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
       List<Change.Id> changeIds =
-          changeInfos.stream().map(c -> Change.id(c._number)).collect(Collectors.toList());
+          changeInfos.stream().map(c -> Change.id(c._virtualIdNumber)).collect(Collectors.toList());
       Set<Change.Id> starredChanges =
           starredChangesUtil.areStarred(
               allUsersRepo, changeIds, userProvider.get().asIdentifiedUser().getAccountId());
       if (starredChanges.isEmpty()) {
         return;
       }
-      changeInfos.stream().forEach(c -> c.starred = starredChanges.contains(Change.id(c._number)));
+      changeInfos.stream()
+          .forEach(c -> c.starred = starredChanges.contains(Change.id(c._virtualIdNumber)));
     } catch (IOException e) {
       logger.atWarning().withCause(e).log("Failed to open All-Users repo.");
     }
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 919586e..f074100 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -161,6 +161,10 @@
     return changeData;
   }
 
+  public Change.Id getVirtualId() {
+    return getChangeData().virtualId();
+  }
+
   // This includes all information relevant for ETag computation
   // unrelated to the UI.
   public void prepareETag(Hasher h, CurrentUser user) {
@@ -240,7 +244,8 @@
                 .build())) {
       Hasher h = Hashing.murmur3_128().newHasher();
       if (user.isIdentifiedUser()) {
-        h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
+        h.putString(
+            starredChangesUtil.getObjectId(user.getAccountId(), getVirtualId()).name(), UTF_8);
       }
       prepareETag(h, user);
       return h.hash().toString();
diff --git a/java/com/google/gerrit/server/change/DeleteChangeOp.java b/java/com/google/gerrit/server/change/DeleteChangeOp.java
index c7ddf19..ac75165 100644
--- a/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -80,7 +80,8 @@
     ensureDeletable(ctx, id, patchSets);
     // Cleaning up is only possible as long as the change and its elements are
     // still part of the database.
-    cleanUpReferences(id);
+    ChangeData cd = changeDataFactory.create(ctx.getChange());
+    cleanUpReferences(cd);
 
     logger.atFine().log(
         "Deleting change %s, current patch set %d is commit %s",
@@ -94,7 +95,7 @@
                     .map(p -> p.commitId().name())
                     .orElse("n/a")));
     ctx.deleteChange();
-    changeDeleted.fire(changeDataFactory.create(ctx.getChange()), ctx.getAccount(), ctx.getWhen());
+    changeDeleted.fire(cd, ctx.getAccount(), ctx.getWhen());
     return true;
   }
 
@@ -123,11 +124,11 @@
         revWalk.parseCommit(patchSet.commitId()), revWalk.parseCommit(destId.get()));
   }
 
-  private void cleanUpReferences(Change.Id id) throws IOException {
-    accountPatchReviewStore.run(s -> s.clearReviewed(id));
+  private void cleanUpReferences(ChangeData cd) throws IOException {
+    accountPatchReviewStore.run(s -> s.clearReviewed(cd.getId()));
 
     // Non-atomic operation on All-Users refs; not much we can do to make it atomic.
-    starredChangesUtil.unstarAllForChangeDeletion(id);
+    starredChangesUtil.unstarAllForChangeDeletion(cd.virtualId());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 01a19df..2658522 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -127,7 +127,7 @@
   // TODO: Rename LEGACY_ID to NUMERIC_ID
   /** Legacy change ID. */
   public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
-      exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getVirtualId().get()));
+      exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.virtualId().get()));
 
   /** Newer style Change-Id key. */
   public static final FieldDef<ChangeData, String> ID =
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 26e660a..a69d837 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -106,6 +106,7 @@
 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.function.Function;
@@ -237,11 +238,11 @@
     }
 
     public ChangeData create(Project.NameKey project, Change.Id id) {
-      return assistedFactory.create(project, id, null, null);
+      return assistedFactory.create(project, id, null, null, null);
     }
 
     public ChangeData create(Project.NameKey project, Change.Id id, ObjectId metaRevision) {
-      ChangeData cd = assistedFactory.create(project, id, null, null);
+      ChangeData cd = assistedFactory.create(project, id, null, null, null);
       cd.setMetaRevision(metaRevision);
       return cd;
     }
@@ -254,19 +255,29 @@
     }
 
     public ChangeData create(Change change) {
-      return assistedFactory.create(change.getProject(), change.getId(), change, null);
+      return create(change, null);
+    }
+
+    public ChangeData create(Change change, Change.Id virtualId) {
+      return assistedFactory.create(
+          change.getProject(),
+          change.getId(),
+          !Objects.equals(virtualId, change.getId()) ? virtualId : null,
+          change,
+          null);
     }
 
     public ChangeData create(ChangeNotes notes) {
       return assistedFactory.create(
-          notes.getChange().getProject(), notes.getChangeId(), notes.getChange(), notes);
+          notes.getChange().getProject(), notes.getChangeId(), null, notes.getChange(), notes);
     }
   }
 
   public interface AssistedFactory {
     ChangeData create(
         Project.NameKey project,
-        Change.Id id,
+        @Assisted("changeId") Change.Id id,
+        @Assisted("virtualId") @Nullable Change.Id virtualId,
         @Nullable Change change,
         @Nullable ChangeNotes notes);
   }
@@ -333,6 +344,7 @@
             project,
             id,
             null,
+            null,
             changeNotes);
     cd.currentPatchSet =
         PatchSet.builder()
@@ -432,6 +444,7 @@
   private ImmutableList<byte[]> refStatePatterns;
   private String changeServerId;
   private ChangeNumberVirtualIdAlgorithm virtualIdFunc;
+  private Change.Id virtualId;
 
   @Inject
   private ChangeData(
@@ -455,7 +468,8 @@
       ChangeNumberVirtualIdAlgorithm virtualIdFunc,
       @SkipCurrentRulesEvaluationOnClosedChanges Boolean skipCurrentRulesEvaluationOnClosedChange,
       @Assisted Project.NameKey project,
-      @Assisted Change.Id id,
+      @Assisted("changeId") Change.Id id,
+      @Assisted("virtualId") @Nullable Change.Id virtualId,
       @Assisted @Nullable Change change,
       @Assisted @Nullable ChangeNotes notes) {
     this.approvalsUtil = approvalsUtil;
@@ -485,6 +499,7 @@
 
     this.changeServerId = notes == null ? null : notes.getServerId();
     this.virtualIdFunc = virtualIdFunc;
+    this.virtualId = virtualId;
   }
 
   /**
@@ -605,8 +620,32 @@
     return legacyId;
   }
 
-  public Change.Id getVirtualId() {
-    return virtualIdFunc == null ? legacyId : virtualIdFunc.apply(changeServerId, legacyId);
+  public static void ensureChangeServerId(Iterable<ChangeData> changes) {
+    ChangeData first = Iterables.getFirst(changes, null);
+    if (first == null) {
+      return;
+    }
+
+    for (ChangeData cd : changes) {
+      cd.changeServerId();
+    }
+  }
+
+  public String changeServerId() {
+    if (changeServerId == null) {
+      if (!lazyload()) {
+        return null;
+      }
+      changeServerId = notes().getServerId();
+    }
+    return changeServerId;
+  }
+
+  public Change.Id virtualId() {
+    if (virtualId == null) {
+      return virtualIdFunc == null ? legacyId : virtualIdFunc.apply(changeServerId, legacyId);
+    }
+    return virtualId;
   }
 
   public Project.NameKey project() {
@@ -1345,7 +1384,7 @@
       if (!lazyload()) {
         return ImmutableMap.of();
       }
-      starRefs = requireNonNull(starredChangesUtil).byChange(legacyId);
+      starRefs = requireNonNull(starredChangesUtil).byChange(virtualId());
     }
     return starRefs;
   }
@@ -1363,7 +1402,7 @@
         if (!lazyload()) {
           return ImmutableSet.of();
         }
-        starsOf = StarsOf.create(accountId, starredChangesUtil.getLabels(accountId, legacyId));
+        starsOf = StarsOf.create(accountId, starredChangesUtil.getLabels(accountId, virtualId()));
       }
     }
     return starsOf.stars();
@@ -1524,7 +1563,7 @@
             // this is suboptimal, but is ok for the purposes of
             // draftsByUser(), and easier than trying to rebuild the change at
             // this point.
-            && !notes().getDraftComments(account, getVirtualId(), ref).isEmpty()) {
+            && !notes().getDraftComments(account, virtualId(), ref).isEmpty()) {
           draftsByUser.put(account, ref.getObjectId());
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 12abf3d..3880bce 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -73,7 +73,7 @@
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
-        .getLabels(user.getAccountId(), change.getId())
+        .getLabels(user.getAccountId(), change.getVirtualId())
         .contains(StarredChangesUtil.DEFAULT_LABEL)) {
       return new AccountResource.StarredChange(user, change);
     }
@@ -133,7 +133,7 @@
         starredChangesUtil.star(
             self.get().getAccountId(),
             change.getProject(),
-            change.getId(),
+            change.getVirtualId(),
             StarredChangesUtil.Operation.ADD);
       } catch (MutuallyExclusiveLabelsException e) {
         throw new ResourceConflictException(e.getMessage());
@@ -184,7 +184,7 @@
       starredChangesUtil.star(
           self.get().getAccountId(),
           rsrc.getChange().getProject(),
-          rsrc.getChange().getId(),
+          rsrc.getVirtualId(),
           StarredChangesUtil.Operation.REMOVE);
       return Response.none();
     }
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index c8e709a..beec379 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -66,7 +66,7 @@
             (s, c) -> encodedChangeNum,
             changeNotesMock);
 
-    assertThat(cd.getVirtualId().get()).isEqualTo(encodedChangeNum.get());
+    assertThat(cd.virtualId().get()).isEqualTo(encodedChangeNum.get());
   }
 
   private static PatchSet newPatchSet(Change.Id changeId, int num) {