Allow to update external IDs with BatchRefUpdate

This change adds a VersionedMetaData subclass to update external IDs,
called ExternalIdNotes. Having this class has several advantages:

- External IDs can now be updated as part of a BatchRefUpdate:
  This will allow us to update accounts and external IDs atomically
  (implemented in follow-up changes).
- External ID updates can now easily be batched and committed
  atomically:
  The ExternalIdsBatchUpdate class which implemented replacing of
  external IDs in a batch becomes unneeded and is removed.
- Reading and writing external IDs is now implemented in the same class
  (ExternalIdNotes)
- External ID updates from init, schema migrations, standalone programs
  and tests become easier.
- Using VersionedMetaData to update the external ID notes branch is
  consistent with using VersionedMetaData to update the group names notes
  branch.

Quite a lot of code had to be refactored for this. The most important
changes are:

- The code to parse external IDs is moved from ExternalIdReader to
  ExternalIdNotes:
  ExternalIdReader still takes care of opening the repository and
  loading the external IDs. In addition it updates the metric for
  reading all external IDs and allows tests to disable reading external
  IDs.
- The code to update external IDs is moved from ExternalIdsUpdate to
  ExternalIdNotes:
  ExternalIdsUpdate now only takes care to retry external ID updates on
  LockFailure. When external IDs are updated atomically with account
  updates (follow-up changes) most methods will be removed. Basically
  all external ID updates that touch external IDs of a single account
  will be done through AccountsUpdate. ExternalIdsUpdate will then only
  be used to update external IDs across multiple accounts (if we have
  such updates).
- External ID updates from init, schema migrations, standalone programs
  and tests are now done via ExternalIdNotes:
  This removes the need for public static methods and reduces code
  duplication.

How does ExternalIdNotes work?

- On load the note map from refs/meta/external-ids is read (but the
  external IDs are not parsed yet).
- After loading the note map callers can access single or all external
  IDs. Only now the requested external IDs are parsed.
- After loading the note map callers can stage various external ID
  updates (insert, upsert, delete, replace).
- On save the staged external ID updates are performed.
- After committing the external IDs ExternalIdNotes can be asked to
  update the ExternalIdCache and to evict affected accounts from the
  account cache (which triggers a reindex of the accounts).

There are several modes of instantiating ExternalIdNotes:

- Through Factories
  - ExternalIdNotes.Factory:
    Used to get the caches injected.
  - ExternalIdNotes.FactoryNoReindex:
    Used to get the external IDs cache injected, the account cache is
    not needed since accounts should not be reindex). Used by the
    LocalUsernamesToLowerCase site program and internally at Google.
- Through static methods:
  - loadReadOnly:
    Used when external IDs are only read (since there are no updates the
    caches are not needed)
  - loadNoCacheUpdate:
    Used when updates should be done but the caches don't need to be
    updated (during init and schema migrations where the caches are
    not bound)
- Through package-private constructor:
  Only used by ExternalIdsUpdate that supports external ID updates with
  and without reindex (hence we don't know which of the factories to
  inject).

I tested the schema migrations, init and the LocalUsernamesToLowerCase
site program manually.

Change-Id: Ib643ae8072c61ec467c92f3d1672e595a9cdcbc8
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2017-12-14 10:12:14 +01:00
parent d42596d8e9
commit c9cb9a8bd5
15 changed files with 1030 additions and 974 deletions

View File

@@ -17,20 +17,28 @@ package com.google.gerrit.pgm;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsBatchUpdate;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.inject.AbstractModule;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
/** Converts the local username for all accounts to lower case */
@@ -38,10 +46,12 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
private final LifecycleManager manager = new LifecycleManager();
private final TextProgressMonitor monitor = new TextProgressMonitor();
@Inject private GitRepositoryManager repoManager;
@Inject private AllUsersName allUsersName;
@Inject private Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
@Inject private ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
@Inject private ExternalIds externalIds;
@Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
@Override
public int run() throws Exception {
Injector dbInjector = createDbInjector(MULTI_USER);
@@ -49,9 +59,12 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
manager.start();
dbInjector
.createChildInjector(
new AbstractModule() {
new FactoryModule() {
@Override
protected void configure() {
bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
factory(MetaDataUpdate.InternalFactory.class);
// The LocalUsernamesToLowerCase program needs to access all external IDs only
// once to update them. After the update they are not accessed again. Hence the
// LocalUsernamesToLowerCase program doesn't benefit from caching external IDs and
@@ -64,12 +77,18 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
Collection<ExternalId> todo = externalIds.all();
monitor.beginTask("Converting local usernames", todo.size());
try (Repository repo = repoManager.openRepository(allUsersName)) {
ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
for (ExternalId extId : todo) {
convertLocalUserToLowerCase(extId);
convertLocalUserToLowerCase(extIdNotes, extId);
monitor.update(1);
}
try (MetaDataUpdate metaDataUpdate = metaDataUpdateServerFactory.get().create(allUsersName)) {
metaDataUpdate.setMessage("Convert local usernames to lower case");
extIdNotes.commit(metaDataUpdate);
}
}
externalIdsBatchUpdate.commit("Convert local usernames to lower case");
monitor.endTask();
int exitCode = reindexAccounts();
@@ -77,7 +96,8 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
return exitCode;
}
private void convertLocalUserToLowerCase(ExternalId extId) {
private void convertLocalUserToLowerCase(ExternalIdNotes extIdNotes, ExternalId extId)
throws OrmDuplicateKeyException, IOException {
if (extId.isScheme(SCHEME_GERRIT)) {
String localUser = extId.key().id();
String localUserLowerCase = localUser.toLowerCase(Locale.US);
@@ -89,7 +109,7 @@ public class LocalUsernamesToLowerCase extends SiteProgram {
extId.accountId(),
extId.email(),
extId.password());
externalIdsBatchUpdate.replace(extId, extIdLowerCase);
extIdNotes.replace(extId, extIdLowerCase);
}
}
}

View File

@@ -19,10 +19,10 @@ import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdReader;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.File;
@@ -31,13 +31,9 @@ import java.nio.file.Path;
import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FS;
public class ExternalIdsOnInit {
@@ -54,32 +50,20 @@ public class ExternalIdsOnInit {
public synchronized void insert(String commitMessage, Collection<ExternalId> extIds)
throws OrmException, IOException, ConfigInvalidException {
File path = getPath();
if (path != null) {
try (Repository repo = new FileRepository(path);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
for (ExternalId extId : extIds) {
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
try (Repository allUsersRepo = new FileRepository(path)) {
ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(allUsersRepo);
extIdNotes.insert(extIds);
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(
GitReferenceUpdated.DISABLED, new Project.NameKey(allUsers), allUsersRepo)) {
PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
ExternalIdsUpdate.commit(
new Project.NameKey(allUsers),
repo,
rw,
ins,
rev,
noteMap,
commitMessage,
serverIdent,
serverIdent,
null,
GitReferenceUpdated.DISABLED);
metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
metaDataUpdate.getCommitBuilder().setMessage(commitMessage);
extIdNotes.commit(metaDataUpdate);
}
}
}
}

View File

@@ -16,11 +16,13 @@ package com.google.gerrit.server.account.externalids;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.Nullable;
@@ -28,6 +30,7 @@ import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.HashedPassword;
import java.io.Serializable;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -123,6 +126,10 @@ public abstract class ExternalId implements Serializable {
public String toString() {
return get();
}
public static ImmutableSet<ExternalId.Key> from(Collection<ExternalId> extIds) {
return extIds.stream().map(ExternalId::key).collect(toImmutableSet());
}
}
public static ExternalId create(String scheme, String id, Account.Id accountId) {

View File

@@ -127,7 +127,7 @@ class ExternalIdCacheImpl implements ExternalIdCache {
Collection<ExternalId> toRemove,
Collection<ExternalId> toAdd)
throws IOException {
ExternalIdsUpdate.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId);
ExternalIdNotes.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId);
updateCache(
oldNotesRev,

View File

@@ -0,0 +1,754 @@
// Copyright (C) 2017 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.account.externalids;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link VersionedMetaData} subclass to update external IDs.
*
* <p>On load the note map from {@code refs/meta/external-ids} is read, but the external IDs are not
* parsed yet (see {@link #onLoad()}).
*
* <p>After loading the note map callers can access single or all external IDs. Only now the
* requested external IDs are parsed.
*
* <p>After loading the note map callers can stage various external ID updates (insert, upsert,
* delete, replace).
*
* <p>On save the staged external ID updates are performed (see {@link #onSave(CommitBuilder)}).
*
* <p>After committing the external IDs a cache update can be requested which also reindexes the
* accounts for which external IDs have been updated (see {@link #updateCaches()}).
*/
public class ExternalIdNotes extends VersionedMetaData {
private static final Logger log = LoggerFactory.getLogger(ExternalIdNotes.class);
private static final int MAX_NOTE_SZ = 1 << 19;
@Singleton
public static class Factory {
private final ExternalIdCache externalIdCache;
private final AccountCache accountCache;
@Inject
Factory(ExternalIdCache externalIdCache, AccountCache accountCache) {
this.externalIdCache = externalIdCache;
this.accountCache = accountCache;
}
public ExternalIdNotes load(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(externalIdCache, accountCache, allUsersRepo).load();
}
}
@Singleton
public static class FactoryNoReindex {
private final ExternalIdCache externalIdCache;
@Inject
FactoryNoReindex(ExternalIdCache externalIdCache) {
this.externalIdCache = externalIdCache;
}
public ExternalIdNotes load(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(externalIdCache, null, allUsersRepo).load();
}
}
public static ExternalIdNotes loadReadOnly(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo)
.setReadOnly()
.load();
}
public static ExternalIdNotes loadReadOnly(Repository allUsersRepo, ObjectId rev)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo)
.setReadOnly()
.load(rev);
}
public static ExternalIdNotes loadNoCacheUpdate(Repository allUsersRepo)
throws IOException, ConfigInvalidException {
return new ExternalIdNotes(new DisabledExternalIdCache(), null, allUsersRepo).load();
}
private final ExternalIdCache externalIdCache;
@Nullable private final AccountCache accountCache;
private final Repository repo;
private NoteMap noteMap;
private ObjectId oldRev;
// Staged note map updates that should be executed on save.
private List<NoteMapUpdate> noteMapUpdates = new ArrayList<>();
// Staged cache updates that should be executed after external ID changes have been committed.
private List<CacheUpdate> cacheUpdates = new ArrayList<>();
private Runnable afterReadRevision;
private boolean readOnly = false;
ExternalIdNotes(
ExternalIdCache externalIdCache,
@Nullable AccountCache accountCache,
Repository allUsersRepo) {
this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
this.accountCache = accountCache;
this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
}
public ExternalIdNotes setAfterReadRevision(Runnable afterReadRevision) {
this.afterReadRevision = afterReadRevision;
return this;
}
private ExternalIdNotes setReadOnly() {
this.readOnly = true;
return this;
}
public Repository getRepository() {
return repo;
}
@Override
protected String getRefName() {
return RefNames.REFS_EXTERNAL_IDS;
}
ExternalIdNotes load() throws IOException, ConfigInvalidException {
load(repo);
return this;
}
ExternalIdNotes load(ObjectId rev) throws IOException, ConfigInvalidException {
load(repo, rev);
return this;
}
/**
* Parses and returns the specified external ID.
*
* @param key the key of the external ID
* @return the external ID, {@code Optional.empty()} if it doesn't exist
*/
public Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
checkLoaded();
ObjectId noteId = key.sha1();
if (!noteMap.contains(noteId)) {
return Optional.empty();
}
try (RevWalk rw = new RevWalk(repo)) {
ObjectId noteDataId = noteMap.get(noteId);
byte[] raw = readNoteData(rw, noteDataId);
return Optional.of(ExternalId.parse(noteId.name(), raw, noteDataId));
}
}
/**
* Parses and returns the specified external IDs.
*
* @param keys the keys of the external IDs
* @return the external IDs
*/
public Set<ExternalId> get(Collection<ExternalId.Key> keys)
throws IOException, ConfigInvalidException {
checkLoaded();
HashSet<ExternalId> externalIds = Sets.newHashSetWithExpectedSize(keys.size());
for (ExternalId.Key key : keys) {
get(key).ifPresent(externalIds::add);
}
return externalIds;
}
/**
* Parses and returns all external IDs.
*
* <p>Invalid external IDs are ignored.
*
* @return all external IDs
*/
public Set<ExternalId> all() throws IOException {
checkLoaded();
try (RevWalk rw = new RevWalk(repo)) {
Set<ExternalId> extIds = new HashSet<>();
for (Note note : noteMap) {
byte[] raw = readNoteData(rw, note.getData());
try {
extIds.add(ExternalId.parse(note.getName(), raw, note.getData()));
} catch (ConfigInvalidException | RuntimeException e) {
log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
}
}
return extIds;
}
}
NoteMap getNoteMap() {
checkLoaded();
return noteMap;
}
static byte[] readNoteData(RevWalk rw, ObjectId noteDataId) throws IOException {
return rw.getObjectReader().open(noteDataId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
}
/**
* Inserts a new external ID.
*
* @throws IOException on IO error while checking if external ID already exists
* @throws OrmDuplicateKeyException if the external ID already exists
*/
public void insert(ExternalId extId) throws IOException, OrmDuplicateKeyException {
insert(Collections.singleton(extId));
}
/**
* Inserts new external IDs.
*
* @throws IOException on IO error while checking if external IDs already exist
* @throws OrmDuplicateKeyException if any of the external ID already exists
*/
public void insert(Collection<ExternalId> extIds) throws IOException, OrmDuplicateKeyException {
checkLoaded();
checkExternalIdsDontExist(extIds);
Set<ExternalId> newExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId extId : extIds) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
newExtIds.add(insertedExtId);
}
});
cacheUpdates.add(cu -> cu.add(newExtIds));
}
/**
* Inserts or updates an external ID.
*
* <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
*/
public void upsert(ExternalId extId) throws IOException, ConfigInvalidException {
upsert(Collections.singleton(extId));
}
/**
* Inserts or updates external IDs.
*
* <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
*/
public void upsert(Collection<ExternalId> extIds) throws IOException, ConfigInvalidException {
checkLoaded();
Set<ExternalId> removedExtIds = get(ExternalId.Key.from(extIds));
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId extId : extIds) {
ExternalId updatedExtId = upsert(rw, inserter, noteMap, extId);
updatedExtIds.add(updatedExtId);
}
});
cacheUpdates.add(cu -> cu.remove(removedExtIds).add(updatedExtIds));
}
/**
* Deletes an external ID.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key, but otherwise doesn't match the specified external ID.
*/
public void delete(ExternalId extId) {
delete(Collections.singleton(extId));
}
/**
* Deletes external IDs.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key as any of the external IDs that should be deleted, but otherwise doesn't match the that
* external ID.
*/
public void delete(Collection<ExternalId> extIds) {
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId extId : extIds) {
remove(rw, noteMap, extId);
removedExtIds.add(extId);
}
});
cacheUpdates.add(cu -> cu.remove(removedExtIds));
}
/**
* Delete an external ID by key.
*
* @throws IllegalStateException is thrown if the external ID does not belong to the specified
* account.
*/
public void delete(Account.Id accountId, ExternalId.Key extIdKey) {
delete(accountId, Collections.singleton(extIdKey));
}
/**
* Delete external IDs by external ID key.
*
* @throws IllegalStateException is thrown if any of the external IDs does not belong to the
* specified account.
*/
public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys) {
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId.Key extIdKey : extIdKeys) {
ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
removedExtIds.add(removedExtId);
}
});
cacheUpdates.add(cu -> cu.remove(removedExtIds));
}
/**
* Delete external IDs by external ID key.
*
* <p>The external IDs are deleted regardless of which account they belong to.
*/
public void deleteByKeys(Collection<ExternalId.Key> extIdKeys) {
checkLoaded();
Set<ExternalId> removedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId.Key extIdKey : extIdKeys) {
ExternalId extId = remove(rw, noteMap, extIdKey, null);
removedExtIds.add(extId);
}
});
cacheUpdates.add(cu -> cu.remove(removedExtIds));
}
/**
* Replaces external IDs for an account by external ID keys.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID key is specified for deletion and an external ID with the same key is specified to
* be added, the old external ID with that key is deleted first and then the new external ID is
* added (so the external ID for that key is replaced).
*
* @throws IllegalStateException is thrown if any of the specified external IDs does not belong to
* the specified account.
*/
public void replace(
Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, OrmDuplicateKeyException {
checkLoaded();
checkSameAccount(toAdd, accountId);
checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), toDelete);
Set<ExternalId> removedExtIds = new HashSet<>();
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId.Key extIdKey : toDelete) {
ExternalId removedExtId = remove(rw, noteMap, extIdKey, accountId);
if (removedExtId != null) {
removedExtIds.add(removedExtId);
}
}
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
updatedExtIds.add(insertedExtId);
}
});
cacheUpdates.add(cu -> cu.add(updatedExtIds).remove(removedExtIds));
}
/**
* Replaces external IDs for an account by external ID keys.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID key is specified for deletion and an external ID with the same key is specified to
* be added, the old external ID with that key is deleted first and then the new external ID is
* added (so the external ID for that key is replaced).
*
* <p>The external IDs are replaced regardless of which account they belong to.
*/
public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, OrmDuplicateKeyException {
checkLoaded();
checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), toDelete);
Set<ExternalId> removedExtIds = new HashSet<>();
Set<ExternalId> updatedExtIds = new HashSet<>();
noteMapUpdates.add(
(rw, n) -> {
for (ExternalId.Key extIdKey : toDelete) {
ExternalId removedExtId = remove(rw, noteMap, extIdKey, null);
removedExtIds.add(removedExtId);
}
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
updatedExtIds.add(insertedExtId);
}
});
cacheUpdates.add(cu -> cu.add(updatedExtIds).remove(removedExtIds));
}
/**
* Replaces an external ID.
*
* @throws IllegalStateException is thrown if the specified external IDs belong to different
* accounts.
*/
public void replace(ExternalId toDelete, ExternalId toAdd)
throws IOException, OrmDuplicateKeyException {
replace(Collections.singleton(toDelete), Collections.singleton(toAdd));
}
/**
* Replaces external IDs.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID is specified for deletion and an external ID with the same key is specified to be
* added, the old external ID with that key is deleted first and then the new external ID is added
* (so the external ID for that key is replaced).
*
* @throws IllegalStateException is thrown if the specified external IDs belong to different
* accounts.
*/
public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
throws IOException, OrmDuplicateKeyException {
Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
if (accountId == null) {
// toDelete and toAdd are empty -> nothing to do
return;
}
replace(accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
noteMap = revision != null ? NoteMap.read(reader, revision) : NoteMap.newEmptyMap();
if (afterReadRevision != null) {
afterReadRevision.run();
}
}
@Override
public RevCommit commit(MetaDataUpdate update) throws IOException {
oldRev = revision != null ? revision.copy() : ObjectId.zeroId();
return super.commit(update);
}
/**
* Updates the caches (external ID cache, account cache) and reindexes the accounts for which
* external IDs were modified.
*
* <p>Must only be called after committing changes.
*
* <p>No-op if this instance was created by {@link #loadNoCacheUpdate(Repository)}.
*
* <p>No eviction from account cache if this instance was created by {@link FactoryNoReindex}.
*/
public void updateCaches() throws IOException {
checkState(oldRev != null, "no changes committed yet");
ExternalIdCacheUpdates externalIdCacheUpdates = new ExternalIdCacheUpdates();
for (CacheUpdate cacheUpdate : cacheUpdates) {
cacheUpdate.execute(externalIdCacheUpdates);
}
externalIdCache.onReplace(
oldRev,
getRevision(),
externalIdCacheUpdates.getRemoved(),
externalIdCacheUpdates.getAdded());
if (accountCache != null) {
for (Account.Id id :
Streams.concat(
externalIdCacheUpdates.getAdded().stream(),
externalIdCacheUpdates.getRemoved().stream())
.map(ExternalId::accountId)
.collect(toSet())) {
accountCache.evict(id);
}
}
cacheUpdates.clear();
oldRev = null;
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkState(!readOnly, "Updating external IDs is disabled");
if (noteMapUpdates.isEmpty()) {
return false;
}
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Update external IDs\n");
}
try (RevWalk rw = new RevWalk(repo)) {
for (NoteMapUpdate noteMapUpdate : noteMapUpdates) {
try {
noteMapUpdate.execute(rw, noteMap);
} catch (OrmDuplicateKeyException e) {
throw new IOException(e);
}
}
noteMapUpdates.clear();
RevTree oldTree = revision != null ? rw.parseTree(revision) : null;
ObjectId newTreeId = noteMap.writeTree(inserter);
if (newTreeId.equals(oldTree)) {
return false;
}
commit.setTreeId(newTreeId);
return true;
}
}
/**
* Checks that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
private static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
return checkSameAccount(extIds, null);
}
/**
* Checks that all specified external IDs belong to specified account. If no account is specified
* it is checked that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
static Account.Id checkSameAccount(Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
for (ExternalId extId : extIds) {
if (accountId == null) {
accountId = extId.accountId();
continue;
}
checkState(
accountId.equals(extId.accountId()),
"external id %s belongs to account %s, expected account %s",
extId.key().get(),
extId.accountId().get(),
accountId.get());
}
return accountId;
}
/**
* Insert or updates an new external ID and sets it in the note map.
*
* <p>If the external ID already exists it is overwritten.
*/
private static ExternalId upsert(
RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
if (noteMap.contains(extId.key().sha1())) {
byte[] raw = readNoteData(rw, noteMap.get(noteId));
try {
c = new BlobBasedConfig(null, raw);
} catch (ConfigInvalidException e) {
throw new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
}
}
extId.writeToConfig(c);
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId noteData = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, noteData);
return ExternalId.create(extId, noteData);
}
/**
* Removes an external ID from the note map.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key, but otherwise doesn't match the specified external ID.
*/
private static ExternalId remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
if (!noteMap.contains(noteId)) {
return null;
}
ObjectId noteDataId = noteMap.get(noteId);
byte[] raw = readNoteData(rw, noteDataId);
ExternalId actualExtId = ExternalId.parse(noteId.name(), raw, noteDataId);
checkState(
extId.equals(actualExtId),
"external id %s should be removed, but it's not matching the actual external id %s",
extId.toString(),
actualExtId.toString());
noteMap.remove(noteId);
return actualExtId;
}
/**
* Removes an external ID from the note map by external ID key.
*
* @throws IllegalStateException is thrown if an expected account ID is provided and an external
* ID with the specified key exists, but belongs to another account.
* @return the external ID that was removed, {@code null} if no external ID with the specified key
* exists
*/
private static ExternalId remove(
RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extIdKey.sha1();
if (!noteMap.contains(noteId)) {
return null;
}
ObjectId noteDataId = noteMap.get(noteId);
byte[] raw = readNoteData(rw, noteDataId);
ExternalId extId = ExternalId.parse(noteId.name(), raw, noteDataId);
if (expectedAccountId != null) {
checkState(
expectedAccountId.equals(extId.accountId()),
"external id %s should be removed for account %s,"
+ " but external id belongs to account %s",
extIdKey.get(),
expectedAccountId.get(),
extId.accountId().get());
}
noteMap.remove(noteId);
return extId;
}
private void checkExternalIdsDontExist(Collection<ExternalId> extIds)
throws OrmDuplicateKeyException, IOException {
checkExternalIdKeysDontExist(ExternalId.Key.from(extIds));
}
private void checkExternalIdKeysDontExist(
Collection<ExternalId.Key> extIdKeysToAdd, Collection<ExternalId.Key> extIdKeysToDelete)
throws OrmDuplicateKeyException, IOException {
HashSet<ExternalId.Key> newKeys = new HashSet<>(extIdKeysToAdd);
newKeys.removeAll(extIdKeysToDelete);
checkExternalIdKeysDontExist(newKeys);
}
private void checkExternalIdKeysDontExist(Collection<ExternalId.Key> extIdKeys)
throws IOException, OrmDuplicateKeyException {
for (ExternalId.Key extIdKey : extIdKeys) {
if (noteMap.contains(extIdKey.sha1())) {
throw new OrmDuplicateKeyException(
String.format("external id %s already exists", extIdKey.get()));
}
}
}
private void checkLoaded() {
checkState(noteMap != null, "External IDs not loaded yet");
}
@FunctionalInterface
private interface NoteMapUpdate {
void execute(RevWalk rw, NoteMap noteMap)
throws IOException, ConfigInvalidException, OrmDuplicateKeyException;
}
@FunctionalInterface
private interface CacheUpdate {
void execute(ExternalIdCacheUpdates cacheUpdates) throws IOException;
}
private static class ExternalIdCacheUpdates {
private final Set<ExternalId> added = new HashSet<>();
private final Set<ExternalId> removed = new HashSet<>();
ExternalIdCacheUpdates add(Collection<ExternalId> extIds) {
this.added.addAll(extIds);
return this;
}
public Set<ExternalId> getAdded() {
return ImmutableSet.copyOf(added);
}
ExternalIdCacheUpdates remove(Collection<ExternalId> extIds) {
this.removed.addAll(extIds);
return this;
}
public Set<ExternalId> getRemoved() {
return ImmutableSet.copyOf(removed);
}
}
}

View File

@@ -14,10 +14,7 @@
package com.google.gerrit.server.account.externalids;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
@@ -29,17 +26,13 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to read external IDs from NoteDb.
@@ -58,10 +51,6 @@ import org.slf4j.LoggerFactory;
*/
@Singleton
public class ExternalIdReader {
private static final Logger log = LoggerFactory.getLogger(ExternalIdReader.class);
public static final int MAX_NOTE_SZ = 1 << 19;
public static ObjectId readRevision(Repository repo) throws IOException {
Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
return ref != null ? ref.getObjectId() : ObjectId.zeroId();
@@ -104,11 +93,12 @@ public class ExternalIdReader {
}
/** Reads and returns all external IDs. */
Set<ExternalId> all() throws IOException {
Set<ExternalId> all() throws IOException, ConfigInvalidException {
checkReadEnabled();
try (Repository repo = repoManager.openRepository(allUsersName)) {
return all(repo, readRevision(repo));
try (Timer0.Context ctx = readAllLatency.start();
Repository repo = repoManager.openRepository(allUsersName)) {
return ExternalIdNotes.loadReadOnly(repo).all();
}
}
@@ -116,34 +106,12 @@ public class ExternalIdReader {
* Reads and returns all external IDs from the specified revision of the refs/meta/external-ids
* branch.
*/
Set<ExternalId> all(ObjectId rev) throws IOException {
Set<ExternalId> all(ObjectId rev) throws IOException, ConfigInvalidException {
checkReadEnabled();
try (Repository repo = repoManager.openRepository(allUsersName)) {
return all(repo, rev);
}
}
/** Reads and returns all external IDs. */
private Set<ExternalId> all(Repository repo, ObjectId rev) throws IOException {
if (rev.equals(ObjectId.zeroId())) {
return ImmutableSet.of();
}
try (Timer0.Context ctx = readAllLatency.start();
RevWalk rw = new RevWalk(repo)) {
NoteMap noteMap = readNoteMap(rw, rev);
Set<ExternalId> extIds = new HashSet<>();
for (Note note : noteMap) {
byte[] raw =
rw.getObjectReader().open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
try {
extIds.add(ExternalId.parse(note.getName(), raw, note.getData()));
} catch (Exception e) {
log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
}
}
return extIds;
Repository repo = repoManager.openRepository(allUsersName)) {
return ExternalIdNotes.loadReadOnly(repo, rev).all();
}
}
@@ -152,14 +120,8 @@ public class ExternalIdReader {
ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
checkReadEnabled();
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo)) {
ObjectId rev = readRevision(repo);
if (rev.equals(ObjectId.zeroId())) {
return null;
}
return parse(key, rw, rev);
try (Repository repo = repoManager.openRepository(allUsersName)) {
return ExternalIdNotes.loadReadOnly(repo).get(key).orElse(null);
}
}
@@ -168,27 +130,9 @@ public class ExternalIdReader {
ExternalId get(ExternalId.Key key, ObjectId rev) throws IOException, ConfigInvalidException {
checkReadEnabled();
if (rev.equals(ObjectId.zeroId())) {
return null;
try (Repository repo = repoManager.openRepository(allUsersName)) {
return ExternalIdNotes.loadReadOnly(repo, rev).get(key).orElse(null);
}
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo)) {
return parse(key, rw, rev);
}
}
private static ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev)
throws IOException, ConfigInvalidException {
NoteMap noteMap = readNoteMap(rw, rev);
ObjectId noteId = key.sha1();
if (!noteMap.contains(noteId)) {
return null;
}
ObjectId noteData = noteMap.get(noteId);
byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
return ExternalId.parse(noteId.name(), raw, noteData);
}
private void checkReadEnabled() throws IOException {

View File

@@ -43,12 +43,12 @@ public class ExternalIds {
}
/** Returns all external IDs. */
public Set<ExternalId> all() throws IOException {
public Set<ExternalId> all() throws IOException, ConfigInvalidException {
return externalIdReader.all();
}
/** Returns all external IDs from the specified revision of the refs/meta/external-ids branch. */
public Set<ExternalId> all(ObjectId rev) throws IOException {
public Set<ExternalId> all(ObjectId rev) throws IOException, ConfigInvalidException {
return externalIdReader.all(rev);
}

View File

@@ -1,129 +0,0 @@
// Copyright (C) 2016 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.account.externalids;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* This class allows to do batch updates to external IDs.
*
* <p>For NoteDb all updates will result in a single commit to the refs/meta/external-ids branch.
* This means callers can prepare many updates by invoking {@link #replace(ExternalId, ExternalId)}
* multiple times and when {@link ExternalIdsBatchUpdate#commit(String)} is invoked a single NoteDb
* commit is created that contains all the prepared updates.
*/
public class ExternalIdsBatchUpdate {
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsersName;
private final PersonIdent serverIdent;
private final ExternalIdCache externalIdCache;
private final Set<ExternalId> toAdd = new HashSet<>();
private final Set<ExternalId> toDelete = new HashSet<>();
@Inject
public ExternalIdsBatchUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
AllUsersName allUsersName,
@GerritPersonIdent PersonIdent serverIdent,
ExternalIdCache externalIdCache) {
this.repoManager = repoManager;
this.gitRefUpdated = gitRefUpdated;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
this.externalIdCache = externalIdCache;
}
/**
* Adds an external ID replacement to the batch.
*
* <p>The actual replacement is only done when {@link #commit(String)} is invoked.
*/
public void replace(ExternalId extIdToDelete, ExternalId extIdToAdd) {
ExternalIdsUpdate.checkSameAccount(ImmutableSet.of(extIdToDelete, extIdToAdd));
toAdd.add(extIdToAdd);
toDelete.add(extIdToDelete);
}
/**
* Commits this batch.
*
* <p>This means external ID replacements which were prepared by invoking {@link
* #replace(ExternalId, ExternalId)} are now executed. Deletion of external IDs is done before
* adding the new external IDs. This means if an external ID is specified for deletion and an
* external ID with the same key is specified to be added, the old external ID with that key is
* deleted first and then the new external ID is added (so the external ID for that key is
* replaced).
*
* <p>For NoteDb a single commit is created that contains all the external ID updates.
*/
public void commit(String commitMessage)
throws IOException, OrmException, ConfigInvalidException {
if (toDelete.isEmpty() && toAdd.isEmpty()) {
return;
}
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
for (ExternalId extId : toDelete) {
ExternalIdsUpdate.remove(rw, noteMap, extId);
}
for (ExternalId extId : toAdd) {
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
ObjectId newRev =
ExternalIdsUpdate.commit(
allUsersName,
repo,
rw,
ins,
rev,
noteMap,
commitMessage,
serverIdent,
serverIdent,
null,
gitRefUpdated);
externalIdCache.onReplace(rev, newRev, toDelete, toAdd);
}
toAdd.clear();
toDelete.clear();
}
}

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.account.externalids;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static java.util.stream.Collectors.joining;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
@@ -58,31 +57,29 @@ public class ExternalIdsConsistencyChecker {
this.validator = validator;
}
public List<ConsistencyProblemInfo> check() throws IOException {
public List<ConsistencyProblemInfo> check() throws IOException, ConfigInvalidException {
try (Repository repo = repoManager.openRepository(allUsers)) {
return check(repo, ExternalIdReader.readRevision(repo));
return check(ExternalIdNotes.loadReadOnly(repo));
}
}
public List<ConsistencyProblemInfo> check(ObjectId rev) throws IOException {
public List<ConsistencyProblemInfo> check(ObjectId rev)
throws IOException, ConfigInvalidException {
try (Repository repo = repoManager.openRepository(allUsers)) {
return check(repo, rev);
return check(ExternalIdNotes.loadReadOnly(repo, rev));
}
}
private List<ConsistencyProblemInfo> check(Repository repo, ObjectId commit) throws IOException {
private List<ConsistencyProblemInfo> check(ExternalIdNotes extIdNotes) throws IOException {
List<ConsistencyProblemInfo> problems = new ArrayList<>();
ListMultimap<String, ExternalId.Key> emails =
MultimapBuilder.hashKeys().arrayListValues().build();
try (RevWalk rw = new RevWalk(repo)) {
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, commit);
try (RevWalk rw = new RevWalk(extIdNotes.getRepository())) {
NoteMap noteMap = extIdNotes.getNoteMap();
for (Note note : noteMap) {
byte[] raw =
rw.getObjectReader()
.open(note.getData(), OBJ_BLOB)
.getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
byte[] raw = ExternalIdNotes.readNoteData(rw, note.getData());
try {
ExternalId extId = ExternalId.parse(note.getName(), raw, note.getData());
problems.addAll(validateExternalId(extId));

View File

@@ -15,35 +15,18 @@
package com.google.gerrit.server.account.externalids;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.account.externalids.ExternalIdReader.MAX_NOTE_SZ;
import static com.google.gerrit.server.account.externalids.ExternalIdReader.readNoteMap;
import static com.google.gerrit.server.account.externalids.ExternalIdReader.readRevision;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
@@ -53,20 +36,8 @@ import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* Updates externalIds in ReviewDb and NoteDb.
@@ -89,8 +60,6 @@ import org.eclipse.jgit.revwalk.RevWalk;
* cache and thus triggers reindex for them.
*/
public class ExternalIdsUpdate {
private static final String COMMIT_MSG = "Update external IDs";
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
*
@@ -100,50 +69,43 @@ public class ExternalIdsUpdate {
@Singleton
public static class Server {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final Provider<PersonIdent> serverIdent;
private final GitReferenceUpdated gitRefUpdated;
private final RetryHelper retryHelper;
@Inject
public Server(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.gitRefUpdated = gitRefUpdated;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateServerFactory.get().create(allUsersName),
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
i,
i,
null,
gitRefUpdated,
retryHelper);
}
}
@@ -160,47 +122,40 @@ public class ExternalIdsUpdate {
@Singleton
public static class ServerNoReindex {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final Provider<PersonIdent> serverIdent;
private final GitReferenceUpdated gitRefUpdated;
private final RetryHelper retryHelper;
@Inject
public ServerNoReindex(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.gitRefUpdated = gitRefUpdated;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateServerFactory.get().create(allUsersName),
null,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
i,
i,
null,
gitRefUpdated,
retryHelper);
}
}
@@ -214,98 +169,74 @@ public class ExternalIdsUpdate {
@Singleton
public static class User {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory;
private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final Provider<PersonIdent> serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
private final GitReferenceUpdated gitRefUpdated;
private final RetryHelper retryHelper;
@Inject
public User(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.User> metaDataUpdateUserFactory,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<IdentifiedUser> identifiedUser,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateUserFactory = metaDataUpdateUserFactory;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.identifiedUser = identifiedUser;
this.gitRefUpdated = gitRefUpdated;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
IdentifiedUser user = identifiedUser.get();
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateUserFactory.get().create(allUsersName),
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
createPersonIdent(i, user),
i,
user,
gitRefUpdated,
retryHelper);
}
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
}
}
private final GitRepositoryManager repoManager;
private final MetaDataUpdateFactory metaDataUpdateFactory;
@Nullable private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final PersonIdent committerIdent;
private final PersonIdent authorIdent;
@Nullable private final IdentifiedUser currentUser;
private final GitReferenceUpdated gitRefUpdated;
private final RetryHelper retryHelper;
private final Runnable afterReadRevision;
private final Counter0 updateCount;
private ExternalIdsUpdate(
GitRepositoryManager repoManager,
MetaDataUpdateFactory metaDataUpdateFactory,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent,
@Nullable IdentifiedUser currentUser,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper) {
this(
repoManager,
metaDataUpdateFactory,
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
committerIdent,
authorIdent,
currentUser,
gitRefUpdated,
retryHelper,
Runnables.doNothing());
}
@@ -313,26 +244,20 @@ public class ExternalIdsUpdate {
@VisibleForTesting
public ExternalIdsUpdate(
GitRepositoryManager repoManager,
MetaDataUpdateFactory metaDataUpdateFactory,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent,
@Nullable IdentifiedUser currentUser,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper,
Runnable afterReadRevision) {
this.repoManager = checkNotNull(repoManager, "repoManager");
this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory");
this.accountCache = accountCache;
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
this.committerIdent = checkNotNull(committerIdent, "committerIdent");
this.externalIds = checkNotNull(externalIds, "externalIds");
this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
this.authorIdent = checkNotNull(authorIdent, "authorIdent");
this.currentUser = currentUser;
this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated");
this.retryHelper = checkNotNull(retryHelper, "retryHelper");
this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
this.updateCount =
@@ -358,18 +283,7 @@ public class ExternalIdsUpdate {
*/
public void insert(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId extId : extIds) {
ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
updatedExtIds.onUpdate(insertedExtId);
}
return updatedExtIds;
});
externalIdCache.onCreate(u.oldRev(), u.newRev(), u.updatedExtIds().getUpdated());
evictAccounts(u);
updateNoteMap(n -> n.insert(extIds));
}
/**
@@ -388,18 +302,7 @@ public class ExternalIdsUpdate {
*/
public void upsert(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId extId : extIds) {
ExternalId updatedExtId = upsert(o.rw(), o.ins(), o.noteMap(), extId);
updatedExtIds.onUpdate(updatedExtId);
}
return updatedExtIds;
});
externalIdCache.onUpdate(u.oldRev(), u.newRev(), u.updatedExtIds().getUpdated());
evictAccounts(u);
updateNoteMap(n -> n.upsert(extIds));
}
/**
@@ -421,18 +324,7 @@ public class ExternalIdsUpdate {
*/
public void delete(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId extId : extIds) {
ExternalId removedExtId = remove(o.rw(), o.noteMap(), extId);
updatedExtIds.onRemove(removedExtId);
}
return updatedExtIds;
});
externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
evictAccounts(u);
updateNoteMap(n -> n.delete(extIds));
}
/**
@@ -454,18 +346,7 @@ public class ExternalIdsUpdate {
*/
public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId.Key extIdKey : extIdKeys) {
ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, accountId);
updatedExtIds.onRemove(removedExtId);
}
return updatedExtIds;
});
externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
evictAccount(accountId);
updateNoteMap(n -> n.delete(accountId, extIdKeys));
}
/**
@@ -475,18 +356,7 @@ public class ExternalIdsUpdate {
*/
public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId.Key extIdKey : extIdKeys) {
ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null);
updatedExtIds.onRemove(extId);
}
return updatedExtIds;
});
externalIdCache.onRemove(u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved());
evictAccounts(u);
updateNoteMap(n -> n.deleteByKeys(extIdKeys));
}
/** Deletes all external IDs of the specified account. */
@@ -509,30 +379,7 @@ public class ExternalIdsUpdate {
public void replace(
Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
checkSameAccount(toAdd, accountId);
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId.Key extIdKey : toDelete) {
ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, accountId);
updatedExtIds.onRemove(removedExtId);
}
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
updatedExtIds.onUpdate(insertedExtId);
}
return updatedExtIds;
});
externalIdCache.onReplace(
u.oldRev(),
u.newRev(),
accountId,
u.updatedExtIds().getRemoved(),
u.updatedExtIds().getUpdated());
evictAccount(accountId);
updateNoteMap(n -> n.replace(accountId, toDelete, toAdd));
}
/**
@@ -547,24 +394,7 @@ public class ExternalIdsUpdate {
*/
public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
RefsMetaExternalIdsUpdate u =
updateNoteMap(
o -> {
UpdatedExternalIds updatedExtIds = new UpdatedExternalIds();
for (ExternalId.Key extIdKey : toDelete) {
ExternalId removedExtId = remove(o.rw(), o.noteMap(), extIdKey, null);
updatedExtIds.onRemove(removedExtId);
}
for (ExternalId extId : toAdd) {
ExternalId insertedExtId = insert(o.rw(), o.ins(), o.noteMap(), extId);
updatedExtIds.onUpdate(insertedExtId);
}
return updatedExtIds;
});
externalIdCache.onReplace(
u.oldRev(), u.newRev(), u.updatedExtIds().getRemoved(), u.updatedExtIds().getUpdated());
evictAccounts(u);
updateNoteMap(n -> n.replaceByKeys(toDelete, toAdd));
}
/**
@@ -591,334 +421,38 @@ public class ExternalIdsUpdate {
*/
public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
if (accountId == null) {
// toDelete and toAdd are empty -> nothing to do
return;
updateNoteMap(n -> n.replace(toDelete, toAdd));
}
replace(accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
}
/**
* Checks that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
return checkSameAccount(extIds, null);
}
/**
* Checks that all specified external IDs belong to specified account. If no account is specified
* it is checked that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
public static Account.Id checkSameAccount(
Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
for (ExternalId extId : extIds) {
if (accountId == null) {
accountId = extId.accountId();
continue;
}
checkState(
accountId.equals(extId.accountId()),
"external id %s belongs to account %s, expected account %s",
extId.key().get(),
extId.accountId().get(),
accountId.get());
}
return accountId;
}
/**
* Inserts a new external ID and sets it in the note map.
*
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
*/
public static ExternalId insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
if (noteMap.contains(extId.key().sha1())) {
throw new OrmDuplicateKeyException(
String.format("external id %s already exists", extId.key().get()));
}
return upsert(rw, ins, noteMap, extId);
}
/**
* Insert or updates an new external ID and sets it in the note map.
*
* <p>If the external ID already exists it is overwritten.
*/
public static ExternalId upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
if (noteMap.contains(extId.key().sha1())) {
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
try {
c.fromText(new String(raw, UTF_8));
} catch (ConfigInvalidException e) {
throw new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
}
}
extId.writeToConfig(c);
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId noteData = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, noteData);
return ExternalId.create(extId, noteData);
}
/**
* Removes an external ID from the note map.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key, but otherwise doesn't match the specified external ID.
*/
public static ExternalId remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
if (!noteMap.contains(noteId)) {
return null;
}
ObjectId noteData = noteMap.get(noteId);
byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
ExternalId actualExtId = ExternalId.parse(noteId.name(), raw, noteData);
checkState(
extId.equals(actualExtId),
"external id %s should be removed, but it's not matching the actual external id %s",
extId.toString(),
actualExtId.toString());
noteMap.remove(noteId);
return actualExtId;
}
/**
* Removes an external ID from the note map by external ID key.
*
* @throws IllegalStateException is thrown if an expected account ID is provided and an external
* ID with the specified key exists, but belongs to another account.
* @return the external ID that was removed, {@code null} if no external ID with the specified key
* exists
*/
private static ExternalId remove(
RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extIdKey.sha1();
if (!noteMap.contains(noteId)) {
return null;
}
ObjectId noteData = noteMap.get(noteId);
byte[] raw = rw.getObjectReader().open(noteData, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
ExternalId extId = ExternalId.parse(noteId.name(), raw, noteData);
if (expectedAccountId != null) {
checkState(
expectedAccountId.equals(extId.accountId()),
"external id %s should be removed for account %s,"
+ " but external id belongs to account %s",
extIdKey.get(),
expectedAccountId.get(),
extId.accountId().get());
}
noteMap.remove(noteId);
return extId;
}
private RefsMetaExternalIdsUpdate updateNoteMap(ExternalIdUpdater updater)
private void updateNoteMap(ExternalIdUpdater updater)
throws IOException, ConfigInvalidException, OrmException {
return retryHelper.execute(
retryHelper.execute(
() -> {
try (Repository repo = repoManager.openRepository(allUsersName);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = readRevision(repo);
afterReadRevision.run();
try (RevWalk rw = new RevWalk(repo)) {
NoteMap noteMap = readNoteMap(rw, rev);
UpdatedExternalIds updatedExtIds =
updater.update(OpenRepo.create(repo, rw, ins, noteMap));
return commit(repo, rw, ins, rev, noteMap, updatedExtIds);
try (Repository repo = repoManager.openRepository(allUsersName)) {
ExternalIdNotes extIdNotes =
new ExternalIdNotes(externalIdCache, accountCache, repo)
.setAfterReadRevision(afterReadRevision)
.load();
updater.update(extIdNotes);
try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create()) {
extIdNotes.commit(metaDataUpdate);
}
extIdNotes.updateCaches();
updateCount.increment();
return null;
}
});
}
private RefsMetaExternalIdsUpdate commit(
Repository repo,
RevWalk rw,
ObjectInserter ins,
ObjectId rev,
NoteMap noteMap,
UpdatedExternalIds updatedExtIds)
throws IOException {
ObjectId newRev =
commit(
allUsersName,
repo,
rw,
ins,
rev,
noteMap,
COMMIT_MSG,
committerIdent,
authorIdent,
currentUser,
gitRefUpdated);
updateCount.increment();
return RefsMetaExternalIdsUpdate.create(rev, newRev, updatedExtIds);
}
/** Commits updates to the external IDs. */
public static ObjectId commit(
Project.NameKey project,
Repository repo,
RevWalk rw,
ObjectInserter ins,
ObjectId rev,
NoteMap noteMap,
String commitMessage,
PersonIdent committerIdent,
PersonIdent authorIdent,
@Nullable IdentifiedUser user,
GitReferenceUpdated gitRefUpdated)
throws IOException {
CommitBuilder cb = new CommitBuilder();
cb.setMessage(commitMessage);
cb.setTreeId(noteMap.writeTree(ins));
cb.setAuthor(authorIdent);
cb.setCommitter(committerIdent);
if (!rev.equals(ObjectId.zeroId())) {
cb.setParentId(rev);
} else {
cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
}
if (cb.getTreeId() == null) {
if (rev.equals(ObjectId.zeroId())) {
cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree.
} else {
RevCommit p = rw.parseCommit(rev);
cb.setTreeId(p.getTree()); // Copy tree from parent.
}
}
ObjectId commitId = ins.insert(cb);
ins.flush();
RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
u.setRefLogIdent(committerIdent);
u.setRefLogMessage("Update external IDs", false);
u.setExpectedOldObjectId(rev);
u.setNewObjectId(commitId);
RefUpdate.Result res = u.update();
switch (res) {
case NEW:
case FAST_FORWARD:
case NO_CHANGE:
case RENAMED:
case FORCED:
break;
case LOCK_FAILURE:
throw new LockFailureException("Updating external IDs failed with " + res, u);
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
throw new IOException("Updating external IDs failed with " + res);
}
gitRefUpdated.fire(project, u, user != null ? user.getAccount() : null);
return rw.parseCommit(commitId);
}
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
return ins.insert(OBJ_TREE, new byte[] {});
}
private void evictAccount(Account.Id accountId) throws IOException {
if (accountCache != null) {
accountCache.evict(accountId);
}
}
private void evictAccounts(RefsMetaExternalIdsUpdate u) throws IOException {
if (accountCache != null) {
for (Account.Id id : u.updatedExtIds().all().map(ExternalId::accountId).collect(toSet())) {
accountCache.evict(id);
}
}
}
@FunctionalInterface
private static interface ExternalIdUpdater {
UpdatedExternalIds update(OpenRepo openRepo)
void update(ExternalIdNotes extIdsNotes)
throws IOException, ConfigInvalidException, OrmException;
}
@AutoValue
abstract static class OpenRepo {
static OpenRepo create(Repository repo, RevWalk rw, ObjectInserter ins, NoteMap noteMap) {
return new AutoValue_ExternalIdsUpdate_OpenRepo(repo, rw, ins, noteMap);
}
abstract Repository repo();
abstract RevWalk rw();
abstract ObjectInserter ins();
abstract NoteMap noteMap();
}
@VisibleForTesting
@AutoValue
public abstract static class RefsMetaExternalIdsUpdate {
static RefsMetaExternalIdsUpdate create(
ObjectId oldRev, ObjectId newRev, UpdatedExternalIds updatedExtIds) {
return new AutoValue_ExternalIdsUpdate_RefsMetaExternalIdsUpdate(
oldRev, newRev, updatedExtIds);
}
abstract ObjectId oldRev();
abstract ObjectId newRev();
abstract UpdatedExternalIds updatedExtIds();
}
public static class UpdatedExternalIds {
private Set<ExternalId> updated = new HashSet<>();
private Set<ExternalId> removed = new HashSet<>();
public void onUpdate(ExternalId extId) {
if (extId != null) {
updated.add(extId);
}
}
public void onRemove(ExternalId extId) {
if (extId != null) {
removed.add(extId);
}
}
public Set<ExternalId> getUpdated() {
return ImmutableSet.copyOf(updated);
}
public Set<ExternalId> getRemoved() {
return ImmutableSet.copyOf(removed);
}
public Stream<ExternalId> all() {
return Streams.concat(removed.stream(), updated.stream());
}
@FunctionalInterface
public static interface MetaDataUpdateFactory {
MetaDataUpdate create() throws IOException;
}
}

View File

@@ -718,7 +718,7 @@ public class CommitValidators {
throw new CommitValidationException("invalid external IDs", msgs);
}
return msgs;
} catch (IOException e) {
} catch (IOException | ConfigInvalidException e) {
String m = "error validating external IDs";
log.warn(m, e);
throw new CommitValidationException(m, e);

View File

@@ -18,11 +18,11 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdReader;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -34,12 +34,8 @@ import java.sql.Statement;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
public class Schema_144 extends SchemaVersion {
private static final String COMMIT_MSG = "Import external IDs from ReviewDb";
@@ -83,29 +79,16 @@ public class Schema_144 extends SchemaVersion {
}
try {
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
for (ExternalId extId : toAdd) {
ExternalIdsUpdate.upsert(rw, ins, noteMap, extId);
try (Repository repo = repoManager.openRepository(allUsersName)) {
ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(repo);
extIdNotes.upsert(toAdd);
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo)) {
metaDataUpdate.getCommitBuilder().setAuthor(serverIdent);
metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
metaDataUpdate.getCommitBuilder().setMessage(COMMIT_MSG);
extIdNotes.commit(metaDataUpdate);
}
ExternalIdsUpdate.commit(
allUsersName,
repo,
rw,
ins,
rev,
noteMap,
COMMIT_MSG,
serverIdent,
serverIdent,
null,
GitReferenceUpdated.DISABLED);
}
} catch (IOException | ConfigInvalidException e) {
throw new OrmException("Failed to migrate external IDs to NoteDb", e);

View File

@@ -14,17 +14,15 @@
package com.google.gerrit.server.schema;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.primitives.Ints;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdReader;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -32,13 +30,8 @@ import java.io.IOException;
import java.sql.SQLException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
public class Schema_148 extends SchemaVersion {
private static final String COMMIT_MSG = "Make account IDs of external IDs human-readable";
@@ -61,44 +54,22 @@ public class Schema_148 extends SchemaVersion {
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
boolean dirty = false;
for (Note note : noteMap) {
byte[] raw =
rw.getObjectReader()
.open(note.getData(), OBJ_BLOB)
.getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
try {
ExternalId extId = ExternalId.parse(note.getName(), raw, note.getData());
try (Repository repo = repoManager.openRepository(allUsersName)) {
ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(repo);
for (ExternalId extId : extIdNotes.all()) {
if (needsUpdate(extId)) {
ExternalIdsUpdate.upsert(rw, ins, noteMap, extId);
dirty = true;
}
} catch (ConfigInvalidException e) {
ui.message(
String.format("Warning: Ignoring invalid external ID note %s", note.getName()));
extIdNotes.upsert(extId);
}
}
if (dirty) {
ExternalIdsUpdate.commit(
allUsersName,
repo,
rw,
ins,
rev,
noteMap,
COMMIT_MSG,
serverUser,
serverUser,
null,
GitReferenceUpdated.DISABLED);
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, repo)) {
metaDataUpdate.getCommitBuilder().setAuthor(serverUser);
metaDataUpdate.getCommitBuilder().setCommitter(serverUser);
metaDataUpdate.getCommitBuilder().setMessage(COMMIT_MSG);
extIdNotes.commit(metaDataUpdate);
}
} catch (IOException e) {
} catch (IOException | ConfigInvalidException e) {
throw new OrmException("Failed to update external IDs", e);
}
}

View File

@@ -1638,7 +1638,7 @@ public class AccountIT extends AbstractDaemonTest {
assertIteratorSize(2, getOnlyKeyFromStore(key).getUserIDs());
pk = PGPPublicKey.removeCertification(pk, "foo:myId");
info = addGpgKey(armor(pk)).get(id);
info = addGpgKeyNoReindex(armor(pk)).get(id);
assertThat(info.userIds).hasSize(1);
assertIteratorSize(1, getOnlyKeyFromStore(key).getUserIDs());
}
@@ -2214,6 +2214,10 @@ public class AccountIT extends AbstractDaemonTest {
return gpgKeys;
}
private Map<String, GpgKeyInfo> addGpgKeyNoReindex(String armored) throws Exception {
return gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
}
private void assertUser(AccountInfo info, TestAccount account) throws Exception {
assertUser(info, account, null);
}

View File

@@ -24,6 +24,7 @@ import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.ImmutableList;
@@ -45,11 +46,13 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIdReader;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gson.reflect.TypeToken;
import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -57,6 +60,7 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -69,11 +73,14 @@ import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
@@ -87,6 +94,7 @@ public class ExternalIdIT extends AbstractDaemonTest {
@Inject private ExternalIdReader externalIdReader;
@Inject private MetricMaker metricMaker;
@Inject private RetryHelper.Metrics retryMetrics;
@Inject private ExternalIdNotes.Factory externalIdNotesFactory;
@Test
public void getExternalIds() throws Exception {
@@ -570,12 +578,11 @@ public class ExternalIdIT extends AbstractDaemonTest {
private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
throws IOException {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
return insertExternalId(
repo,
rw,
(ins, noteMap) -> {
ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
try (ObjectInserter ins = repo.newObjectInserter()) {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
extId.writeToConfig(c);
@@ -583,104 +590,105 @@ public class ExternalIdIT extends AbstractDaemonTest {
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
ExternalIdsUpdate.commit(
allUsers,
repo,
rw,
ins,
rev,
noteMap,
"Add external ID",
admin.getIdent(),
admin.getIdent(),
null,
GitReferenceUpdated.DISABLED);
return noteId.getName();
}
return noteId;
});
}
private String insertExternalIdWithKeyThatDoesntMatchNoteId(
Repository repo, RevWalk rw, String externalId) throws IOException {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
return insertExternalId(
repo,
rw,
(ins, noteMap) -> {
ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
try (ObjectInserter ins = repo.newObjectInserter()) {
ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
Config c = new Config();
extId.writeToConfig(c);
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
ExternalIdsUpdate.commit(
allUsers,
repo,
rw,
ins,
rev,
noteMap,
"Add external ID",
admin.getIdent(),
admin.getIdent(),
null,
GitReferenceUpdated.DISABLED);
return noteId.getName();
}
return noteId;
});
}
private String insertExternalIdWithInvalidConfig(Repository repo, RevWalk rw, String externalId)
throws IOException {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
try (ObjectInserter ins = repo.newObjectInserter()) {
return insertExternalId(
repo,
rw,
(ins, noteMap) -> {
ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
byte[] raw = "bad-config".getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
ExternalIdsUpdate.commit(
allUsers,
repo,
rw,
ins,
rev,
noteMap,
"Add external ID",
admin.getIdent(),
admin.getIdent(),
null,
GitReferenceUpdated.DISABLED);
return noteId.getName();
}
return noteId;
});
}
private String insertExternalIdWithEmptyNote(Repository repo, RevWalk rw, String externalId)
throws IOException {
return insertExternalId(
repo,
rw,
(ins, noteMap) -> {
ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
byte[] raw = "".getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
return noteId;
});
}
private String insertExternalId(Repository repo, RevWalk rw, ExternalIdInserter extIdInserter)
throws IOException {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
try (ObjectInserter ins = repo.newObjectInserter()) {
ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
byte[] raw = "".getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
ObjectId noteId = extIdInserter.addNote(ins, noteMap);
ExternalIdsUpdate.commit(
allUsers,
repo,
rw,
ins,
rev,
noteMap,
"Add external ID",
admin.getIdent(),
admin.getIdent(),
null,
GitReferenceUpdated.DISABLED);
CommitBuilder cb = new CommitBuilder();
cb.setMessage("Update external IDs");
cb.setTreeId(noteMap.writeTree(ins));
cb.setAuthor(admin.getIdent());
cb.setCommitter(admin.getIdent());
if (!rev.equals(ObjectId.zeroId())) {
cb.setParentId(rev);
} else {
cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
}
if (cb.getTreeId() == null) {
if (rev.equals(ObjectId.zeroId())) {
cb.setTreeId(ins.insert(OBJ_TREE, new byte[] {})); // No parent, assume empty tree.
} else {
RevCommit p = rw.parseCommit(rev);
cb.setTreeId(p.getTree()); // Copy tree from parent.
}
}
ObjectId commitId = ins.insert(cb);
ins.flush();
RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
u.setExpectedOldObjectId(rev);
u.setNewObjectId(commitId);
RefUpdate.Result res = u.update();
switch (res) {
case NEW:
case FAST_FORWARD:
case NO_CHANGE:
case RENAMED:
case FORCED:
break;
case LOCK_FAILURE:
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
throw new IOException("Updating external IDs failed with " + res);
}
return noteId.getName();
}
}
@@ -718,15 +726,12 @@ public class ExternalIdIT extends AbstractDaemonTest {
ExternalIdsUpdate update =
new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateFactory.create(allUsers),
accountCache,
allUsers,
metricMaker,
externalIds,
new DisabledExternalIdCache(),
serverIdent.get(),
serverIdent.get(),
null,
GitReferenceUpdated.DISABLED,
new RetryHelper(
cfg,
retryMetrics,
@@ -762,15 +767,12 @@ public class ExternalIdIT extends AbstractDaemonTest {
ExternalIdsUpdate update =
new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateFactory.create(allUsers),
accountCache,
allUsers,
metricMaker,
externalIds,
new DisabledExternalIdCache(),
serverIdent.get(),
serverIdent.get(),
null,
GitReferenceUpdated.DISABLED,
new RetryHelper(
cfg,
retryMetrics,
@@ -867,49 +869,29 @@ public class ExternalIdIT extends AbstractDaemonTest {
}
private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception {
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIdReader.readRevision(repo);
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
ExternalIdsUpdate.commit(
allUsers,
repo,
rw,
ins,
rev,
noteMap,
"insert new ID",
serverIdent.get(),
serverIdent.get(),
null,
GitReferenceUpdated.DISABLED);
try (Repository repo = repoManager.openRepository(allUsers)) {
// Inserting an external ID "behind Gerrit's back" means that the caches are not updated.
ExternalIdNotes extIdNotes = ExternalIdNotes.loadNoCacheUpdate(repo);
extIdNotes.insert(extId);
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, repo)) {
metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
extIdNotes.commit(metaDataUpdate);
}
}
}
private void addExtId(TestRepository<?> testRepo, ExternalId... extIds)
throws IOException, OrmDuplicateKeyException, ConfigInvalidException {
ObjectId rev = ExternalIdReader.readRevision(testRepo.getRepository());
try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
NoteMap noteMap = ExternalIdReader.readNoteMap(testRepo.getRevWalk(), rev);
for (ExternalId extId : extIds) {
ExternalIdsUpdate.insert(testRepo.getRevWalk(), ins, noteMap, extId);
}
ExternalIdsUpdate.commit(
allUsers,
testRepo.getRepository(),
testRepo.getRevWalk(),
ins,
rev,
noteMap,
"Add external ID",
admin.getIdent(),
admin.getIdent(),
null,
GitReferenceUpdated.DISABLED);
ExternalIdNotes extIdNotes = externalIdNotesFactory.load(testRepo.getRepository());
extIdNotes.insert(Arrays.asList(extIds));
try (MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, null, testRepo.getRepository())) {
metaDataUpdate.getCommitBuilder().setAuthor(admin.getIdent());
metaDataUpdate.getCommitBuilder().setCommitter(admin.getIdent());
extIdNotes.commit(metaDataUpdate);
extIdNotes.updateCaches();
}
}
@@ -950,4 +932,9 @@ public class ExternalIdIT extends AbstractDaemonTest {
}
};
}
@FunctionalInterface
private interface ExternalIdInserter {
public ObjectId addNote(ObjectInserter ins, NoteMap noteMap) throws IOException;
}
}