Merge "bazlets: Replace native.git_repository with skylark rule" into stable-2.14
diff --git a/BUILD b/BUILD
index 46ddcfe..f5393ad 100644
--- a/BUILD
+++ b/BUILD
@@ -16,6 +16,7 @@
"Gerrit-SshModule: com.googlesource.gerrit.plugins.deleteproject.SshModule",
],
resources = glob(["src/main/resources/**/*"]),
+ deps = ["@commons-io//jar"],
)
junit_tests(
@@ -31,6 +32,7 @@
visibility = ["//visibility:public"],
exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
":delete-project__plugin",
+ "@commons-io//jar",
"@mockito//jar",
],
)
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 6bb1ec6..bd864bf 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -31,3 +31,8 @@
artifact = "org.objenesis:objenesis:2.6",
sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
)
+ maven_jar(
+ name = "commons-io",
+ artifact = "commons-io:commons-io:2.6",
+ sha1 = "815893df5f31da2ece4040fe0a12fd44b577afaf",
+ )
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 cd11508..e83befc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Configuration.java
@@ -14,33 +14,64 @@
package com.googlesource.gerrit.plugins.deleteproject;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.toList;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
@Singleton
public class Configuration {
- private static String DELETED_PROJECTS_PARENT = "Deleted-Projects";
+ private static final Logger log = LoggerFactory.getLogger(Configuration.class);
+ private static final String DELETED_PROJECTS_PARENT = "Deleted-Projects";
+ private static final long DEFAULT_ARCHIVE_DURATION_DAYS = 180;
private final boolean allowDeletionWithTags;
+ private final boolean archiveDeletedRepos;
+ private final boolean enablePreserveOption;
private final boolean hideProjectOnPreserve;
+ private final long deleteArchivedReposAfter;
private final String deletedProjectsParent;
+ private final Path archiveFolder;
private final List<Pattern> protectedProjects;
+ private final PluginConfig cfg;
+
+ private File pluginData;
@Inject
- public Configuration(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
- PluginConfig cfg = pluginConfigFactory.getFromGerritConfig(pluginName);
- allowDeletionWithTags = cfg.getBoolean("allowDeletionOfReposWithTags", true);
- hideProjectOnPreserve = cfg.getBoolean("hideProjectOnPreserve", false);
- deletedProjectsParent = cfg.getString("parentForDeletedProjects", DELETED_PROJECTS_PARENT);
- protectedProjects =
+ public Configuration(
+ PluginConfigFactory pluginConfigFactory,
+ @PluginName String pluginName,
+ @PluginData File pluginData) {
+ this.cfg = pluginConfigFactory.getFromGerritConfig(pluginName);
+ this.pluginData = pluginData;
+ this.allowDeletionWithTags = cfg.getBoolean("allowDeletionOfReposWithTags", true);
+ this.hideProjectOnPreserve = cfg.getBoolean("hideProjectOnPreserve", false);
+ this.deletedProjectsParent = cfg.getString("parentForDeletedProjects", DELETED_PROJECTS_PARENT);
+ this.archiveDeletedRepos = cfg.getBoolean("archiveDeletedRepos", false);
+ this.enablePreserveOption = cfg.getBoolean("enablePreserveOption", true);
+ this.archiveFolder =
+ getArchiveFolderFromConfig(cfg.getString("archiveFolder", pluginData.toString()));
+ this.deleteArchivedReposAfter =
+ getArchiveDurationFromConfig(
+ Strings.nullToEmpty(cfg.getString("deleteArchivedReposAfter")));
+ this.protectedProjects =
Arrays.asList(cfg.getStringList("protectedProject"))
.stream()
.map(Pattern::compile)
@@ -62,4 +93,46 @@
public List<Pattern> protectedProjects() {
return protectedProjects;
}
+
+ public boolean shouldArchiveDeletedRepos() {
+ return archiveDeletedRepos;
+ }
+
+ public Path getArchiveFolder() {
+ return archiveFolder;
+ }
+
+ public long getArchiveDuration() {
+ return deleteArchivedReposAfter;
+ }
+
+ public boolean enablePreserveOption() {
+ return enablePreserveOption;
+ }
+
+ private Path getArchiveFolderFromConfig(String configValue) {
+ try {
+ return Files.createDirectories(Paths.get(configValue));
+ } catch (Exception e) {
+ log.warn(
+ "Failed to create folder {}: {}; using default path: {}",
+ configValue,
+ e.getMessage(),
+ pluginData);
+ return pluginData.toPath();
+ }
+ }
+
+ private long getArchiveDurationFromConfig(String configValue) {
+ try {
+ return ConfigUtil.getTimeUnit(
+ configValue, DAYS.toMillis(DEFAULT_ARCHIVE_DURATION_DAYS), MILLISECONDS);
+ } catch (IllegalArgumentException e) {
+ log.warn(
+ "The configured archive duration is not valid: {}; using the default value: {} days",
+ e.getMessage(),
+ DEFAULT_ARCHIVE_DURATION_DAYS);
+ return DAYS.toMillis(DEFAULT_ARCHIVE_DURATION_DAYS);
+ }
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java
index b833d7d..c6a0f1f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java
@@ -49,11 +49,14 @@
@Option(name = "--preserve-git-repository", usage = "don't delete git repository directory")
private boolean preserveGitRepository = false;
+ private final Configuration cfg;
private final DeleteProject deleteProject;
private final DeletePreconditions preConditions;
@Inject
- protected DeleteCommand(DeleteProject deleteProject, DeletePreconditions preConditions) {
+ protected DeleteCommand(
+ Configuration cfg, DeleteProject deleteProject, DeletePreconditions preConditions) {
+ this.cfg = cfg;
this.deleteProject = deleteProject;
this.preConditions = preConditions;
}
@@ -61,6 +64,13 @@
@Override
public void run() throws Failure {
try {
+ if (preserveGitRepository && !cfg.enablePreserveOption()) {
+ throw new UnloggedFailure(
+ "Given the enablePreserveOption is configured to be false, "
+ + "the --preserve-git-repository option is not allowed.\n"
+ + "Please remove this option and retry.");
+ }
+
DeleteProject.Input input = new DeleteProject.Input();
input.force = force;
input.preserve = preserveGitRepository;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HttpModule.java
index a5470c9..4d63379 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/HttpModule.java
@@ -18,11 +18,24 @@
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
+import com.google.inject.Inject;
public class HttpModule extends HttpPluginModule {
+ private final Configuration cfg;
+
+ @Inject
+ HttpModule(Configuration cfg) {
+ this.cfg = cfg;
+ }
+
@Override
protected void configureServlets() {
- DynamicSet.bind(binder(), WebUiPlugin.class)
- .toInstance(new JavaScriptPlugin("delete-project.js"));
+ if (cfg.enablePreserveOption()) {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("delete-project.js"));
+ } else {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("delete-project-with-preserve-disabled.js"));
+ }
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Module.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Module.java
index 8fc69d4..4826666 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/Module.java
@@ -23,14 +23,23 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.internal.UniqueAnnotations;
import com.googlesource.gerrit.plugins.deleteproject.cache.CacheDeleteHandler;
import com.googlesource.gerrit.plugins.deleteproject.database.DatabaseDeleteHandler;
+import com.googlesource.gerrit.plugins.deleteproject.fs.ArchiveRepositoryRemover;
import com.googlesource.gerrit.plugins.deleteproject.fs.DeleteTrashFolders;
import com.googlesource.gerrit.plugins.deleteproject.fs.FilesystemDeleteHandler;
public class Module extends AbstractModule {
+ private final boolean scheduleCleaning;
+
+ @Inject
+ Module(Configuration config) {
+ this.scheduleCleaning = config.getArchiveDuration() > 0;
+ }
+
@Override
protected void configure() {
bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create()).to(DeleteLog.class);
@@ -47,6 +56,12 @@
bind(DatabaseDeleteHandler.class);
bind(FilesystemDeleteHandler.class);
bind(DeletePreconditions.class);
+ if (scheduleCleaning) {
+ bind(LifecycleListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(ArchiveRepositoryRemover.class);
+ }
+
install(
new RestApiModule() {
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemover.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemover.java
new file mode 100644
index 0000000..ec6c29a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemover.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2018 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 java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.io.MoreFiles;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+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.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ArchiveRepositoryRemover implements LifecycleListener {
+
+ private final WorkQueue queue;
+ private final Provider<RepositoryCleanupTask> repositoryCleanupTaskProvider;
+ private ScheduledFuture<?> scheduledCleanupTask;
+
+ @Inject
+ ArchiveRepositoryRemover(
+ WorkQueue queue, Provider<RepositoryCleanupTask> repositoryCleanupTaskProvider) {
+ this.queue = queue;
+ this.repositoryCleanupTaskProvider = repositoryCleanupTaskProvider;
+ }
+
+ @Override
+ public void start() {
+ scheduledCleanupTask =
+ queue
+ .getDefaultQueue()
+ .scheduleAtFixedRate(
+ repositoryCleanupTaskProvider.get(),
+ SECONDS.toMillis(1),
+ TimeUnit.DAYS.toMillis(1),
+ MILLISECONDS);
+ }
+
+ @Override
+ public void stop() {
+ if (scheduledCleanupTask != null) {
+ scheduledCleanupTask.cancel(true);
+ scheduledCleanupTask = null;
+ }
+ }
+}
+
+class RepositoryCleanupTask implements Runnable {
+ private static final Logger logger = LoggerFactory.getLogger(RepositoryCleanupTask.class);
+
+ private final Configuration config;
+ private final String pluginName;
+
+ @Inject
+ RepositoryCleanupTask(Configuration config, @PluginName String pluginName) {
+ this.config = config;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ public void run() {
+ logger.info("Cleaning up expired git repositories...");
+ cleanUpOverdueRepositories();
+ logger.info("Cleaning up expired git repositories... Done");
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "[%s]: Clean up expired git repositories from the archive [%s]",
+ pluginName, config.getArchiveFolder());
+ }
+
+ private void cleanUpOverdueRepositories() {
+ for (Path path : listOverdueFiles(config.getArchiveDuration())) {
+ try {
+ MoreFiles.deleteRecursively(path);
+ } catch (IOException e) {
+ logger.warn("Error trying to clean the archived git repository: {}", path, e);
+ }
+ }
+ }
+
+ private List<Path> listOverdueFiles(long duration) {
+ List<Path> files = new ArrayList<>();
+ File targetDir = config.getArchiveFolder().toFile();
+ FileTime nowTime = FileTime.fromMillis(TimeMachine.now().toEpochMilli());
+
+ for (File repo : targetDir.listFiles()) {
+ try {
+ FileTime lastModifiedTime = Files.getLastModifiedTime(repo.toPath());
+ FileTime expires = FileTime.fromMillis(lastModifiedTime.toMillis() + duration);
+ if (nowTime.compareTo(expires) > 0) {
+ files.add(repo.toPath());
+ }
+ } catch (IOException e) {
+ logger.warn("Error trying to get last modified time for file: {} ", repo.toPath(), e);
+ }
+ }
+ return files;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFolders.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFolders.java
index 72bc172..bd63e1f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFolders.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFolders.java
@@ -56,9 +56,17 @@
*/
private static final Pattern TRASH_2 = Pattern.compile(".*\\.\\d{13}.%deleted%.git");
+ /**
+ * Newer trash folder name format. Besides the changes in TRASH_2, it uses a timestamp format
+ * (YYYYMMddHHmmss) instead of the epoch one for increased readability.
+ */
+ private static final Pattern TRASH_3 = Pattern.compile(".*\\.\\d{14}.%deleted%.git");
+
@VisibleForTesting
static final boolean match(String fName) {
- return TRASH_1.matcher(fName).matches() || TRASH_2.matcher(fName).matches();
+ return TRASH_1.matcher(fName).matches()
+ || TRASH_2.matcher(fName).matches()
+ || TRASH_3.matcher(fName).matches();
}
static boolean match(Path dir) {
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 ecf14f4..b1a1c74 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,6 +14,8 @@
package com.googlesource.gerrit.plugins.deleteproject.fs;
+import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
+
import com.google.common.io.MoreFiles;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -21,6 +23,7 @@
import com.google.gerrit.reviewdb.client.Project;
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;
@@ -28,6 +31,9 @@
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 org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
@@ -36,15 +42,21 @@
public class FilesystemDeleteHandler {
private static final Logger log = LoggerFactory.getLogger(FilesystemDeleteHandler.class);
+ private static final DateTimeFormatter FORMAT =
+ DateTimeFormatter.ofPattern("YYYYMMddHHmmss").withZone(ZoneId.of("UTC"));
private final GitRepositoryManager repoManager;
private final DynamicSet<ProjectDeletedListener> deletedListeners;
+ private final Configuration config;
@Inject
public FilesystemDeleteHandler(
- GitRepositoryManager repoManager, DynamicSet<ProjectDeletedListener> deletedListeners) {
+ GitRepositoryManager repoManager,
+ DynamicSet<ProjectDeletedListener> deletedListeners,
+ Configuration config) {
this.repoManager = repoManager;
this.deletedListeners = deletedListeners;
+ this.config = config;
}
public void delete(Project project, boolean preserveGitRepository)
@@ -53,7 +65,13 @@
Repository repository = repoManager.openRepository(project.getNameKey());
cleanCache(repository);
if (!preserveGitRepository) {
- deleteGitRepository(project.getNameKey(), repository.getDirectory());
+ Path repoPath = repository.getDirectory().toPath();
+ String projectName = project.getNameKey().get();
+ if (config.shouldArchiveDeletedRepos()) {
+ archiveGitRepository(projectName, repoPath);
+ } else {
+ deleteGitRepository(projectName, repoPath);
+ }
}
}
@@ -62,33 +80,51 @@
RepositoryCache.close(repository);
}
- private void deleteGitRepository(final Project.NameKey project, final File repoFile)
- throws IOException {
+ 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.warn("Error trying to archive {}", renamedProjectDir, e);
+ }
+ }
+
+ 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(repoFile.toPath(), project);
- Path trash = moveToTrash(repoFile.toPath(), basePath, project);
+ Path basePath = getBasePath(repoPath, projectName);
+ Path trash = renameRepository(repoPath, basePath, projectName, "deleted");
try {
MoreFiles.deleteRecursively(trash);
- recursivelyDeleteEmptyParents(repoFile.getParentFile(), basePath.toFile());
+ recursivelyDeleteEmptyParents(repoPath.toFile().getParentFile(), basePath.toFile());
} catch (IOException e) {
// Only log if delete failed - repo already moved to trash.
log.warn("Error trying to delete {} or its parents", trash, e);
} finally {
- sendProjectDeletedEvent(project);
+ sendProjectDeletedEvent(projectName);
}
}
- private Path getBasePath(Path repo, Project.NameKey project) {
- Path projectPath = Paths.get(project.get());
+ 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 moveToTrash(Path directory, Path basePath, Project.NameKey nameKey)
+ private Path renameRepository(Path directory, Path basePath, String projectName, String option)
throws IOException {
- Path trashRepo =
- basePath.resolve(nameKey.get() + "." + TimeMachine.now().toEpochMilli() + ".%deleted%.git");
- return Files.move(directory, trashRepo, StandardCopyOption.ATOMIC_MOVE);
+ Path newRepo =
+ basePath.resolve(
+ projectName + "." + FORMAT.format(TimeMachine.now()) + ".%" + option + "%.git");
+ return Files.move(directory, newRepo, StandardCopyOption.ATOMIC_MOVE);
}
/**
@@ -107,16 +143,15 @@
}
}
- private void sendProjectDeletedEvent(Project.NameKey project) {
+ private void sendProjectDeletedEvent(String projectName) {
if (!deletedListeners.iterator().hasNext()) {
return;
}
-
ProjectDeletedListener.Event event =
new ProjectDeletedListener.Event() {
@Override
public String getProjectName() {
- return project.get();
+ return projectName;
}
@Override
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 65f996d..c4f98ff 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -34,6 +34,19 @@
By default false.
+<a id="enablePreserveOption">
+`plugin.@PLUGIN@.enablePreserveOption`
+: Whether the "Preserve git repository" option is enabled for the user on the
+ UI and ssh delete-project command.
+
+ Disabling the preserve option means the user does not have access to the
+ preserve option on the UI and ssh delete-project command.
+
+ If this is set to false, then preserving deleted git repositories is
+ disabled.
+
+ By default true.
+
<a id="parentForDeletedProjects">
`plugin.@PLUGIN@.parentForDeletedProjects`
: The name of the project that is used as parent for all deleted
@@ -53,3 +66,59 @@
patterns.
By default not set.
+
+<a id="archiveDeletedRepos">
+`plugin.@PLUGIN@.archiveDeletedRepos`
+: Whether to archive repositories instead of deleting them.
+
+ Archiving the git repository means that the repository is stored
+ in a folder which is not visible to the users which, from the user
+ perspective, is equivalent to the repository being deleted. The
+ advantage is, archived repositories can be later recovered if, needed,
+ just by moving them back to their former name and location.
+ The target folder used to archive the repositories can be set by
+ [archiveFolder](#archiveFolder). Archived repositories are moved
+ under the [archiveFolder](#archiveFolder) and renamed to add a
+ timestamp and the %archived% suffix to the original name.
+ Archived repositories are kept in the archive for a time period which
+ can be set by [deleteArchivedReposAfter](#deleteArchivedReposAfter).
+
+ If this option is enabled, the project will not be deleted but rather
+ renamed and moved into the archive folder.
+
+ If the repository has been archived for a time period longer than
+ [deleteArchivedReposAfter](#deleteArchivedReposAfter), it will be
+ deleted from the archive by a periodic task which runs once a day.
+
+ By default false.
+
+<a id="archiveFolder">
+`plugin.@PLUGIN@.archiveFolder`
+: The absolute path of the archive folder to store archived repositories.
+
+ The git repository is archived to this target folder only if
+ [archiveDeletedRepos](#archiveDeletedRepos) is set to true.
+
+ By default `$site_path/data/delete-project`.
+
+<a id="deleteArchivedReposAfter">
+`plugin.@PLUGIN@.deleteArchivedReposAfter`
+: The time duration for the git repository to be archived.
+
+ The following suffixes are supported to define the time unit:\n
+ 1. d, day, days\n
+ 2. w, week, weeks (1 week is treated as 7 days)\n
+ 3. mon, month, months (1 month is treated as 30 days)\n
+ 4. y, year, years (1 year is treated as 365 days)\n
+
+ If not specified, the default time unit is in days.
+
+ The project git repository is archived to this target folder only if
+ [archiveDeletedRepos](#archiveDeletedRepos) is set to true.
+
+ If [archiveDeletedRepos](#archiveDeletedRepos) is set to true but this
+ option is set to zero, the periodic task will not be executed and the
+ archived repositories need to be deleted manually or using an external
+ task.
+
+ By default 180 (days).
diff --git a/src/main/resources/static/delete-project-with-preserve-disabled.js b/src/main/resources/static/delete-project-with-preserve-disabled.js
new file mode 100644
index 0000000..da4478d
--- /dev/null
+++ b/src/main/resources/static/delete-project-with-preserve-disabled.js
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 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.
+
+Gerrit.install(function(self) {
+ function onDeleteProject(c) {
+ var f = c.checkbox();
+ var b = c.button('Delete',
+ {onclick: function(){
+ c.call(
+ {force: f.checked, preserve: false},
+ function(r) {
+ c.hide();
+ window.alert('The project: "'
+ + c.project
+ + '" was deleted.'),
+ Gerrit.go('/admin/projects/');
+ });
+ }});
+ c.popup(c.div(
+ c.msg('Are you really sure you want to delete the project: "'
+ + c.project
+ + '"?'),
+ c.br(),
+ c.label(f, 'Delete project even if open changes exist?'),
+ c.br(),
+ b));
+ }
+ self.onAction('project', 'delete', onDeleteProject);
+ });
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ConfigurationTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ConfigurationTest.java
index 3d069ef..8686916 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ConfigurationTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ConfigurationTest.java
@@ -19,30 +19,53 @@
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
+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.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ConfigurationTest {
+ private static final long DEFAULT_ARCHIVE_DURATION_MS = TimeUnit.DAYS.toMillis(180);
+ private static final String CUSTOM_DURATION = "100";
private static final String CUSTOM_PARENT = "customParent";
+ private static final String INVALID_CUSTOM_FOLDER = "\0";
+ private static final String INVALID_ARCHIVE_DURATION = "180weeks180years";
private static final String PLUGIN_NAME = "delete-project";
@Mock private PluginConfigFactory pluginConfigFactoryMock;
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+ private Path customArchiveFolder;
+ private File pluginDataDir;
private Configuration deleteConfig;
+ @Before
+ public void setUp() throws Exception {
+ pluginDataDir = tempFolder.newFolder("data");
+ customArchiveFolder = tempFolder.newFolder("archive").toPath();
+ }
+
@Test
public void defaultValuesAreLoaded() {
when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME))
.thenReturn(new PluginConfig(PLUGIN_NAME, new Config()));
- deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginDataDir);
assertThat(deleteConfig.getDeletedProjectsParent()).isEqualTo("Deleted-Projects");
assertThat(deleteConfig.deletionWithTagsAllowed()).isTrue();
assertThat(deleteConfig.projectOnPreserveHidden()).isFalse();
+ assertThat(deleteConfig.shouldArchiveDeletedRepos()).isFalse();
+ assertThat(deleteConfig.getArchiveDuration()).isEqualTo(DEFAULT_ARCHIVE_DURATION_MS);
+ assertThat(deleteConfig.getArchiveFolder().toString()).isEqualTo(pluginDataDir.toString());
+ assertThat(deleteConfig.enablePreserveOption()).isTrue();
}
@Test
@@ -51,11 +74,55 @@
pluginConfig.setString("parentForDeletedProjects", CUSTOM_PARENT);
pluginConfig.setBoolean("allowDeletionOfReposWithTags", false);
pluginConfig.setBoolean("hideProjectOnPreserve", true);
+ pluginConfig.setBoolean("archiveDeletedRepos", true);
+ pluginConfig.setBoolean("enablePreserveOption", false);
+ pluginConfig.setString("deleteArchivedReposAfter", CUSTOM_DURATION);
+ pluginConfig.setString("archiveFolder", customArchiveFolder.toString());
+
when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
- deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginDataDir);
assertThat(deleteConfig.getDeletedProjectsParent()).isEqualTo(CUSTOM_PARENT);
assertThat(deleteConfig.deletionWithTagsAllowed()).isFalse();
assertThat(deleteConfig.projectOnPreserveHidden()).isTrue();
+ assertThat(deleteConfig.shouldArchiveDeletedRepos()).isTrue();
+ assertThat(deleteConfig.enablePreserveOption()).isFalse();
+ assertThat(deleteConfig.getArchiveDuration()).isEqualTo(Long.parseLong(CUSTOM_DURATION));
+ assertThat(deleteConfig.getArchiveFolder().toString())
+ .isEqualTo(customArchiveFolder.toString());
+ }
+
+ @Test
+ public void archiveDurationWithUnitIsLoaded() {
+ PluginConfig pluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
+ pluginConfig.setString("deleteArchivedReposAfter", CUSTOM_DURATION + "years");
+
+ when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginDataDir);
+
+ assertThat(deleteConfig.getArchiveDuration())
+ .isEqualTo(TimeUnit.DAYS.toMillis(Long.parseLong(CUSTOM_DURATION)) * 365);
+ }
+
+ @Test
+ public void invalidArchiveDuration() {
+ PluginConfig pluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
+ pluginConfig.setString("deleteArchivedReposAfter", INVALID_ARCHIVE_DURATION);
+
+ when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginDataDir);
+
+ assertThat(deleteConfig.getArchiveDuration()).isEqualTo(DEFAULT_ARCHIVE_DURATION_MS);
+ }
+
+ @Test
+ public void invalidTargetArchiveFolder() {
+ PluginConfig pluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
+ pluginConfig.setString("archiveFolder", INVALID_CUSTOM_FOLDER);
+
+ when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginDataDir);
+
+ assertThat(deleteConfig.getArchiveFolder().toString()).isEqualTo(pluginDataDir.toString());
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
new file mode 100644
index 0000000..af3ff0d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
@@ -0,0 +1,324 @@
+// Copyright (C) 2018 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 com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
+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.util.stream.Stream;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@UseSsh
+@TestPlugin(
+ name = "delete-project",
+ sysModule = "com.googlesource.gerrit.plugins.deleteproject.Module",
+ sshModule = "com.googlesource.gerrit.plugins.deleteproject.SshModule",
+ httpModule = "com.googlesource.gerrit.plugins.deleteproject.HttpModule")
+public class DeleteProjectIT extends LightweightPluginDaemonTest {
+
+ private static final String PLUGIN = "delete-project";
+ private static final String ARCHIVE_FOLDER = "archiveFolder";
+ private static final String PARENT_FOLDER = "parentFolder";
+
+ private File archiveFolder;
+ private File projectDir;
+
+ @Before
+ public void setUpArchiveFolder() throws IOException {
+ archiveFolder = Files.createDirectories(Paths.get(ARCHIVE_FOLDER)).toFile();
+ projectDir = verifyProjectRepoExists(project);
+ }
+
+ @After
+ public void removeArchiveFolder() {
+ FileUtils.deleteQuietly(archiveFolder);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testHttpDeleteProjectForce() throws Exception {
+ RestResponse r = httpDeleteProjectHelper(true);
+ r.assertNoContent();
+ assertThat(projectDir.exists()).isFalse();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testHttpDeleteProjectNotForce() throws Exception {
+ createChange();
+ RestResponse r = httpDeleteProjectHelper(false);
+ r.assertConflict();
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testHttpDeleteProjectWithWatches() throws Exception {
+ watch(project.get(), null);
+ RestResponse r = httpDeleteProjectHelper(true);
+ r.assertNoContent();
+ assertThat(projectDir.exists()).isFalse();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testSshDeleteProjectWithoutOptions() throws Exception {
+ createChange();
+ String cmd = Joiner.on(" ").join(PLUGIN, "delete", project.get());
+ String expected =
+ String.format(
+ "Really delete '%s'?\n"
+ + "This is an operation which permanently deletes data. This cannot be undone!\n"
+ + "If you are sure you wish to delete this project, re-run with the --yes-really-delete flag.\n\n",
+ project.get());
+ adminSshSession.exec(cmd);
+
+ assertThat(projectDir.exists()).isTrue();
+ assertThat(adminSshSession.getError()).isEqualTo(expected);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testSshDeleteProjYesReallyDelete() throws Exception {
+ createChange();
+ String cmd = createDeleteCommand(project.get());
+ String expected =
+ String.format(
+ "Project '%s' has open changes. - To really delete '%s', re-run with the --force flag.%n",
+ project.get(), project.get());
+ adminSshSession.exec(cmd);
+
+ assertThat(projectDir.exists()).isTrue();
+ assertThat(adminSshSession.getError()).isEqualTo(expected);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testSshDeleteProjYesReallyDeleteForce() throws Exception {
+ createChange();
+ String cmd = createDeleteCommand("--force", project.get());
+ adminSshSession.exec(cmd);
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(projectDir.exists()).isFalse();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.enablePreserveOption", value = "true")
+ public void testSshDeleteProjPreserveGitRepoEnabled() throws Exception {
+ String cmd = createDeleteCommand("--preserve-git-repository", project.get());
+ adminSshSession.exec(cmd);
+
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.enablePreserveOption", value = "false")
+ public void testSshDeleteProjPreserveGitRepoNotEnabled() throws Exception {
+ String cmd = createDeleteCommand("--preserve-git-repository", project.get());
+ adminSshSession.exec(cmd);
+ String expected =
+ "Given the enablePreserveOption is configured to be false, "
+ + "the --preserve-git-repository option is not allowed.\n"
+ + "Please remove this option and retry.\n";
+ assertThat(adminSshSession.getError()).isEqualTo(expected);
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.hideProjectOnPreserve", value = "true")
+ public void testSshHideProject() throws Exception {
+ String cmd = createDeleteCommand("--preserve-git-repository", project.get());
+ adminSshSession.exec(cmd);
+
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ ProjectState state = cfg.getProject().getState();
+
+ assertThat(state).isEqualTo(ProjectState.HIDDEN);
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testDeleteProjWithChildren() throws Exception {
+ String childrenString = createProject("foo", project, true).get();
+ verifyProjectRepoExists(Project.NameKey.parse(childrenString));
+
+ String cmd = createDeleteCommand(project.get());
+ adminSshSession.exec(cmd);
+ assertThat(adminSshSession.getError())
+ .isEqualTo(
+ "fatal: Cannot delete project because it has children: " + childrenString + "\n");
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ public void testDeleteAllProject() throws Exception {
+ String name = allProjects.get();
+ String cmd = createDeleteCommand(name);
+ adminSshSession.exec(cmd);
+
+ assertThat(adminSshSession.getError())
+ .isEqualTo("fatal: Cannot delete project because it is protected against deletion" + "\n");
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.allowDeletionOfReposWithTags", value = "false")
+ public void testDeleteProjWithTags() throws Exception {
+ grant(Permission.CREATE, project, "refs/tags/*", false, REGISTERED_USERS);
+ pushTagOldCommitNotForce(Status.OK);
+
+ String cmd = createDeleteCommand(project.get());
+ adminSshSession.exec(cmd);
+ assertThat(adminSshSession.getError())
+ .isEqualTo("fatal: Project " + project.get() + " has tags" + "\n");
+ assertThat(projectDir.exists()).isTrue();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.archiveDeletedRepos", value = "true")
+ @GerritConfig(name = "plugin.delete-project.archiveFolder", value = ARCHIVE_FOLDER)
+ public void testArchiveProject() throws Exception {
+ assertThat(archiveFolder.exists()).isTrue();
+ assertThat(isEmpty(archiveFolder.toPath())).isTrue();
+
+ String cmd = createDeleteCommand(project.get());
+ adminSshSession.exec(cmd);
+
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(isEmpty(archiveFolder.toPath())).isFalse();
+ assertThat(containsDeletedProject(archiveFolder.toPath(), project.get())).isTrue();
+ assertThat(projectDir.exists()).isFalse();
+ }
+
+ @Test
+ @UseLocalDisk
+ @GerritConfig(name = "plugin.delete-project.archiveDeletedRepos", value = "true")
+ @GerritConfig(name = "plugin.delete-project.archiveFolder", value = ARCHIVE_FOLDER)
+ public void testDeleteAndArchiveProjectWithParentFolder() throws Exception {
+ assertThat(archiveFolder.exists()).isTrue();
+ assertThat(isEmpty(archiveFolder.toPath())).isTrue();
+
+ String name = "pj1";
+ String projectName = createProject(name).get();
+ File projectDir = verifyProjectRepoExists(Project.NameKey.parse(projectName));
+
+ Path parentFolder = projectDir.toPath().getParent().resolve(PARENT_FOLDER).resolve(projectName);
+ parentFolder.toFile().mkdirs();
+ assertThat(parentFolder.toFile().exists()).isTrue();
+ assertThat(isEmpty(parentFolder)).isTrue();
+
+ Files.move(projectDir.toPath(), parentFolder, REPLACE_EXISTING);
+ assertThat(parentFolder.toFile().exists()).isTrue();
+ assertThat(isEmpty(parentFolder)).isFalse();
+
+ String cmd = createDeleteCommand(PARENT_FOLDER + "/" + projectName);
+ adminSshSession.exec(cmd);
+
+ assertThat(adminSshSession.getError()).isNull();
+ assertThat(isEmpty(archiveFolder.toPath())).isFalse();
+ assertThat(containsDeletedProject(archiveFolder.toPath().resolve(PARENT_FOLDER), name))
+ .isTrue();
+ assertThat(projectDir.exists()).isFalse();
+
+ assertThat(parentFolder.toFile().exists()).isFalse();
+ }
+
+ private File verifyProjectRepoExists(Project.NameKey name) throws IOException {
+ File projectDir;
+ try (Repository projectRepo = repoManager.openRepository(name)) {
+ projectDir = projectRepo.getDirectory();
+ }
+ assertThat(projectDir.exists()).isTrue();
+ return projectDir;
+ }
+
+ private RestResponse httpDeleteProjectHelper(boolean force) throws Exception {
+ setApiUser(user);
+ sender.clear();
+ String endPoint = "/projects/" + project.get() + "/delete-project~delete";
+ Input i = new Input();
+ i.force = force;
+
+ return adminRestSession.post(endPoint, i);
+ }
+
+ private String createDeleteCommand(String cmd, String... params) {
+ return Joiner.on(" ")
+ .join(PLUGIN, "delete", "--yes-really-delete", cmd, Joiner.on(" ").join(params));
+ }
+
+ private String pushTagOldCommitNotForce(Status expectedStatus) throws Exception {
+ testRepo = cloneProject(project, user);
+ commitBuilder().ident(user.getIdent()).message("subject (" + System.nanoTime() + ")").create();
+ String tagName = MoreObjects.firstNonNull(null, "v1_" + System.nanoTime());
+
+ grant(Permission.SUBMIT, project, "refs/for/refs/heads/master", false, REGISTERED_USERS);
+ pushHead(testRepo, "refs/for/master%submit");
+
+ String tagRef = RefNames.REFS_TAGS + tagName;
+ PushResult r = pushHead(testRepo, tagRef, false, false);
+ RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
+ assertThat(refUpdate.getStatus()).named("LIGHTWEIGHT").isEqualTo(expectedStatus);
+ return tagName;
+ }
+
+ private boolean isEmpty(Path dir) throws IOException {
+ try (Stream<Path> dirStream = Files.list(dir)) {
+ return !dirStream.iterator().hasNext();
+ }
+ }
+
+ private boolean containsDeletedProject(Path dir, String projectName) throws IOException {
+ try (Stream<Path> dirStream = Files.list(dir)) {
+ return dirStream.anyMatch(d -> d.toString().contains(projectName));
+ }
+ }
+}
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 9133f64..3b18ba3 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProtectedProjectsTest.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.config.AllUsersNameProvider;
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;
@@ -44,6 +45,7 @@
private PluginConfig pluginConfig;
private Configuration deleteConfig;
private ProtectedProjects protectedProjects;
+ private File pluginData = new File("data");
@Before
public void setup() throws Exception {
@@ -51,7 +53,7 @@
when(allUsersMock.get()).thenReturn(new AllUsersName("All-Users"));
pluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
- deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginData);
protectedProjects = new ProtectedProjects(allProjectsMock, allUsersMock, deleteConfig);
}
@@ -75,7 +77,7 @@
List<String> projects = ImmutableList.of("Custom-Parent", "^protected-.*");
pluginConfig.setStringList("protectedProject", projects);
when(pluginConfigFactoryMock.getFromGerritConfig(PLUGIN_NAME)).thenReturn(pluginConfig);
- deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME);
+ deleteConfig = new Configuration(pluginConfigFactoryMock, PLUGIN_NAME, pluginData);
assertThat(deleteConfig.protectedProjects()).hasSize(projects.size());
protectedProjects = new ProtectedProjects(allProjectsMock, allUsersMock, deleteConfig);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemoverTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemoverTest.java
new file mode 100644
index 0000000..9c26878
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/ArchiveRepositoryRemoverTest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2018 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 java.util.concurrent.TimeUnit.SECONDS;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Executor;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.deleteproject.Configuration;
+import com.googlesource.gerrit.plugins.deleteproject.TimeMachine;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+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.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ArchiveRepositoryRemoverTest {
+
+ private static final long ARCHIVE_DURATION = 1;
+ private static final long CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(1);
+ private static final int NUMBER_OF_REPOS = 10;
+ private static final String PLUGIN_NAME = "delete-project";
+
+ @Mock private Executor executorMock;
+ @Mock private ScheduledFuture<?> scheduledFutureMock;
+ @Mock private WorkQueue workQueueMock;
+ @Mock private Provider<RepositoryCleanupTask> cleanupTaskProviderMock;
+ @Mock private Configuration configMock;
+
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private ArchiveRepositoryRemover remover;
+ private Path archiveRepo;
+
+ @Before
+ public void setUp() throws Exception {
+ when(cleanupTaskProviderMock.get()).thenReturn(new RepositoryCleanupTask(null, null));
+ when(workQueueMock.getDefaultQueue()).thenReturn(executorMock);
+ doReturn(scheduledFutureMock)
+ .when(executorMock)
+ .scheduleAtFixedRate(
+ isA(RepositoryCleanupTask.class), anyLong(), anyLong(), isA(TimeUnit.class));
+ remover = new ArchiveRepositoryRemover(workQueueMock, cleanupTaskProviderMock);
+ archiveRepo = tempFolder.newFolder("archive").toPath();
+ when(configMock.getArchiveFolder()).thenReturn(archiveRepo);
+ when(configMock.getArchiveDuration()).thenReturn(ARCHIVE_DURATION);
+ }
+
+ @Test
+ public void cleanUpOverdueRepositoriesTest() throws IOException {
+ setupArchiveFolder();
+ try {
+ TimeMachine.useFixedClockAt(
+ Instant.ofEpochMilli(Files.getLastModifiedTime(archiveRepo).toMillis())
+ .plusMillis(TimeUnit.DAYS.toMillis(ARCHIVE_DURATION) + 10));
+
+ RepositoryCleanupTask task = new RepositoryCleanupTask(configMock, PLUGIN_NAME);
+ task.run();
+ assertThat(task.toString())
+ .isEqualTo(
+ String.format(
+ "[%s]: Clean up expired git repositories from the archive [%s]",
+ PLUGIN_NAME, archiveRepo));
+ assertThat(isDirEmpty(archiveRepo)).isTrue();
+ } finally {
+ TimeMachine.useSystemDefaultZoneClock();
+ }
+ }
+
+ @Test
+ public void testRepositoryCleanupTaskIsScheduledOnStart() {
+ remover.start();
+ verify(executorMock, times(1))
+ .scheduleAtFixedRate(
+ isA(RepositoryCleanupTask.class),
+ eq(SECONDS.toMillis(1)),
+ eq(CLEANUP_INTERVAL),
+ eq(TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testRepositoryCleanupTaskIsCancelledOnStop() {
+ remover.start();
+ remover.stop();
+ verify(scheduledFutureMock, times(1)).cancel(true);
+ }
+
+ private void setupArchiveFolder() throws IOException {
+ for (int i = 0; i < NUMBER_OF_REPOS; i++) {
+ createRepository("Repo_" + i);
+ }
+ assertThat(isDirEmpty(archiveRepo)).isFalse();
+ }
+
+ private FileRepository createRepository(String repoName) throws IOException {
+ Path repoPath = Files.createDirectories(archiveRepo.resolve(repoName));
+ Repository repository = new FileRepository(repoPath.toFile());
+ repository.create(true);
+ return (FileRepository) repository;
+ }
+
+ private boolean isDirEmpty(final Path dir) throws IOException {
+ try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dir)) {
+ return !dirStream.iterator().hasNext();
+ }
+ }
+}
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 21880c5..c8039f7 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
@@ -21,9 +21,13 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Project;
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 org.junit.Before;
@@ -39,6 +43,7 @@
@Mock private GitRepositoryManager repoManager;
@Mock private ProjectDeletedListener projectDeleteListener;
+ @Mock private Configuration config;
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
@@ -60,7 +65,8 @@
Project.NameKey nameKey = new Project.NameKey(repoName);
Project project = new Project(nameKey);
when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener);
+ when(config.shouldArchiveDeletedRepos()).thenReturn(false);
+ fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
fsDeleteHandler.delete(project, false);
assertThat(repository.getDirectory().exists()).isFalse();
}
@@ -72,7 +78,7 @@
Project.NameKey nameKey = new Project.NameKey(repoName);
Project project = new Project(nameKey);
when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener);
+ fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
fsDeleteHandler.delete(project, false);
assertThat(repository.getDirectory().exists()).isFalse();
}
@@ -88,7 +94,7 @@
Project.NameKey nameKey = new Project.NameKey(repoToDeleteName);
Project project = new Project(nameKey);
when(repoManager.openRepository(nameKey)).thenReturn(repoToDelete);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener);
+ fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
fsDeleteHandler.delete(project, false);
assertThat(repoToDelete.getDirectory().exists()).isFalse();
assertThat(repoToKeep.getDirectory().exists()).isTrue();
@@ -101,7 +107,7 @@
Project.NameKey nameKey = new Project.NameKey(repoName);
Project project = new Project(nameKey);
when(repoManager.openRepository(nameKey)).thenReturn(repository);
- fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener);
+ fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
fsDeleteHandler.delete(project, true);
assertThat(repository.getDirectory().exists()).isTrue();
}
@@ -112,4 +118,29 @@
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 = new Project.NameKey(repoName);
+ Project project = new Project(nameKey);
+ when(repoManager.openRepository(nameKey)).thenReturn(repository);
+ fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
+ fsDeleteHandler.delete(project, 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);
+ }
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/TrashFolderNameMatcherTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/TrashFolderNameMatcherTest.java
index 6ce8206..102d8d1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/TrashFolderNameMatcherTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/TrashFolderNameMatcherTest.java
@@ -30,6 +30,10 @@
matches("a.1234567890123.%deleted%.git");
matches("aa.1234567890123.%deleted%.git");
matches("a.b.c.1234567890123.%deleted%.git");
+
+ matches("a.20181010120101.%deleted%.git");
+ matches("aa.20181010120101.%deleted%.git");
+ matches("a.b.c.20181010120101.%deleted%.git");
}
@Test
@@ -47,10 +51,13 @@
// missing .git suffix
doesNotMatch("a.1234567890123.%deleted%");
+ doesNotMatch("a.20181010120101.%deleted%");
// additional characters after the "git" suffix
doesNotMatch("a.1234567890123.%deleted%.git.");
doesNotMatch("a.1234567890123.%deleted%.git.git");
+ doesNotMatch("a.20181010120101.%deleted%.git.");
+ doesNotMatch("a.20181010120101.%deleted%.git.git");
}
private void matches(String name) {