blob: 503da4192737d876e93b6e1916c3acc042f345a0 [file] [log] [blame]
// 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.plugins.findowners;
import com.googlesource.gerrit.plugins.findowners.Util.Owner2Weights;
import com.googlesource.gerrit.plugins.findowners.Util.String2StringSet;
import com.googlesource.gerrit.plugins.findowners.Util.StringSet;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Keep all information about owners and owned files. */
class OwnersDb {
private static final Logger log = LoggerFactory.getLogger(OwnersDb.class);
private Server server; // could be set to a mocked server in unit tests
private int numOwners; // # of owners of all given files.
String revision; // tip of branch revision, where OWENRS were found.
String2StringSet dir2Globs; // (directory) => file globs in the directory
String2StringSet owner2Paths; // (owner email) => owned dirs or file globs
String2StringSet path2Owners; // (directory or file glob) => owner emails
StringSet readDirs; // directories in which we have checked OWNERS
StringSet stopLooking; // directories where OWNERS has "set noparent"
private void init(Server s) {
numOwners = -1;
revision = "";
dir2Globs = new String2StringSet();
owner2Paths = new String2StringSet();
path2Owners = new String2StringSet();
readDirs = new StringSet();
stopLooking = new StringSet();
server = (null != s) ? s : new Server();
}
OwnersDb(Server s) {
init(s);
}
OwnersDb(Server s, String key, Repository repository,
String branch, Collection<String> files) {
init(s, key, repository, null, null, branch, files);
}
OwnersDb(Server s, String key, String url, String project,
String branch, Collection<String> files) {
init(s, key, null, url, project, branch, files);
}
int getNumOwners() {
return (numOwners >= 0) ? numOwners : owner2Paths.keySet().size();
}
private void countNumOwners(Collection<String> files) {
String2StringSet file2Owners = findOwners(files, null);
if (null != file2Owners) {
StringSet emails = new StringSet();
for (String key : file2Owners.keySet()) {
for (String owner : file2Owners.get(key)) {
emails.add(owner);
}
}
numOwners = emails.size();
} else {
numOwners = owner2Paths.keySet().size();
}
}
private static void addToMap(String2StringSet map,
String key, String value) {
if (null == map.get(key)) {
map.put(key, new StringSet());
}
map.get(key).add(value);
}
void addOwnerPathPair(String owner, String path) {
addToMap(owner2Paths, owner, path);
addToMap(path2Owners, path, owner);
if (path.length() > 0 && path.charAt(path.length() - 1) != '/') {
addToMap(dir2Globs, Util.getDirName(path) + "/", path); // A file glob.
}
}
void addFile(String path, String file, String[] lines) {
int n = 0;
for (String line : lines) {
String error = Parser.parseLine(this, path, file, line, ++n);
if (null != error && server.getReportSyntaxError()) {
log.warn(error);
}
}
}
private void addOwnerWeights(
ArrayList<String> paths, ArrayList<Integer> distances,
String file, String2StringSet file2Owners, Owner2Weights map) {
for (int i = 0; i < paths.size(); i++) {
StringSet owners = path2Owners.get(paths.get(i));
if (null == owners) {
continue;
}
for (String name : owners) {
addToMap(file2Owners, file, name);
if (null == map) {
continue;
}
if (map.containsKey(name)) {
map.get(name).addFile(file, distances.get(i));
} else {
map.put(name, new OwnerWeights(file, distances.get(i)));
}
}
}
}
/** Quick method to find owner emails of every file. */
String2StringSet findOwners(Collection<String> files) {
return findOwners(files, null);
}
/** Returns owner emails of every file and set up ownerWeights. */
String2StringSet findOwners(Collection<String> files,
Owner2Weights ownerWeights) {
return findOwners(files.toArray(new String[0]), ownerWeights);
}
/** Returns true if path has '*' owner. */
private boolean findStarOwner(String path, int distance,
ArrayList<String> paths,
ArrayList<Integer> distances) {
StringSet owners = path2Owners.get(path);
if (null != owners) {
paths.add(path);
distances.add(new Integer(distance));
if (owners.contains("*")) {
return true;
}
}
return false;
}
/** Returns owner emails of every file and set up ownerWeights. */
String2StringSet findOwners(String[] files, Owner2Weights ownerWeights) {
// Returns a map of file to set of owner emails.
// If ownerWeights is not null, add to it owner to distance-from-dir;
// a distance of 1 is the lowest/closest possible distance
// (which makes the subsequent math easier).
String2StringSet file2Owners = new String2StringSet();
for (String fileName : files) {
fileName = Util.normalizedFilePath(fileName);
String dirPath = Util.normalizedDirPath(fileName);
String baseName = fileName.substring(dirPath.length() + 1);
int distance = 1;
FileSystem fileSystem = FileSystems.getDefault();
// Collect all matched (path, distance) in all OWNERS files for
// fileName. Add them only if there is no special "*" owner.
ArrayList<String> paths = new ArrayList<String>();
ArrayList<Integer> distances = new ArrayList<Integer>();
boolean foundStar = false;
while (true) {
int savedSizeOfPaths = paths.size();
if (dir2Globs.containsKey(dirPath + "/")) {
StringSet patterns = dir2Globs.get(dirPath + "/");
for (String pat : patterns) {
PathMatcher matcher = fileSystem.getPathMatcher("glob:" + pat);
if (matcher.matches(Paths.get(dirPath + "/" + baseName))) {
foundStar |= findStarOwner(pat, distance, paths, distances);
// Do not break here, a file could match multiple globs
// with different owners.
// OwnerWeights.add won't add duplicated files.
}
}
// NOTE: A per-file directive can only specify owner emails,
// not "set noparent".
}
// If baseName does not match per-file glob, paths is not changed.
// Then we should check the general non-per-file owners.
if (paths.size() == savedSizeOfPaths) {
foundStar |= findStarOwner(dirPath + "/", distance, paths, distances);
}
if (foundStar // This file can be approved by anyone, no owner.
|| stopLooking.contains(dirPath + "/") // stop looking parent
|| !dirPath.contains("/") /* root */ ) {
break;
}
if (paths.size() != savedSizeOfPaths) {
distance++; // increase distance for each found OWNERS
}
dirPath = Util.getDirName(dirPath); // go up one level
}
if (!foundStar) {
addOwnerWeights(paths, distances, fileName,
file2Owners, ownerWeights);
}
}
return file2Owners;
}
private void init(
Server s, String key, Repository repository, String url,
String project, String branch, Collection<String> files) {
init(s);
for (String fileName : files) {
// Find OWNERS in fileName's directory and parent directories.
// Stop looking for a parent directory if OWNERS has "set noparent".
fileName = Util.normalizedFilePath(fileName);
String dir = Util.normalizedDirPath(fileName);
while (!readDirs.contains(dir)) {
readDirs.add(dir);
String content = server.getOWNERS(dir, repository, url,
project, branch);
if (null != content && !content.equals("")) {
addFile(dir + "/", dir + "/OWNERS", content.split("\\R+"));
}
if (stopLooking.contains(dir + "/") || !dir.contains("/")) {
break; // stop looking through parent directory
}
dir = Util.getDirName(dir); // go up one level
}
}
countNumOwners(files);
revision = server.getBranchRevision(repository, url, project, branch);
}
}