Merge branch 'stable-3.8'
* stable-3.8:
Move gr-overlay to dialog element for delete-project plugin
Change-Id: I8f11ac208695ee3cac8812d6e472059225f47b98
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/DeleteAction.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
index 459d819..47feddf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
@@ -14,8 +14,12 @@
package com.googlesource.gerrit.plugins.deleteproject;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.events.EventDispatcher;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -36,7 +40,9 @@
DeleteLog deleteLog,
DeletePreconditions preConditions,
Configuration cfg,
- HideProject hideProject) {
+ HideProject hideProject,
+ DynamicItem<EventDispatcher> dispatcher,
+ @Nullable @GerritInstanceId String instanceId) {
super(
dbHandler,
fsHandler,
@@ -45,7 +51,9 @@
deleteLog,
preConditions,
cfg,
- hideProject);
+ hideProject,
+ dispatcher,
+ instanceId);
this.protectedProjects = protectedProjects;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
index 40463b2..75a948a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
@@ -14,13 +14,17 @@
package com.googlesource.gerrit.plugins.deleteproject;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.events.EventDispatcher;
import com.google.gerrit.server.project.ProjectResource;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -48,6 +52,8 @@
private final DeleteLog deleteLog;
private final Configuration cfg;
private final HideProject hideProject;
+ private final DynamicItem<EventDispatcher> dispatcher;
+ private final String instanceId;
@Inject
DeleteProject(
@@ -58,7 +64,9 @@
DeleteLog deleteLog,
DeletePreconditions preConditions,
Configuration cfg,
- HideProject hideProject) {
+ HideProject hideProject,
+ DynamicItem<EventDispatcher> dispatcher,
+ @Nullable @GerritInstanceId String instanceId) {
this.dbHandler = dbHandler;
this.fsHandler = fsHandler;
this.cacheHandler = cacheHandler;
@@ -67,6 +75,8 @@
this.preConditions = preConditions;
this.cfg = cfg;
this.hideProject = hideProject;
+ this.dispatcher = dispatcher;
+ this.instanceId = instanceId;
}
@Override
@@ -94,6 +104,20 @@
} else {
hideProject.apply(rsrc);
}
+
+ ProjectDeletedEvent event = new ProjectDeletedEvent();
+ event.projectName = project.getName();
+ event.instanceId = instanceId;
+
+ /**
+ * EventBroker checks if user has the permission to access the project. But because this
+ * project is already deleted, check will always fail. That's why this event will be delivered
+ * only to the unrestricted listeners. Unrestricted events listeners are implementing {@link
+ * com.google.gerrit.server.events.EventListener} which allows to listen to events without
+ * user visibility restrictions. For example these events are going to be delivered to
+ * multi-site {@link com.googlesource.gerrit.plugins.multisite.event.EventHandler}
+ */
+ dispatcher.get().postEvent(project.getNameKey(), event);
} catch (Exception e) {
ex = e;
throw e;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
index 4d0a802..a2e3f15 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
@@ -22,6 +22,7 @@
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.events.EventTypes;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.internal.UniqueAnnotations;
@@ -60,6 +61,8 @@
.to(ArchiveRepositoryRemover.class);
}
+ EventTypes.register(ProjectDeletedEvent.TYPE, ProjectDeletedEvent.class);
+
install(
new RestApiModule() {
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java
new file mode 100644
index 0000000..31a41cc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2022 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;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.events.ProjectEvent;
+
+public class ProjectDeletedEvent extends ProjectEvent {
+ public static final String TYPE = "project-deleted";
+ public String projectName;
+
+ public ProjectDeletedEvent() {
+ super(TYPE);
+ }
+
+ @Override
+ public NameKey getProjectNameKey() {
+ return Project.nameKey(projectName);
+ }
+}
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 053b2fc..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,156 +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);
- }
- }
-
- 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..91bded5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/RepositoryDelete.java
@@ -0,0 +1,216 @@
+// 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.Paths;
+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);
+ } 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) 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);
+ }
+ }
+
+ 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 = Paths.get(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/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index b72ee3a..2f0e7f6 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -35,6 +35,28 @@
can be configured to listen to the project deletion event and to
replicate project deletions.
+Event after project deletion
+-----------------------------------
+
+This plugin generates an event after project deletion. Format of
+the event:
+
+=== Project Deleted
+
+Sent after project deletion.
+
+type:: "project-deleted"
+
+projectName:: Name of the deleted project
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+*NOTE*: This event will be delivered only to the unrestricted listeners.
+Unrestricted events listeners implement
+`com.google.gerrit.server.events.EventListener` without performing any
+permission checking.
+
Access
------
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java
new file mode 100644
index 0000000..07e3ee4
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2022 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
+import com.googlesource.gerrit.plugins.deleteproject.cache.CacheDeleteHandler;
+import com.googlesource.gerrit.plugins.deleteproject.database.DatabaseDeleteHandler;
+import com.googlesource.gerrit.plugins.deleteproject.fs.FilesystemDeleteHandler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectDeletedEventTest {
+
+ private static final NameKey PROJECT_NAME_KEY = Project.nameKey("test-project");
+ private static final String INSTANCE_ID = "test-instance-id";
+
+ @Mock private DatabaseDeleteHandler dbHandler;
+ @Mock private FilesystemDeleteHandler fsHandler;
+ @Mock private CacheDeleteHandler cacheHandler;
+ @Mock private Provider<CurrentUser> userProvider;
+ @Mock private DeleteLog deleteLog;
+ @Mock private DeletePreconditions preConditions;
+ @Mock private Configuration cfg;
+ @Mock private EventDispatcher dispatcher;
+ @Mock private DynamicItem<EventDispatcher> dispatcherProvider;
+ @Mock private HideProject hideProject;
+ @Mock private IdentifiedUser currentUser;
+ @Mock private ProjectState state;
+ @Captor private ArgumentCaptor<ProjectDeletedEvent> projectDeletedEventCaptor;
+
+ private Project project = Project.builder(PROJECT_NAME_KEY).build();
+
+ private DeleteProject objectUnderTest;
+
+ @Before
+ public void setup() throws Exception {
+ when(dispatcherProvider.get()).thenReturn(dispatcher);
+ when(userProvider.get()).thenReturn(currentUser);
+ when(state.getProject()).thenReturn(project);
+ objectUnderTest =
+ new DeleteProject(
+ dbHandler,
+ fsHandler,
+ cacheHandler,
+ userProvider,
+ deleteLog,
+ preConditions,
+ cfg,
+ hideProject,
+ dispatcherProvider,
+ INSTANCE_ID);
+ }
+
+ @Test
+ public void shouldSendProjectDeletedEventAfterProjectDeletion() throws Exception {
+ Input input = new Input();
+ input.force = false;
+ input.preserve = false;
+ objectUnderTest.doDelete(new ProjectResource(state, currentUser), input);
+
+ verify(dispatcher).postEvent(eq(PROJECT_NAME_KEY), projectDeletedEventCaptor.capture());
+ ProjectDeletedEvent event = projectDeletedEventCaptor.getValue();
+ assertThat(event.instanceId).isEqualTo(INSTANCE_ID);
+ assertThat(event.getProjectNameKey()).isEqualTo(PROJECT_NAME_KEY);
+ assertThat(event.type).isEqualTo(ProjectDeletedEvent.TYPE);
+ }
+}
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);
+ }
+ }
+}