Merge changes from topic "import-changes" into stable-3.7

* changes:
  Resolve accounts by imported externalId
  Introduce imported external ids for accounts from other servers
  Allow the rendering of changes imported from other GerritServerIds
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 234389e..be6e140 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2543,6 +2543,26 @@
 by the target version of the upgrade. Refer to the release notes and check whether
 the rolling upgrade is possible or not and the associated constraints.
 
+[[gerrit.importedServerId]]gerrit.importedServerId::
++
+ServerId of the repositories imported from other Gerrit servers. Changes coming
+associated with the imported serverIds are indexed and displayed in the UI.
++
+Specify multiple `gerrit.importedServerId` for allowing the import from multiple
+Gerrit servers with different serverIds.
++
+[NOTE]
+The account-ids referenced in the imported changes are used for looking up the
+associated account-id locally, using the `imported:` external-id.
+Example: the account-id 1000 from the imported server-id 59a4964e-6376-4ed9-beef
+will be looked up in the local accounts using the `imported:1000@59a4964e-6376-4ed9-beef`
+external-id.
++
+If this value is not set, all changes imported from other Gerrit servers will be
+ignored.
++
+By default empty.
+
 [[gerrit.serverId]]gerrit.serverId::
 +
 Used by NoteDb to, amongst other things, identify author identities from
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 303e79f..85dbdeb 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -46,6 +46,10 @@
  */
 @AutoValue
 public abstract class Account {
+
+  /** Placeholder for indicating an account-id that does not correspond to any local account */
+  public static final Id UNKNOWN_ACCOUNT_ID = id(0);
+
   public static Id id(int id) {
     return new AutoValue_Account_Id(id);
   }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 0a51171..1616198 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -135,6 +135,9 @@
   /** Scheme used for GPG public keys. */
   public static final String SCHEME_GPGKEY = "gpgkey";
 
+  /** Scheme for imported accounts from other servers with different GerritServerId */
+  public static final String SCHEME_IMPORTED = "imported";
+
   /** Scheme for external auth used during authentication, e.g. OAuth Token */
   public static final String SCHEME_EXTERNAL = "external";
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index 9f766d0..fe8feac 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -30,7 +30,7 @@
  *
  * <p>All returned collections are unmodifiable.
  */
-interface ExternalIdCache {
+public interface ExternalIdCache {
   Optional<ExternalId> byKey(ExternalId.Key key) throws IOException;
 
   ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
diff --git a/java/com/google/gerrit/server/config/GerritImportedServerIds.java b/java/com/google/gerrit/server/config/GerritImportedServerIds.java
new file mode 100644
index 0000000..c47d3be
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritImportedServerIds.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2022 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/**
+ * List of ServerIds of the Gerrit data imported from other servers.
+ *
+ * <p>This values correspond to the {@code GerritServerId} of other servers.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface GerritImportedServerIds {}
diff --git a/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
new file mode 100644
index 0000000..f3f7645
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritImportedServerIdsProvider.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 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.config;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+
+public class GerritImportedServerIdsProvider implements Provider<ImmutableSet<String>> {
+  public static final String SECTION = "gerrit";
+  public static final String KEY = "importedServerId";
+
+  private final ImmutableSet<String> importedIds;
+
+  @Inject
+  public GerritImportedServerIdsProvider(@GerritServerConfig Config cfg) {
+    importedIds = ImmutableSet.copyOf(cfg.getStringList(SECTION, null, KEY));
+  }
+
+  @Override
+  public ImmutableSet<String> get() {
+    return importedIds;
+  }
+}
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 158972f..3757244 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -17,6 +17,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.entities.Change;
@@ -24,6 +25,7 @@
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritImportedServerIds;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
@@ -51,6 +53,7 @@
     public final AllUsersName allUsers;
     public final NoteDbMetrics metrics;
     public final String serverId;
+    public final ImmutableSet<String> importedServerIds;
 
     // Providers required to avoid dependency cycles.
 
@@ -64,7 +67,8 @@
         ChangeNoteJson changeNoteJson,
         NoteDbMetrics metrics,
         Provider<ChangeNotesCache> cache,
-        @GerritServerId String serverId) {
+        @GerritServerId String serverId,
+        @GerritImportedServerIds ImmutableSet<String> importedServerIds) {
       this.failOnLoadForTest = new AtomicBoolean();
       this.repoManager = repoManager;
       this.allUsers = allUsers;
@@ -72,6 +76,7 @@
       this.metrics = metrics;
       this.cache = cache;
       this.serverId = serverId;
+      this.importedServerIds = importedServerIds;
     }
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 26d5933..31934d5 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -406,6 +406,10 @@
     return state.metaId();
   }
 
+  public String getServerId() {
+    return state.serverId();
+  }
+
   public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
     if (patchSets == null) {
       ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
@@ -655,7 +659,9 @@
      * be to bump the cache version, but that would invalidate all persistent cache entries, what we
      * rather try to avoid.
      */
-    if (!Strings.isNullOrEmpty(stateServerId) && !args.serverId.equals(stateServerId)) {
+    if (!Strings.isNullOrEmpty(stateServerId)
+        && !args.serverId.equals(stateServerId)
+        && !args.importedServerIds.contains(stateServerId)) {
       throw new InvalidServerIdException(args.serverId, stateServerId);
     }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 19044e6..0f2c877 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.proto.Protos;
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
@@ -366,7 +367,13 @@
           "Load change notes for change %s of project %s", key.changeId(), key.project());
       ChangeNotesParser parser =
           new ChangeNotesParser(
-              key.changeId(), key.id(), walkSupplier.get(), args.changeNoteJson, args.metrics);
+              key.changeId(),
+              key.id(),
+              walkSupplier.get(),
+              args.changeNoteJson,
+              args.metrics,
+              args.serverId,
+              externalIdCache);
       ChangeNotesState result = parser.parseAll();
       // This assignment only happens if call() was actually called, which only
       // happens when Cache#get(K, Callable<V>) incurs a cache miss.
@@ -377,11 +384,16 @@
 
   private final Cache<Key, ChangeNotesState> cache;
   private final Args args;
+  private final ExternalIdCache externalIdCache;
 
   @Inject
-  ChangeNotesCache(@Named(CACHE_NAME) Cache<Key, ChangeNotesState> cache, Args args) {
+  ChangeNotesCache(
+      @Named(CACHE_NAME) Cache<Key, ChangeNotesState> cache,
+      Args args,
+      ExternalIdCache externalIdCache) {
     this.cache = cache;
     this.args = args;
+    this.externalIdCache = externalIdCache;
   }
 
   Value get(
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 6e5e19c..f2a659d 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -79,6 +79,7 @@
 import com.google.gerrit.server.ReviewerByEmailSet;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
 import com.google.gerrit.server.notedb.ChangeNoteUtil.ParsedPatchSetApproval;
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.LabelVote;
@@ -199,18 +200,24 @@
   // the latest record unsets the field).
   private Optional<PatchSet.Id> cherryPickOf;
   private Instant mergedOn;
+  private final ExternalIdCache externalIdCache;
+  private final String gerritServerId;
 
   ChangeNotesParser(
       Change.Id changeId,
       ObjectId tip,
       ChangeNotesRevWalk walk,
       ChangeNoteJson changeNoteJson,
-      NoteDbMetrics metrics) {
+      NoteDbMetrics metrics,
+      String gerritServerId,
+      ExternalIdCache externalIdCache) {
     this.id = changeId;
     this.tip = tip;
     this.walk = walk;
     this.changeNoteJson = changeNoteJson;
     this.metrics = metrics;
+    this.externalIdCache = externalIdCache;
+    this.gerritServerId = gerritServerId;
     approvals = new LinkedHashMap<>();
     bufferedApprovals = new ArrayList<>();
     reviewers = HashBasedTable.create();
@@ -1406,7 +1413,7 @@
   }
 
   private Account.Id parseIdent(PersonIdent ident) throws ConfigInvalidException {
-    return NoteDbUtil.parseIdent(ident)
+    return NoteDbUtil.parseIdent(ident, gerritServerId, externalIdCache)
         .orElseThrow(
             () -> parseException("cannot retrieve account id: %s", ident.getEmailAddress()));
   }
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUtil.java b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
index 396e29b..64bf430 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUtil.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUtil.java
@@ -20,8 +20,12 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.GitDateFormatter;
 import org.eclipse.jgit.util.GitDateFormatter.Format;
@@ -48,6 +52,45 @@
     return Optional.empty();
   }
 
+  /**
+   * Returns an AccountId for the given email address and the current serverId. Reverse lookup the
+   * AccountId using the ExternalIdCache if the account has a foreign serverId.
+   *
+   * @param ident the accountId@serverId identity
+   * @param serverId the Gerrit's serverId
+   * @param externalIdCache reference to the cache for looking up the external ids
+   * @return a defined accountId if the account was found, {@link Account#UNKNOWN_ACCOUNT_ID} if the
+   *     lookup via external-id did not return any account, or an empty value if the identity was
+   *     malformed.
+   * @throws ConfigInvalidException when the lookup of the external-id failed
+   */
+  public static Optional<Account.Id> parseIdent(
+      PersonIdent ident, String serverId, ExternalIdCache externalIdCache)
+      throws ConfigInvalidException {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      Integer id = Ints.tryParse(email.substring(0, at));
+      String accountServerId = email.substring(at + 1);
+      if (id != null) {
+        if (accountServerId.equals(serverId)) {
+          return Optional.of(Account.id(id));
+        }
+
+        ExternalId.Key extIdKey = ExternalId.Key.create(ExternalId.SCHEME_IMPORTED, email, false);
+        try {
+          return externalIdCache
+              .byKey(extIdKey)
+              .map(ExternalId::accountId)
+              .or(() -> Optional.of(Account.UNKNOWN_ACCOUNT_ID));
+        } catch (IOException e) {
+          throw new ConfigInvalidException("Unable to lookup external id from cache", e);
+        }
+      }
+    }
+    return Optional.empty();
+  }
+
   public static String extractHostPartFromPersonIdent(PersonIdent ident) {
     String email = ident.getEmailAddress();
     int at = email.indexOf('@');
diff --git a/java/com/google/gerrit/server/schema/SchemaModule.java b/java/com/google/gerrit/server/schema/SchemaModule.java
index ff2073d..e0e64a3 100644
--- a/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -25,9 +26,12 @@
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.GerritImportedServerIds;
+import com.google.gerrit.server.config.GerritImportedServerIdsProvider;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.config.GerritServerIdProvider;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.inject.TypeLiteral;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /** Bindings for low-level Gerrit schema data. */
@@ -51,6 +55,11 @@
         .toProvider(GerritServerIdProvider.class)
         .in(SINGLETON);
 
+    bind(new TypeLiteral<ImmutableSet<String>>() {})
+        .annotatedWith(GerritImportedServerIds.class)
+        .toProvider(GerritImportedServerIdsProvider.class)
+        .in(SINGLETON);
+
     // It feels wrong to have this binding in a seemingly unrelated module, but it's a dependency of
     // SchemaCreatorImpl, so it's needed.
     // TODO(dborowitz): Is there any way to untangle this?
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b00cadb..b1ab6b1 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -19,6 +19,7 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
@@ -61,6 +62,8 @@
 import com.google.gerrit.server.config.FileBasedAllProjectsConfigProvider;
 import com.google.gerrit.server.config.FileBasedGlobalPluginConfigProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritImportedServerIds;
+import com.google.gerrit.server.config.GerritImportedServerIdsProvider;
 import com.google.gerrit.server.config.GerritInstanceIdModule;
 import com.google.gerrit.server.config.GerritInstanceNameModule;
 import com.google.gerrit.server.config.GerritOptions;
@@ -311,6 +314,17 @@
 
   @Provides
   @Singleton
+  @GerritImportedServerIds
+  public ImmutableSet<String> createImportedServerIds() {
+    ImmutableSet<String> serverIds =
+        ImmutableSet.copyOf(
+            cfg.getStringList(
+                GerritServerIdProvider.SECTION, null, GerritImportedServerIdsProvider.KEY));
+    return serverIds;
+  }
+
+  @Provides
+  @Singleton
   @SendEmailExecutor
   public ExecutorService createSendEmailExecutor() {
     return newDirectExecutorService();
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index d7c779e..1b286d1 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -18,6 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -39,6 +40,8 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
 import com.google.gerrit.server.approval.testing.TestPatchSetApprovalUuidGenerator;
 import com.google.gerrit.server.config.AllUsersName;
@@ -48,11 +51,13 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.DefaultUrlFormatter.DefaultUrlFormatterModule;
 import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
+import com.google.gerrit.server.config.GerritImportedServerIds;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerId;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.NullProjectCache;
 import com.google.gerrit.server.project.ProjectCache;
@@ -67,9 +72,12 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -84,10 +92,13 @@
 @Ignore
 @RunWith(ConfigSuite.class)
 public abstract class AbstractChangeNotesTest {
+  protected static final String LOCAL_SERVER_ID = "gerrit";
+
   private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
 
   @ConfigSuite.Parameter public Config testConfig;
 
+  protected Account.Id changeOwnerId;
   protected Account.Id otherUserId;
   protected FakeAccountCache accountCache;
   protected IdentifiedUser changeOwner;
@@ -111,11 +122,21 @@
 
   @Inject @GerritServerId protected String serverId;
 
+  @Inject protected ExternalIdCache externalIdCache;
+
   protected Injector injector;
   private String systemTimeZone;
 
   @Before
   public void setUpTestEnvironment() throws Exception {
+    setupTestPrerequisites();
+
+    injector = createTestInjector(LOCAL_SERVER_ID);
+    createAllUsers(injector);
+    injector.injectMembers(this);
+  }
+
+  protected void setupTestPrerequisites() throws Exception {
     setTimeForTesting();
 
     serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.now(), ZONE_ID);
@@ -134,60 +155,77 @@
     ou.setPreferredEmail("other@account.com");
     accountCache.put(ou.build());
     assertableFanOutExecutor = new AssertableExecutorService();
-
-    injector =
-        Guice.createInjector(
-            new FactoryModule() {
-              @Override
-              public void configure() {
-                install(new GitModule());
-
-                install(new DefaultUrlFormatterModule());
-                install(NoteDbModule.forTest());
-                bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
-                bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
-                bind(GitRepositoryManager.class).toInstance(repoManager);
-                bind(ProjectCache.class).to(NullProjectCache.class);
-                bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
-                bind(String.class)
-                    .annotatedWith(AnonymousCowardName.class)
-                    .toProvider(AnonymousCowardNameProvider.class);
-                bind(String.class)
-                    .annotatedWith(CanonicalWebUrl.class)
-                    .toInstance("http://localhost:8080/");
-                bind(Boolean.class)
-                    .annotatedWith(EnablePeerIPInReflogRecord.class)
-                    .toInstance(Boolean.FALSE);
-                bind(Realm.class).to(FakeRealm.class);
-                bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
-                bind(AccountCache.class).toInstance(accountCache);
-                bind(PersonIdent.class)
-                    .annotatedWith(GerritPersonIdent.class)
-                    .toInstance(serverIdent);
-                bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
-                bind(MetricMaker.class).to(DisabledMetricMaker.class);
-                bind(ExecutorService.class)
-                    .annotatedWith(FanOutExecutor.class)
-                    .toInstance(assertableFanOutExecutor);
-                bind(ServiceUserClassifier.class).to(ServiceUserClassifier.NoOp.class);
-                bind(InternalChangeQuery.class)
-                    .toProvider(
-                        () -> {
-                          throw new UnsupportedOperationException();
-                        });
-                bind(PatchSetApprovalUuidGenerator.class)
-                    .to(TestPatchSetApprovalUuidGenerator.class);
-              }
-            });
-
-    injector.injectMembers(this);
-    repoManager.createRepository(allUsers);
-    changeOwner = userFactory.create(co.id());
-    otherUser = userFactory.create(ou.id());
-    otherUserId = otherUser.getAccountId();
+    changeOwnerId = co.id();
+    otherUserId = ou.id();
     internalUser = new InternalUser();
   }
 
+  protected Injector createTestInjector(String serverId, String... importedServerIds)
+      throws Exception {
+    return createTestInjector(DisabledExternalIdCache.module(), serverId, importedServerIds);
+  }
+
+  protected Injector createTestInjector(
+      Module extraGuiceModule, String serverId, String... importedServerIds) throws Exception {
+
+    return Guice.createInjector(
+        new FactoryModule() {
+          @Override
+          public void configure() {
+            install(extraGuiceModule);
+            install(new GitModule());
+
+            install(new DefaultUrlFormatterModule());
+            install(NoteDbModule.forTest());
+            bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
+            bind(String.class).annotatedWith(GerritServerId.class).toInstance(serverId);
+            bind(new TypeLiteral<ImmutableSet<String>>() {})
+                .annotatedWith(GerritImportedServerIds.class)
+                .toInstance(new ImmutableSet.Builder<String>().add(importedServerIds).build());
+            bind(GitRepositoryManager.class).toInstance(repoManager);
+            bind(ProjectCache.class).to(NullProjectCache.class);
+            bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig);
+            bind(String.class)
+                .annotatedWith(AnonymousCowardName.class)
+                .toProvider(AnonymousCowardNameProvider.class);
+            bind(String.class)
+                .annotatedWith(CanonicalWebUrl.class)
+                .toInstance("http://localhost:8080/");
+            bind(Boolean.class)
+                .annotatedWith(EnablePeerIPInReflogRecord.class)
+                .toInstance(Boolean.FALSE);
+            bind(Realm.class).to(FakeRealm.class);
+            bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+            bind(AccountCache.class).toInstance(accountCache);
+            bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class).toInstance(serverIdent);
+            bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+            bind(MetricMaker.class).to(DisabledMetricMaker.class);
+            bind(ExecutorService.class)
+                .annotatedWith(FanOutExecutor.class)
+                .toInstance(assertableFanOutExecutor);
+            bind(ServiceUserClassifier.class).to(ServiceUserClassifier.NoOp.class);
+            bind(InternalChangeQuery.class)
+                .toProvider(
+                    () -> {
+                      throw new UnsupportedOperationException();
+                    });
+            bind(PatchSetApprovalUuidGenerator.class).to(TestPatchSetApprovalUuidGenerator.class);
+          }
+        });
+  }
+
+  protected void createAllUsers(Injector injector)
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException {
+    AllUsersName allUsersName = injector.getInstance(AllUsersName.class);
+
+    repoManager.createRepository(allUsersName);
+
+    IdentifiedUser.GenericFactory identifiedUserFactory =
+        injector.getInstance(IdentifiedUser.GenericFactory.class);
+    changeOwner = identifiedUserFactory.create(changeOwnerId);
+    otherUser = identifiedUserFactory.create(otherUserId);
+  }
+
   private void setTimeForTesting() {
     systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
     TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -200,8 +238,12 @@
   }
 
   protected Change newChange(boolean workInProgress) throws Exception {
+    return newChange(injector, workInProgress);
+  }
+
+  protected Change newChange(Injector injector, boolean workInProgress) throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId());
-    ChangeUpdate u = newUpdateForNewChange(c, changeOwner);
+    ChangeUpdate u = newUpdate(injector, c, changeOwner, false);
     u.setChangeId(c.getKey().get());
     u.setBranch(c.getDest().branch());
     u.setWorkInProgress(workInProgress);
@@ -218,15 +260,20 @@
   }
 
   protected ChangeUpdate newUpdateForNewChange(Change c, CurrentUser user) throws Exception {
-    return newUpdate(c, user, false);
+    return newUpdate(injector, c, user, false);
   }
 
   protected ChangeUpdate newUpdate(Change c, CurrentUser user) throws Exception {
-    return newUpdate(c, user, true);
+    return newUpdate(injector, c, user, true);
   }
 
   protected ChangeUpdate newUpdate(Change c, CurrentUser user, boolean shouldExist)
       throws Exception {
+    return newUpdate(injector, c, user, shouldExist);
+  }
+
+  protected ChangeUpdate newUpdate(
+      Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
     ChangeUpdate update = TestChanges.newUpdate(injector, c, user, shouldExist);
     update.setPatchSetId(c.currentPatchSetId());
     update.setAllowWriteToNewRef(true);
@@ -237,6 +284,10 @@
     return new ChangeNotes(args, c, true, null).load();
   }
 
+  protected ChangeNotes newNotes(AbstractChangeNotes.Args cArgs, Change c) {
+    return new ChangeNotes(cArgs, c, true, null).load();
+  }
+
   protected static SubmitRecord submitRecord(
       String status, String errorMessage, SubmitRecord.Label... labels) {
     SubmitRecord rec = new SubmitRecord();
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
index 666b8fc..9445f4a 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
@@ -98,7 +98,8 @@
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
     walk.reset();
     ChangeNoteJson changeNoteJson = injector.getInstance(ChangeNoteJson.class);
-    return new ChangeNotesParser(newChange().getId(), tip, walk, changeNoteJson, args.metrics);
+    return new ChangeNotesParser(
+        newChange().getId(), tip, walk, changeNoteJson, args.metrics, serverId, externalIdCache);
   }
 
   private RevCommit writeCommit(String body) throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 276f093..4543b50 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -201,11 +201,11 @@
             + "Branch: refs/heads/master\n"
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
-            + "Copied-Label: Label1=+1 Account <1@gerrit>,Other Account <2@Gerrit>\n"
+            + "Copied-Label: Label1=+1 Account <1@gerrit>,Other Account <2@gerrit>\n"
             + "Copied-Label: Label2=+1 Account <1@gerrit>\n"
-            + "Copied-Label: Label3=+1 Account <1@gerrit>,Other Account <2@Gerrit> :\"tag\"\n"
-            + "Copied-Label: Label4=+1 Account <1@Gerrit> :\"tag with characters %^#@^( *::!\"\n"
-            + "Copied-Label: -Label1 Account <1@gerrit>,Other Account <2@Gerrit>\\n"
+            + "Copied-Label: Label3=+1 Account <1@gerrit>,Other Account <2@gerrit> :\"tag\"\n"
+            + "Copied-Label: Label4=+1 Account <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+            + "Copied-Label: -Label1 Account <1@gerrit>,Other Account <2@gerrit>\\n"
             + "Copied-Label: -Label1 Account <1@gerrit>\n"
             + "Subject: This is a test change\n");
 
@@ -782,6 +782,7 @@
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
     walk.reset();
     ChangeNoteJson changeNoteJson = injector.getInstance(ChangeNoteJson.class);
-    return new ChangeNotesParser(newChange().getId(), tip, walk, changeNoteJson, args.metrics);
+    return new ChangeNotesParser(
+        newChange().getId(), tip, walk, changeNoteJson, args.metrics, serverId, externalIdCache);
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 8def660..4edfa8b4 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -2084,7 +2084,13 @@
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithComments =
           new ChangeNotesParser(
-              c.getId(), commitWithComments.copy(), rw, changeNoteJson, args.metrics);
+              c.getId(),
+              commitWithComments.copy(),
+              rw,
+              changeNoteJson,
+              args.metrics,
+              serverId,
+              externalIdCache);
       ChangeNotesState state = notesWithComments.parseAll();
       assertThat(state.approvals()).isEmpty();
       assertThat(state.publishedComments()).hasSize(1);
@@ -2093,7 +2099,13 @@
     try (ChangeNotesRevWalk rw = ChangeNotesCommit.newRevWalk(repo)) {
       ChangeNotesParser notesWithApprovals =
           new ChangeNotesParser(
-              c.getId(), commitWithApprovals.copy(), rw, changeNoteJson, args.metrics);
+              c.getId(),
+              commitWithApprovals.copy(),
+              rw,
+              changeNoteJson,
+              args.metrics,
+              serverId,
+              externalIdCache);
 
       ChangeNotesState state = notesWithApprovals.parseAll();
       assertThat(state.approvals()).hasSize(1);
diff --git a/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
new file mode 100644
index 0000000..bb49a6d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ImportedChangeNotesTest.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2022 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.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCache;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.inject.AbstractModule;
+import java.util.Optional;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ImportedChangeNotesTest extends AbstractChangeNotesTest {
+
+  private static final String FOREIGN_SERVER_ID = "foreign-server-id";
+  private static final String IMPORTED_SERVER_ID = "gerrit-imported-1";
+
+  private ExternalIdCache externalIdCacheMock;
+
+  @Before
+  @Override
+  public void setUpTestEnvironment() throws Exception {
+    setupTestPrerequisites();
+  }
+
+  private void initServerIds(String serverId, String... importedServerIds)
+      throws Exception, RepositoryCaseMismatchException, RepositoryNotFoundException {
+    externalIdCacheMock = mock(ExternalIdCache.class);
+    injector =
+        createTestInjector(
+            new AbstractModule() {
+              @Override
+              protected void configure() {
+                bind(ExternalIdCache.class).toInstance(externalIdCacheMock);
+              }
+            },
+            serverId,
+            importedServerIds);
+    injector.injectMembers(this);
+    createAllUsers(injector);
+  }
+
+  @Test
+  public void allowChangeFromImportedServerId() throws Exception {
+    initServerIds(LOCAL_SERVER_ID, IMPORTED_SERVER_ID);
+
+    ExternalId.Key importedAccountIdKey =
+        ExternalId.Key.create(
+            ExternalId.SCHEME_IMPORTED,
+            changeOwner.getAccountId() + "@" + IMPORTED_SERVER_ID,
+            false);
+    ExternalId importedAccountId =
+        ExternalId.create(importedAccountIdKey, changeOwner.getAccountId(), null, null, null);
+
+    when(externalIdCacheMock.byKey(eq(importedAccountIdKey)))
+        .thenReturn(Optional.of(importedAccountId));
+
+    Change importedChange = newChange(createTestInjector(IMPORTED_SERVER_ID), false);
+    Change localChange = newChange();
+
+    assertThat(newNotes(importedChange).getServerId()).isEqualTo(IMPORTED_SERVER_ID);
+    assertThat(newNotes(localChange).getServerId()).isEqualTo(LOCAL_SERVER_ID);
+  }
+
+  @Test
+  public void rejectChangeWithForeignServerId() throws Exception {
+    initServerIds(LOCAL_SERVER_ID);
+    when(externalIdCacheMock.byKey(any())).thenReturn(Optional.empty());
+
+    Change foreignChange = newChange(createTestInjector(FOREIGN_SERVER_ID), false);
+
+    InvalidServerIdException invalidServerIdEx =
+        assertThrows(InvalidServerIdException.class, () -> newNotes(foreignChange));
+
+    String invalidServerIdMessage = invalidServerIdEx.getMessage();
+    assertThat(invalidServerIdMessage).contains("expected " + LOCAL_SERVER_ID);
+    assertThat(invalidServerIdMessage).contains("actual: " + FOREIGN_SERVER_ID);
+  }
+
+  @Test
+  public void changeFromImportedServerIdWithUnknownAccountId() throws Exception {
+    initServerIds(LOCAL_SERVER_ID, IMPORTED_SERVER_ID);
+
+    when(externalIdCacheMock.byKey(any())).thenReturn(Optional.empty());
+
+    Change importedChange = newChange(createTestInjector(IMPORTED_SERVER_ID), false);
+    assertThat(newNotes(importedChange).getServerId()).isEqualTo(IMPORTED_SERVER_ID);
+
+    assertThat(newNotes(importedChange).getChange().getOwner())
+        .isEqualTo(Account.UNKNOWN_ACCOUNT_ID);
+  }
+}