blob: bbfacbc7fdac17feb7a9d2fda775a13613a6768a [file] [log] [blame]
/*
* Copyright (c) 2013 VMware, Inc. All Rights Reserved.
*/
package com.vmware.gerrit.owners.common;
import static com.vmware.gerrit.owners.common.JgitWrapper.getBlobAsBytes;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
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;
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.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
/**
* 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 Repository repository;
private final PatchList patchList;
private final ConfigurationParser parser;
private final Accounts accounts;
private Map<String, Matcher> matchers;
private Map<String, Set<Id>> fileOwners;
public PathOwners(Accounts accounts,
Repository repository,
PatchList patchList) {
this.repository = repository;
this.patchList = patchList;
this.parser = new ConfigurationParser(accounts);
this.accounts = accounts;
OwnersMap map = fetchOwners();
owners = Multimaps.unmodifiableSetMultimap(map.getPathOwners());
matchers = map.getMatchers();
fileOwners = map.getFileOwners();
}
/**
* 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;
}
public Map<String, Matcher> getMatchers() {
return matchers;
}
public Map<String, Set<Account.Id>> getFileOwners() {
return fileOwners;
}
/**
* Fetched the owners for the associated patch list.
*
* @return A structure containing matchers paths to owners
*/
private OwnersMap fetchOwners() {
OwnersMap ownersMap = new OwnersMap();
try {
String rootPath = "OWNERS";
PathOwnersEntry rootEntry =
getOwnersConfig(rootPath).map(
conf -> new PathOwnersEntry(rootPath, conf, accounts, Collections
.emptySet())).orElse(new PathOwnersEntry());
Set<String> modifiedPaths = getModifiedPaths();
Map<String, PathOwnersEntry> entries = new HashMap<>();
PathOwnersEntry currentEntry = null;
for (String path : modifiedPaths) {
currentEntry =
resolvePathEntry(path, rootEntry, entries);
// add owners to file for matcher predicates
ownersMap.addFileOwners(path,currentEntry.getOwners());
// 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.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 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());
}
}
}
private PathOwnersEntry resolvePathEntry(String path,
PathOwnersEntry rootEntry, Map<String, PathOwnersEntry> entries)
throws IOException {
String[] parts = path.split("/");
PathOwnersEntry currentEntry = rootEntry;
Set<Id> currentOwners = currentEntry.getOwners();
StringBuilder builder = new StringBuilder();
// 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(ownersPath);
currentEntry =
conf.map(
c -> new PathOwnersEntry(ownersPath, c, accounts, currentOwners))
.orElse(currentEntry);
if (conf.map(OwnersConfig::isInherited).orElse(false)) {
for (Matcher m : currentEntry.getMatchers().values()) {
currentEntry.addMatcher(m);
}
}
entries.put(partial, currentEntry);
}
}
return currentEntry;
}
/**
* 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
if (!patch.getNewName().equals("/COMMIT_MSG")) {
paths.add(patch.getNewName());
// 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(String ownersPath)
throws IOException {
return getBlobAsBytes(repository, "master", ownersPath).flatMap(
bytes -> parser.getOwnersConfig(bytes));
}
}