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