Merge branch 'stable-2.14' into stable-2.15

* stable-2.14:
  Align Eclipse compiler settings with core Gerrit's
  WORKSPACE: Make commented out lines spaces indent consistent
  Add IT test
  FilesystemDeleteHandler: Use more readable name for trash folder
  Configure whether to allow user to select preserve
  Introduce an archiving mechanism for deleted repos
  bazlets: Replace native.git_repository with skylark rule
  Harmonize external dependency names to use hyphen
  Upgrade mockito to 2.23.0
  Update bazlets to latest stable-2.14 to build with 2.14.15 API
  Update bazlets to latest stable-2.14 to build with 2.14.14 API
  Update bazlets to latest stable-2.14 to build with 2.14.13 API
  FilesystemDeleteHandler: Don't instantiate event if there are no listeners
  FilesystemDeleteHandler: Rename deletedListener to deletedListeners
  DatabaseDeleteHandler.java: Remove unused Provider<InternalChangeQuery>
  Add RecursiveDeleteOption.ALLOW_INSECURE for recursive deletion
  Simplify DeleteTrashFolder
  DeleteTrashFolders: Extract predicate to its own class
  Add unit test for DeleteTrashFolders
  Update bazlets to latest stable-2.14 to use 2.14.12 API
  Migrate `tools/bazel.rc` to `.bazelrc`
  Tidy up FilesystemDeleteHandler
  Add unit tests for FilesystemDeleteHandler
  Extract deletion preconditions to its own class

Change-Id: Iba2e175783cf18ace0329efe81fc417640f77aa0
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/WORKSPACE b/WORKSPACE
index a9f0710..7442807 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "b54eaed487d37188120da6933b97c571519954ca",
+    commit = "6d3fd710ea4ca44805bb8c858d72b53bd3a500f3",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 7efc19c..bd864bf 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -3,27 +3,27 @@
 def external_plugin_deps():
     maven_jar(
         name = "mockito",
-        artifact = "org.mockito:mockito-core:2.16.0",
-        sha1 = "a022ee494c753789a1e7cae75099de81d8a5cea6",
+        artifact = "org.mockito:mockito-core:2.23.0",
+        sha1 = "497ddb32fd5d01f9dbe99a2ec790aeb931dff1b1",
         deps = [
-            "@byte_buddy//jar",
-            "@byte_buddy_agent//jar",
+            "@byte-buddy//jar",
+            "@byte-buddy-agent//jar",
             "@objenesis//jar",
         ],
     )
 
-    BYTE_BUDDY_VERSION = "1.7.9"
+    BYTE_BUDDY_VERSION = "1.9.0"
 
     maven_jar(
-        name = "byte_buddy",
+        name = "byte-buddy",
         artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
-        sha1 = "51218a01a882c04d0aba8c028179cce488bbcb58",
+        sha1 = "8cb0d5baae526c9df46ae17693bbba302640538b",
     )
 
     maven_jar(
-        name = "byte_buddy_agent",
+        name = "byte-buddy-agent",
         artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
-        sha1 = "a6c65f9da7f467ee1f02ff2841ffd3155aee2fc9",
+        sha1 = "37b5703b4a6290be3fffc63ae9c6bcaaee0ff856",
     )
 
     maven_jar(
@@ -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/DeleteAction.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
index 19ce6e3..040f7e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
@@ -14,18 +14,15 @@
 
 package com.googlesource.gerrit.plugins.deleteproject;
 
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 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 com.googlesource.gerrit.plugins.deleteproject.projectconfig.ProjectConfigDeleteHandler;
 
 public class DeleteAction extends DeleteProject implements UiAction<ProjectResource> {
   private final ProtectedProjects protectedProjects;
@@ -36,25 +33,21 @@
       DatabaseDeleteHandler dbHandler,
       FilesystemDeleteHandler fsHandler,
       CacheDeleteHandler cacheHandler,
-      ProjectConfigDeleteHandler pcHandler,
       Provider<CurrentUser> userProvider,
-      @PluginName String pluginName,
       DeleteLog deleteLog,
+      DeletePreconditions preConditions,
       Configuration cfg,
       HideProject hideProject,
-      PermissionBackend permissionBackend,
       NotesMigration migration) {
     super(
         dbHandler,
         fsHandler,
         cacheHandler,
-        pcHandler,
         userProvider,
-        pluginName,
         deleteLog,
+        preConditions,
         cfg,
         hideProject,
-        permissionBackend,
         migration);
     this.protectedProjects = protectedProjects;
   }
@@ -68,6 +61,6 @@
                 ? String.format("Not allowed to delete %s", rsrc.getName())
                 : String.format("Delete project %s", rsrc.getName()))
         .setEnabled(!protectedProjects.isProtected(rsrc))
-        .setVisible(canDelete(rsrc));
+        .setVisible(preConditions.canDelete(rsrc));
   }
 }
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 f7124e4..c6a0f1f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteCommand.java
@@ -24,12 +24,19 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.util.Collection;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
 @CommandMetaData(name = "delete", description = "Delete specific project")
 public final class DeleteCommand extends SshCommand {
+  private static final String FORCE_DELETE =
+      "%s - To really delete '%s', re-run with the --force flag.";
+  private static final String REALLY_DELETE =
+      "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";
+
   @Argument(index = 0, required = true, metaVar = "NAME", usage = "project to delete")
   private ProjectControl projectControl;
 
@@ -42,56 +49,48 @@
   @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) {
+  protected DeleteCommand(
+      Configuration cfg, DeleteProject deleteProject, DeletePreconditions preConditions) {
+    this.cfg = cfg;
     this.deleteProject = deleteProject;
+    this.preConditions = preConditions;
   }
 
   @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;
 
       ProjectResource rsrc = new ProjectResource(projectControl);
-      deleteProject.assertDeletePermission(rsrc);
-      deleteProject.assertCanDelete(rsrc, input);
+      preConditions.assertDeletePermission(rsrc);
 
       if (!yesReallyDelete) {
-        StringBuilder msgBuilder = new StringBuilder();
-        msgBuilder.append("Really delete ");
-        msgBuilder.append(rsrc.getName());
-        msgBuilder.append("?\n");
-        msgBuilder.append("This is an operation which permanently deletes ");
-        msgBuilder.append("data. This cannot be undone!\n");
-        msgBuilder.append("If you are sure you wish to delete this project, ");
-        msgBuilder.append("re-run\n");
-        msgBuilder.append("with the --yes-really-delete flag.\n");
-        throw new UnloggedFailure(msgBuilder.toString());
+        throw new UnloggedFailure(String.format(REALLY_DELETE, rsrc.getName()));
       }
 
       if (!force) {
-        Collection<String> warnings = deleteProject.getWarnings(rsrc);
-        if (warnings != null && !warnings.isEmpty()) {
-          StringBuilder msgBuilder = new StringBuilder();
-          msgBuilder.append("There are warnings against deleting ");
-          msgBuilder.append(rsrc.getName());
-          msgBuilder.append(":\n");
-          for (String warning : warnings) {
-            msgBuilder.append(" * ");
-            msgBuilder.append(warning);
-            msgBuilder.append("\n");
-          }
-          msgBuilder.append("To really delete ");
-          msgBuilder.append(rsrc.getName());
-          msgBuilder.append(", re-run with the --force flag.");
-          throw new UnloggedFailure(msgBuilder.toString());
+        try {
+          preConditions.assertHasOpenChanges(rsrc.getNameKey(), false);
+        } catch (CannotDeleteProjectException e) {
+          throw new UnloggedFailure(String.format(FORCE_DELETE, e.getMessage(), rsrc.getName()));
         }
       }
 
+      preConditions.assertCanBeDeleted(rsrc, input);
       deleteProject.doDelete(rsrc, input);
     } catch (AuthException
         | ResourceNotFoundException
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditions.java
new file mode 100644
index 0000000..5543961
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditions.java
@@ -0,0 +1,188 @@
+// 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.googlesource.gerrit.plugins.deleteproject.DeleteOwnProjectCapability.DELETE_OWN_PROJECT;
+import static com.googlesource.gerrit.plugins.deleteproject.DeleteProjectCapability.DELETE_PROJECT;
+import static java.util.stream.Collectors.joining;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOpRepoManager;
+import com.google.gerrit.server.git.SubmoduleException;
+import com.google.gerrit.server.git.SubmoduleOp;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+class DeletePreconditions {
+  private final Configuration config;
+  private final Provider<ListChildProjects> listChildProjectsProvider;
+  private final Provider<MergeOpRepoManager> mergeOpProvider;
+  private final String pluginName;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final GitRepositoryManager repoManager;
+  private final SubmoduleOp.Factory subOpFactory;
+  private final Provider<CurrentUser> userProvider;
+  private final ProtectedProjects protectedProjects;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  public DeletePreconditions(
+      Configuration config,
+      Provider<ListChildProjects> listChildProjectsProvider,
+      Provider<MergeOpRepoManager> mergeOpProvider,
+      @PluginName String pluginName,
+      Provider<InternalChangeQuery> queryProvider,
+      GitRepositoryManager repoManager,
+      SubmoduleOp.Factory subOpFactory,
+      Provider<CurrentUser> userProvider,
+      ProtectedProjects protectedProjects,
+      PermissionBackend permissionBackend) {
+    this.config = config;
+    this.listChildProjectsProvider = listChildProjectsProvider;
+    this.mergeOpProvider = mergeOpProvider;
+    this.pluginName = pluginName;
+    this.queryProvider = queryProvider;
+    this.repoManager = repoManager;
+    this.subOpFactory = subOpFactory;
+    this.userProvider = userProvider;
+    this.protectedProjects = protectedProjects;
+    this.permissionBackend = permissionBackend;
+  }
+
+  void assertDeletePermission(ProjectResource rsrc) throws AuthException {
+    if (!canDelete(rsrc)) {
+      throw new AuthException("not allowed to delete project");
+    }
+  }
+
+  protected boolean canDelete(ProjectResource rsrc) {
+    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider);
+    return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
+        || userPermission.testOrFalse(new PluginPermission(pluginName, DELETE_PROJECT))
+        || (userPermission.testOrFalse(new PluginPermission(pluginName, DELETE_OWN_PROJECT))
+            && rsrc.getControl().isOwner());
+  }
+
+  void assertCanBeDeleted(ProjectResource rsrc, Input input) throws ResourceConflictException {
+    try {
+      protectedProjects.assertIsNotProtected(rsrc);
+      assertHasNoChildProjects(rsrc);
+      Project.NameKey projectNameKey = rsrc.getNameKey();
+      assertIsNotSubmodule(projectNameKey);
+      assertDeleteWithTags(projectNameKey, input != null && input.preserve);
+      assertHasOpenChanges(projectNameKey, input != null && input.force);
+    } catch (CannotDeleteProjectException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+
+  public void assertHasOpenChanges(Project.NameKey projectNameKey, boolean force)
+      throws CannotDeleteProjectException {
+    if (!force) {
+      try {
+        List<ChangeData> openChanges = queryProvider.get().byProjectOpen(projectNameKey);
+        if (!openChanges.isEmpty()) {
+          throw new CannotDeleteProjectException(
+              String.format("Project '%s' has open changes.", projectNameKey.get()));
+        }
+      } catch (OrmException e) {
+        throw new CannotDeleteProjectException(
+            String.format("Unable to verify if '%s' has open changes.", projectNameKey.get()));
+      }
+    }
+  }
+
+  private void assertHasNoChildProjects(ProjectResource rsrc) throws CannotDeleteProjectException {
+    try {
+      List<ProjectInfo> children = listChildProjectsProvider.get().apply(rsrc);
+      if (!children.isEmpty()) {
+        throw new CannotDeleteProjectException(
+            "Cannot delete project because it has children: "
+                + children.stream().map(info -> info.name).collect(joining(",")));
+      }
+    } catch (PermissionBackendException e) {
+      throw new CannotDeleteProjectException(
+          String.format("Unable to verify if '%s' has children projects.", rsrc.getName()));
+    }
+  }
+
+  private void assertIsNotSubmodule(Project.NameKey projectNameKey)
+      throws CannotDeleteProjectException {
+    try (Repository repo = repoManager.openRepository(projectNameKey);
+        MergeOpRepoManager mergeOp = mergeOpProvider.get()) {
+      Set<Branch.NameKey> branches = new HashSet<>();
+      for (Ref ref : repo.getRefDatabase().getRefs(RefNames.REFS_HEADS).values()) {
+        branches.add(new Branch.NameKey(projectNameKey, ref.getName()));
+      }
+      SubmoduleOp sub = subOpFactory.create(branches, mergeOp);
+      for (Branch.NameKey b : branches) {
+        if (!sub.superProjectSubscriptionsForSubmoduleBranch(b).isEmpty()) {
+          throw new CannotDeleteProjectException("Project is subscribed by other projects.");
+        }
+      }
+    } catch (RepositoryNotFoundException e) {
+      // we're trying to delete the repository,
+      // so this exception should not stop us
+    } catch (IOException | SubmoduleException e) {
+      throw new CannotDeleteProjectException("Project is subscribed by other projects.");
+    }
+  }
+
+  private void assertDeleteWithTags(Project.NameKey projectNameKey, boolean preserveGitRepository)
+      throws CannotDeleteProjectException {
+    if (!preserveGitRepository && !config.deletionWithTagsAllowed()) {
+      assertHasNoTags(projectNameKey);
+    }
+  }
+
+  private void assertHasNoTags(Project.NameKey projectNameKey) throws CannotDeleteProjectException {
+    try (Repository repo = repoManager.openRepository(projectNameKey)) {
+      if (!repo.getRefDatabase().getRefs(Constants.R_TAGS).isEmpty()) {
+        throw new CannotDeleteProjectException(
+            String.format("Project %s has tags", projectNameKey));
+      }
+    } catch (IOException e) {
+      throw new CannotDeleteProjectException(e);
+    }
+  }
+}
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 472198e..58844eb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
@@ -14,11 +14,6 @@
 
 package com.googlesource.gerrit.plugins.deleteproject;
 
-import static com.googlesource.gerrit.plugins.deleteproject.DeleteOwnProjectCapability.DELETE_OWN_PROJECT;
-import static com.googlesource.gerrit.plugins.deleteproject.DeleteProjectCapability.DELETE_PROJECT;
-
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -28,8 +23,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -39,9 +32,7 @@
 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 com.googlesource.gerrit.plugins.deleteproject.projectconfig.ProjectConfigDeleteHandler;
 import java.io.IOException;
-import java.util.Collection;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
 @Singleton
@@ -51,16 +42,15 @@
     boolean force;
   }
 
+  protected final DeletePreconditions preConditions;
+
   private final DatabaseDeleteHandler dbHandler;
   private final FilesystemDeleteHandler fsHandler;
   private final CacheDeleteHandler cacheHandler;
-  private final ProjectConfigDeleteHandler pcHandler;
   private final Provider<CurrentUser> userProvider;
-  private final String pluginName;
   private final DeleteLog deleteLog;
   private final Configuration cfg;
   private final HideProject hideProject;
-  private PermissionBackend permissionBackend;
   private NotesMigration migration;
 
   @Inject
@@ -68,24 +58,20 @@
       DatabaseDeleteHandler dbHandler,
       FilesystemDeleteHandler fsHandler,
       CacheDeleteHandler cacheHandler,
-      ProjectConfigDeleteHandler pcHandler,
       Provider<CurrentUser> userProvider,
-      @PluginName String pluginName,
       DeleteLog deleteLog,
+      DeletePreconditions preConditions,
       Configuration cfg,
       HideProject hideProject,
-      PermissionBackend permissionBackend,
       NotesMigration migration) {
     this.dbHandler = dbHandler;
     this.fsHandler = fsHandler;
     this.cacheHandler = cacheHandler;
-    this.pcHandler = pcHandler;
     this.userProvider = userProvider;
-    this.pluginName = pluginName;
     this.deleteLog = deleteLog;
+    this.preConditions = preConditions;
     this.cfg = cfg;
     this.hideProject = hideProject;
-    this.permissionBackend = permissionBackend;
     this.migration = migration;
   }
 
@@ -93,49 +79,13 @@
   public Object apply(ProjectResource rsrc, Input input)
       throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
           AuthException {
-    assertDeletePermission(rsrc);
-    assertCanDelete(rsrc, input);
-
-    if (input == null || !input.force) {
-      Collection<String> warnings = getWarnings(rsrc);
-      if (!warnings.isEmpty()) {
-        throw new ResourceConflictException(
-            String.format("Project %s has open changes", rsrc.getName()));
-      }
-    }
+    preConditions.assertDeletePermission(rsrc);
+    preConditions.assertCanBeDeleted(rsrc, input);
 
     doDelete(rsrc, input);
     return Response.none();
   }
 
-  public void assertDeletePermission(ProjectResource rsrc) throws AuthException {
-    if (!canDelete(rsrc)) {
-      throw new AuthException("not allowed to delete project");
-    }
-  }
-
-  protected boolean canDelete(ProjectResource rsrc) {
-    PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider);
-    return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
-        || userPermission.testOrFalse(new PluginPermission(pluginName, DELETE_PROJECT))
-        || (userPermission.testOrFalse(new PluginPermission(pluginName, DELETE_OWN_PROJECT))
-            && rsrc.getControl().isOwner());
-  }
-
-  public void assertCanDelete(ProjectResource rsrc, Input input) throws ResourceConflictException {
-    try {
-      pcHandler.assertCanDelete(rsrc);
-      dbHandler.assertCanDelete(rsrc.getControl().getProject());
-      fsHandler.assertCanDelete(rsrc, input == null ? false : input.preserve);
-    } catch (CannotDeleteProjectException e) {
-      throw new ResourceConflictException(e.getMessage());
-    }
-  }
-
-  public Collection<String> getWarnings(ProjectResource rsrc) throws OrmException {
-    return dbHandler.getWarnings(rsrc.getControl().getProject());
-  }
-
   public void doDelete(ProjectResource rsrc, Input input)
       throws OrmException, IOException, ResourceNotFoundException, ResourceConflictException {
     Project project = rsrc.getControl().getProject();
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 857555e..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,15 +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;
-import com.googlesource.gerrit.plugins.deleteproject.projectconfig.ProjectConfigDeleteHandler;
 
 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,7 +55,13 @@
         .to(DeleteOwnProjectCapability.class);
     bind(DatabaseDeleteHandler.class);
     bind(FilesystemDeleteHandler.class);
-    bind(ProjectConfigDeleteHandler.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/database/DatabaseDeleteHandler.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
index e6d79b6..420a6d5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/database/DatabaseDeleteHandler.java
@@ -16,14 +16,11 @@
 
 import static java.util.Collections.singleton;
 
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.StarredChangesUtil;
@@ -32,35 +29,22 @@
 import com.google.gerrit.server.account.WatchConfig.Accessor;
 import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOpRepoManager;
-import com.google.gerrit.server.git.SubmoduleException;
-import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.googlesource.gerrit.plugins.deleteproject.CannotDeleteProjectException;
 import java.io.IOException;
 import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -68,10 +52,6 @@
   private static final Logger log = LoggerFactory.getLogger(DatabaseDeleteHandler.class);
 
   private final Provider<ReviewDb> dbProvider;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final GitRepositoryManager repoManager;
-  private final SubmoduleOp.Factory subOpFactory;
-  private final Provider<MergeOpRepoManager> ormProvider;
   private final StarredChangesUtil starredChangesUtil;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
   private final ChangeIndexer indexer;
@@ -81,10 +61,6 @@
   @Inject
   public DatabaseDeleteHandler(
       Provider<ReviewDb> dbProvider,
-      Provider<InternalChangeQuery> queryProvider,
-      GitRepositoryManager repoManager,
-      SubmoduleOp.Factory subOpFactory,
-      Provider<MergeOpRepoManager> ormProvider,
       StarredChangesUtil starredChangesUtil,
       DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
       ChangeIndexer indexer,
@@ -93,27 +69,11 @@
     this.accountQueryProvider = accountQueryProvider;
     this.watchConfig = watchConfig;
     this.dbProvider = dbProvider;
-    this.queryProvider = queryProvider;
-    this.repoManager = repoManager;
-    this.subOpFactory = subOpFactory;
-    this.ormProvider = ormProvider;
     this.starredChangesUtil = starredChangesUtil;
     this.accountPatchReviewStore = accountPatchReviewStore;
     this.indexer = indexer;
   }
 
-  public Collection<String> getWarnings(Project project) throws OrmException {
-    Collection<String> ret = Lists.newArrayList();
-
-    // Warn against open changes
-    List<ChangeData> openChanges = queryProvider.get().byProjectOpen(project.getNameKey());
-    if (openChanges.iterator().hasNext()) {
-      ret.add(project.getName() + " has open changes");
-    }
-
-    return ret;
-  }
-
   public void delete(Project project) throws OrmException {
     ReviewDb db = ReviewDbUtil.unwrapDb(dbProvider.get());
     Connection conn = ((JdbcSchema) db).getConnection();
@@ -188,31 +148,6 @@
     }
   }
 
-  public void assertCanDelete(Project project) throws CannotDeleteProjectException {
-
-    Project.NameKey proj = project.getNameKey();
-    try (Repository repo = repoManager.openRepository(proj);
-        MergeOpRepoManager orm = ormProvider.get()) {
-      Set<Branch.NameKey> branches = new HashSet<>();
-      for (Ref ref : repo.getRefDatabase().getRefs(RefNames.REFS_HEADS).values()) {
-        branches.add(new Branch.NameKey(proj, ref.getName()));
-      }
-      SubmoduleOp sub = subOpFactory.create(branches, orm);
-      for (Branch.NameKey b : branches) {
-        if (!sub.superProjectSubscriptionsForSubmoduleBranch(b).isEmpty()) {
-          throw new CannotDeleteProjectException("Project is subscribed by other projects.");
-        }
-      }
-    } catch (RepositoryNotFoundException e) {
-      // we're trying to delete the repository,
-      // so this exception should not stop us
-    } catch (SubmoduleException e) {
-      throw new CannotDeleteProjectException("Project has submodule.");
-    } catch (IOException e) {
-      throw new CannotDeleteProjectException("Project is subscribed by other projects.");
-    }
-  }
-
   public void atomicDelete(ReviewDb db, Project project, List<Change.Id> changeIds)
       throws OrmException {
 
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 14214c4..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
@@ -13,49 +13,69 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.deleteproject.fs;
 
+import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Sets;
+import com.google.common.io.MoreFiles;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitOption;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Set;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class DeleteTrashFolders implements LifecycleListener {
   private static final Logger log = LoggerFactory.getLogger(DeleteTrashFolders.class);
 
-  /**
-   * Search for name which ends with a dot, 13 digits and the string ".deleted". A folder 'f' is
-   * renamed to 'f.<currentTimeMillis>.deleted'. <currentTimeMillis> happens to be exactly 13 digits
-   * for commits created between 2002 (before git was born) and 2285.
-   */
-  private static final Pattern TRASH_1 = Pattern.compile(".*\\.\\d{13}.deleted");
+  static class TrashFolderPredicate {
 
-  /**
-   * New trash folder name format. It adds % chars around the "deleted" string and keeps the ".git"
-   * extension.
-   */
-  private static final Pattern TRASH_2 = Pattern.compile(".*\\.\\d{13}.%deleted%.git");
+    private TrashFolderPredicate() {
+      // Avoid this class being instantiated by using the default empty constructor
+    }
 
-  @VisibleForTesting
-  static final boolean isTrashFolderName(String fName) {
-    return TRASH_1.matcher(fName).matches() || TRASH_2.matcher(fName).matches();
+    /**
+     * Search for name which ends with a dot, 13 digits and the string ".deleted". A folder 'f' is
+     * renamed to 'f.<currentTimeMillis>.deleted'. <currentTimeMillis> happens to be exactly 13
+     * digits for commits created between 2002 (before git was born) and 2285.
+     */
+    private static final Pattern TRASH_1 = Pattern.compile(".*\\.\\d{13}.deleted");
+
+    /**
+     * New trash folder name format. It adds % chars around the "deleted" string and keeps the
+     * ".git" extension.
+     */
+    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()
+          || TRASH_3.matcher(fName).matches();
+    }
+
+    static boolean match(Path dir) {
+      return match(dir.getFileName().toString());
+    }
   }
 
   private Set<Path> repoFolders;
+  private Thread thread;
 
   @Inject
   public DeleteTrashFolders(
@@ -65,70 +85,34 @@
     repoFolders.addAll(repositoryCfg.getAllBasePaths());
   }
 
-  class TrashFolderRemover extends SimpleFileVisitor<Path> {
+  @Override
+  public void start() {
+    thread =
+        new Thread(() -> repoFolders.stream().forEach(this::evaluateIfTrash), "DeleteTrashFolders");
+    thread.start();
+  }
 
-    @Override
-    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
-        throws IOException {
-      String fName = dir.getFileName().toString();
-      if (isTrashFolderName(fName)) {
-        log.warn("Will delete this folder: {}", dir);
-        recursiveDelete(dir);
-        return FileVisitResult.SKIP_SUBTREE;
-      } else if (FileKey.isGitRepository(dir.toFile(), FS.DETECTED)) {
-        // We are in a GITDIR and don't expect trash folders inside GITDIR's.
-        return FileVisitResult.SKIP_SUBTREE;
-      }
-
-      return super.preVisitDirectory(dir, attrs);
-    }
-
-    /**
-     * Recursively delete the specified file and all of its contents.
-     *
-     * @throws IOException
-     */
-    private void recursiveDelete(Path file) throws IOException {
-      Files.walkFileTree(
-          file,
-          new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
-                throws IOException {
-              Files.delete(file);
-              return FileVisitResult.CONTINUE;
-            }
-
-            @Override
-            public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
-              if (e != null) {
-                throw e;
-              }
-              Files.delete(dir);
-              return FileVisitResult.CONTINUE;
-            }
-          });
+  private void evaluateIfTrash(Path folder) {
+    try (Stream<Path> dir = Files.walk(folder, FileVisitOption.FOLLOW_LINKS)) {
+      dir.filter(Files::isDirectory)
+          .filter(TrashFolderPredicate::match)
+          .forEach(this::recursivelyDelete);
+    } catch (IOException e) {
+      log.error("Failed to evaluate {}", folder, e);
     }
   }
 
-  @Override
-  public void start() {
-    new Thread(
-            () -> {
-              for (Path folder : repoFolders) {
-                if (!folder.toFile().exists()) {
-                  log.debug("Base path {} does not exist", folder);
-                  continue;
-                }
-                try {
-                  Files.walkFileTree(folder, new TrashFolderRemover());
-                } catch (IOException e) {
-                  log.warn("Exception while trying to delete trash folders", e);
-                }
-              }
-            },
-            "DeleteTrashFolders")
-        .start();
+  @VisibleForTesting
+  Thread getWorkerThread() {
+    return thread;
+  }
+
+  private void recursivelyDelete(Path folder) {
+    try {
+      MoreFiles.deleteRecursively(folder, ALLOW_INSECURE);
+    } catch (IOException e) {
+      log.error("Failed to delete {}", folder, e);
+    }
   }
 
   @Override
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 31fafea..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,27 +14,27 @@
 
 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;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.deleteproject.CannotDeleteProjectException;
 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.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
 import java.nio.file.StandardCopyOption;
-import java.nio.file.attribute.BasicFileAttributes;
+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.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.slf4j.Logger;
@@ -42,18 +42,20 @@
 
 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> deletedListener;
+  private final DynamicSet<ProjectDeletedListener> deletedListeners;
   private final Configuration config;
 
   @Inject
   public FilesystemDeleteHandler(
       GitRepositoryManager repoManager,
-      DynamicSet<ProjectDeletedListener> deletedListener,
+      DynamicSet<ProjectDeletedListener> deletedListeners,
       Configuration config) {
     this.repoManager = repoManager;
-    this.deletedListener = deletedListener;
+    this.deletedListeners = deletedListeners;
     this.config = config;
   }
 
@@ -61,120 +63,68 @@
       throws IOException, RepositoryNotFoundException {
     // Remove from the jgit cache
     Repository repository = repoManager.openRepository(project.getNameKey());
-    File repoFile = repository.getDirectory();
     cleanCache(repository);
     if (!preserveGitRepository) {
-      deleteGitRepository(project.getNameKey(), repoFile);
-    }
-  }
-
-  public void assertCanDelete(ProjectResource rsrc, boolean preserveGitRepository)
-      throws CannotDeleteProjectException {
-    if (!preserveGitRepository && !config.deletionWithTagsAllowed()) {
-      assertHasNoTags(rsrc);
-    }
-  }
-
-  private void assertHasNoTags(ProjectResource rsrc) throws CannotDeleteProjectException {
-    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
-      if (!repo.getRefDatabase().getRefs(Constants.R_TAGS).isEmpty()) {
-        throw new CannotDeleteProjectException(
-            String.format("Project %s has tags", rsrc.getName()));
-      }
-    } catch (IOException e) {
-      throw new CannotDeleteProjectException(e);
-    }
-  }
-
-  private void deleteGitRepository(final Project.NameKey project, final File repoFile)
-      throws IOException {
-    // Delete the repository from disk
-    Path basePath = getBasePath(repoFile.toPath(), project);
-    Path trash = moveToTrash(repoFile.toPath(), basePath, project);
-    boolean ok = false;
-    try {
-      recursiveDelete(trash);
-      ok = true;
-    } catch (IOException e) {
-      // Only log if delete failed - repo already moved to trash.
-      // Otherwise, listeners are never called.
-      log.warn("Error trying to delete {}", trash, e);
-    }
-
-    // Delete parent folders if they are (now) empty
-    if (ok) {
-      try {
-        recursiveDeleteParent(repoFile.getParentFile(), basePath.toFile());
-      } catch (IOException e) {
-        log.warn("Couldn't delete (empty) parents of {}", repoFile, e);
-      }
-    }
-
-    // Send an event that the repository was deleted
-    ProjectDeletedListener.Event event =
-        new ProjectDeletedListener.Event() {
-          @Override
-          public String getProjectName() {
-            return project.get();
-          }
-
-          @Override
-          public NotifyHandling getNotify() {
-            return NotifyHandling.NONE;
-          }
-        };
-    for (ProjectDeletedListener l : deletedListener) {
-      try {
-        l.onProjectDeleted(event);
-      } catch (RuntimeException e) {
-        log.warn("Failure in ProjectDeletedListener", e);
+      Path repoPath = repository.getDirectory().toPath();
+      String projectName = project.getNameKey().get();
+      if (config.shouldArchiveDeletedRepos()) {
+        archiveGitRepository(projectName, repoPath);
+      } else {
+        deleteGitRepository(projectName, repoPath);
       }
     }
   }
 
-  private Path getBasePath(Path repo, Project.NameKey project) {
-    Path projectPath = Paths.get(project.get());
-    return repo.getRoot()
-        .resolve(repo.subpath(0, repo.getNameCount() - projectPath.getNameCount()));
-  }
-
-  private Path moveToTrash(Path directory, Path basePath, Project.NameKey nameKey)
-      throws IOException {
-    Path trashRepo =
-        basePath.resolve(nameKey.get() + "." + TimeMachine.now().toEpochMilli() + ".%deleted%.git");
-    return Files.move(directory, trashRepo, StandardCopyOption.ATOMIC_MOVE);
-  }
-
-  private void cleanCache(final Repository repository) {
+  private void cleanCache(Repository repository) {
     repository.close();
     RepositoryCache.close(repository);
   }
 
-  /**
-   * Recursively delete the specified file and all of its contents.
-   *
-   * @throws IOException
-   */
-  private void recursiveDelete(Path file) throws IOException {
-    Files.walkFileTree(
-        file,
-        new SimpleFileVisitor<Path>() {
-          @Override
-          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
-              throws IOException {
-            Files.delete(file);
-            return FileVisitResult.CONTINUE;
-          }
+  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);
+    }
+  }
 
-          @Override
-          public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
-            if (e != null) {
-              throw e;
-            }
-            Files.delete(dir);
-            return FileVisitResult.CONTINUE;
-          }
-        });
+  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);
+      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(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);
   }
 
   /**
@@ -182,14 +132,39 @@
    * 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 recursiveDeleteParent(File file, File until) throws IOException {
+  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());
-      recursiveDeleteParent(parent, until);
+      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.warn("Failure in ProjectDeletedListener", e);
+      }
     }
   }
 }
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/DeletePreconditionsTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java
new file mode 100644
index 0000000..b02763d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeletePreconditionsTest.java
@@ -0,0 +1,164 @@
+// 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.googlesource.gerrit.plugins.deleteproject.DeleteOwnProjectCapability.DELETE_OWN_PROJECT;
+import static com.googlesource.gerrit.plugins.deleteproject.DeleteProjectCapability.DELETE_PROJECT;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeOpRepoManager;
+import com.google.gerrit.server.git.SubmoduleOp;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ListChildProjects;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DeletePreconditionsTest {
+  private static final String PLUGIN_NAME = "delete-project";
+  private static final Project.NameKey PROJECT_NAMEKEY = new Project.NameKey("test-project");
+
+  @Mock private Configuration config;
+  @Mock private Provider<ListChildProjects> listChildProjectsProvider;
+  @Mock private Provider<MergeOpRepoManager> mergeOpProvider;
+  @Mock private Provider<InternalChangeQuery> queryProvider;
+  @Mock private GitRepositoryManager repoManager;
+  @Mock private SubmoduleOp.Factory subOpFactory;
+  @Mock private Provider<CurrentUser> userProvider;
+  @Mock private ProtectedProjects protectedProjects;
+  @Mock private ProjectControl control;
+  @Mock private PermissionBackend permissionBackend;
+  @Mock private PermissionBackend.WithUser userPermission;
+
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+
+  private ProjectResource rsrc;
+  private DeletePreconditions preConditions;
+
+  @Before
+  public void setUp() {
+    rsrc = new ProjectResource(control);
+    preConditions =
+        new DeletePreconditions(
+            config,
+            listChildProjectsProvider,
+            mergeOpProvider,
+            PLUGIN_NAME,
+            queryProvider,
+            repoManager,
+            subOpFactory,
+            userProvider,
+            protectedProjects,
+            permissionBackend);
+  }
+
+  @Test
+  public void testUserCanDeleteIfAdmin() {
+    when(permissionBackend.user(userProvider)).thenReturn(userPermission);
+    when(userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)).thenReturn(true);
+    assertThat(preConditions.canDelete(rsrc)).isTrue();
+  }
+
+  @Test
+  public void testUserCanDeleteIfHasDeletePermission() {
+    when(permissionBackend.user(userProvider)).thenReturn(userPermission);
+    when(userPermission.testOrFalse(new PluginPermission(PLUGIN_NAME, DELETE_PROJECT)))
+        .thenReturn(true);
+    assertThat(preConditions.canDelete(rsrc)).isTrue();
+  }
+
+  @Test
+  public void testUserCanDeleteIfIsOwnerAndHasDeleteOwnPermission() {
+    when(permissionBackend.user(userProvider)).thenReturn(userPermission);
+    when(userPermission.testOrFalse(new PluginPermission(PLUGIN_NAME, DELETE_OWN_PROJECT)))
+        .thenReturn(true);
+    when(control.isOwner()).thenReturn(true);
+    assertThat(preConditions.canDelete(rsrc)).isTrue();
+  }
+
+  @Test
+  public void testUserCannotDelete() throws Exception {
+    when(permissionBackend.user(userProvider)).thenReturn(userPermission);
+    expectedException.expect(AuthException.class);
+    expectedException.expectMessage("not allowed to delete project");
+    preConditions.assertDeletePermission(rsrc);
+  }
+
+  @Test
+  public void testIsProtectedSoCannotBeDeleted() throws Exception {
+    doThrow(CannotDeleteProjectException.class).when(protectedProjects).assertIsNotProtected(rsrc);
+    expectedException.expect(ResourceConflictException.class);
+    preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input());
+  }
+
+  @Test
+  public void testHasChildrenSoCannotBeDeleted() throws Exception {
+    doNothing().when(protectedProjects).assertIsNotProtected(rsrc);
+    ListChildProjects childProjects = mock(ListChildProjects.class);
+    when(listChildProjectsProvider.get()).thenReturn(childProjects);
+    when(childProjects.apply(rsrc)).thenReturn(ImmutableList.of(new ProjectInfo()));
+    expectedException.expect(ResourceConflictException.class);
+    expectedException.expectMessage("Cannot delete project because it has children:");
+    preConditions.assertCanBeDeleted(rsrc, new DeleteProject.Input());
+  }
+
+  @Test
+  public void testAssertHasOpenChangesNoForceSet() throws Exception {
+    InternalChangeQuery queryChange = mock(InternalChangeQuery.class);
+    ChangeData cd = mock(ChangeData.class);
+    when(queryChange.byProjectOpen(PROJECT_NAMEKEY)).thenReturn(ImmutableList.of(cd));
+    when(queryProvider.get()).thenReturn(queryChange);
+    String expectedMessage = String.format("Project '%s' has open changes.", PROJECT_NAMEKEY.get());
+    expectedException.expectMessage(expectedMessage);
+    expectedException.expect(CannotDeleteProjectException.class);
+    preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false);
+  }
+
+  @Test
+  public void testUnableToAssertOpenChanges() throws Exception {
+    InternalChangeQuery queryChange = mock(InternalChangeQuery.class);
+    doThrow(OrmException.class).when(queryChange).byProjectOpen(PROJECT_NAMEKEY);
+    when(queryProvider.get()).thenReturn(queryChange);
+    String expectedMessage =
+        String.format("Unable to verify if '%s' has open changes.", PROJECT_NAMEKEY.get());
+    expectedException.expectMessage(expectedMessage);
+    expectedException.expect(CannotDeleteProjectException.class);
+    preConditions.assertHasOpenChanges(PROJECT_NAMEKEY, false);
+  }
+}
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..a68edf8
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProjectIT.java
@@ -0,0 +1,322 @@
+// 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.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());
+    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(project, "refs/tags/*", Permission.CREATE, false, REGISTERED_USERS);
+    pushTagOldCommitNotForce();
+
+    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 void pushTagOldCommitNotForce() throws Exception {
+    testRepo = cloneProject(project, user);
+    commitBuilder().ident(user.getIdent()).message("subject (" + System.nanoTime() + ")").create();
+    String tagName = "v1_" + System.nanoTime();
+
+    grant(project, "refs/for/refs/heads/master", Permission.SUBMIT, 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(Status.OK);
+  }
+
+  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..b1265bc
--- /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.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.ScheduledExecutorService;
+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 ScheduledExecutorService 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/DeleteTrashFoldersTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFoldersTest.java
new file mode 100644
index 0000000..acb7a03
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/DeleteTrashFoldersTest.java
@@ -0,0 +1,73 @@
+// 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 org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.config.RepositoryConfig;
+import com.google.gerrit.server.config.SitePaths;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Config;
+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 DeleteTrashFoldersTest {
+
+  @Mock private RepositoryConfig repositoryCfg;
+
+  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+  private Path basePath;
+  private DeleteTrashFolders trashFolders;
+
+  @Before
+  public void setUp() throws Exception {
+    SitePaths sitePaths = new SitePaths(tempFolder.newFolder("gerrit_site").toPath());
+    basePath = sitePaths.resolve("base");
+    Config cfg = new Config();
+    cfg.setString("gerrit", null, "basePath", basePath.toString());
+    when(repositoryCfg.getAllBasePaths()).thenReturn(ImmutableList.of());
+    trashFolders = new DeleteTrashFolders(sitePaths, cfg, repositoryCfg);
+  }
+
+  @Test
+  public void testStart() throws Exception {
+    FileRepository repoToDelete = createRepository("repo.1234567890123.deleted");
+    FileRepository repoToKeep = createRepository("anotherRepo.git");
+    trashFolders.start();
+    trashFolders.getWorkerThread().join();
+    assertThat(repoToDelete.getDirectory().exists()).isFalse();
+    assertThat(repoToKeep.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;
+  }
+}
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
new file mode 100644
index 0000000..c8039f7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/fs/FilesystemDeleteHandlerTest.java
@@ -0,0 +1,146 @@
+// 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 org.mockito.Mockito.when;
+
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+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;
+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 FilesystemDeleteHandlerTest {
+
+  @Mock private GitRepositoryManager repoManager;
+  @Mock private ProjectDeletedListener projectDeleteListener;
+  @Mock private Configuration config;
+
+  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+  private DynamicSet<ProjectDeletedListener> deletedListener;
+  private FilesystemDeleteHandler fsDeleteHandler;
+  private Path basePath;
+
+  @Before
+  public void setUp() throws Exception {
+    basePath = tempFolder.newFolder().toPath().resolve("base");
+    deletedListener = new DynamicSet<>();
+    deletedListener.add(projectDeleteListener);
+  }
+
+  @Test
+  public void shouldDeleteRepository() throws Exception {
+    String repoName = "testRepo";
+    Repository repository = createRepository(repoName);
+    Project.NameKey nameKey = new Project.NameKey(repoName);
+    Project project = new Project(nameKey);
+    when(repoManager.openRepository(nameKey)).thenReturn(repository);
+    when(config.shouldArchiveDeletedRepos()).thenReturn(false);
+    fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
+    fsDeleteHandler.delete(project, false);
+    assertThat(repository.getDirectory().exists()).isFalse();
+  }
+
+  @Test
+  public void shouldDeleteEmptyParentFolders() throws Exception {
+    String repoName = "a/b/c";
+    Repository repository = createRepository(repoName);
+    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();
+  }
+
+  @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 = new Project.NameKey(repoToDeleteName);
+    Project project = new Project(nameKey);
+    when(repoManager.openRepository(nameKey)).thenReturn(repoToDelete);
+    fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
+    fsDeleteHandler.delete(project, 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 = new Project.NameKey(repoName);
+    Project project = new Project(nameKey);
+    when(repoManager.openRepository(nameKey)).thenReturn(repository);
+    fsDeleteHandler = new FilesystemDeleteHandler(repoManager, deletedListener, config);
+    fsDeleteHandler.delete(project, 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 = 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 7b9b055..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
@@ -15,7 +15,7 @@
 package com.googlesource.gerrit.plugins.deleteproject.fs;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.googlesource.gerrit.plugins.deleteproject.fs.DeleteTrashFolders.isTrashFolderName;
+import static com.googlesource.gerrit.plugins.deleteproject.fs.DeleteTrashFolders.TrashFolderPredicate.match;
 
 import org.junit.Test;
 
@@ -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,17 +51,20 @@
 
     // 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) {
-    assertThat(isTrashFolderName(name)).isTrue();
+    assertThat(match(name)).isTrue();
   }
 
   private void doesNotMatch(String name) {
-    assertThat(isTrashFolderName(name)).isFalse();
+    assertThat(match(name)).isFalse();
   }
 }