| // Copyright (C) 2020 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.google.gerrit.plugins.codeowners.backend; |
| |
| import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidConfigCause; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.Optional; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** |
| * Class to scan a branch for code owner config files. |
| * |
| * <p>Whether the scan includes the code owner config file at the root of {@code refs/meta/config} |
| * branch that contains the default code owners for the whole repository can be controlled via |
| * {@link #includeDefaultCodeOwnerConfig(boolean)}. |
| */ |
| public class CodeOwnerConfigScanner { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| public interface Factory { |
| CodeOwnerConfigScanner create(); |
| } |
| |
| private final GitRepositoryManager repoManager; |
| private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; |
| |
| private boolean includeDefaultCodeOwnerConfig = true; |
| |
| @Inject |
| CodeOwnerConfigScanner( |
| GitRepositoryManager repoManager, |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration) { |
| this.repoManager = repoManager; |
| this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; |
| } |
| |
| /** |
| * Whether the scan should include the code owner config file at the root of {@code |
| * refs/meta/config} branch that contains the default code owners for the whole repository. |
| */ |
| public CodeOwnerConfigScanner includeDefaultCodeOwnerConfig( |
| boolean includeDefaultCodeOwnerConfig) { |
| this.includeDefaultCodeOwnerConfig = includeDefaultCodeOwnerConfig; |
| return this; |
| } |
| |
| /** |
| * Visits all code owner config files in the given project and branch. |
| * |
| * @param branchNameKey the project and branch for which the code owner config files should be |
| * visited |
| * @param codeOwnerConfigVisitor the callback that is invoked for each code owner config file |
| * @param invalidCodeOwnerConfigCallback callback that is invoked for invalid code owner config |
| * files |
| */ |
| public void visit( |
| BranchNameKey branchNameKey, |
| CodeOwnerConfigVisitor codeOwnerConfigVisitor, |
| InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback) { |
| visit( |
| branchNameKey, |
| codeOwnerConfigVisitor, |
| invalidCodeOwnerConfigCallback, |
| /* pathGlob= */ null); |
| } |
| |
| /** |
| * Visits all code owner config files in the given project and branch. |
| * |
| * @param branchNameKey the project and branch for which the code owner config files should be |
| * visited |
| * @param codeOwnerConfigVisitor the callback that is invoked for each code owner config file |
| * @param invalidCodeOwnerConfigCallback callback that is invoked for invalid code owner config |
| * files |
| * @param pathGlob optional Java NIO glob that the paths of code owner config files must match |
| */ |
| public void visit( |
| BranchNameKey branchNameKey, |
| CodeOwnerConfigVisitor codeOwnerConfigVisitor, |
| InvalidCodeOwnerConfigCallback invalidCodeOwnerConfigCallback, |
| @Nullable String pathGlob) { |
| requireNonNull(branchNameKey, "branchNameKey"); |
| requireNonNull(codeOwnerConfigVisitor, "codeOwnerConfigVisitor"); |
| requireNonNull(invalidCodeOwnerConfigCallback, "invalidCodeOwnerConfigCallback"); |
| |
| CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey); |
| logger.atFine().log( |
| "scanning code owner files in branch %s of project %s (path glob = %s)", |
| branchNameKey.branch(), branchNameKey.project(), pathGlob); |
| |
| if (includeDefaultCodeOwnerConfig && !RefNames.REFS_CONFIG.equals(branchNameKey.branch())) { |
| logger.atFine().log("Scanning code owner config file in %s", RefNames.REFS_CONFIG); |
| Optional<CodeOwnerConfig> metaCodeOwnerConfig = |
| codeOwnerBackend.getCodeOwnerConfig( |
| CodeOwnerConfig.Key.createWithFileName( |
| codeOwnerBackend, branchNameKey.project(), RefNames.REFS_CONFIG, "/"), |
| /** revision */ |
| null); |
| if (metaCodeOwnerConfig.isPresent()) { |
| boolean visitFurtherCodeOwnerConfigFiles = |
| codeOwnerConfigVisitor.visit(metaCodeOwnerConfig.get()); |
| if (!visitFurtherCodeOwnerConfigFiles) { |
| // By returning false the callback told us to not visit any further code owner config |
| // files, hence we are done and do not need to search for further code owner config files |
| // in the branch. |
| return; |
| } |
| } |
| } |
| |
| try (Repository repository = repoManager.openRepository(branchNameKey.project()); |
| RevWalk rw = new RevWalk(repository); |
| CodeOwnerConfigTreeWalk treeWalk = |
| new CodeOwnerConfigTreeWalk( |
| codeOwnerBackend, branchNameKey, repository, rw, pathGlob)) { |
| while (treeWalk.next()) { |
| CodeOwnerConfig codeOwnerConfig; |
| try { |
| codeOwnerConfig = treeWalk.getCodeOwnerConfig(); |
| } catch (StorageException storageException) { |
| Optional<ConfigInvalidException> configInvalidException = |
| getInvalidConfigCause(storageException); |
| if (!configInvalidException.isPresent()) { |
| // Propagate any failure that is not related to the contents of the code owner config. |
| throw storageException; |
| } |
| |
| // The code owner config is invalid and cannot be parsed. |
| invalidCodeOwnerConfigCallback.onInvalidCodeOwnerConfig( |
| treeWalk.getFilePath(), configInvalidException.get()); |
| continue; |
| } |
| |
| boolean visitFurtherCodeOwnerConfigFiles = codeOwnerConfigVisitor.visit(codeOwnerConfig); |
| if (!visitFurtherCodeOwnerConfigFiles) { |
| break; |
| } |
| } |
| } catch (IOException e) { |
| throw new StorageException( |
| String.format( |
| "Failed to scan for code owner configs in branch %s of project %s", |
| branchNameKey.branch(), branchNameKey.project()), |
| e); |
| } |
| } |
| |
| /** |
| * Returns an {@link InvalidCodeOwnerConfigCallback} instance that ignores invalid code owner |
| * config files. |
| */ |
| public static InvalidCodeOwnerConfigCallback ignoreInvalidCodeOwnerConfigFiles() { |
| return (codeOwnerConfigFilePath, configInvalidException) -> |
| logger.atFine().log("ignoring invalid code owner config file %s", codeOwnerConfigFilePath); |
| } |
| } |