Merge branch 'stable-3.9'
* stable-3.9:
Add support for newer plugin-node-resolve versions
Change-Id: I203162ccb9013b401ea9ab6b8086e266a52725b5
diff --git a/.gitignore b/.gitignore
index 81322bc..8967117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,7 @@
/bazel-*
/eclipse-out
/node_modules
+
+# IntelliJ files
+.idea
+*.iml
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java
index ce41836..f5ad7e6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java
@@ -30,7 +30,6 @@
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
@@ -104,7 +103,7 @@
private Path getArchiveFolderFromConfig(String configValue) {
try {
- return Files.createDirectories(Paths.get(configValue));
+ return Files.createDirectories(Path.of(configValue));
} catch (Exception e) {
log.atWarning().log(
"Failed to create folder %s: %s; using default path: %s",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HideProject.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HideProject.java
index 4716c5b..ff5bb88 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HideProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HideProject.java
@@ -83,7 +83,9 @@
private void createProjectIfMissing(String projectName) throws IOException, RestApiException {
if (!projectCache.get(Project.nameKey(projectName)).isPresent()) {
try {
- createProject.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(projectName), null);
+ @SuppressWarnings("unused")
+ var unused =
+ createProject.apply(TopLevelResource.INSTANCE, IdString.fromDecoded(projectName), null);
} catch (RestApiException | ConfigInvalidException | PermissionBackendException e) {
throw new ResourceConflictException(
String.format("Failed to create project %s", projectName), e);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/SshModule.java
index 3d8818d..1ca3d2a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/SshModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/SshModule.java
@@ -14,9 +14,16 @@
package com.googlesource.gerrit.plugins.deleteproject;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.sshd.PluginCommandModule;
+import com.google.inject.Inject;
public class SshModule extends PluginCommandModule {
+ @Inject
+ SshModule(@PluginName String pluginName) {
+ super(pluginName);
+ }
+
@Override
protected void configureCommands() {
command(DeleteCommand.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
index 9ee3cbb..2f3d047 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
@@ -21,7 +21,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.StarredChangesUtil;
+import com.google.gerrit.server.StarredChangesWriter;
import com.google.gerrit.server.UserInitiated;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
@@ -41,7 +41,7 @@
public class DatabaseDeleteHandler {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
- private final StarredChangesUtil starredChangesUtil;
+ private final StarredChangesWriter starredChangesWriter;
private final ChangeIndexer indexer;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -50,13 +50,13 @@
@Inject
public DatabaseDeleteHandler(
- StarredChangesUtil starredChangesUtil,
+ StarredChangesWriter starredChangesWriter,
ChangeIndexer indexer,
ChangeNotes.Factory schemaFactoryNoteDb,
GitRepositoryManager repoManager,
Provider<InternalAccountQuery> accountQueryProvider,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
- this.starredChangesUtil = starredChangesUtil;
+ this.starredChangesWriter = starredChangesWriter;
this.indexer = indexer;
this.accountQueryProvider = accountQueryProvider;
this.accountsUpdateProvider = accountsUpdateProvider;
@@ -85,7 +85,7 @@
for (Change.Id id : changeIds) {
try {
- starredChangesUtil.unstarAllForChangeDeletion(id);
+ starredChangesWriter.unstarAllForChangeDeletion(id);
} catch (NoSuchChangeException | IOException e) {
// we can ignore the exception during delete
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandler.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandler.java
index bbfa633..80d4eea 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandler.java
@@ -14,158 +14,37 @@
package com.googlesource.gerrit.plugins.deleteproject.fs;
-import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
-
-import com.google.common.flogger.FluentLogger;
-import com.google.common.io.MoreFiles;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.deleteproject.Configuration;
-import com.googlesource.gerrit.plugins.deleteproject.TimeMachine;
-import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import org.apache.commons.io.FileUtils;
+import java.util.Optional;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache;
public class FilesystemDeleteHandler {
- private static final FluentLogger log = FluentLogger.forEnclosingClass();
- private static final DateTimeFormatter FORMAT =
- DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("UTC"));
-
- private final GitRepositoryManager repoManager;
+ private final RepositoryDelete repositoryDelete;
private final DynamicSet<ProjectDeletedListener> deletedListeners;
private final Configuration config;
@Inject
public FilesystemDeleteHandler(
- GitRepositoryManager repoManager,
+ RepositoryDelete repositoryDelete,
DynamicSet<ProjectDeletedListener> deletedListeners,
Configuration config) {
- this.repoManager = repoManager;
+ this.repositoryDelete = repositoryDelete;
this.deletedListeners = deletedListeners;
this.config = config;
}
public void delete(Project.NameKey project, boolean preserveGitRepository)
throws IOException, RepositoryNotFoundException {
- // Remove from the jgit cache
- Repository repository = repoManager.openRepository(project);
- cleanCache(repository);
- if (!preserveGitRepository) {
- Path repoPath = repository.getDirectory().toPath();
- String projectName = project.get();
- if (config.shouldArchiveDeletedRepos()) {
- archiveGitRepository(projectName, repoPath);
- } else {
- deleteGitRepository(projectName, repoPath);
- }
- }
- }
-
- private void cleanCache(Repository repository) {
- repository.close();
- RepositoryCache.close(repository);
- }
-
- private void archiveGitRepository(String projectName, Path repoPath) throws IOException {
- Path basePath = getBasePath(repoPath, projectName);
- Path renamedProjectDir = renameRepository(repoPath, basePath, projectName, "archived");
- try {
- Path archive = getArchivePath(renamedProjectDir, basePath);
- FileUtils.copyDirectory(renamedProjectDir.toFile(), archive.toFile());
- MoreFiles.deleteRecursively(renamedProjectDir, ALLOW_INSECURE);
- } catch (IOException e) {
- log.atWarning().withCause(e).log("Error trying to archive %s", renamedProjectDir);
- } finally {
- sendProjectDeletedEvent(projectName);
- }
- }
-
- private Path getArchivePath(Path renamedProjectDir, Path basePath) {
- Path configArchiveRepo = config.getArchiveFolder().toAbsolutePath();
- Path relativePath = basePath.relativize(renamedProjectDir);
- return configArchiveRepo.resolve(relativePath);
- }
-
- private void deleteGitRepository(String projectName, Path repoPath) throws IOException {
- // Delete the repository from disk
- Path basePath = getBasePath(repoPath, projectName);
- Path trash = renameRepository(repoPath, basePath, projectName, "deleted");
- try {
- MoreFiles.deleteRecursively(trash, ALLOW_INSECURE);
- recursivelyDeleteEmptyParents(repoPath.toFile().getParentFile(), basePath.toFile());
- } catch (IOException e) {
- // Only log if delete failed - repo already moved to trash.
- log.atWarning().withCause(e).log("Error trying to delete %s or its parents", trash);
- } finally {
- sendProjectDeletedEvent(projectName);
- }
- }
-
- private Path getBasePath(Path repo, String projectName) {
- Path projectPath = Paths.get(projectName);
- return repo.getRoot()
- .resolve(repo.subpath(0, repo.getNameCount() - projectPath.getNameCount()));
- }
-
- private Path renameRepository(Path directory, Path basePath, String projectName, String option)
- throws IOException {
- Path newRepo =
- basePath.resolve(
- projectName + "." + FORMAT.format(TimeMachine.now()) + ".%" + option + "%.git");
- return Files.move(directory, newRepo, StandardCopyOption.ATOMIC_MOVE);
- }
-
- /**
- * Recursively delete the specified file and its parent files until we hit the file {@code Until}
- * or the parent file is populated. This is used when we have a tree structure such as a/b/c/d.git
- * and a/b/e.git - if we delete a/b/c/d.git, we no longer need a/b/c/.
- */
- private void recursivelyDeleteEmptyParents(File file, File until) throws IOException {
- if (file.equals(until)) {
- return;
- }
- if (file.listFiles().length == 0) {
- File parent = file.getParentFile();
- Files.delete(file.toPath());
- recursivelyDeleteEmptyParents(parent, until);
- }
- }
-
- private void sendProjectDeletedEvent(String projectName) {
- if (!deletedListeners.iterator().hasNext()) {
- return;
- }
- ProjectDeletedListener.Event event =
- new ProjectDeletedListener.Event() {
- @Override
- public String getProjectName() {
- return projectName;
- }
-
- @Override
- public NotifyHandling getNotify() {
- return NotifyHandling.NONE;
- }
- };
- for (ProjectDeletedListener l : deletedListeners) {
- try {
- l.onProjectDeleted(event);
- } catch (RuntimeException e) {
- log.atWarning().withCause(e).log("Failure in ProjectDeletedListener");
- }
- }
+ repositoryDelete.execute(
+ project,
+ preserveGitRepository,
+ config.shouldArchiveDeletedRepos(),
+ Optional.ofNullable(config.getArchiveFolder()),
+ deletedListeners);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDelete.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDelete.java
new file mode 100644
index 0000000..a871817
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDelete.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2023 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.deleteproject.fs;
+
+import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.common.io.MoreFiles;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.googlesource.gerrit.plugins.deleteproject.TimeMachine;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+
+/**
+ * This class contains methods that remove a git repository from the filesystem and the jgit cache,
+ * and optionally notify downstream listeners. It can therefore be reused by other plugins who need
+ * to delete a git repository.
+ */
+public class RepositoryDelete {
+
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ public RepositoryDelete(GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
+ }
+
+ private static final FluentLogger log = FluentLogger.forEnclosingClass();
+ private static final DateTimeFormatter FORMAT =
+ DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("UTC"));
+
+ /**
+ * Removes a git repository from the filesystem and the jgit cache and optionally notifies
+ * downstream listeners. You can choose if the git repo should either be deleted or archived.
+ *
+ * <p>In order to delete the git directory, the logic will first rename the directory, a two-step
+ * process involving moving all the files in a different directory, and immediately deleting that
+ * directory. This helps release any open file handlers, which would on NFS filesystems prevent
+ * the directory from being empty (and therefore deletion would fail). For more details see <a
+ * href="https://bugs.chromium.org/p/gerrit/issues/detail?id=16730">...</a>
+ *
+ * @param project - the git repo name that is eligible for deletion
+ * @param preserveGitRepository - if true, just remove the repo from the git cache, but keep the
+ * repo on disk.
+ * @param archiveDeletedRepos - if true, copy the repo to an archived path, and delete the
+ * original directory.
+ * @param archivedFolder - only used when `archiveDeletedRepos` is true, provides the archived
+ * directory.
+ * @param deletedListeners - a set of `ProjectDeletedListener`s - when provided these listeners
+ * will be notified when a directory is deleted. This is not used for archiving.
+ * @throws RepositoryNotFoundException - if the repository does not exist
+ * @throws IOException - if any of the underlying operations during repo deletion fails
+ */
+ public void execute(
+ Project.NameKey project,
+ boolean preserveGitRepository,
+ boolean archiveDeletedRepos,
+ Optional<Path> archivedFolder,
+ DynamicSet<ProjectDeletedListener> deletedListeners)
+ throws RepositoryNotFoundException, IOException {
+ Repository repository = repoManager.openRepository(project);
+ cleanCache(repository);
+ if (!preserveGitRepository) {
+ Path repoPath = repository.getDirectory().toPath();
+ String projectName = project.get();
+ if (archiveDeletedRepos) {
+ archiveGitRepository(projectName, repoPath, archivedFolder, deletedListeners);
+ } else {
+ deleteGitRepository(projectName, repoPath, deletedListeners);
+ }
+ }
+ }
+
+ /**
+ * Removes a git repository from the filesystem and the jgit cache. The git repo is neither
+ * preserved (ie kept on disk) nor archived, and no downstream listeners are notified.
+ *
+ * @param project - the git repo name that is eligible for deletion
+ * @throws RepositoryNotFoundException - if the repository does not exist
+ * @throws IOException - if any of the underlying operations during repo deletion fails
+ */
+ @UsedAt(UsedAt.Project.PLUGIN_PULL_REPLICATION)
+ public void execute(Project.NameKey project) throws RepositoryNotFoundException, IOException {
+ execute(project, false, false, Optional.empty(), DynamicSet.emptySet());
+ }
+
+ private static void cleanCache(Repository repository) {
+ repository.close();
+ RepositoryCache.close(repository);
+ }
+
+ private static void archiveGitRepository(
+ String projectName, Path repoPath, Optional<Path> archivedFolder, DynamicSet<ProjectDeletedListener> deletedListeners) throws IOException {
+ Path basePath = getBasePath(repoPath, projectName);
+ if (archivedFolder.isEmpty()) {
+ throw new IllegalArgumentException(
+ "An archive path must be provided for the " + basePath + " repo to be archived");
+ }
+ Path renamedProjectDir = renameRepository(repoPath, basePath, projectName, "archived");
+ try {
+ Path archive = getArchivePath(archivedFolder.get(), renamedProjectDir, basePath);
+ FileUtils.copyDirectory(renamedProjectDir.toFile(), archive.toFile());
+ MoreFiles.deleteRecursively(renamedProjectDir, ALLOW_INSECURE);
+ } catch (IOException e) {
+ log.atWarning().withCause(e).log("Error trying to archive %s", renamedProjectDir);
+ } finally {
+ sendProjectDeletedEvent(projectName, deletedListeners);
+ }
+ }
+
+ private static Path getArchivePath(Path archivedFolder, Path renamedProjectDir, Path basePath) {
+ Path configArchiveRepo = archivedFolder.toAbsolutePath();
+ Path relativePath = basePath.relativize(renamedProjectDir);
+ return configArchiveRepo.resolve(relativePath);
+ }
+
+ private static void deleteGitRepository(
+ String projectName, Path repoPath, DynamicSet<ProjectDeletedListener> deletedListeners)
+ throws IOException {
+ // Delete the repository from disk
+ Path basePath = getBasePath(repoPath, projectName);
+ Path trash = renameRepository(repoPath, basePath, projectName, "deleted");
+ try {
+ MoreFiles.deleteRecursively(trash, ALLOW_INSECURE);
+ recursivelyDeleteEmptyParents(repoPath.toFile().getParentFile(), basePath.toFile());
+ } catch (IOException e) {
+ // Only log if delete failed - repo already moved to trash.
+ log.atWarning().withCause(e).log("Error trying to delete %s or its parents", trash);
+ } finally {
+ sendProjectDeletedEvent(projectName, deletedListeners);
+ }
+ }
+
+ private static Path getBasePath(Path repo, String projectName) {
+ Path projectPath = Path.of(projectName);
+ return repo.getRoot()
+ .resolve(repo.subpath(0, repo.getNameCount() - projectPath.getNameCount()));
+ }
+
+ private static Path renameRepository(
+ Path directory, Path basePath, String projectName, String option) throws IOException {
+ Path newRepo =
+ basePath.resolve(
+ projectName + "." + FORMAT.format(TimeMachine.now()) + ".%" + option + "%.git");
+ return Files.move(directory, newRepo, StandardCopyOption.ATOMIC_MOVE);
+ }
+
+ /**
+ * Recursively delete the specified file and its parent files until we hit the file {@code Until}
+ * or the parent file is populated. This is used when we have a tree structure such as a/b/c/d.git
+ * and a/b/e.git - if we delete a/b/c/d.git, we no longer need a/b/c/.
+ */
+ private static void recursivelyDeleteEmptyParents(File file, File until) throws IOException {
+ if (file.equals(until)) {
+ return;
+ }
+ if (file.listFiles().length == 0) {
+ File parent = file.getParentFile();
+ Files.delete(file.toPath());
+ recursivelyDeleteEmptyParents(parent, until);
+ }
+ }
+
+ private static void sendProjectDeletedEvent(
+ String projectName, DynamicSet<ProjectDeletedListener> deletedListeners) {
+ if (!deletedListeners.iterator().hasNext()) {
+ return;
+ }
+ ProjectDeletedListener.Event event =
+ new ProjectDeletedListener.Event() {
+ @Override
+ public String getProjectName() {
+ return projectName;
+ }
+
+ @Override
+ public NotifyHandling getNotify() {
+ return NotifyHandling.NONE;
+ }
+ };
+ for (ProjectDeletedListener l : deletedListeners) {
+ try {
+ l.onProjectDeleted(event);
+ } catch (RuntimeException e) {
+ log.atWarning().withCause(e).log("Failure in ProjectDeletedListener");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
index bfc44a1..b2b6bb4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
@@ -43,7 +43,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.lib.Constants;
@@ -75,7 +74,7 @@
@Before
public void setUpArchiveFolder() throws IOException {
- archiveFolder = Files.createDirectories(Paths.get(ARCHIVE_FOLDER)).toFile();
+ archiveFolder = Files.createDirectories(Path.of(ARCHIVE_FOLDER)).toFile();
projectDir = verifyProjectRepoExists(project);
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java
index 75ea96b..045592d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java
@@ -26,7 +26,6 @@
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import java.io.File;
-import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
import org.junit.Test;
@@ -75,7 +74,7 @@
@Test
public void customProjectIsProtected() throws Exception {
- List<String> projects = ImmutableList.of("Custom-Parent", "^protected-.*");
+ ImmutableList<String> projects = ImmutableList.of("Custom-Parent", "^protected-.*");
pluginConfig.setStringList("protectedProject", projects);
when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME))
.thenReturn(pluginConfig.asPluginConfig());
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandlerTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandlerTest.java
index 9cffd14..317de9b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandlerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandlerTest.java
@@ -14,128 +14,54 @@
package com.googlesource.gerrit.plugins.deleteproject.fs;
-import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.when;
-
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.googlesource.gerrit.plugins.deleteproject.Configuration;
-import java.io.IOException;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.PathMatcher;
-import java.util.stream.Stream;
-import org.eclipse.jgit.internal.storage.file.FileRepository;
-import org.eclipse.jgit.lib.Repository;
+import java.util.Optional;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class FilesystemDeleteHandlerTest {
- @Mock private GitRepositoryManager repoManager;
+ @Mock private RepositoryDelete repositoryDelete;
@Mock private ProjectDeletedListener projectDeleteListener;
@Mock private Configuration config;
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
- private DynamicSet<ProjectDeletedListener> deletedListener;
- private FilesystemDeleteHandler fsDeleteHandler;
+ private DynamicSet<ProjectDeletedListener> deletedListeners;
private Path basePath;
@Before
public void setUp() throws Exception {
- basePath = tempFolder.newFolder().toPath().resolve("base");
- deletedListener = new DynamicSet<>();
- deletedListener.add("", projectDeleteListener);
+ basePath = tempFolder.newFolder().toPath().resolve("archive");
+ deletedListeners = new DynamicSet<>();
+ deletedListeners.add("", projectDeleteListener);
}
@Test
- public void shouldDeleteRepository() throws Exception {
- String repoName = "testRepo";
- Repository repository = createRepository(repoName);
- Project.NameKey nameKey = Project.nameKey(repoName);
- when(repoManager.openRepository(nameKey)).thenReturn(repository);
- when(config.shouldArchiveDeletedRepos()).thenReturn(false);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
- fsDeleteHandler.delete(nameKey, false);
- assertThat(repository.getDirectory().exists()).isFalse();
- }
+ public void shouldExtractArchivingParamsFromConfig() throws Exception {
+ boolean doArchive = true;
+ Project.NameKey project = Project.NameKey.parse("testProject");
+ boolean noPreserveGitRepository = false;
- @Test
- public void shouldDeleteEmptyParentFolders() throws Exception {
- String repoName = "a/b/c";
- Repository repository = createRepository(repoName);
- Project.NameKey nameKey = Project.nameKey(repoName);
- when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
- fsDeleteHandler.delete(nameKey, false);
- assertThat(repository.getDirectory().exists()).isFalse();
- }
+ Mockito.when(config.shouldArchiveDeletedRepos()).thenReturn(doArchive);
+ Mockito.when(config.getArchiveFolder()).thenReturn(basePath);
- @Test
- public void shouldKeepCommonFolders() throws Exception {
- String repoToDeleteName = "a/b/c/d";
- Repository repoToDelete = createRepository(repoToDeleteName);
-
- String repoToKeepName = "a/b/e";
- Repository repoToKeep = createRepository(repoToKeepName);
-
- Project.NameKey nameKey = Project.nameKey(repoToDeleteName);
- when(repoManager.openRepository(nameKey)).thenReturn(repoToDelete);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
- fsDeleteHandler.delete(nameKey, false);
- assertThat(repoToDelete.getDirectory().exists()).isFalse();
- assertThat(repoToKeep.getDirectory().exists()).isTrue();
- }
-
- @Test
- public void shouldPreserveRepository() throws Exception {
- String repoName = "preservedRepo";
- Repository repository = createRepository(repoName);
- Project.NameKey nameKey = Project.nameKey(repoName);
- when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
- fsDeleteHandler.delete(nameKey, true);
- assertThat(repository.getDirectory().exists()).isTrue();
- }
-
- private FileRepository createRepository(String repoName) throws IOException {
- Path repoPath = Files.createDirectories(basePath.resolve(repoName));
- Repository repository = new FileRepository(repoPath.toFile());
- repository.create(true);
- return (FileRepository) repository;
- }
-
- @Test
- public void archiveRepository() throws Exception {
- String repoName = "parent_project/p3";
- Repository repository = createRepository(repoName);
- Path archiveFolder = basePath.resolve("test_archive");
- when(config.shouldArchiveDeletedRepos()).thenReturn(true);
- when(config.getArchiveFolder()).thenReturn(archiveFolder);
- Project.NameKey nameKey = Project.nameKey(repoName);
- when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
- fsDeleteHandler.delete(nameKey, false);
- assertThat(repository.getDirectory().exists()).isFalse();
- String patternToVerify = archiveFolder.resolve(repoName).toString() + "*%archived%.git";
- assertThat(pathExistsWithPattern(archiveFolder, patternToVerify)).isTrue();
- }
-
- private boolean pathExistsWithPattern(Path archiveFolder, String patternToVerify)
- throws IOException {
- PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + patternToVerify);
- try (Stream<Path> stream = Files.walk(archiveFolder)) {
- return stream.anyMatch(matcher::matches);
- }
+ FilesystemDeleteHandler filesystemDeleteHandler =
+ new FilesystemDeleteHandler(repositoryDelete, deletedListeners, config);
+ filesystemDeleteHandler.delete(project, noPreserveGitRepository);
+ Mockito.verify(repositoryDelete)
+ .execute(
+ project, noPreserveGitRepository, doArchive, Optional.of(basePath), deletedListeners);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDeleteTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDeleteTest.java
new file mode 100644
index 0000000..512b8b5
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDeleteTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2023 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.deleteproject.fs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.Optional;
+import java.util.stream.Stream;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RepositoryDeleteTest {
+
+ private static final Optional<Path> NO_ARCHIVE_PATH = Optional.empty();
+
+ @Mock private GitRepositoryManager repoManager;
+ @Mock private ProjectDeletedListener projectDeleteListener;
+
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private DynamicSet<ProjectDeletedListener> deletedListeners;
+ private RegistrationHandle handle;
+ private RepositoryDelete repositoryDelete;
+ private Path basePath;
+
+ @Before
+ public void setUp() throws Exception {
+ deletedListeners = new DynamicSet<>();
+ handle = deletedListeners.add("testPlugin", projectDeleteListener);
+ basePath = tempFolder.newFolder().toPath().resolve("base");
+ }
+
+ @Test
+ public void shouldDeleteRepository() throws Exception {
+ String repoName = "testRepo";
+ Repository repository = createRepository(repoName);
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey);
+ assertThat(repository.getDirectory().exists()).isFalse();
+ }
+
+ @Test
+ public void shouldDeleteEmptyParentFolders() throws Exception {
+ String repoName = "a/b/c";
+ Repository repository = createRepository(repoName);
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey);
+ assertThat(repository.getDirectory().exists()).isFalse();
+ }
+
+ @Test
+ public void shouldKeepCommonFolders() throws Exception {
+ String repoToDeleteName = "a/b/c/d";
+ Repository repoToDelete = createRepository(repoToDeleteName);
+
+ String repoToKeepName = "a/b/e";
+ Repository repoToKeep = createRepository(repoToKeepName);
+
+ Project.NameKey nameKey = Project.nameKey(repoToDeleteName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repoToDelete);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey);
+ assertThat(repoToDelete.getDirectory().exists()).isFalse();
+ assertThat(repoToKeep.getDirectory().exists()).isTrue();
+ }
+
+ @Test
+ public void shouldPreserveRepository() throws Exception {
+ String repoName = "preservedRepo";
+ Repository repository = createRepository(repoName);
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey, true, false, NO_ARCHIVE_PATH, deletedListeners);
+ assertThat(repository.getDirectory().exists()).isTrue();
+ }
+
+ private FileRepository createRepository(String repoName) throws IOException {
+ Path repoPath = Files.createDirectories(basePath.resolve(repoName));
+ Repository repository = new FileRepository(repoPath.toFile());
+ repository.create(true);
+ return (FileRepository) repository;
+ }
+
+ @Test
+ public void archiveRepository() throws Exception {
+ String repoName = "parent_project/p3";
+ Repository repository = createRepository(repoName);
+ Path archiveFolder = basePath.resolve("test_archive");
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey, false, true, Optional.of(archiveFolder), deletedListeners);
+ assertThat(repository.getDirectory().exists()).isFalse();
+ String patternToVerify = archiveFolder.resolve(repoName).toString() + "*%archived%.git";
+ assertThat(pathExistsWithPattern(archiveFolder, patternToVerify)).isTrue();
+ }
+
+ @Test
+ public void shouldNotifyListenersOnSuccessfulRepoDeletion() throws Exception {
+ String repoName = "testRepo";
+ Repository repository = createRepository(repoName);
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ repositoryDelete.execute(nameKey, false, false, NO_ARCHIVE_PATH, deletedListeners);
+ Mockito.verify(projectDeleteListener).onProjectDeleted(any());
+ }
+
+ @Test
+ public void shouldNotNotifyListenersIfTheListenersSetIsEmpty() throws Exception {
+ String repoName = "testRepo";
+ Repository repository = createRepository(repoName);
+ Project.NameKey nameKey = Project.nameKey(repoName);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ repositoryDelete = new RepositoryDelete(repoManager);
+ handle.remove();
+ repositoryDelete.execute(nameKey, false, false, NO_ARCHIVE_PATH, deletedListeners);
+ Mockito.verify(projectDeleteListener, never()).onProjectDeleted(any());
+ }
+
+ private boolean pathExistsWithPattern(Path archiveFolder, String patternToVerify)
+ throws IOException {
+ PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + patternToVerify);
+ try (Stream<Path> stream = Files.walk(archiveFolder)) {
+ return stream.anyMatch(matcher::matches);
+ }
+ }
+}