| /* |
| * 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)); |
| } |
| } |