<commit_before>// 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;

/**
 * ‘account.config’ file in the user branch in the All-Users repository that contains the properties
 * of the account.
 *
 * <p>The 'account.config' file is a git config file that has one 'account' section with the
 * properties of the account:
 *
 * <pre>
 *   [account]
 *     active = false
 *     fullName = <NAME>
 *     preferredEmail = <EMAIL>
 *     status = Overloaded with reviews
 * </pre>
 *
 * <p>All keys are optional. This means 'account.config' may not exist on the user branch if no
 * properties are set.
 *
 * <p>Not setting a key and setting a key to an empty string are treated the same way and result in
 * a {@code null} value.
 *
 * <p>If no value for 'active' is specified, by default the account is considered as active.
 *
 * <p>The commit date of the first commit on the user branch is used as registration date of the
 * account. The first commit may be an empty commit (if no properties were set and 'account.config'
 * doesn't exist).
 */
public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
  public static final String ACCOUNT_CONFIG = "account.config";
  public static final String ACCOUNT = "account";
  public static final String KEY_ACTIVE = "active";
  public static final String KEY_FULL_NAME = "fullName";
  public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
  public static final String KEY_STATUS = "status";

  private final Account.Id accountId;
  private final Repository repo;
  private final String ref;

  private Optional<Account> loadedAccount;
  private Optional<ObjectId> externalIdsRev;
  private WatchConfig watchConfig;
  private PreferencesConfig prefConfig;
  private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
  private Timestamp registeredOn;
  private boolean eagerParsing;
  private List<ValidationError> validationErrors;

  public AccountConfig(Account.Id accountId, Repository allUsersRepo) {
    this.accountId = checkNotNull(accountId, "accountId");
    this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
    this.ref = RefNames.refsUsers(accountId);
  }

  /**
   * Sets whether all account data should be eagerly parsed.
   *
   * <p>Eager parsing should only be used if the caller is interested in validation errors for all
   * account data (see {@link #getValidationErrors()}.
   *
   * @param eagerParsing whether all account data should be eagerly parsed
   * @return this AccountConfig instance for chaining
   */
  public AccountConfig setEagerParsing(boolean eagerParsing) {
    checkState(loadedAccount == null, "Account %s already loaded", accountId.get());
    this.eagerParsing = eagerParsing;
    return this;
  }

  @Override
  protected String getRefName() {
    return ref;
  }

  public AccountConfig load() throws IOException, ConfigInvalidException {
    load(repo);
    return this;
  }

  /**
   * Get the loaded account.
   *
   * @return the loaded account, {@link Optional#empty()} if load didn't find the account because it
   *     doesn't exist
   * @throws IllegalStateException if the account was not loaded yet
   */
  public Optional<Account> getLoadedAccount() {
    checkLoaded();
    return loadedAccount;
  }

  /**
   * Returns the revision of the {@code refs/meta/external-ids} branch.
   *
   * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
   * ExternalIds#byAccount(com.google.gerrit.reviewdb.client.Account.Id, ObjectId)}.
   *
   * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
   *     {@code refs/meta/external-ids} branch exists
   */
  public Optional<ObjectId> getExternalIdsRev() {
    checkLoaded();
    return externalIdsRev;
  }

  /**
   * Get the project watches of the loaded account.
   *
   * @return the project watches of the loaded account
   */
  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
    checkLoaded();
    return watchConfig.getProjectWatches();
  }

  /**
   * Get the general preferences of the loaded account.
   *
   * @return the general preferences of the loaded account
   */
  public GeneralPreferencesInfo getGeneralPreferences() {
    checkLoaded();
    return prefConfig.getGeneralPreferences();
  }

  /**
   * Sets the account. This means the loaded account will be overwritten with the given account.
   *
   * <p>Changing the registration date of an account is not supported.
   *
   * @param account account that should be set
   * @throws IllegalStateException if the account was not loaded yet
   */
  public AccountConfig setAccount(Account account) {
    checkLoaded();
    this.loadedAccount = Optional.of(account);
    this.accountUpdate =
        Optional.of(
            InternalAccountUpdate.builder()
                .setActive(account.isActive())
                .setFullName(account.getFullName())
                .setPreferredEmail(account.getPreferredEmail())
                .setStatus(account.getStatus())
                .build());
    this.registeredOn = account.getRegisteredOn();
    return this;
  }

  /**
   * Creates a new account.
   *
   * @return the new account
   * @throws OrmDuplicateKeyException if the user branch already exists
   */
  public Account getNewAccount() throws OrmDuplicateKeyException {
    return getNewAccount(TimeUtil.nowTs());
  }

  /**
   * Creates a new account.
   *
   * @return the new account
   * @throws OrmDuplicateKeyException if the user branch already exists
   */
  Account getNewAccount(Timestamp registeredOn) throws OrmDuplicateKeyException {
    checkLoaded();
    if (revision != null) {
      throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
    }
    this.registeredOn = registeredOn;
    this.loadedAccount = Optional.of(new Account(accountId, registeredOn));
    return loadedAccount.get();
  }

  public AccountConfig setAccountUpdate(InternalAccountUpdate accountUpdate) {
    this.accountUpdate = Optional.of(accountUpdate);
    return this;
  }

  @Override
  protected void onLoad() throws IOException, ConfigInvalidException {
    if (revision != null) {
      rw.reset();
      rw.markStart(revision);
      rw.sort(RevSort.REVERSE);
      registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);

      Config accountConfig = readConfig(ACCOUNT_CONFIG);
      loadedAccount = Optional.of(parse(accountConfig, revision.name()));

      Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
      externalIdsRev =
          externalIdsRef != null ? Optional.of(externalIdsRef.getObjectId()) : Optional.empty();

      watchConfig = new WatchConfig(accountId, readConfig(WatchConfig.WATCH_CONFIG), this);

      prefConfig =
          new PreferencesConfig(
              accountId,
              readConfig(PreferencesConfig.PREFERENCES_CONFIG),
              PreferencesConfig.readDefaultConfig(repo),
              this);

      if (eagerParsing) {
        watchConfig.parse();
        prefConfig.parse();
      }
    } else {
      loadedAccount = Optional.empty();
    }
  }

  private Account parse(Config cfg, String metaId) {
    Account account = new Account(accountId, registeredOn);
    account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
    account.setFullName(get(cfg, KEY_FULL_NAME));

    String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
    account.setPreferredEmail(preferredEmail);<commit_msg>AccountConfig: Also set externalIdsRev, watchConfig, prefConfig if new account<commit_after>// 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;

/**
 * ‘account.config’ file in the user branch in the All-Users repository that contains the properties
 * of the account.
 *
 * <p>The 'account.config' file is a git config file that has one 'account' section with the
 * properties of the account:
 *
 * <pre>
 *   [account]
 *     active = false
 *     fullName = <NAME>
 *     preferredEmail = <EMAIL>
 *     status = Overloaded with reviews
 * </pre>
 *
 * <p>All keys are optional. This means 'account.config' may not exist on the user branch if no
 * properties are set.
 *
 * <p>Not setting a key and setting a key to an empty string are treated the same way and result in
 * a {@code null} value.
 *
 * <p>If no value for 'active' is specified, by default the account is considered as active.
 *
 * <p>The commit date of the first commit on the user branch is used as registration date of the
 * account. The first commit may be an empty commit (if no properties were set and 'account.config'
 * doesn't exist).
 */
public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
  public static final String ACCOUNT_CONFIG = "account.config";
  public static final String ACCOUNT = "account";
  public static final String KEY_ACTIVE = "active";
  public static final String KEY_FULL_NAME = "fullName";
  public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
  public static final String KEY_STATUS = "status";

  private final Account.Id accountId;
  private final Repository repo;
  private final String ref;

  private Optional<Account> loadedAccount;
  private Optional<ObjectId> externalIdsRev;
  private WatchConfig watchConfig;
  private PreferencesConfig prefConfig;
  private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
  private Timestamp registeredOn;
  private boolean eagerParsing;
  private List<ValidationError> validationErrors;

  public AccountConfig(Account.Id accountId, Repository allUsersRepo) {
    this.accountId = checkNotNull(accountId, "accountId");
    this.repo = checkNotNull(allUsersRepo, "allUsersRepo");
    this.ref = RefNames.refsUsers(accountId);
  }

  /**
   * Sets whether all account data should be eagerly parsed.
   *
   * <p>Eager parsing should only be used if the caller is interested in validation errors for all
   * account data (see {@link #getValidationErrors()}.
   *
   * @param eagerParsing whether all account data should be eagerly parsed
   * @return this AccountConfig instance for chaining
   */
  public AccountConfig setEagerParsing(boolean eagerParsing) {
    checkState(loadedAccount == null, "Account %s already loaded", accountId.get());
    this.eagerParsing = eagerParsing;
    return this;
  }

  @Override
  protected String getRefName() {
    return ref;
  }

  public AccountConfig load() throws IOException, ConfigInvalidException {
    load(repo);
    return this;
  }

  /**
   * Get the loaded account.
   *
   * @return the loaded account, {@link Optional#empty()} if load didn't find the account because it
   *     doesn't exist
   * @throws IllegalStateException if the account was not loaded yet
   */
  public Optional<Account> getLoadedAccount() {
    checkLoaded();
    return loadedAccount;
  }

  /**
   * Returns the revision of the {@code refs/meta/external-ids} branch.
   *
   * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
   * ExternalIds#byAccount(com.google.gerrit.reviewdb.client.Account.Id, ObjectId)}.
   *
   * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
   *     {@code refs/meta/external-ids} branch exists
   */
  public Optional<ObjectId> getExternalIdsRev() {
    checkLoaded();
    return externalIdsRev;
  }

  /**
   * Get the project watches of the loaded account.
   *
   * @return the project watches of the loaded account
   */
  public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
    checkLoaded();
    return watchConfig.getProjectWatches();
  }

  /**
   * Get the general preferences of the loaded account.
   *
   * @return the general preferences of the loaded account
   */
  public GeneralPreferencesInfo getGeneralPreferences() {
    checkLoaded();
    return prefConfig.getGeneralPreferences();
  }

  /**
   * Sets the account. This means the loaded account will be overwritten with the given account.
   *
   * <p>Changing the registration date of an account is not supported.
   *
   * @param account account that should be set
   * @throws IllegalStateException if the account was not loaded yet
   */
  public AccountConfig setAccount(Account account) {
    checkLoaded();
    this.loadedAccount = Optional.of(account);
    this.accountUpdate =
        Optional.of(
            InternalAccountUpdate.builder()
                .setActive(account.isActive())
                .setFullName(account.getFullName())
                .setPreferredEmail(account.getPreferredEmail())
                .setStatus(account.getStatus())
                .build());
    this.registeredOn = account.getRegisteredOn();
    return this;
  }

  /**
   * Creates a new account.
   *
   * @return the new account
   * @throws OrmDuplicateKeyException if the user branch already exists
   */
  public Account getNewAccount() throws OrmDuplicateKeyException {
    return getNewAccount(TimeUtil.nowTs());
  }

  /**
   * Creates a new account.
   *
   * @return the new account
   * @throws OrmDuplicateKeyException if the user branch already exists
   */
  Account getNewAccount(Timestamp registeredOn) throws OrmDuplicateKeyException {
    checkLoaded();
    if (revision != null) {
      throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId));
    }
    this.registeredOn = registeredOn;
    this.loadedAccount = Optional.of(new Account(accountId, registeredOn));
    return loadedAccount.get();
  }

  public AccountConfig setAccountUpdate(InternalAccountUpdate accountUpdate) {
    this.accountUpdate = Optional.of(accountUpdate);
    return this;
  }

  @Override
  protected void onLoad() throws IOException, ConfigInvalidException {
    if (revision != null) {
      rw.reset();
      rw.markStart(revision);
      rw.sort(RevSort.REVERSE);
      registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);

      Config accountConfig = readConfig(ACCOUNT_CONFIG);
      loadedAccount = Optional.of(parse(accountConfig, revision.name()));

      watchConfig = new WatchConfig(accountId, readConfig(WatchConfig.WATCH_CONFIG), this);

      prefConfig =
          new PreferencesConfig(
              accountId,
              readConfig(PreferencesConfig.PREFERENCES_CONFIG),
              PreferencesConfig.readDefaultConfig(repo),
              this);

      if (eagerParsing) {
        watchConfig.parse();
        prefConfig.parse();
      }
    } else {
      loadedAccount = Optional.empty();

      watchConfig = new WatchConfig(accountId, new Config(), this);

      prefConfig =
          new PreferencesConfig(
              accountId, new Config(), PreferencesConfig.readDefaultConfig(repo), this);
    }

    Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
    externalIdsRev = Optional.ofNullable(externalIdsRef).map(Ref::getObjectId);
  }

  private Account parse(Config cfg, String metaId) {
    Account account = new Account(accountId, registeredOn);
    account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
    account.setFullName(get(cfg, KEY_FULL_NAME));

    String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL);
    account.setPreferredEmail(preferredEmail);
{'fraction_non_alphanumeric': 0.09383983572895277, 'fraction_numerical': 0.0012320328542094457, 'mean_word_length': 3.7665769513090286, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 43, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2977856', 'n_tokens_mistral': 5442, 'n_tokens_neox': 5348, 'n_words': 1622}