Merge "Use List Code Owners for Path in change REST endpoint in frontend"
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
new file mode 100644
index 0000000..6222b7d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -0,0 +1,209 @@
+// 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.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Class to scan a branch for code owner config files and update them.
+ *
+ * <p>Doesn't parse the code owner config files but provides the raw content to the callback.
+ *
+ * <p>All updates to the code owner config files are done atomically with a single commit.
+ */
+@Singleton
+public class CodeOwnerConfigFileUpdateScanner {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final GitRepositoryManager repoManager;
+ private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+ private final Provider<PersonIdent> serverIdentProvider;
+ private final Provider<IdentifiedUser> identifiedUser;
+
+ @Inject
+ CodeOwnerConfigFileUpdateScanner(
+ GitRepositoryManager repoManager,
+ CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+ @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
+ Provider<IdentifiedUser> identifiedUser) {
+ this.repoManager = repoManager;
+ this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+ this.serverIdentProvider = serverIdentProvider;
+ this.identifiedUser = identifiedUser;
+ }
+
+ /**
+ * Visits and updates all code owner config files in the given project and branch.
+ *
+ * <p>All updates are done in a single commit. If none of the code owner config files is updated,
+ * no new commit is created.
+ *
+ * @param branchNameKey the project and branch for which the code owner config files should be
+ * updated
+ * @param commitMessage commit message for the new commit if an update is performed
+ * @param codeOwnerConfigFileUpdater the callback that is invoked for each code owner config file
+ * @return the commit that renamed the email if any update was performed
+ */
+ public Optional<RevCommit> update(
+ BranchNameKey branchNameKey,
+ String commitMessage,
+ CodeOwnerConfigFileUpdater codeOwnerConfigFileUpdater) {
+ requireNonNull(branchNameKey, "branchNameKey");
+ requireNonNull(commitMessage, "commitMessage");
+ requireNonNull(codeOwnerConfigFileUpdater, "codeOwnerConfigFileUpdater");
+
+ CodeOwnerBackend codeOwnerBackend = codeOwnersPluginConfiguration.getBackend(branchNameKey);
+ logger.atFine().log(
+ "updating code owner files in branch %s of project %s",
+ branchNameKey.branch(), branchNameKey.project());
+
+ try (Repository repository = repoManager.openRepository(branchNameKey.project());
+ RevWalk rw = new RevWalk(repository);
+ ObjectInserter oi = repository.newObjectInserter();
+ CodeOwnerConfigTreeWalk treeWalk =
+ new CodeOwnerConfigTreeWalk(
+ codeOwnerBackend,
+ branchNameKey,
+ repository,
+ rw,
+ /** pathGlob */
+ null)) {
+ RevCommit revision = treeWalk.getRevision();
+ DirCache newTree = DirCache.newInCore();
+ DirCacheEditor editor = newTree.editor();
+
+ boolean dirty = false;
+ while (treeWalk.next()) {
+ Optional<String> updatedContent =
+ codeOwnerConfigFileUpdater.update(treeWalk.getFilePath(), treeWalk.getFileContent());
+ if (updatedContent.isPresent()) {
+ dirty = true;
+
+ // insert blob with new file content
+ ObjectId blobId = oi.insert(Constants.OBJ_BLOB, updatedContent.get().getBytes(UTF_8));
+
+ // append edit command to set the new blob for the code owner config file
+ editor.add(createEditCommand(treeWalk.getPathString(), blobId));
+ }
+ }
+
+ if (!dirty) {
+ return Optional.empty();
+ }
+
+ editor.finish();
+ ObjectId treeId = newTree.writeTree(oi);
+ ObjectId commitId = createCommit(oi, commitMessage, revision, treeId);
+ updateBranch(branchNameKey.branch(), repository, revision, commitId);
+ return Optional.of(rw.parseCommit(commitId));
+ } 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);
+ }
+ }
+
+ /**
+ * Creates an edit command that sets the given blob for the given path
+ *
+ * @param jgitFilePath path of the file for which the blob should be set, as jgit path (not
+ * starting with '/')
+ * @param blobId the ID of the blob that should be set for the file path
+ * @return the edit command
+ */
+ private PathEdit createEditCommand(String jgitFilePath, ObjectId blobId) {
+ return new PathEdit(jgitFilePath) {
+ @Override
+ public void apply(DirCacheEntry entry) {
+ entry.setFileMode(FileMode.REGULAR_FILE);
+ entry.setObjectId(blobId);
+ }
+ };
+ }
+
+ /**
+ * Creates a new commit.
+ *
+ * @param objectInserter object inserter that should be used to insert the new commit
+ * @param commitMessage the commit message that should be used for the new commit
+ * @param parentCommit the commit that should be set as parent commit of the new commit
+ * @param treeId the tree of the new commit
+ * @return the commit ID
+ */
+ private ObjectId createCommit(
+ ObjectInserter objectInserter, String commitMessage, ObjectId parentCommit, ObjectId treeId)
+ throws IOException {
+ PersonIdent serverIdent = serverIdentProvider.get();
+ CommitBuilder cb = new CommitBuilder();
+ cb.setParentId(parentCommit);
+ cb.setTreeId(treeId);
+ cb.setCommitter(serverIdent);
+ cb.setAuthor(
+ identifiedUser.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()));
+ cb.setMessage(commitMessage);
+ ObjectId id = objectInserter.insert(cb);
+ objectInserter.flush();
+ return id;
+ }
+
+ /**
+ * Update the given branch.
+ *
+ * @param branchName the name of the branch that should be updated
+ * @param repository the repository in which the branch should be updated
+ * @param oldObjectId the expected old object ID of the branch
+ * @param newObjectId the new object ID that should be set for the branch
+ */
+ private void updateBranch(
+ String branchName, Repository repository, ObjectId oldObjectId, ObjectId newObjectId)
+ throws IOException {
+ RefUpdate ru = repository.updateRef(branchName);
+ ru.setExpectedOldObjectId(oldObjectId);
+ ru.setNewObjectId(newObjectId);
+ ru.update();
+ RefUpdateUtil.checkResult(ru);
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdater.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdater.java
new file mode 100644
index 0000000..1c38737
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdater.java
@@ -0,0 +1,32 @@
+// 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 java.nio.file.Path;
+import java.util.Optional;
+
+/** Callback interface to update a code owner config file. */
+public interface CodeOwnerConfigFileUpdater {
+ /**
+ * Callback for a code owner config file.
+ *
+ * @param codeOwnerConfigFilePath absolute path of the code owner config file
+ * @param codeOwnerConfigFileContent the content of the code owner config, can be also the content
+ * of a non-parseable code owner config
+ * @return the updated content of the code owner config file, {@link Optional#empty()} if no
+ * update should be performed
+ */
+ Optional<String> update(Path codeOwnerConfigFilePath, String codeOwnerConfigFileContent);
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index 06bd4ef..f1e8a3b 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -14,33 +14,23 @@
package com.google.gerrit.plugins.codeowners.backend;
-import static com.google.common.base.Preconditions.checkState;
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.Project;
import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.nio.file.FileSystems;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.TreeFilter;
/** Class to scan a branch for code owner config files. */
@Singleton
@@ -120,33 +110,13 @@
try (Repository repository = repoManager.openRepository(branchNameKey.project());
RevWalk rw = new RevWalk(repository);
- TreeWalk treeWalk = new TreeWalk(repository)) {
- Ref ref = repository.exactRef(branchNameKey.branch());
- checkState(
- ref != null,
- "branch %s of project %s not found",
- branchNameKey.branch(),
- branchNameKey.project());
-
- RevCommit revision = rw.parseCommit(ref.getObjectId());
- treeWalk.addTree(revision.getTree());
- treeWalk.setRecursive(true);
- treeWalk.setFilter(
- createCodeOwnerConfigFilter(codeOwnerBackend, branchNameKey.project(), pathGlob));
-
+ CodeOwnerConfigTreeWalk treeWalk =
+ new CodeOwnerConfigTreeWalk(
+ codeOwnerBackend, branchNameKey, repository, rw, pathGlob)) {
while (treeWalk.next()) {
- Path filePath = Paths.get(treeWalk.getPathString());
- Path folderPath =
- filePath.getParent() != null
- ? JgitPath.of(filePath.getParent()).getAsAbsolutePath()
- : Paths.get("/");
- String fileName = filePath.getFileName().toString();
- CodeOwnerConfig.Key codeOwnerConfigKey =
- CodeOwnerConfig.Key.create(branchNameKey, folderPath, fileName);
- Optional<CodeOwnerConfig> codeOwnerConfig;
-
+ CodeOwnerConfig codeOwnerConfig;
try {
- codeOwnerConfig = codeOwnerBackend.getCodeOwnerConfig(codeOwnerConfigKey, revision);
+ codeOwnerConfig = treeWalk.getCodeOwnerConfig();
} catch (StorageException storageException) {
Optional<ConfigInvalidException> configInvalidException =
getInvalidConfigCause(storageException);
@@ -157,13 +127,11 @@
// The code owner config is invalid and cannot be parsed.
invalidCodeOwnerConfigCallback.onInvalidCodeOwnerConfig(
- folderPath.resolve(fileName), configInvalidException.get());
+ treeWalk.getFilePath(), configInvalidException.get());
continue;
}
- checkState(codeOwnerConfig.isPresent(), "code owner config %s not found", codeOwnerConfig);
- boolean visitFurtherCodeOwnerConfigFiles =
- codeOwnerConfigVisitor.visit(codeOwnerConfig.get());
+ boolean visitFurtherCodeOwnerConfigFiles = codeOwnerConfigVisitor.visit(codeOwnerConfig);
if (!visitFurtherCodeOwnerConfigFiles) {
break;
}
@@ -178,47 +146,6 @@
}
/**
- * Creates a {@link TreeFilter} that matches code owner config files in the given project.
- *
- * @param codeOwnerBackend the code owner backend that is being used
- * @param project the name of the project in which code owner config files should be matched
- * @param pathGlob optional Java NIO glob that the paths of code owner config files must match
- * @return the created {@link TreeFilter}
- */
- private static TreeFilter createCodeOwnerConfigFilter(
- CodeOwnerBackend codeOwnerBackend, Project.NameKey project, @Nullable String pathGlob) {
- return new TreeFilter() {
- @Override
- public boolean shouldBeRecursive() {
- return true;
- }
-
- @Override
- public boolean include(TreeWalk walker) throws IOException {
- if (walker.isSubtree()) {
- walker.enterSubtree();
- return false;
- }
- if (pathGlob != null
- && !FileSystems.getDefault()
- .getPathMatcher("glob:" + pathGlob)
- .matches(JgitPath.of(walker.getPathString()).getAsAbsolutePath())) {
- logger.atFine().log(
- "%s filtered out because it doesn't match the path glob", walker.getPathString());
- return false;
- }
- String fileName = Paths.get(walker.getPathString()).getFileName().toString();
- return codeOwnerBackend.isCodeOwnerConfigFile(project, fileName);
- }
-
- @Override
- public TreeFilter clone() {
- return this;
- }
- };
- }
-
- /**
* Returns an {@link InvalidCodeOwnerConfigCallback} instance that ignores invalid code owner
* config files.
*/
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java
new file mode 100644
index 0000000..1ba8f74
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigTreeWalk.java
@@ -0,0 +1,175 @@
+// 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.common.base.Preconditions.checkState;
+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.Project;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/** {@link TreeWalk} that filters for code owner config files in the tree. */
+public class CodeOwnerConfigTreeWalk extends TreeWalk {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CodeOwnerBackend codeOwnerBackend;
+ private final BranchNameKey branchNameKey;
+ private final RevCommit revision;
+
+ public CodeOwnerConfigTreeWalk(
+ CodeOwnerBackend codeOwnerBackend,
+ BranchNameKey branchNameKey,
+ Repository repository,
+ RevWalk revWalk,
+ @Nullable String pathGlob)
+ throws IOException {
+ super(repository);
+
+ this.codeOwnerBackend = requireNonNull(codeOwnerBackend, "codeOwnerBackend");
+ this.branchNameKey = requireNonNull(branchNameKey, "branchNameKey");
+ this.revision =
+ getRevision(
+ branchNameKey,
+ requireNonNull(repository, "repository"),
+ requireNonNull(revWalk, "revWalk"));
+
+ addTree(revision.getTree());
+ setRecursive(true);
+ setFilter(createCodeOwnerConfigFilter(codeOwnerBackend, branchNameKey.project(), pathGlob));
+ }
+
+ /**
+ * Returns the revision from which the tree was loaded.
+ *
+ * @return the revision ID
+ */
+ public RevCommit getRevision() {
+ return revision;
+ }
+
+ /** Returns the absolute file path of the current entry. */
+ public Path getFilePath() {
+ return JgitPath.of(getPathString()).getAsAbsolutePath();
+ }
+
+ /** Returns the file content of the current entry. */
+ public String getFileContent() throws IOException {
+ ObjectLoader obj = getObjectReader().open(getObjectId(0), Constants.OBJ_BLOB);
+ byte[] raw = obj.getCachedBytes(Integer.MAX_VALUE);
+ return raw.length != 0 ? RawParseUtils.decode(raw) : "";
+ }
+
+ /** Returns the code owner config key of the current entry. */
+ public CodeOwnerConfig.Key getCodeOwnerConfigKey() {
+ Path filePath = getFilePath();
+ Path folderPath =
+ filePath.getParent() != null
+ ? JgitPath.of(filePath.getParent()).getAsAbsolutePath()
+ : Paths.get("/");
+ String fileName = Paths.get(getPathString()).getFileName().toString();
+ return CodeOwnerConfig.Key.create(branchNameKey, folderPath, fileName);
+ }
+
+ /**
+ * Loads the code owner config file at the current entry's path.
+ *
+ * @return the loaded code owner config
+ */
+ public CodeOwnerConfig getCodeOwnerConfig() {
+ CodeOwnerConfig.Key codeOwnerConfigKey = getCodeOwnerConfigKey();
+ return codeOwnerBackend
+ .getCodeOwnerConfig(codeOwnerConfigKey, revision)
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format("code owner config %s not found", codeOwnerConfigKey)));
+ }
+
+ /**
+ * Looks up the current revision of the branch.
+ *
+ * @param branchNameKey the project and branch for which the current revision should be loaded
+ * @param repository the repository from which the branch revision should be loaded
+ * @return the current revision of the branch
+ */
+ private static RevCommit getRevision(
+ BranchNameKey branchNameKey, Repository repository, RevWalk revWalk) throws IOException {
+ Ref ref = repository.exactRef(branchNameKey.branch());
+ checkState(
+ ref != null,
+ "branch %s of project %s not found",
+ branchNameKey.branch(),
+ branchNameKey.project());
+
+ return revWalk.parseCommit(ref.getObjectId());
+ }
+
+ /**
+ * Creates a {@link TreeFilter} that matches code owner config files in the given project.
+ *
+ * @param codeOwnerBackend the code owner backend that is being used
+ * @param project the name of the project in which code owner config files should be matched
+ * @param pathGlob optional Java NIO glob that the paths of code owner config files must match
+ * @return the created {@link TreeFilter}
+ */
+ private static TreeFilter createCodeOwnerConfigFilter(
+ CodeOwnerBackend codeOwnerBackend, Project.NameKey project, @Nullable String pathGlob) {
+ return new TreeFilter() {
+ @Override
+ public boolean shouldBeRecursive() {
+ return true;
+ }
+
+ @Override
+ public boolean include(TreeWalk walker) throws IOException {
+ if (walker.isSubtree()) {
+ walker.enterSubtree();
+ return false;
+ }
+ if (pathGlob != null
+ && !FileSystems.getDefault()
+ .getPathMatcher("glob:" + pathGlob)
+ .matches(JgitPath.of(walker.getPathString()).getAsAbsolutePath())) {
+ logger.atFine().log(
+ "%s filtered out because it doesn't match the path glob", walker.getPathString());
+ return false;
+ }
+ String fileName = Paths.get(walker.getPathString()).getFileName().toString();
+ return codeOwnerBackend.isCodeOwnerConfigFile(project, fileName);
+ }
+
+ @Override
+ public TreeFilter clone() {
+ return this;
+ }
+ };
+ }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
index e18ddb3..07788ca 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -154,7 +154,7 @@
problemsByPath.put(
codeOwnerConfigFilePath.toString(),
new ConsistencyProblemInfo(
- ConsistencyProblemInfo.Status.ERROR, configInvalidException.getMessage()));
+ ConsistencyProblemInfo.Status.FATAL, configInvalidException.getMessage()));
},
pathGlob);
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 7fbda44..85cfca4 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -517,11 +517,11 @@
}
/**
- * Returns the {@link com.google.gerrit.server.git.validators.ValidationMessage.Type} (ERROR or
+ * Returns the {@link com.google.gerrit.server.git.validators.ValidationMessage.Type} (FATAL or
* WARNING) that should be used for a parsing error of the code owner config file (specified as
* {@code changedFile}).
*
- * <p>If {@link com.google.gerrit.server.git.validators.ValidationMessage.Type#ERROR} is returned
+ * <p>If {@link com.google.gerrit.server.git.validators.ValidationMessage.Type#FATAL} is returned
* the upload will be blocked, if {@link
* com.google.gerrit.server.git.validators.ValidationMessage.Type#WARNING} is returned the upload
* can succeed and the parsing error will only be shown as warning.
@@ -531,7 +531,7 @@
* it is not making anything worse. Hence in this case the parsing error should be returned as
* {@link com.google.gerrit.server.git.validators.ValidationMessage.Type#WARNING}, whereas a new
* parsing error should be returned as {@link
- * com.google.gerrit.server.git.validators.ValidationMessage.Type#ERROR}.
+ * com.google.gerrit.server.git.validators.ValidationMessage.Type#FATAL}.
*
* @param codeOwnerBackend the code owner backend from which the code owner config can be loaded
* @param branchNameKey the project and branch of the code owner config
@@ -567,8 +567,8 @@
codeOwnerBackend.getCodeOwnerConfig(baseCodeOwnerConfigKey, revWalk, parentRevision);
// The code owner config at the parent revision is parseable. This means the parsing error
// is introduced by the new commit and we should block uploading it, which we achieve by
- // setting the validation message type to error.
- return ValidationMessage.Type.ERROR;
+ // setting the validation message type to fatal.
+ return ValidationMessage.Type.FATAL;
} catch (StorageException storageException) {
// Loading the base code owner config has failed.
if (getInvalidConfigCause(storageException).isPresent()) {
@@ -584,8 +584,8 @@
// The code owner config is newly created. Hence the parsing error comes from the commit
// that is being pushed and we want to block it from uploading. To do this we set the
- // validation message type to error.
- return ValidationMessage.Type.ERROR;
+ // validation message type to fatal.
+ return ValidationMessage.Type.FATAL;
}
/**
@@ -608,12 +608,13 @@
/**
* Returns a copy of the given commit validation message with type warning if the type the given
- * commit validation message is error. Otherwise it returns the given commit validation message
- * unchanged.
+ * commit validation message is fatal or error. Otherwise it returns the given commit validation
+ * message unchanged.
*/
private static CommitValidationMessage downgradeErrorToWarning(
CommitValidationMessage commitValidationMessage) {
- if (CommitValidationMessage.Type.ERROR.equals(commitValidationMessage.getType())) {
+ if (CommitValidationMessage.Type.FATAL.equals(commitValidationMessage.getType())
+ || CommitValidationMessage.Type.ERROR.equals(commitValidationMessage.getType())) {
return new CommitValidationMessage(
commitValidationMessage.getMessage(), ValidationMessage.Type.WARNING);
}
@@ -977,7 +978,8 @@
return validationMessages().stream()
.anyMatch(
validationMessage ->
- ValidationMessage.Type.ERROR.equals(validationMessage.getType()));
+ ValidationMessage.Type.FATAL.equals(validationMessage.getType())
+ || ValidationMessage.Type.ERROR.equals(validationMessage.getType()));
}
private ImmutableList<CommitValidationMessage> validationMessagesWithIncludedSummaryMessage() {
@@ -995,6 +997,7 @@
* <p>The following validation message type will be returned:
*
* <ul>
+ * <li>FATAL: if any of the validation message has type fatal
* <li>ERROR: if any of the validation message has type error
* <li>WARNING: if any of the validation message has type warning and none has type error
* <li>HINT: otherwise
@@ -1005,6 +1008,10 @@
if (!validationMessages().isEmpty()) {
for (CommitValidationMessage validationMessage : validationMessages()) {
+ if (ValidationMessage.Type.FATAL.equals(validationMessage.getType())) {
+ validationMessageType = ValidationMessage.Type.FATAL;
+ break;
+ }
if (ValidationMessage.Type.ERROR.equals(validationMessage.getType())) {
validationMessageType = ValidationMessage.Type.ERROR;
break;
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
index c1d7f0b..2c7b15d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
@@ -159,7 +159,7 @@
ImmutableMap.of(
codeOwnerConfigPath,
ImmutableList.of(
- error(
+ fatal(
String.format(
"invalid code owner config file '%s':\n %s",
codeOwnerConfigPath,
@@ -475,6 +475,10 @@
pathOfInvalidConfig3)))));
}
+ private ConsistencyProblemInfo fatal(String message) {
+ return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.FATAL, message);
+ }
+
private ConsistencyProblemInfo error(String message) {
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesInRevisionIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesInRevisionIT.java
index 9f4d64d..57b9506 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesInRevisionIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesInRevisionIT.java
@@ -126,7 +126,7 @@
.containsExactly(
codeOwnerConfigPath,
ImmutableList.of(
- error(
+ fatal(
String.format(
"invalid code owner config file '%s':\n %s",
codeOwnerConfigPath,
@@ -398,6 +398,10 @@
unknownEmail3, codeOwnerConfigPath3))));
}
+ private ConsistencyProblemInfo fatal(String message) {
+ return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.FATAL, message);
+ }
+
private ConsistencyProblemInfo error(String message) {
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
index 357c5d8..bc42987 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnerConfigValidatorIT.java
@@ -319,7 +319,7 @@
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
- assertOkWithErrors(
+ assertOkWithFatals(
r,
"invalid code owner config files",
String.format(
@@ -552,7 +552,7 @@
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
- assertErrorWithMessages(
+ assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
@@ -582,7 +582,7 @@
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getJGitFilePath(),
"ALSO-INVALID"));
PushOneCommit.Result r = push.to("refs/for/master");
- assertErrorWithMessages(
+ assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
@@ -1481,6 +1481,7 @@
private static void assertOkWithoutMessages(PushOneCommit.Result pushResult) {
pushResult.assertOkStatus();
+ pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
@@ -1493,10 +1494,23 @@
pushResult.assertMessage(
String.format("hint: commit %s: %s", abbreviateName(pushResult.getCommit()), hint));
}
+ pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
}
+ private void assertOkWithFatals(PushOneCommit.Result pushResult, String... errors)
+ throws Exception {
+ pushResult.assertOkStatus();
+ for (String error : errors) {
+ pushResult.assertMessage(
+ String.format("fatal: commit %s: %s", abbreviateName(pushResult.getCommit()), error));
+ }
+ pushResult.assertNotMessage("error");
+ pushResult.assertNotMessage("warning");
+ pushResult.assertNotMessage("hint");
+ }
+
private void assertOkWithErrors(PushOneCommit.Result pushResult, String... errors)
throws Exception {
pushResult.assertOkStatus();
@@ -1504,6 +1518,7 @@
pushResult.assertMessage(
String.format("error: commit %s: %s", abbreviateName(pushResult.getCommit()), error));
}
+ pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
@@ -1515,6 +1530,7 @@
pushResult.assertMessage(
String.format("warning: commit %s: %s", abbreviateName(pushResult.getCommit()), warning));
}
+ pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("hint");
}
@@ -1526,6 +1542,19 @@
for (String error : errors) {
pushResult.assertMessage(String.format("error: commit %s: %s", abbreviatedCommit, error));
}
+ pushResult.assertNotMessage("fatal");
+ pushResult.assertNotMessage("warning");
+ pushResult.assertNotMessage("hint");
+ }
+
+ private void assertFatalWithMessages(
+ PushOneCommit.Result pushResult, String summaryMessage, String... errors) throws Exception {
+ String abbreviatedCommit = abbreviateName(pushResult.getCommit());
+ pushResult.assertErrorStatus(String.format("commit %s: %s", abbreviatedCommit, summaryMessage));
+ for (String error : errors) {
+ pushResult.assertMessage(String.format("fatal: commit %s: %s", abbreviatedCommit, error));
+ }
+ pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java
new file mode 100644
index 0000000..b00b8b1
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScannerTest.java
@@ -0,0 +1,287 @@
+// 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.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations;
+import com.google.inject.Inject;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+/** Tests for {@link CodeOwnerConfigFileUpdateScanner}. */
+public class CodeOwnerConfigFileUpdateScannerTest extends AbstractCodeOwnersTest {
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+ @Mock private CodeOwnerConfigFileUpdater updater;
+
+ @Inject private ProjectOperations projectOperations;
+
+ private CodeOwnerConfigOperations codeOwnerConfigOperations;
+ private CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner;
+
+ @Before
+ public void setUpCodeOwnersPlugin() throws Exception {
+ codeOwnerConfigOperations =
+ plugin.getSysInjector().getInstance(CodeOwnerConfigOperations.class);
+ codeOwnerConfigFileUpdateScanner =
+ plugin.getSysInjector().getInstance(CodeOwnerConfigFileUpdateScanner.class);
+ }
+
+ @Test
+ public void cannotUpdateCodeOwnerConfigsForNullBranch() throws Exception {
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ codeOwnerConfigFileUpdateScanner.update(
+ null,
+ "Update code owner configs",
+ (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> Optional.empty()));
+ assertThat(npe).hasMessageThat().isEqualTo("branchNameKey");
+ }
+
+ @Test
+ public void cannotUpdateCodeOwnerConfigsWithNullCommitMessage() throws Exception {
+ BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ codeOwnerConfigFileUpdateScanner.update(
+ branchNameKey,
+ null,
+ (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> Optional.empty()));
+ assertThat(npe).hasMessageThat().isEqualTo("commitMessage");
+ }
+
+ @Test
+ public void cannotUpdateCodeOwnerConfigsWithNullUpdater() throws Exception {
+ BranchNameKey branchNameKey = BranchNameKey.create(project, "master");
+ NullPointerException npe =
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ codeOwnerConfigFileUpdateScanner.update(
+ branchNameKey, "Update code owner configs", null));
+ assertThat(npe).hasMessageThat().isEqualTo("codeOwnerConfigFileUpdater");
+ }
+
+ @Test
+ public void cannotUpdateCodeOwnerConfigsForNonExistingBranch() throws Exception {
+ BranchNameKey branchNameKey = BranchNameKey.create(project, "non-existing");
+ IllegalStateException exception =
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ codeOwnerConfigFileUpdateScanner.update(
+ branchNameKey,
+ "Update code owner configs",
+ (codeOwnerConfigFilePath, codeOwnerConfigFileContent) -> Optional.empty()));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "branch %s of project %s not found", branchNameKey.branch(), project.get()));
+ }
+
+ @Test
+ public void noUpdateIfNoCodeOwnerConfigFilesExists() throws Exception {
+ Optional<RevCommit> commit =
+ codeOwnerConfigFileUpdateScanner.update(
+ BranchNameKey.create(project, "master"), "Update code owner configs", updater);
+ assertThat(commit).isEmpty();
+ verifyZeroInteractions(updater);
+ }
+
+ @Test
+ public void noUpdateForNonCodeOwnerConfigFiles() throws Exception {
+ // Create some non code owner config files.
+ try (TestRepository<Repository> testRepo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ Ref ref = testRepo.getRepository().exactRef("refs/heads/master");
+ RevCommit head = testRepo.getRevWalk().parseCommit(ref.getObjectId());
+ testRepo.update(
+ "refs/heads/master",
+ testRepo
+ .commit()
+ .parent(head)
+ .message("Add some non code owner config files")
+ .add("owners.txt", "some content")
+ .add("owners", "some content")
+ .add("foo/bar/owners.txt", "some content")
+ .add("foo/bar/owners", "some content"));
+ }
+
+ Optional<RevCommit> commit =
+ codeOwnerConfigFileUpdateScanner.update(
+ BranchNameKey.create(project, "master"), "Update code owner configs", updater);
+ assertThat(commit).isEmpty();
+ verifyZeroInteractions(updater);
+ }
+
+ @Test
+ public void noUpdateIfCallbackDoesntReturnNewFileContent() throws Exception {
+ CodeOwnerConfig.Key codeOwnerConfigKey =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .fileName("OWNERS")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+ Path path =
+ Paths.get(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath());
+ String content = codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent();
+
+ RevCommit oldHead = projectOperations.project(project).getHead("master");
+
+ when(updater.update(path, content)).thenReturn(Optional.empty());
+ Optional<RevCommit> commit =
+ codeOwnerConfigFileUpdateScanner.update(
+ BranchNameKey.create(project, "master"), "Update code owner configs", updater);
+ assertThat(commit).isEmpty();
+
+ // Verify the code owner config file was not updated.
+ assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent())
+ .isEqualTo(content);
+
+ // Check that no commit was created.
+ RevCommit newHead = projectOperations.project(project).getHead("master");
+ assertThat(newHead).isEqualTo(oldHead);
+ }
+
+ @Test
+ public void updateCodeOwnerConfigFiles() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+
+ CodeOwnerConfig.Key codeOwnerConfigKey1 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/")
+ .fileName("OWNERS")
+ .addCodeOwnerEmail(admin.email())
+ .create();
+ Path path1 =
+ Paths.get(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath());
+ String oldContent1 =
+ codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getContent();
+ String newContent1 = user.email() + "\n";
+ when(updater.update(path1, oldContent1)).thenReturn(Optional.of(newContent1));
+
+ CodeOwnerConfig.Key codeOwnerConfigKey2 =
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .fileName("OWNERS")
+ .addCodeOwnerEmail(user.email())
+ .create();
+ Path path2 =
+ Paths.get(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath());
+ String oldContent2 =
+ codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getContent();
+ String newContent2 = user2.email() + "\n";
+ when(updater.update(path2, oldContent2)).thenReturn(Optional.of(newContent2));
+
+ RevCommit oldHead = projectOperations.project(project).getHead("master");
+
+ String commitMessage = "Update code owner configs";
+ Optional<RevCommit> commit =
+ codeOwnerConfigFileUpdateScanner.update(
+ BranchNameKey.create(project, "master"), commitMessage, updater);
+ assertThat(commit).isPresent();
+
+ // Verify that we received the expected callbacks for the invalid code onwer config.
+ Mockito.verify(updater).update(path1, oldContent1);
+ Mockito.verify(updater).update(path2, oldContent2);
+ verifyNoMoreInteractions(updater);
+
+ // Verify the code owner config files were updated.
+ assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getContent())
+ .isEqualTo(newContent1);
+ assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getContent())
+ .isEqualTo(newContent2);
+
+ // Check that exactly 1 commit was created.
+ RevCommit newHead = projectOperations.project(project).getHead("master");
+ assertThat(commit.get()).isEqualTo(newHead);
+ assertThat(newHead).isNotEqualTo(oldHead);
+ assertThat(newHead.getShortMessage()).isEqualTo(commitMessage);
+ assertThat(newHead.getParent(0)).isEqualTo(oldHead);
+ }
+
+ @Test
+ public void updateInvalidCodeOwnerConfigFile() throws Exception {
+ CodeOwnerConfig.Key codeOwnerConfigKey = createInvalidCodeOwnerConfig("/OWNERS", "INVALID");
+
+ when(updater.update(any(Path.class), any(String.class)))
+ .thenReturn(Optional.of("STILL INVALID"));
+ Optional<RevCommit> update =
+ codeOwnerConfigFileUpdateScanner.update(
+ BranchNameKey.create(project, "master"), "Update code owner configs", updater);
+ assertThat(update).isPresent();
+
+ // Verify that we received the expected callbacks for the invalid code onwer config.
+ Mockito.verify(updater).update(Paths.get("/OWNERS"), "INVALID");
+ verifyNoMoreInteractions(updater);
+
+ // Verify the code owner config file was updated.
+ assertThat(codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getContent())
+ .isEqualTo("STILL INVALID");
+ }
+
+ private CodeOwnerConfig.Key createInvalidCodeOwnerConfig(String filePath, String content)
+ throws Exception {
+ disableCodeOwnersForProject(project);
+ String changeId =
+ createChange("Add invalid code owners file", JgitPath.of(filePath).get(), content)
+ .getChangeId();
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+ enableCodeOwnersForProject(project);
+ Path path = Paths.get(filePath);
+ return CodeOwnerConfig.Key.create(
+ project, "master", path.getParent().toString(), path.getFileName().toString());
+ }
+}