| // 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; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.ACCOUNTS_UPDATE; |
| import static java.util.Objects.requireNonNull; |
| import static java.util.stream.Collectors.toList; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.exceptions.DuplicateKeyException; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.git.LockFailureException; |
| import com.google.gerrit.git.RefUpdateUtil; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.account.externalids.ExternalIdNotes; |
| import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader; |
| import com.google.gerrit.server.account.externalids.ExternalIds; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.config.CachedPreferences; |
| import com.google.gerrit.server.config.VersionedDefaultPreferences; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.index.change.ReindexAfterRefUpdate; |
| import com.google.gerrit.server.notedb.Sequences; |
| import com.google.gerrit.server.update.RetryHelper; |
| import com.google.gerrit.server.update.RetryableAction.Action; |
| import com.google.gerrit.server.update.context.RefUpdateContext; |
| import com.google.inject.Provider; |
| import com.google.inject.assistedinject.Assisted; |
| import com.google.inject.assistedinject.AssistedInject; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.BatchRefUpdate; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| |
| /** |
| * Creates and updates accounts. |
| * |
| * <p>This class should be used for all account updates. See {@link AccountDelta} for what can be |
| * updated. |
| * |
| * <p>Batch updates of multiple different accounts can be performed atomically, see {@link |
| * #updateBatch(List)}. Batch creation is not supported. |
| * |
| * <p>For any account update the caller must provide a commit message, the account ID and an {@link |
| * ConfigureDeltaFromState}. The account updater reads the current {@link AccountState} and prepares |
| * updates to the account by calling setters on the provided {@link AccountDelta.Builder}. If the |
| * current account state is of no interest the caller may also provide a {@link Consumer} for {@link |
| * AccountDelta.Builder} instead of the account updater. |
| * |
| * <p>The provided commit message is used for the update of the user branch. Using a precise and |
| * unique commit message allows to identify the code from which an update was made when looking at a |
| * commit in the user branch, and thus help debugging. |
| * |
| * <p>For creating a new account a new account ID can be retrieved from {@link |
| * Sequences#nextAccountId()}. |
| * |
| * <p>The account updates are written to NoteDb. In NoteDb accounts are represented as user branches |
| * in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file |
| * that stores account properties, such as full name, display name, preferred email, status and the |
| * active flag. The timestamp of the first commit on a user branch denotes the registration date. |
| * The initial commit on the user branch may be empty (since having an 'account.config' is |
| * optional). See {@link AccountConfig} for details of the 'account.config' file format. In addition |
| * the user branch can contain a 'preferences.config' config file to store preferences (see {@link |
| * StoredPreferences}) and a 'watch.config' config file to store project watches (see {@link |
| * ProjectWatches}). External IDs are stored separately in the {@code refs/meta/external-ids} notes |
| * branch (see {@link ExternalIdNotes}). |
| * |
| * <p>On updating an account the account is evicted from the account cache and reindexed. The |
| * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate} |
| * class which receives the event about updating the user branch that is triggered by this class. |
| * |
| * <p>If external IDs are updated, the ExternalIdCache is automatically updated by {@link |
| * ExternalIdNotes}. In addition {@link ExternalIdNotes} takes care about evicting and reindexing |
| * corresponding accounts. This is needed because external ID updates don't touch the user branches. |
| * Hence in this case the accounts are not evicted and reindexed via {@link ReindexAfterRefUpdate}. |
| * |
| * <p>Reindexing and flushing accounts from the account cache can be disabled by |
| * |
| * <ul> |
| * <li>binding {@link GitReferenceUpdated#DISABLED} and |
| * <li>passing an {@link |
| * com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as |
| * parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser, |
| * ExternalIdNotes.ExternalIdNotesLoader)} |
| * </ul> |
| * |
| * <p>If there are concurrent account updates updating the user branch in NoteDb may fail with |
| * {@link LockFailureException}. In this case the account update is automatically retried and the |
| * account updater is invoked once more with the updated account state. This means the whole |
| * read-modify-write sequence is atomic. Retrying is limited by a timeout. If the timeout is |
| * exceeded the account update can still fail with {@link LockFailureException}. |
| */ |
| public class AccountsUpdate { |
| public interface Factory { |
| /** |
| * Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for |
| * all commits related to accounts. The server identity will be used as committer. |
| * |
| * <p><strong>Note</strong>: Please use this method with care and consider using the {@link |
| * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code |
| * AccountsUpdate} instead. |
| * |
| * @param currentUser the user to which modifications should be attributed |
| * @param externalIdNotesLoader the loader that should be used to load external ID notes |
| */ |
| AccountsUpdate create(IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader); |
| |
| /** |
| * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for |
| * all commits related to accounts. |
| * |
| * <p><strong>Note</strong>: Please use this method with care and consider using the {@link |
| * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code |
| * AccountsUpdate} instead. |
| * |
| * @param externalIdNotesLoader the loader that should be used to load external ID notes |
| */ |
| AccountsUpdate createWithServerIdent(ExternalIdNotesLoader externalIdNotesLoader); |
| } |
| |
| /** |
| * Account updates are commonly performed by evaluating the current account state and creating a |
| * delta to be applied to it in a later step. This is done by implementing this interface. |
| * |
| * <p>If the current account state is not needed, use a {@link Consumer} of {@link |
| * AccountDelta.Builder} instead. |
| */ |
| @FunctionalInterface |
| public interface ConfigureDeltaFromState { |
| /** |
| * Receives the current {@link AccountState} (which is immutable) and configures an {@link |
| * AccountDelta.Builder} with changes to the account. |
| * |
| * @param accountState the state of the account that is being updated |
| * @param delta the changes to be applied |
| */ |
| void configure(AccountState accountState, AccountDelta.Builder delta) throws IOException; |
| } |
| |
| /** Data holder for the set of arguments required to update an account. Used for batch updates. */ |
| public static class UpdateArguments { |
| private final String message; |
| private final Account.Id accountId; |
| private final ConfigureDeltaFromState configureDeltaFromState; |
| |
| public UpdateArguments( |
| String message, Account.Id accountId, ConfigureDeltaFromState configureDeltaFromState) { |
| this.message = message; |
| this.accountId = accountId; |
| this.configureDeltaFromState = configureDeltaFromState; |
| } |
| } |
| |
| private final GitRepositoryManager repoManager; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final Optional<IdentifiedUser> currentUser; |
| private final AllUsersName allUsersName; |
| private final ExternalIds externalIds; |
| private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory; |
| private final RetryHelper retryHelper; |
| private final ExternalIdNotesLoader extIdNotesLoader; |
| private final PersonIdent committerIdent; |
| private final PersonIdent authorIdent; |
| |
| /** Invoked after reading the account config. */ |
| private final Runnable afterReadRevision; |
| |
| /** Invoked after updating the account but before committing the changes. */ |
| private final Runnable beforeCommit; |
| |
| /** Single instance that accumulates updates from the batch. */ |
| @Nullable private ExternalIdNotes externalIdNotes; |
| |
| @AssistedInject |
| AccountsUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| AllUsersName allUsersName, |
| ExternalIds externalIds, |
| Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory, |
| RetryHelper retryHelper, |
| @GerritPersonIdent PersonIdent serverIdent, |
| @Assisted ExternalIdNotesLoader extIdNotesLoader) { |
| this( |
| repoManager, |
| gitRefUpdated, |
| Optional.empty(), |
| allUsersName, |
| externalIds, |
| metaDataUpdateInternalFactory, |
| retryHelper, |
| extIdNotesLoader, |
| serverIdent, |
| createPersonIdent(serverIdent, Optional.empty()), |
| AccountsUpdate::doNothing, |
| AccountsUpdate::doNothing); |
| } |
| |
| @AssistedInject |
| AccountsUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| AllUsersName allUsersName, |
| ExternalIds externalIds, |
| Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory, |
| RetryHelper retryHelper, |
| @GerritPersonIdent PersonIdent serverIdent, |
| @Assisted IdentifiedUser currentUser, |
| @Assisted ExternalIdNotesLoader extIdNotesLoader) { |
| this( |
| repoManager, |
| gitRefUpdated, |
| Optional.of(currentUser), |
| allUsersName, |
| externalIds, |
| metaDataUpdateInternalFactory, |
| retryHelper, |
| extIdNotesLoader, |
| serverIdent, |
| createPersonIdent(serverIdent, Optional.of(currentUser)), |
| AccountsUpdate::doNothing, |
| AccountsUpdate::doNothing); |
| } |
| |
| @VisibleForTesting |
| public AccountsUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| Optional<IdentifiedUser> currentUser, |
| AllUsersName allUsersName, |
| ExternalIds externalIds, |
| Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory, |
| RetryHelper retryHelper, |
| ExternalIdNotesLoader extIdNotesLoader, |
| PersonIdent committerIdent, |
| PersonIdent authorIdent, |
| Runnable afterReadRevision, |
| Runnable beforeCommit) { |
| this.repoManager = requireNonNull(repoManager, "repoManager"); |
| this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated"); |
| this.currentUser = currentUser; |
| this.allUsersName = requireNonNull(allUsersName, "allUsersName"); |
| this.externalIds = requireNonNull(externalIds, "externalIds"); |
| this.metaDataUpdateInternalFactory = |
| requireNonNull(metaDataUpdateInternalFactory, "metaDataUpdateInternalFactory"); |
| this.retryHelper = requireNonNull(retryHelper, "retryHelper"); |
| this.extIdNotesLoader = requireNonNull(extIdNotesLoader, "extIdNotesLoader"); |
| this.committerIdent = requireNonNull(committerIdent, "committerIdent"); |
| this.authorIdent = requireNonNull(authorIdent, "authorIdent"); |
| this.afterReadRevision = requireNonNull(afterReadRevision, "afterReadRevision"); |
| this.beforeCommit = requireNonNull(beforeCommit, "beforeCommit"); |
| } |
| |
| /** Returns an instance that runs all specified consumers. */ |
| public static ConfigureDeltaFromState joinConsumers( |
| List<Consumer<AccountDelta.Builder>> consumers) { |
| return (accountStateIgnored, update) -> consumers.forEach(c -> c.accept(update)); |
| } |
| |
| private static ConfigureDeltaFromState fromConsumer(Consumer<AccountDelta.Builder> consumer) { |
| return (a, u) -> consumer.accept(u); |
| } |
| |
| private static PersonIdent createPersonIdent( |
| PersonIdent serverIdent, Optional<IdentifiedUser> user) { |
| return user.isPresent() ? user.get().newCommitterIdent(serverIdent) : serverIdent; |
| } |
| |
| /** |
| * Like {@link #insert(String, Account.Id, ConfigureDeltaFromState)}, but using a {@link Consumer} |
| * instead, i.e. the update does not depend on the current account state (which, for insertion, |
| * would only contain the account ID). |
| */ |
| public AccountState insert( |
| String message, Account.Id accountId, Consumer<AccountDelta.Builder> init) |
| throws IOException, ConfigInvalidException { |
| return insert(message, accountId, fromConsumer(init)); |
| } |
| |
| /** |
| * Inserts a new account. |
| * |
| * @param message commit message for the account creation, must not be {@code null or empty} |
| * @param accountId ID of the new account |
| * @param init to populate the new account |
| * @return the newly created account |
| * @throws DuplicateKeyException if the account already exists |
| * @throws IOException if creating the user branch fails due to an IO error |
| * @throws ConfigInvalidException if any of the account fields has an invalid value |
| */ |
| public AccountState insert(String message, Account.Id accountId, ConfigureDeltaFromState init) |
| throws IOException, ConfigInvalidException { |
| return execute( |
| ImmutableList.of( |
| repo -> { |
| AccountConfig accountConfig = read(repo, accountId); |
| Account account = accountConfig.getNewAccount(committerIdent.getWhenAsInstant()); |
| AccountState accountState = AccountState.forAccount(account); |
| AccountDelta.Builder deltaBuilder = AccountDelta.builder(); |
| init.configure(accountState, deltaBuilder); |
| |
| AccountDelta accountDelta = deltaBuilder.build(); |
| accountConfig.setAccountDelta(accountDelta); |
| externalIdNotes = |
| createExternalIdNotes( |
| repo, accountConfig.getExternalIdsRev(), accountId, accountDelta); |
| CachedPreferences defaultPreferences = |
| CachedPreferences.fromConfig( |
| VersionedDefaultPreferences.get(repo, allUsersName)); |
| |
| return new UpdatedAccount(message, accountConfig, defaultPreferences, true); |
| })) |
| .get(0) |
| .get(); |
| } |
| |
| /** |
| * Like {@link #update(String, Account.Id, ConfigureDeltaFromState)}, but using a {@link Consumer} |
| * instead, i.e. the update does not depend on the current account state. |
| */ |
| public Optional<AccountState> update( |
| String message, Account.Id accountId, Consumer<AccountDelta.Builder> update) |
| throws IOException, ConfigInvalidException { |
| return update(message, accountId, fromConsumer(update)); |
| } |
| |
| /** |
| * Gets the account and updates it atomically. |
| * |
| * <p>Changing the registration date of an account is not supported. |
| * |
| * @param message commit message for the account update, must not be {@code null or empty} |
| * @param accountId ID of the account |
| * @param configureDeltaFromState deltaBuilder to update the account, only invoked if the account |
| * exists |
| * @return the updated account, {@link Optional#empty} if the account doesn't exist |
| * @throws IOException if updating the user branch fails due to an IO error |
| * @throws LockFailureException if updating the user branch still fails due to concurrent updates |
| * after the retry timeout exceeded |
| * @throws ConfigInvalidException if any of the account fields has an invalid value |
| */ |
| public Optional<AccountState> update( |
| String message, Account.Id accountId, ConfigureDeltaFromState configureDeltaFromState) |
| throws LockFailureException, IOException, ConfigInvalidException { |
| return updateBatch( |
| ImmutableList.of(new UpdateArguments(message, accountId, configureDeltaFromState))) |
| .get(0); |
| } |
| |
| private ExecutableUpdate createExecutableUpdate(UpdateArguments updateArguments) { |
| return repo -> { |
| AccountConfig accountConfig = read(repo, updateArguments.accountId); |
| CachedPreferences defaultPreferences = |
| CachedPreferences.fromConfig(VersionedDefaultPreferences.get(repo, allUsersName)); |
| Optional<AccountState> accountState = |
| AccountState.fromAccountConfig(externalIds, accountConfig, defaultPreferences); |
| if (!accountState.isPresent()) { |
| return null; |
| } |
| |
| AccountDelta.Builder deltaBuilder = AccountDelta.builder(); |
| updateArguments.configureDeltaFromState.configure(accountState.get(), deltaBuilder); |
| |
| AccountDelta delta = deltaBuilder.build(); |
| accountConfig.setAccountDelta(delta); |
| ExternalIdNotes.checkSameAccount( |
| Iterables.concat( |
| delta.getCreatedExternalIds(), |
| delta.getUpdatedExternalIds(), |
| delta.getDeletedExternalIds()), |
| updateArguments.accountId); |
| |
| if (delta.hasExternalIdUpdates()) { |
| // Only load the externalIds if they are going to be updated |
| // This makes e.g. preferences updates faster. |
| if (externalIdNotes == null) { |
| externalIdNotes = |
| extIdNotesLoader.load( |
| repo, accountConfig.getExternalIdsRev().orElse(ObjectId.zeroId())); |
| } |
| externalIdNotes.replace(delta.getDeletedExternalIds(), delta.getCreatedExternalIds()); |
| externalIdNotes.upsert(delta.getUpdatedExternalIds()); |
| } |
| |
| CachedPreferences cachedDefaultPreferences = |
| CachedPreferences.fromConfig(VersionedDefaultPreferences.get(repo, allUsersName)); |
| |
| return new UpdatedAccount( |
| updateArguments.message, accountConfig, cachedDefaultPreferences, false); |
| }; |
| } |
| |
| /** |
| * Updates multiple different accounts atomically. This will only store a single new value (aka |
| * set of all external IDs of the host) in the external ID cache, which is important for storage |
| * economy. All {@code updates} must be for different accounts. |
| * |
| * <p>NOTE on error handling: Since updates are executed in multiple stages, with some stages |
| * resulting from the union of all individual updates, we cannot point to the update that caused |
| * the error. Callers should be aware that a single "update of death" (or a set of updates that |
| * together have this property) will always prevent the entire batch from being executed. |
| */ |
| public ImmutableList<Optional<AccountState>> updateBatch(List<UpdateArguments> updates) |
| throws IOException, ConfigInvalidException { |
| checkArgument( |
| updates.stream().map(u -> u.accountId.get()).distinct().count() == updates.size(), |
| "updates must all be for different accounts"); |
| return execute(updates.stream().map(this::createExecutableUpdate).collect(toList())); |
| } |
| |
| private AccountConfig read(Repository allUsersRepo, Account.Id accountId) |
| throws IOException, ConfigInvalidException { |
| AccountConfig accountConfig = new AccountConfig(accountId, allUsersName, allUsersRepo).load(); |
| afterReadRevision.run(); |
| return accountConfig; |
| } |
| |
| private ImmutableList<Optional<AccountState>> execute(List<ExecutableUpdate> executableUpdates) |
| throws IOException, ConfigInvalidException { |
| try (RefUpdateContext ctx = RefUpdateContext.open(ACCOUNTS_UPDATE)) { |
| List<Optional<AccountState>> accountState = new ArrayList<>(); |
| List<UpdatedAccount> updatedAccounts = new ArrayList<>(); |
| executeWithRetry( |
| () -> { |
| |
| // Reset state for retry. |
| externalIdNotes = null; |
| accountState.clear(); |
| updatedAccounts.clear(); |
| try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { |
| for (ExecutableUpdate executableUpdate : executableUpdates) { |
| updatedAccounts.add(executableUpdate.execute(allUsersRepo)); |
| } |
| commit( |
| allUsersRepo, |
| updatedAccounts.stream().filter(Objects::nonNull).collect(toList())); |
| for (UpdatedAccount ua : updatedAccounts) { |
| accountState.add(ua == null ? Optional.empty() : ua.getAccountState()); |
| } |
| } |
| return null; |
| }); |
| |
| return ImmutableList.copyOf(accountState); |
| } |
| } |
| |
| private void executeWithRetry(Action<Void> action) throws IOException, ConfigInvalidException { |
| try { |
| retryHelper.accountUpdate("updateAccount", action).call(); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| Throwables.throwIfInstanceOf(e, IOException.class); |
| Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); |
| throw new StorageException(e); |
| } |
| } |
| |
| private ExternalIdNotes createExternalIdNotes( |
| Repository allUsersRepo, Optional<ObjectId> rev, Account.Id accountId, AccountDelta update) |
| throws IOException, ConfigInvalidException, DuplicateKeyException { |
| ExternalIdNotes.checkSameAccount( |
| Iterables.concat( |
| update.getCreatedExternalIds(), |
| update.getUpdatedExternalIds(), |
| update.getDeletedExternalIds()), |
| accountId); |
| |
| ExternalIdNotes extIdNotes = extIdNotesLoader.load(allUsersRepo, rev.orElse(ObjectId.zeroId())); |
| extIdNotes.replace(update.getDeletedExternalIds(), update.getCreatedExternalIds()); |
| extIdNotes.upsert(update.getUpdatedExternalIds()); |
| return extIdNotes; |
| } |
| |
| private void commit(Repository allUsersRepo, List<UpdatedAccount> updatedAccounts) |
| throws IOException { |
| if (updatedAccounts.isEmpty()) { |
| return; |
| } |
| |
| beforeCommit.run(); |
| |
| BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate(); |
| // External ids may be not updated if: |
| // * externalIdNotes is not loaded (there were no externalId updates in the delta) |
| // * new revCommit is identical to the previous externalId tip |
| boolean externalIdsUpdated = false; |
| if (externalIdNotes != null) { |
| String externalIdUpdateMessage = |
| updatedAccounts.size() == 1 |
| ? Iterables.getOnlyElement(updatedAccounts).message |
| : "Batch update for " + updatedAccounts.size() + " accounts"; |
| ObjectId oldExternalIdsRevision = externalIdNotes.getRevision(); |
| // These update the same ref, so they need to be stacked on top of one another using the same |
| // ExternalIdNotes instance. |
| RevCommit revCommit = |
| commitExternalIdUpdates(externalIdUpdateMessage, allUsersRepo, batchRefUpdate); |
| externalIdsUpdated = !Objects.equals(revCommit.getId(), oldExternalIdsRevision); |
| } |
| for (UpdatedAccount updatedAccount : updatedAccounts) { |
| |
| // These updates are all for different refs (because batches never update the same account |
| // more than once), so there can be multiple commits in the same batch, all with the same base |
| // revision in their AccountConfig. |
| // We allow empty commits: |
| // 1) When creating a new account, so that the user branch gets created with an empty commit |
| // when no account properties are set and hence no |
| // 'account.config' file will be created. |
| // 2) When updating "refs/meta/external-ids", so that refs/users/* meta ref is updated too. |
| // This allows to schedule reindexing of account transactionally on refs/users/* meta |
| // updates. |
| boolean allowEmptyCommit = externalIdsUpdated || updatedAccount.created; |
| commitAccountConfig( |
| updatedAccount.message, |
| allUsersRepo, |
| batchRefUpdate, |
| updatedAccount.accountConfig, |
| allowEmptyCommit); |
| } |
| |
| RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo); |
| |
| Set<Account.Id> accountsToSkipForReindex = getUpdatedAccountIds(batchRefUpdate); |
| if (externalIdsUpdated) { |
| extIdNotesLoader.updateExternalIdCacheAndMaybeReindexAccounts( |
| externalIdNotes, accountsToSkipForReindex); |
| } |
| |
| gitRefUpdated.fire( |
| allUsersName, batchRefUpdate, currentUser.map(IdentifiedUser::state).orElse(null)); |
| } |
| |
| private static Set<Account.Id> getUpdatedAccountIds(BatchRefUpdate batchRefUpdate) { |
| return batchRefUpdate.getCommands().stream() |
| .map(c -> Account.Id.fromRef(c.getRefName())) |
| .filter(Objects::nonNull) |
| .collect(toSet()); |
| } |
| |
| private void commitAccountConfig( |
| String message, |
| Repository allUsersRepo, |
| BatchRefUpdate batchRefUpdate, |
| AccountConfig accountConfig, |
| boolean allowEmptyCommit) |
| throws IOException { |
| try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) { |
| md.setAllowEmpty(allowEmptyCommit); |
| accountConfig.commit(md); |
| } |
| } |
| |
| private RevCommit commitExternalIdUpdates( |
| String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate) throws IOException { |
| try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) { |
| return externalIdNotes.commit(md); |
| } |
| } |
| |
| private MetaDataUpdate createMetaDataUpdate( |
| String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate) { |
| MetaDataUpdate metaDataUpdate = |
| metaDataUpdateInternalFactory.get().create(allUsersName, allUsersRepo, batchRefUpdate); |
| if (!message.endsWith("\n")) { |
| message = message + "\n"; |
| } |
| |
| metaDataUpdate.getCommitBuilder().setMessage(message); |
| metaDataUpdate.getCommitBuilder().setCommitter(committerIdent); |
| metaDataUpdate.getCommitBuilder().setAuthor(authorIdent); |
| return metaDataUpdate; |
| } |
| |
| private static void doNothing() {} |
| |
| @FunctionalInterface |
| private interface ExecutableUpdate { |
| UpdatedAccount execute(Repository allUsersRepo) throws IOException, ConfigInvalidException; |
| } |
| |
| private class UpdatedAccount { |
| final String message; |
| final AccountConfig accountConfig; |
| final CachedPreferences defaultPreferences; |
| final boolean created; |
| |
| UpdatedAccount( |
| String message, |
| AccountConfig accountConfig, |
| CachedPreferences defaultPreferences, |
| boolean created) { |
| checkState(!Strings.isNullOrEmpty(message), "message for account update must be set"); |
| this.message = requireNonNull(message); |
| this.accountConfig = requireNonNull(accountConfig); |
| this.defaultPreferences = defaultPreferences; |
| this.created = created; |
| } |
| |
| Optional<AccountState> getAccountState() throws IOException { |
| return AccountState.fromAccountConfig( |
| externalIds, accountConfig, externalIdNotes, defaultPreferences); |
| } |
| } |
| } |