All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.project.ProjectState Maven / Gradle / Ivy

// Copyright (C) 2008 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.project;

import static com.google.gerrit.common.data.PermissionRule.Action.ALLOW;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.common.data.SubscribeSection;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
import com.google.gerrit.extensions.api.projects.ThemeInfo;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.BranchOrderSection;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.ProjectLevelConfig;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Cached information on a project. Must not contain any data derived from parents other than it's
 * immediate parent's {@link com.google.gerrit.reviewdb.client.Project.NameKey}.
 */
public class ProjectState {
  private static final Logger log = LoggerFactory.getLogger(ProjectState.class);

  public interface Factory {
    ProjectState create(ProjectConfig config);
  }

  private final boolean isAllProjects;
  private final boolean isAllUsers;
  private final SitePaths sitePaths;
  private final AllProjectsName allProjectsName;
  private final ProjectCache projectCache;
  private final ProjectControl.AssistedFactory projectControlFactory;
  private final PrologEnvironment.Factory envFactory;
  private final GitRepositoryManager gitMgr;
  private final RulesCache rulesCache;
  private final List commentLinks;

  private final ProjectConfig config;
  private final Map configs;
  private final Set localOwners;
  private final long globalMaxObjectSizeLimit;
  private final boolean inheritProjectMaxObjectSizeLimit;

  /** Prolog rule state. */
  private volatile PrologMachineCopy rulesMachine;

  /** Last system time the configuration's revision was examined. */
  private volatile long lastCheckGeneration;

  /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
  private volatile List localAccessSections;

  /** Theme information loaded from site_path/themes. */
  private volatile ThemeInfo theme;

  /** If this is all projects, the capabilities used by the server. */
  private final CapabilityCollection capabilities;

  @Inject
  public ProjectState(
      SitePaths sitePaths,
      ProjectCache projectCache,
      AllProjectsName allProjectsName,
      AllUsersName allUsersName,
      ProjectControl.AssistedFactory projectControlFactory,
      PrologEnvironment.Factory envFactory,
      GitRepositoryManager gitMgr,
      RulesCache rulesCache,
      List commentLinks,
      CapabilityCollection.Factory limitsFactory,
      TransferConfig transferConfig,
      @Assisted ProjectConfig config) {
    this.sitePaths = sitePaths;
    this.projectCache = projectCache;
    this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
    this.isAllUsers = config.getProject().getNameKey().equals(allUsersName);
    this.allProjectsName = allProjectsName;
    this.projectControlFactory = projectControlFactory;
    this.envFactory = envFactory;
    this.gitMgr = gitMgr;
    this.rulesCache = rulesCache;
    this.commentLinks = commentLinks;
    this.config = config;
    this.configs = new HashMap<>();
    this.capabilities =
        isAllProjects
            ? limitsFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
            : null;
    this.globalMaxObjectSizeLimit = transferConfig.getMaxObjectSizeLimit();
    this.inheritProjectMaxObjectSizeLimit = transferConfig.getInheritProjectMaxObjectSizeLimit();

    if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
      localOwners = Collections.emptySet();
    } else {
      HashSet groups = new HashSet<>();
      AccessSection all = config.getAccessSection(AccessSection.ALL);
      if (all != null) {
        Permission owner = all.getPermission(Permission.OWNER);
        if (owner != null) {
          for (PermissionRule rule : owner.getRules()) {
            GroupReference ref = rule.getGroup();
            if (rule.getAction() == ALLOW && ref.getUUID() != null) {
              groups.add(ref.getUUID());
            }
          }
        }
      }
      localOwners = Collections.unmodifiableSet(groups);
    }
  }

  void initLastCheck(long generation) {
    lastCheckGeneration = generation;
  }

  boolean needsRefresh(long generation) {
    if (generation <= 0) {
      return isRevisionOutOfDate();
    }
    if (lastCheckGeneration != generation) {
      lastCheckGeneration = generation;
      return isRevisionOutOfDate();
    }
    return false;
  }

  private boolean isRevisionOutOfDate() {
    try (Repository git = gitMgr.openRepository(getNameKey())) {
      Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
      if (ref == null || ref.getObjectId() == null) {
        return true;
      }
      return !ref.getObjectId().equals(config.getRevision());
    } catch (IOException gone) {
      return true;
    }
  }

  /**
   * @return cached computation of all global capabilities. This should only be invoked on the state
   *     from {@link ProjectCache#getAllProjects()}. Null on any other project.
   */
  public CapabilityCollection getCapabilityCollection() {
    return capabilities;
  }

  /** @return Construct a new PrologEnvironment for the calling thread. */
  public PrologEnvironment newPrologEnvironment() throws CompileException {
    PrologMachineCopy pmc = rulesMachine;
    if (pmc == null) {
      pmc = rulesCache.loadMachine(getNameKey(), config.getRulesId());
      rulesMachine = pmc;
    }
    return envFactory.create(pmc);
  }

  /**
   * Like {@link #newPrologEnvironment()} but instead of reading the rules.pl read the provided
   * input stream.
   *
   * @param name a name of the input stream. Could be any name.
   * @param in stream to read prolog rules from
   * @throws CompileException
   */
  public PrologEnvironment newPrologEnvironment(String name, Reader in) throws CompileException {
    PrologMachineCopy pmc = rulesCache.loadMachine(name, in);
    return envFactory.create(pmc);
  }

  public Project getProject() {
    return config.getProject();
  }

  public Project.NameKey getNameKey() {
    return getProject().getNameKey();
  }

  public String getName() {
    return getNameKey().get();
  }

  public ProjectConfig getConfig() {
    return config;
  }

  public ProjectLevelConfig getConfig(String fileName) {
    if (configs.containsKey(fileName)) {
      return configs.get(fileName);
    }

    ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
    try (Repository git = gitMgr.openRepository(getNameKey())) {
      cfg.load(git);
    } catch (IOException | ConfigInvalidException e) {
      log.warn("Failed to load " + fileName + " for " + getName(), e);
    }

    configs.put(fileName, cfg);
    return cfg;
  }

  public static class EffectiveMaxObjectSizeLimit {
    public long value;
    public String summary;
  }

  private static final String MAY_NOT_SET = "This project may not set a higher limit.";

  @VisibleForTesting
  public static final String INHERITED_FROM_PARENT = "Inherited from parent project '%s'.";

  @VisibleForTesting
  public static final String OVERRIDDEN_BY_PARENT =
      "Overridden by parent project '%s'. " + MAY_NOT_SET;

  @VisibleForTesting
  public static final String INHERITED_FROM_GLOBAL = "Inherited from the global config.";

  @VisibleForTesting
  public static final String OVERRIDDEN_BY_GLOBAL =
      "Overridden by the global config. " + MAY_NOT_SET;

  public EffectiveMaxObjectSizeLimit getEffectiveMaxObjectSizeLimit() {
    EffectiveMaxObjectSizeLimit result = new EffectiveMaxObjectSizeLimit();

    result.value = config.getMaxObjectSizeLimit();

    if (inheritProjectMaxObjectSizeLimit) {
      for (ProjectState parent : parents()) {
        long parentValue = parent.config.getMaxObjectSizeLimit();
        if (parentValue > 0 && result.value > 0) {
          if (parentValue < result.value) {
            result.value = parentValue;
            result.summary = String.format(OVERRIDDEN_BY_PARENT, parent.config.getName());
          }
        } else if (parentValue > 0) {
          result.value = parentValue;
          result.summary = String.format(INHERITED_FROM_PARENT, parent.config.getName());
        }
      }
    }

    if (globalMaxObjectSizeLimit > 0 && result.value > 0) {
      if (globalMaxObjectSizeLimit < result.value) {
        result.value = globalMaxObjectSizeLimit;
        result.summary = OVERRIDDEN_BY_GLOBAL;
      }
    } else if (globalMaxObjectSizeLimit > result.value) {
      // zero means "no limit", in this case the max is more limiting
      result.value = globalMaxObjectSizeLimit;
      result.summary = INHERITED_FROM_GLOBAL;
    }
    return result;
  }

  /** Get the sections that pertain only to this project. */
  List getLocalAccessSections() {
    List sm = localAccessSections;
    if (sm == null) {
      Collection fromConfig = config.getAccessSections();
      sm = new ArrayList<>(fromConfig.size());
      for (AccessSection section : fromConfig) {
        if (isAllProjects) {
          List copy = Lists.newArrayListWithCapacity(section.getPermissions().size());
          for (Permission p : section.getPermissions()) {
            if (Permission.canBeOnAllProjects(section.getName(), p.getName())) {
              copy.add(p);
            }
          }
          section = new AccessSection(section.getName());
          section.setPermissions(copy);
        }

        SectionMatcher matcher = SectionMatcher.wrap(getNameKey(), section);
        if (matcher != null) {
          sm.add(matcher);
        }
      }
      localAccessSections = sm;
    }
    return sm;
  }

  /**
   * Obtain all local and inherited sections. This collection is looked up dynamically and is not
   * cached. Callers should try to cache this result per-request as much as possible.
   */
  List getAllSections() {
    if (isAllProjects) {
      return getLocalAccessSections();
    }

    List all = new ArrayList<>();
    for (ProjectState s : tree()) {
      all.addAll(s.getLocalAccessSections());
    }
    return all;
  }

  /**
   * @return all {@link AccountGroup}'s to which the owner privilege for 'refs/*' is assigned for
   *     this project (the local owners), if there are no local owners the local owners of the
   *     nearest parent project that has local owners are returned
   */
  public Set getOwners() {
    for (ProjectState p : tree()) {
      if (!p.localOwners.isEmpty()) {
        return p.localOwners;
      }
    }
    return Collections.emptySet();
  }

  /**
   * @return all {@link AccountGroup}'s that are allowed to administrate the complete project. This
   *     includes all groups to which the owner privilege for 'refs/*' is assigned for this project
   *     (the local owners) and all groups to which the owner privilege for 'refs/*' is assigned for
   *     one of the parent projects (the inherited owners).
   */
  public Set getAllOwners() {
    Set result = new HashSet<>();

    for (ProjectState p : tree()) {
      result.addAll(p.localOwners);
    }

    return result;
  }

  public ProjectControl controlFor(CurrentUser user) {
    return projectControlFactory.create(user, this);
  }

  /**
   * @return an iterable that walks through this project and then the parents of this project.
   *     Starts from this project and progresses up the hierarchy to All-Projects.
   */
  public Iterable tree() {
    return new Iterable() {
      @Override
      public Iterator iterator() {
        return new ProjectHierarchyIterator(projectCache, allProjectsName, ProjectState.this);
      }
    };
  }

  /**
   * @return an iterable that walks in-order from All-Projects through the project hierarchy to this
   *     project.
   */
  public Iterable treeInOrder() {
    List projects = Lists.newArrayList(tree());
    Collections.reverse(projects);
    return projects;
  }

  /**
   * @return an iterable that walks through the parents of this project. Starts from the immediate
   *     parent of this project and progresses up the hierarchy to All-Projects.
   */
  public FluentIterable parents() {
    return FluentIterable.from(tree()).skip(1);
  }

  public boolean isAllProjects() {
    return isAllProjects;
  }

  public boolean isAllUsers() {
    return isAllUsers;
  }

  public boolean isUseContributorAgreements() {
    return getInheritableBoolean(Project::getUseContributorAgreements);
  }

  public boolean isUseContentMerge() {
    return getInheritableBoolean(Project::getUseContentMerge);
  }

  public boolean isUseSignedOffBy() {
    return getInheritableBoolean(Project::getUseSignedOffBy);
  }

  public boolean isRequireChangeID() {
    return getInheritableBoolean(Project::getRequireChangeID);
  }

  public boolean isCreateNewChangeForAllNotInTarget() {
    return getInheritableBoolean(Project::getCreateNewChangeForAllNotInTarget);
  }

  public boolean isEnableSignedPush() {
    return getInheritableBoolean(Project::getEnableSignedPush);
  }

  public boolean isRequireSignedPush() {
    return getInheritableBoolean(Project::getRequireSignedPush);
  }

  public boolean isRejectImplicitMerges() {
    return getInheritableBoolean(Project::getRejectImplicitMerges);
  }

  public boolean isPrivateByDefault() {
    return getInheritableBoolean(Project::getPrivateByDefault);
  }

  public boolean isWorkInProgressByDefault() {
    return getInheritableBoolean(Project::getWorkInProgressByDefault);
  }

  public boolean isEnableReviewerByEmail() {
    return getInheritableBoolean(Project::getEnableReviewerByEmail);
  }

  public boolean isMatchAuthorToCommitterDate() {
    return getInheritableBoolean(Project::getMatchAuthorToCommitterDate);
  }

  /** All available label types. */
  public LabelTypes getLabelTypes() {
    Map types = new LinkedHashMap<>();
    for (ProjectState s : treeInOrder()) {
      for (LabelType type : s.getConfig().getLabelSections().values()) {
        String lower = type.getName().toLowerCase();
        LabelType old = types.get(lower);
        if (old == null || old.canOverride()) {
          types.put(lower, type);
        }
      }
    }
    List all = Lists.newArrayListWithCapacity(types.size());
    for (LabelType type : types.values()) {
      if (!type.getValues().isEmpty()) {
        all.add(type);
      }
    }
    return new LabelTypes(Collections.unmodifiableList(all));
  }

  /** All available label types for this change and user. */
  public LabelTypes getLabelTypes(ChangeNotes notes, CurrentUser user) {
    return getLabelTypes(notes.getChange().getDest(), user);
  }

  /** All available label types for this branch and user. */
  public LabelTypes getLabelTypes(Branch.NameKey destination, CurrentUser user) {
    List all = getLabelTypes().getLabelTypes();

    List r = Lists.newArrayListWithCapacity(all.size());
    for (LabelType l : all) {
      List refs = l.getRefPatterns();
      if (refs == null) {
        r.add(l);
      } else {
        for (String refPattern : refs) {
          if (RefConfigSection.isValid(refPattern) && match(destination, refPattern, user)) {
            r.add(l);
            break;
          }
        }
      }
    }

    return new LabelTypes(r);
  }

  public List getCommentLinks() {
    Map cls = new LinkedHashMap<>();
    for (CommentLinkInfo cl : commentLinks) {
      cls.put(cl.name.toLowerCase(), cl);
    }
    for (ProjectState s : treeInOrder()) {
      for (CommentLinkInfoImpl cl : s.getConfig().getCommentLinkSections()) {
        String name = cl.name.toLowerCase();
        if (cl.isOverrideOnly()) {
          CommentLinkInfo parent = cls.get(name);
          if (parent == null) {
            continue; // Ignore invalid overrides.
          }
          cls.put(name, cl.inherit(parent));
        } else {
          cls.put(name, cl);
        }
      }
    }
    return ImmutableList.copyOf(cls.values());
  }

  public BranchOrderSection getBranchOrderSection() {
    for (ProjectState s : tree()) {
      BranchOrderSection section = s.getConfig().getBranchOrderSection();
      if (section != null) {
        return section;
      }
    }
    return null;
  }

  public Collection getSubscribeSections(Branch.NameKey branch) {
    Collection ret = new ArrayList<>();
    for (ProjectState s : tree()) {
      ret.addAll(s.getConfig().getSubscribeSections(branch));
    }
    return ret;
  }

  public ThemeInfo getTheme() {
    ThemeInfo theme = this.theme;
    if (theme == null) {
      synchronized (this) {
        theme = this.theme;
        if (theme == null) {
          theme = loadTheme();
          this.theme = theme;
        }
      }
    }
    if (theme == ThemeInfo.INHERIT) {
      ProjectState parent = Iterables.getFirst(parents(), null);
      return parent != null ? parent.getTheme() : null;
    }
    return theme;
  }

  public Set getAllGroups() {
    return getGroups(getAllSections());
  }

  public Set getLocalGroups() {
    return getGroups(getLocalAccessSections());
  }

  private static Set getGroups(List sectionMatcherList) {
    final Set all = new HashSet<>();
    for (SectionMatcher matcher : sectionMatcherList) {
      final AccessSection section = matcher.section;
      for (Permission permission : section.getPermissions()) {
        for (PermissionRule rule : permission.getRules()) {
          all.add(rule.getGroup());
        }
      }
    }
    return all;
  }

  private ThemeInfo loadTheme() {
    String name = getConfig().getProject().getName();
    Path dir = sitePaths.themes_dir.resolve(name);
    if (!Files.exists(dir)) {
      return ThemeInfo.INHERIT;
    } else if (!Files.isDirectory(dir)) {
      log.warn("Bad theme for {}: not a directory", name);
      return ThemeInfo.INHERIT;
    }
    try {
      return new ThemeInfo(
          readFile(dir.resolve(SitePaths.CSS_FILENAME)),
          readFile(dir.resolve(SitePaths.HEADER_FILENAME)),
          readFile(dir.resolve(SitePaths.FOOTER_FILENAME)));
    } catch (IOException e) {
      log.error("Error reading theme for " + name, e);
      return ThemeInfo.INHERIT;
    }
  }

  private String readFile(Path p) throws IOException {
    return Files.exists(p) ? new String(Files.readAllBytes(p), UTF_8) : null;
  }

  private boolean getInheritableBoolean(Function func) {
    for (ProjectState s : tree()) {
      switch (func.apply(s.getProject())) {
        case TRUE:
          return true;
        case FALSE:
          return false;
        case INHERIT:
        default:
          continue;
      }
    }
    return false;
  }

  private boolean match(Branch.NameKey destination, String refPattern, CurrentUser user) {
    return RefPatternMatcher.getMatcher(refPattern).match(destination.get(), user);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy