| // 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 java.util.Objects.requireNonNull; |
| |
| import com.google.common.base.Throwables; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.update.RetryHelper; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.Optional; |
| import java.util.regex.Pattern; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** |
| * Base class for all {@link CodeOwnerBackend}'s that store {@link CodeOwnerConfig}'s in files |
| * inside the corresponding folders in the source branch. |
| * |
| * <p>E.g. the code owner configuration for folder {@code /foo/bar} folder of the {@code master} |
| * branch is stored as a file in the {@code /foo/bar} folder of the {@code master} |
| */ |
| public abstract class AbstractFileBasedCodeOwnerBackend implements CodeOwnerBackend { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; |
| private final GitRepositoryManager repoManager; |
| private final PersonIdent serverIdent; |
| private final MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory; |
| private final RetryHelper retryHelper; |
| private final String defaultFileName; |
| private final CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory; |
| private final CodeOwnerConfigParser codeOwnerConfigParser; |
| |
| protected AbstractFileBasedCodeOwnerBackend( |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, |
| GitRepositoryManager repoManager, |
| @GerritPersonIdent PersonIdent serverIdent, |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| RetryHelper retryHelper, |
| String defaultFileName, |
| CodeOwnerConfigFile.Factory codeOwnerConfigFileFactory, |
| CodeOwnerConfigParser codeOwnerConfigParser) { |
| this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; |
| this.repoManager = repoManager; |
| this.serverIdent = serverIdent; |
| this.metaDataUpdateInternalFactory = metaDataUpdateInternalFactory; |
| this.retryHelper = retryHelper; |
| this.defaultFileName = defaultFileName; |
| this.codeOwnerConfigFileFactory = codeOwnerConfigFileFactory; |
| this.codeOwnerConfigParser = codeOwnerConfigParser; |
| } |
| |
| @Override |
| public final Optional<CodeOwnerConfig> getCodeOwnerConfig( |
| CodeOwnerConfig.Key codeOwnerConfigKey, |
| @Nullable RevWalk revWalk, |
| @Nullable ObjectId revision) { |
| String fileName = |
| codeOwnerConfigKey.fileName().orElse(getFileName(codeOwnerConfigKey.project())); |
| |
| if (!isCodeOwnerConfigFile(codeOwnerConfigKey.project(), fileName)) { |
| // The file name can mismatch if we resolve imported code owner configs. When code owner |
| // configs are imported the user specifies the full path of the code owner config (including |
| // the file name) in the importing code owner config. If the user specifies a file name that |
| // is not supported, this is not a server error, but a user error. Invalid imports are |
| // silently ignored, but users can detect them by running validation checks on the code owner |
| // configs. Although this is a normal situation, still log a warning so that we can see in the |
| // server logs when this happens. |
| logger.atWarning().log( |
| "Cannot load code owner config %s: unsupported file name", codeOwnerConfigKey); |
| return Optional.empty(); |
| } |
| |
| return loadCodeOwnerConfigFile(codeOwnerConfigKey, fileName, revWalk, revision) |
| .getLoadedCodeOwnerConfig(); |
| } |
| |
| private CodeOwnerConfigFile loadCodeOwnerConfigFile( |
| CodeOwnerConfig.Key codeOwnerConfigKey, |
| String fileName, |
| @Nullable RevWalk revWalk, |
| @Nullable ObjectId revision) { |
| try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) { |
| if (revision == null) { |
| return codeOwnerConfigFileFactory.loadCurrent( |
| fileName, codeOwnerConfigParser, repository, codeOwnerConfigKey); |
| } |
| |
| boolean closeRevWalk = false; |
| if (revWalk == null) { |
| closeRevWalk = true; |
| revWalk = new RevWalk(repository); |
| } |
| try { |
| return codeOwnerConfigFileFactory.load( |
| fileName, codeOwnerConfigParser, revWalk, revision, codeOwnerConfigKey); |
| } finally { |
| if (closeRevWalk) { |
| revWalk.close(); |
| } |
| } |
| } catch (IOException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format("failed to load code owner config %s", codeOwnerConfigKey), e); |
| } catch (ConfigInvalidException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "invalid code owner config file %s (project = %s, branch = %s)", |
| codeOwnerConfigKey.filePath(defaultFileName), |
| codeOwnerConfigKey.project(), |
| codeOwnerConfigKey.branchNameKey().branch()), |
| e); |
| } |
| } |
| |
| @Override |
| public Path getFilePath(CodeOwnerConfig.Key codeOwnerConfigKey) { |
| return codeOwnerConfigKey.filePath(defaultFileName); |
| } |
| |
| @Override |
| public boolean isCodeOwnerConfigFile(Project.NameKey project, String fileName) { |
| requireNonNull(project, "project"); |
| requireNonNull(fileName, "fileName"); |
| |
| if (getFileName(project).equals(fileName)) { |
| return true; |
| } |
| |
| return isCodeOwnerConfigFileWithExtension(project, fileName); |
| } |
| |
| /** |
| * Checks whether the given file name is code owner config file with an extension in the name. |
| * |
| * <p>Name extensions can appear as post- or pre-fix: |
| * |
| * <ul> |
| * <li>Post-fix: E.g. {@code OWNERS_<extension>} or {@code OWNERS_<extension>.<file-extension>} |
| * <li>Pre-fix: E.g. {@code <extension>_OWNERS} or {@code <extension>_OWNERS.<file-extension>} |
| * </ul> |
| * |
| * @param project the project in which the code owner config files are stored |
| * @param fileName the name of the file for which it should be checked whether is a code owner |
| * config file with extension |
| * @return whether the given file name is code owner config file with an extension in the name |
| */ |
| private boolean isCodeOwnerConfigFileWithExtension(Project.NameKey project, String fileName) { |
| String quotedDefaultFileName = Pattern.quote(defaultFileName); |
| String quotedFileExtension = |
| Pattern.quote( |
| codeOwnersPluginConfiguration |
| .getProjectConfig(project) |
| .getFileExtension() |
| .map(ext -> "." + ext) |
| .orElse("")); |
| String nameExtension = "(\\w)+"; |
| |
| return Pattern.compile( |
| "^" + quotedDefaultFileName + "_" + nameExtension + quotedFileExtension + "$") |
| .matcher(fileName) |
| .matches() |
| || Pattern.compile( |
| "^" + nameExtension + "_" + quotedDefaultFileName + quotedFileExtension + "$") |
| .matcher(fileName) |
| .matches(); |
| } |
| |
| private String getFileName(Project.NameKey project) { |
| return defaultFileName |
| + codeOwnersPluginConfiguration |
| .getProjectConfig(project) |
| .getFileExtension() |
| .map(ext -> "." + ext) |
| .orElse(""); |
| } |
| |
| @Override |
| public final Optional<CodeOwnerConfig> upsertCodeOwnerConfig( |
| CodeOwnerConfig.Key codeOwnerConfigKey, |
| CodeOwnerConfigUpdate codeOwnerConfigUpdate, |
| @Nullable IdentifiedUser currentUser) { |
| try { |
| return retryHelper |
| .pluginUpdate( |
| "upsertCodeOwnerConfigInSourceBranch", |
| () -> |
| upsertCodeOwnerConfigInSourceBranch( |
| currentUser, codeOwnerConfigKey, codeOwnerConfigUpdate)) |
| .call(); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| throw new CodeOwnersInternalServerErrorException( |
| String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e); |
| } |
| } |
| |
| private Optional<CodeOwnerConfig> upsertCodeOwnerConfigInSourceBranch( |
| @Nullable IdentifiedUser currentUser, |
| CodeOwnerConfig.Key codeOwnerConfigKey, |
| CodeOwnerConfigUpdate codeOwnerConfigUpdate) { |
| try (Repository repository = repoManager.openRepository(codeOwnerConfigKey.project())) { |
| CodeOwnerConfigFile codeOwnerConfigFile = |
| codeOwnerConfigFileFactory |
| .loadCurrent( |
| getFileName(codeOwnerConfigKey.project()), |
| codeOwnerConfigParser, |
| repository, |
| codeOwnerConfigKey) |
| .setCodeOwnerConfigUpdate(codeOwnerConfigUpdate); |
| |
| try (MetaDataUpdate metaDataUpdate = |
| createMetaDataUpdate(codeOwnerConfigKey.project(), repository, currentUser)) { |
| codeOwnerConfigFile.commit(metaDataUpdate); |
| } |
| |
| return codeOwnerConfigFile.getLoadedCodeOwnerConfig(); |
| } catch (IOException | ConfigInvalidException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e); |
| } |
| } |
| |
| private MetaDataUpdate createMetaDataUpdate( |
| Project.NameKey project, Repository repository, @Nullable IdentifiedUser currentUser) { |
| MetaDataUpdate metaDataUpdate = metaDataUpdateInternalFactory.create(project, repository, null); |
| try { |
| metaDataUpdate.getCommitBuilder().setCommitter(serverIdent); |
| if (currentUser != null) { |
| // Using MetaDataUpdate#setAuthor copies the timezone and timestamp from the committer |
| // identity, so that it's ensured that the author and committer identities have the same |
| // timezone and timestamp. |
| metaDataUpdate.setAuthor(currentUser); |
| } else { |
| // In this case the author identity is the same as the committer identity, hence it already |
| // has the correct timezone and timestamp and we can set it on the commit builder directly. |
| metaDataUpdate.getCommitBuilder().setAuthor(serverIdent); |
| } |
| return metaDataUpdate; |
| } catch (Throwable t) { |
| metaDataUpdate.close(); |
| Throwables.throwIfUnchecked(t); |
| throw new CodeOwnersInternalServerErrorException("Failed to create MetaDataUpdate", t); |
| } |
| } |
| } |