blob: db37147fc529a2eed552a93c300ccac25ef7deea [file] [log] [blame]
// 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 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.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
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.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
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.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
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.
*
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
* is a git config file that contains an external ID. It has exactly one externalId subsection with
* an accountId and optionally email and password:
*
* <pre>
* [externalId "username:jdoe"]
* accountId = 1003407
* email = jdoe@example.com
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
* </pre>
*
* For NoteDb each method call results in one commit on refs/meta/external-ids branch.
*
* <p>On updating external IDs this class takes care to evict affected accounts from the account
* 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.
*
* <p>The Gerrit server identity will be used as author and committer for all commits that update
* the external IDs.
*/
@Singleton
public static class Server {
private final GitRepositoryManager repoManager;
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;
@Inject
public Server(
GitRepositoryManager repoManager,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
GitReferenceUpdated gitRefUpdated) {
this.repoManager = repoManager;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.gitRefUpdated = gitRefUpdated;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
i,
i,
null,
gitRefUpdated);
}
}
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
*
* <p>Using this class no reindex will be performed for the affected accounts and they will also
* not be evicted from the account cache.
*
* <p>The Gerrit server identity will be used as author and committer for all commits that update
* the external IDs.
*/
@Singleton
public static class ServerNoReindex {
private final GitRepositoryManager repoManager;
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;
@Inject
public ServerNoReindex(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
GitReferenceUpdated gitRefUpdated) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.gitRefUpdated = gitRefUpdated;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
null,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
i,
i,
null,
gitRefUpdated);
}
}
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user.
*
* <p>The identity of the current user will be used as author for all commits that update the
* external IDs. The Gerrit server identity will be used as committer.
*/
@Singleton
public static class User {
private final GitRepositoryManager repoManager;
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;
@Inject
public User(
GitRepositoryManager repoManager,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<IdentifiedUser> identifiedUser,
GitReferenceUpdated gitRefUpdated) {
this.repoManager = repoManager;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.identifiedUser = identifiedUser;
this.gitRefUpdated = gitRefUpdated;
}
public ExternalIdsUpdate create() {
IdentifiedUser user = identifiedUser.get();
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager,
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
createPersonIdent(i, user),
i,
user,
gitRefUpdated);
}
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
}
}
@VisibleForTesting
public static RetryerBuilder<RefsMetaExternalIdsUpdate> retryerBuilder() {
return RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder()
.retryIfException(e -> e instanceof LockFailureException)
.withWaitStrategy(
WaitStrategies.join(
WaitStrategies.exponentialWait(2, TimeUnit.SECONDS),
WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS)))
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS));
}
private static final Retryer<RefsMetaExternalIdsUpdate> RETRYER = retryerBuilder().build();
private final GitRepositoryManager repoManager;
@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 Runnable afterReadRevision;
private final Retryer<RefsMetaExternalIdsUpdate> retryer;
private final Counter0 updateCount;
private ExternalIdsUpdate(
GitRepositoryManager repoManager,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent,
@Nullable IdentifiedUser currentUser,
GitReferenceUpdated gitRefUpdated) {
this(
repoManager,
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
committerIdent,
authorIdent,
currentUser,
gitRefUpdated,
Runnables.doNothing(),
RETRYER);
}
@VisibleForTesting
public ExternalIdsUpdate(
GitRepositoryManager repoManager,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent,
@Nullable IdentifiedUser currentUser,
GitReferenceUpdated gitRefUpdated,
Runnable afterReadRevision,
Retryer<RefsMetaExternalIdsUpdate> retryer) {
this.repoManager = checkNotNull(repoManager, "repoManager");
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.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
this.retryer = checkNotNull(retryer, "retryer");
this.updateCount =
metricMaker.newCounter(
"notedb/external_id_update_count",
new Description("Total number of external ID updates.").setRate().setUnit("updates"));
}
/**
* Inserts a new external ID.
*
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
*/
public void insert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
insert(Collections.singleton(extId));
}
/**
* Inserts new external IDs.
*
* <p>If any of the external ID already exists, the insert fails with {@link
* OrmDuplicateKeyException}.
*/
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);
}
/**
* 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, OrmException {
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, 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);
}
/**
* 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) throws IOException, ConfigInvalidException, OrmException {
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)
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);
}
/**
* 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)
throws IOException, ConfigInvalidException, OrmException {
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)
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);
}
/**
* 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)
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);
}
/** Deletes all external IDs of the specified account. */
public void deleteAll(Account.Id accountId)
throws IOException, ConfigInvalidException, OrmException {
delete(externalIds.byAccount(accountId));
}
/**
* 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, 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);
}
/**
* 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, 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);
}
/**
* 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, ConfigInvalidException, OrmException {
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, ConfigInvalidException, OrmException {
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);
}
/**
* 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)
throws IOException, ConfigInvalidException, OrmException {
try {
return retryer.call(
() -> {
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);
}
}
});
} catch (ExecutionException | RetryException e) {
if (e.getCause() != null) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
Throwables.throwIfInstanceOf(e.getCause(), ConfigInvalidException.class);
Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
}
throw new OrmException(e);
}
}
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)
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());
}
}
}