blob: 681b2ea989ebd165bd88594b55b12fc2445631ec [file] [log] [blame]
// Copyright (c) 2013 VMware, Inc. All Rights Reserved.
// 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.googlesource.gerrit.owners.common;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.googlesource.gerrit.owners.common.JgitWrapper.getBlobAsBytes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Account.Id;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Calculates the owners of a patch list. */
// TODO(vspivak): provide assisted factory
public class PathOwners {
private static final Logger log = LoggerFactory.getLogger(PathOwners.class);
private final SetMultimap<String, Account.Id> owners;
private final SetMultimap<String, Account.Id> reviewers;
private final Repository repository;
private final List<Project.NameKey> parentProjectsNames;
private final PatchList patchList;
private final ConfigurationParser parser;
private final Accounts accounts;
private final GitRepositoryManager repositoryManager;
private Map<String, Matcher> matchers;
private Map<String, Set<Id>> fileOwners;
private Map<String, Set<String>> fileGroupOwners;
private final boolean expandGroups;
public PathOwners(
Accounts accounts,
GitRepositoryManager repositoryManager,
Repository repository,
List<Project.NameKey> parentProjectsNames,
Optional<String> branchWhenEnabled,
PatchList patchList,
boolean expandGroups) {
this.repositoryManager = repositoryManager;
this.repository = repository;
this.parentProjectsNames = parentProjectsNames;
this.patchList = patchList;
this.parser = new ConfigurationParser(accounts);
this.accounts = accounts;
this.expandGroups = expandGroups;
OwnersMap map = branchWhenEnabled.map(branch -> fetchOwners(branch)).orElse(new OwnersMap());
owners = Multimaps.unmodifiableSetMultimap(map.getPathOwners());
reviewers = Multimaps.unmodifiableSetMultimap(map.getPathReviewers());
matchers = map.getMatchers();
fileOwners = map.getFileOwners();
fileGroupOwners = map.getFileGroupOwners();
}
/**
* Returns a read only view of the paths to owners mapping.
*
* @return multimap of paths to owners
*/
public SetMultimap<String, Account.Id> get() {
return owners;
}
/**
* Returns a read only view of the paths to reviewers mapping.
*
* @return multimap of paths to reviewers
*/
public SetMultimap<String, Account.Id> getReviewers() {
return reviewers;
}
public Map<String, Matcher> getMatchers() {
return matchers;
}
public Map<String, Set<Account.Id>> getFileOwners() {
return fileOwners;
}
public Map<String, Set<String>> getFileGroupOwners() {
return fileGroupOwners;
}
public boolean expandGroups() {
return expandGroups;
}
/**
* Fetched the owners for the associated patch list.
*
* @return A structure containing matchers paths to owners
*/
private OwnersMap fetchOwners(String branch) {
OwnersMap ownersMap = new OwnersMap();
try {
// Using a `map` would have needed a try/catch inside the lamba, resulting in more code
List<PathOwnersEntry> parentsPathOwnersEntries =
getPathOwnersEntries(parentProjectsNames, RefNames.REFS_CONFIG);
PathOwnersEntry projectEntry = getPathOwnersEntry(repository, RefNames.REFS_CONFIG);
PathOwnersEntry rootEntry = getPathOwnersEntry(repository, branch);
Set<String> modifiedPaths = getModifiedPaths();
Map<String, PathOwnersEntry> entries = new HashMap<>();
PathOwnersEntry currentEntry = null;
for (String path : modifiedPaths) {
currentEntry =
resolvePathEntry(
path, branch, projectEntry, parentsPathOwnersEntries, rootEntry, entries);
// add owners and reviewers to file for matcher predicates
ownersMap.addFileOwners(path, currentEntry.getOwners());
ownersMap.addFileReviewers(path, currentEntry.getReviewers());
ownersMap.addFileGroupOwners(path, currentEntry.getGroupOwners());
// Only add the path to the OWNERS file to reduce the number of
// entries in the result
if (currentEntry.getOwnersPath() != null) {
ownersMap.addPathOwners(currentEntry.getOwnersPath(), currentEntry.getOwners());
ownersMap.addPathReviewers(currentEntry.getOwnersPath(), currentEntry.getReviewers());
}
ownersMap.addMatchers(currentEntry.getMatchers());
}
// We need to only keep matchers that match files in the patchset
Map<String, Matcher> matchers = ownersMap.getMatchers();
if (matchers.size() > 0) {
HashMap<String, Matcher> newMatchers = Maps.newHashMap();
// extra loop
for (String path : modifiedPaths) {
processMatcherPerPath(matchers, newMatchers, path, ownersMap);
}
if (matchers.size() != newMatchers.size()) {
ownersMap.setMatchers(newMatchers);
}
}
return ownersMap;
} catch (IOException e) {
log.warn("Invalid OWNERS file", e);
return ownersMap;
}
}
private List<PathOwnersEntry> getPathOwnersEntries(
List<Project.NameKey> projectNames, String branch) throws IOException {
ImmutableList.Builder<PathOwnersEntry> pathOwnersEntries = ImmutableList.builder();
for (Project.NameKey projectName : projectNames) {
try (Repository repo = repositoryManager.openRepository(projectName)) {
pathOwnersEntries = pathOwnersEntries.add(getPathOwnersEntry(repo, branch));
}
}
return pathOwnersEntries.build();
}
private PathOwnersEntry getPathOwnersEntry(Repository repo, String branch) throws IOException {
String rootPath = "OWNERS";
return getOwnersConfig(repo, rootPath, branch)
.map(
conf ->
new PathOwnersEntry(
rootPath,
conf,
accounts,
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet(),
Collections.emptySet()))
.orElse(new PathOwnersEntry());
}
private void processMatcherPerPath(
Map<String, Matcher> fullMatchers,
HashMap<String, Matcher> newMatchers,
String path,
OwnersMap ownersMap) {
Iterator<Matcher> it = fullMatchers.values().iterator();
while (it.hasNext()) {
Matcher matcher = it.next();
if (matcher.matches(path)) {
newMatchers.put(matcher.getPath(), matcher);
ownersMap.addFileOwners(path, matcher.getOwners());
ownersMap.addFileGroupOwners(path, matcher.getGroupOwners());
ownersMap.addFileReviewers(path, matcher.getReviewers());
}
}
}
private PathOwnersEntry resolvePathEntry(
String path,
String branch,
PathOwnersEntry projectEntry,
List<PathOwnersEntry> parentsPathOwnersEntries,
PathOwnersEntry rootEntry,
Map<String, PathOwnersEntry> entries)
throws IOException {
String[] parts = path.split("/");
PathOwnersEntry currentEntry = rootEntry;
StringBuilder builder = new StringBuilder();
// Inherit from Project if OWNER in root enables inheritance
calculateCurrentEntry(rootEntry, projectEntry, currentEntry);
// Inherit from Parent Project if OWNER in Project enables inheritance
for (PathOwnersEntry parentPathOwnersEntry : parentsPathOwnersEntries) {
calculateCurrentEntry(projectEntry, parentPathOwnersEntry, currentEntry);
}
// Iterate through the parent paths, not including the file name
// itself
for (int i = 0; i < parts.length - 1; i++) {
String part = parts[i];
builder.append(part).append("/");
String partial = builder.toString();
// Skip if we already parsed this path
if (entries.containsKey(partial)) {
currentEntry = entries.get(partial);
} else {
String ownersPath = partial + "OWNERS";
Optional<OwnersConfig> conf = getOwnersConfig(repository, ownersPath, branch);
final Set<Id> owners = currentEntry.getOwners();
final Set<Id> reviewers = currentEntry.getReviewers();
Collection<Matcher> inheritedMatchers = currentEntry.getMatchers().values();
Set<String> groupOwners = currentEntry.getGroupOwners();
currentEntry =
conf.map(
c ->
new PathOwnersEntry(
ownersPath,
c,
accounts,
owners,
reviewers,
inheritedMatchers,
groupOwners))
.orElse(currentEntry);
entries.put(partial, currentEntry);
}
}
return currentEntry;
}
private void calculateCurrentEntry(
PathOwnersEntry rootEntry, PathOwnersEntry projectEntry, PathOwnersEntry currentEntry) {
if (rootEntry.isInherited()) {
for (Matcher matcher : projectEntry.getMatchers().values()) {
if (!currentEntry.hasMatcher(matcher.getPath())) {
currentEntry.addMatcher(matcher);
}
}
if (currentEntry.getOwners().isEmpty()) {
currentEntry.setOwners(projectEntry.getOwners());
}
if (currentEntry.getOwnersPath() == null) {
currentEntry.setOwnersPath(projectEntry.getOwnersPath());
}
}
}
/**
* Parses the patch list for any paths that were modified.
*
* @return set of modified paths.
*/
private Set<String> getModifiedPaths() {
Set<String> paths = Sets.newHashSet();
for (PatchListEntry patch : patchList.getPatches()) {
// Ignore commit message and Merge List
String newName = patch.getNewName();
if (!COMMIT_MSG.equals(newName) && !MERGE_LIST.equals(newName)) {
paths.add(newName);
// If a file was moved then we need approvals for old and new
// path
if (patch.getChangeType() == Patch.ChangeType.RENAMED) {
paths.add(patch.getOldName());
}
}
}
return paths;
}
/**
* Returns the parsed FileOwnersConfig file for the given path if it exists.
*
* @param ownersPath path to OWNERS file in the git repo
* @return config or null if it doesn't exist
* @throws IOException
*/
private Optional<OwnersConfig> getOwnersConfig(Repository repo, String ownersPath, String branch)
throws IOException {
return getBlobAsBytes(repo, branch, ownersPath).flatMap(parser::getOwnersConfig);
}
}