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); + } + } +}