com.googlesource.gerrit.plugins.findowners.OwnersDb.java Source code

Java tutorial

Introduction

Here is the source code for com.googlesource.gerrit.plugins.findowners.OwnersDb.java

Source

// 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 static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import java.net.InetAddress;
import java.net.UnknownHostException;
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.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;

/** Keep all information about owners and owned files. */
class OwnersDb {
    private static final FluentLogger logger = FluentLogger.forEnclosingClass();

    private AccountCache accountCache;
    private GitRepositoryManager repoManager;
    private Emails emails;
    private int numOwners = -1; // # of owners of all given files.

    String key = ""; // key to find this OwnersDb in a cache.
    String revision = ""; // tip of branch revision, where OWENRS were found.
    Map<String, Set<String>> dir2Globs = new HashMap<>(); // directory to file globs in the directory
    Map<String, Set<String>> owner2Paths = new HashMap<>(); // owner email to owned dirs or file globs
    Map<String, Set<String>> path2Owners = new HashMap<>(); // dir or file glob to owner emails
    Set<String> readDirs = new HashSet<>(); // directories in which we have checked OWNERS
    Set<String> stopLooking = new HashSet<>(); // directories where OWNERS has "set noparent"
    Map<String, String> preferredEmails = new HashMap<>(); // owner email to preferred email
    List<String> errors = new ArrayList<>(); // error messages
    List<String> logs = new ArrayList<>(); // trace/debug messages

    OwnersDb() {
    }

    OwnersDb(ProjectState projectState, AccountCache accountCache, Emails emails, String key,
            GitRepositoryManager repoManager, ChangeData changeData, String branch, Collection<String> files) {
        this.accountCache = accountCache;
        this.repoManager = repoManager;
        this.emails = emails;
        this.key = key;
        try {
            InetAddress inetAddress = InetAddress.getLocalHost();
            logs.add("HostName:" + inetAddress.getHostName());
        } catch (UnknownHostException e) {
            logException(logs, "HostName:", e);
        }
        logs.add("key:" + key);
        preferredEmails.put("*", "*");
        String projectName = projectState.getName();
        logs.add("project:" + projectName);
        String ownersFileName = Config.getOwnersFileName(projectState, changeData);
        logs.add("ownersFileName:" + ownersFileName);
        try (Repository repo = repoManager.openRepository(projectState.getNameKey())) {
            // Some hacked CL could have a target branch that is not created yet.
            ObjectId id = getBranchId(repo, branch, changeData, logs);
            revision = "";
            if (id != null) {
                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.addDotPrefix(fileName); // e.g.   "./" "./d1/f1" "./d2/d3/"
                    String dir = Util.getParentDir(fileName); // e.g. "."  "./d1"    "./d2"
                    logs.add("findOwnersFileFor:" + fileName);
                    while (!readDirs.contains(dir)) {
                        readDirs.add(dir);
                        logs.add("findOwnersFileIn:" + dir);
                        String filePath = dir + "/" + ownersFileName;
                        String content = getFile(repo, id, filePath, logs);
                        if (content != null && !content.isEmpty()) {
                            addFile(projectName, branch, dir + "/", dir + "/" + ownersFileName,
                                    content.split("\\R+"));
                        }
                        if (stopLooking.contains(dir + "/") || !dir.contains("/")) {
                            break; // stop looking through parent directory
                        }
                        dir = Util.getDirName(dir); // go up one level
                    }
                }
                try {
                    revision = repo.exactRef(branch).getObjectId().getName();
                } catch (Exception e) {
                    logger.atSevere().withCause(e).log("Fail to get branch revision for %s",
                            Config.getChangeId(changeData));
                    logException(logs, "OwnersDb get revision", e);
                }
            }
        } catch (Exception e) {
            logger.atSevere().withCause(e).log("Fail to find repository of project %s", projectName);
            logException(logs, "OwnersDb get repository", e);
        }
        countNumOwners(files);
    }

    int getNumOwners() {
        return (numOwners >= 0) ? numOwners : owner2Paths.keySet().size();
    }

    private void countNumOwners(Collection<String> files) {
        logs.add("countNumOwners");
        Map<String, Set<String>> file2Owners = findOwners(files, null, logs);
        if (file2Owners != null) {
            Set<String> emails = new HashSet<>();
            file2Owners.values().forEach(emails::addAll);
            numOwners = emails.size();
        } else {
            numOwners = owner2Paths.keySet().size();
        }
    }

    void addOwnerPathPair(String owner, String path) {
        Util.addToMap(owner2Paths, owner, path);
        Util.addToMap(path2Owners, path, owner);
        if (path.length() > 0 && path.charAt(path.length() - 1) != '/') {
            Util.addToMap(dir2Globs, Util.getDirName(path) + "/", path); // A file glob.
        }
    }

    void addPreferredEmails(Set<String> ownerEmails) {
        List<String> owners = new ArrayList<>(ownerEmails);
        owners.removeIf(o -> preferredEmails.get(o) != null);
        if (!owners.isEmpty()) {
            String[] ownerEmailsAsArray = new String[owners.size()];
            owners.toArray(ownerEmailsAsArray);
            Multimap<String, Account.Id> email2ids = null;
            try {
                email2ids = emails.getAccountsFor(ownerEmailsAsArray);
            } catch (Exception e) {
                logger.atSevere().withCause(e).log("accounts.byEmails failed");
                logException(logs, "getAccountsFor:" + ownerEmailsAsArray[0], e);
            }
            for (String owner : ownerEmailsAsArray) {
                String email = owner;
                try {
                    if (email2ids == null) {
                        errors.add(owner);
                    } else {
                        Collection<Account.Id> ids = email2ids.get(owner);
                        if (ids == null || ids.size() != 1) {
                            errors.add(owner);
                        } else {
                            // Accounts may have no preferred email.
                            email = accountCache.get(ids.iterator().next())
                                    .map(a -> a.getAccount().getPreferredEmail()).orElse(null);
                        }
                    }
                } catch (Exception e) {
                    logger.atSevere().withCause(e).log("Fail to find preferred email of %s", owner);
                    errors.add(owner);
                }
                preferredEmails.put(owner, email);
            }
        }
    }

    void addFile(String project, String branch, String dirPath, String filePath, String[] lines) {
        Parser parser = new Parser(repoManager, project, branch, filePath, logs);
        Parser.Result result = parser.parseFile(dirPath, lines);
        if (result.stopLooking) {
            stopLooking.add(dirPath);
        }
        addPreferredEmails(result.owner2paths.keySet());
        for (String owner : result.owner2paths.keySet()) {
            String email = preferredEmails.get(owner);
            for (String path : result.owner2paths.get(owner)) {
                addOwnerPathPair(email, path);
            }
        }
        if (Config.getReportSyntaxError()) {
            result.warnings.forEach(w -> logger.atWarning().log(w));
            result.errors.forEach(w -> logger.atSevere().log(w));
        }
    }

    private void addOwnerWeights(ArrayList<String> paths, ArrayList<Integer> distances, String file,
            Map<String, Set<String>> file2Owners, Map<String, OwnerWeights> map, List<String> logs) {
        for (int i = 0; i < paths.size(); i++) {
            logs.add("addOwnerWeightsIn:" + paths.get(i));
            Set<String> owners = path2Owners.get(paths.get(i));
            if (owners == null) {
                continue;
            }
            for (String name : owners) {
                Util.addToMap(file2Owners, file, name);
                if (map == null) {
                    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. */
    Map<String, Set<String>> findOwners(Collection<String> files) {
        return findOwners(files, null, new ArrayList<>());
    }

    /** Returns owner emails of every file and set up ownerWeights. */
    Map<String, Set<String>> findOwners(Collection<String> files, Map<String, OwnerWeights> ownerWeights,
            List<String> logs) {
        return findOwners(files.toArray(new String[0]), ownerWeights, logs);
    }

    /** Returns owner emails of every file and set up ownerWeights. */
    Map<String, Set<String>> findOwners(String[] files, Map<String, OwnerWeights> ownerWeights, List<String> logs) {
        // 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).
        logs.add("findOwners");
        Arrays.sort(files); // Force an ordered search sequence.
        Map<String, Set<String>> file2Owners = new HashMap<>();
        for (String fileName : files) {
            fileName = Util.addDotPrefix(fileName);
            logs.add("checkFile:" + fileName);
            String dirPath = Util.getParentDir(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<>();
            ArrayList<Integer> distances = new ArrayList<>();
            boolean foundStar = false;
            while (true) {
                int savedSizeOfPaths = paths.size();
                logs.add("checkDir:" + dirPath);
                if (dir2Globs.containsKey(dirPath + "/")) {
                    Set<String> 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, logs);
            } else {
                logs.add("found * in:" + fileName);
            }
        }
        return file2Owners;
    }

    /** Returns true if path has '*' owner. */
    private boolean findStarOwner(String path, int distance, ArrayList<String> paths,
            ArrayList<Integer> distances) {
        Set<String> owners = path2Owners.get(path);
        if (owners != null) {
            paths.add(path);
            distances.add(distance);
            if (owners.contains("*")) {
                return true;
            }
        }
        return false;
    }

    /** Returns ObjectId of the given branch, or null. */
    private static ObjectId getBranchId(Repository repo, String branch, ChangeData changeData, List<String> logs) {
        String header = "getBranchId:" + branch;
        try {
            ObjectId id = repo.resolve(branch);
            if (id == null && changeData != null && !Checker.isExemptFromOwnerApproval(changeData)) {
                logger.atSevere().log("cannot find branch %s for %s", branch, Config.getChangeId(changeData));
                logs.add(header + " (NOT FOUND)");
            } else {
                logs.add(header + " (FOUND)");
            }
            return id;
        } catch (Exception e) {
            logger.atSevere().withCause(e).log("cannot find branch %s for %s", branch,
                    Config.getChangeId(changeData));
            logException(logs, header, e);
        }
        return null;
    }

    /** Returns file content or empty string; uses project+branch+file names. */
    public static String getRepoFile(GitRepositoryManager repoManager, String project, String branch, String file,
            List<String> logs) {
        // 'file' must be an absolute path from the root of 'project'.
        logs.add("getRepoFile:" + project + ":" + branch + ":" + file);
        try (Repository repo = repoManager.openRepository(new Project.NameKey(project))) {
            ObjectId id = repo.resolve(branch);
            if (id != null) {
                return getFile(repo, id, file, logs);
            }
            logs.add("getRepoFile not found branch " + branch);
        } catch (Exception e) {
            logger.atSevere().withCause(e).log("Fail to find repository of project %s", project);
            logException(logs, "getRepoFile", e);
        }
        return "";
    }

    /** Returns file content or empty string; uses Repository. */
    private static String getFile(Repository repo, ObjectId id, String file, List<String> logs) {
        file = Util.gitRepoFilePath(file);
        String header = "getFile:" + file;
        try (RevWalk revWalk = new RevWalk(repo)) {
            RevTree tree = revWalk.parseCommit(id).getTree();
            ObjectReader reader = revWalk.getObjectReader();
            TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
            if (treeWalk != null) {
                String content = new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
                logs.add(header + ":" + content);
                return content;
            }
            logs.add(header + " (NOT FOUND)");
        } catch (Exception e) {
            logger.atSevere().withCause(e).log("get file %s", file);
            logException(logs, "getFile", e);
        }
        return "";
    }

    /** Adds a header + exception message to the logs. */
    private static void logException(List<String> logs, String header, Exception e) {
        logs.add(header + " Exception:" + e.getMessage());
    }
}