Initial version for Gerrit 2.14

This plugin provides an automated way of detecting, managing and
cleaning up (garbage collecting) the 'dirty' repositories in a Gerrit
instance.

It has two components: gc-conductor and gc-executor.

gc-conductor is a Gerrit plugin deployed in the plugins folder of a
Gerrit site. Its main function is to evaluate the dirtiness of
repositories and add them to a queue of repositories to be garbage
collected. This queue is maintained as a database in a postgresql
server.

gc-executor is a runnable jar that picks up the repositories from the
queue and performs the garbage collection operation on them. gc-executor
can be deployed in the same machine that hosts the Gerrit application or
in a different machine that has access to the repositories and the
postgresql server holding the queue.

This initial version is a squash of Ericsson's internal plugin. The code
(script, logging, unit tests) assume that a folder /opt/gerrit exist and
it modifiable by the user running Gerrit. This limitation will be lifted
in a following up change.

Another limitation is because of a bug in JGit fixed by [1], gc-executor
application can only run one GC at a time, even if more than one worker
is configured. One way to lift this limitation is to build a custom JGit
version including that change and use that custom version instead in
gc-executor.

[1]https://git.eclipse.org/r/#/c/127447/

Change-Id: Iadc0854cc0edff2ef42dc01e8e60e3c9c8d7226e
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72f041f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.classpath
+/.primary_build_tool
+/.project
+/.settings/
+/bazel-*
+/eclipse-out/
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..98aa1c2
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,91 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+CONDUCTOR_DEPS = [
+    "@postgresql//jar",
+    "@dbcp//jar",
+    "@pool//jar",
+]
+
+EXECUTOR_DEPS = CONDUCTOR_DEPS + [
+    "@jgit//jar",
+    "@javaewah//jar",
+    "@guava//jar",
+    "@guice//jar",
+    "@guice-assistedinject//jar",
+    "@javax_inject//jar",
+    "@aopalliance//jar",
+    "@slf4j-api//jar",
+    "@log4j-slf4j-impl//jar",
+    "@log4j-api//jar",
+    "@log4j-core//jar",
+    "@retry//jar",
+]
+
+gerrit_plugin(
+    name = "gc-conductor",
+    srcs = glob(
+        ["src/main/java/**/*.java"],
+        exclude = ["**/executor/**"],
+    ),
+    manifest_entries = [
+        "Gerrit-PluginName: gc-conductor",
+        "Gerrit-Module: com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorModule",
+        "Gerrit-SshModule: com.ericsson.gerrit.plugins.gcconductor.command.SshModule",
+        "Implementation-Title: gc-conductor plugin",
+        "Implementation-URL: https://gerrit-review.googlesource.com/admin/repos/plugins/gc-conductor",
+        "Implementation-Vendor: Ericsson",
+    ],
+    resources = glob(
+        ["src/main/resources/**/*"],
+        exclude = ["src/main/resources/log4j2.xml"],
+    ),
+    deps = CONDUCTOR_DEPS,
+)
+
+java_library(
+    name = "gc-executor_lib",
+    srcs = glob([
+        "src/main/java/com/ericsson/gerrit/plugins/gcconductor/*.java",
+        "src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/*.java",
+        "src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/*.java",
+    ]),
+    resources = glob([
+        "bin/**/*",
+        "src/main/resources/log4j2.xml",
+    ]),
+    deps = EXECUTOR_DEPS,
+)
+
+java_binary(
+    name = "gc-executor",
+    main_class = "com.ericsson.gerrit.plugins.gcconductor.executor.GcExecutor",
+    runtime_deps = [":gc-executor_lib"],
+)
+
+junit_tests(
+    name = "gc_conductor_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    tags = ["gc-conductor"],
+    deps = [
+        ":gc-conductor__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "gc-conductor__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = EXECUTOR_DEPS + PLUGIN_TEST_DEPS + [
+        ":gc-conductor__plugin",
+        ":gc-executor_lib",
+        "@byte-buddy//jar",
+        "@mockito//jar",
+        "@objenesis//jar",
+    ],
+)
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..b90b611
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,30 @@
+workspace(name = "gc_executor")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+    commit = "11ce7521051ca73598d099aa8a396c9ffe932a74",
+    #local_path = "/home/ehugare/workspaces/bazlets",
+)
+
+#Snapshot Plugin API
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+#    "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
+# Release Plugin API
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+    "gerrit_api",
+)
+
+# Load release Plugin API
+gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..f97b72c
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,16 @@
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+        commit,
+        local_path = None):
+    if not local_path:
+        native.git_repository(
+            name = NAME,
+            remote = "https://gerrit.googlesource.com/bazlets",
+            commit = commit,
+        )
+    else:
+        native.local_repository(
+            name = NAME,
+            path = local_path,
+        )
diff --git a/bin/gc_ctl b/bin/gc_ctl
new file mode 100755
index 0000000..fcbd38c
--- /dev/null
+++ b/bin/gc_ctl
@@ -0,0 +1,120 @@
+#!/bin/bash
+#
+# controlling script for gc-executor
+export LC_ALL="en_US.UTF-8"
+
+# assume that the current script is in the bin folder
+readonly SERVICE_HOME=$(dirname "$(dirname "$(readlink -f "$0")")")
+readonly SERVICE_NAME=gc-executor
+readonly CONFIG=${SERVICE_HOME}/gc.config
+readonly PATH_TO_JAR="${SERVICE_HOME}/${SERVICE_NAME}.jar"
+readonly PID_FILE="${SERVICE_HOME}/bin/${SERVICE_NAME}-pid"
+
+usage() {
+  me=$(basename "$0")
+  echo "Usage: $me {start|stop [--now]|restart [--now]|status|check}" >&2
+  exit 1
+}
+
+is_running() {
+  [[ -f ${PID_FILE} && $(cat "${PID_FILE}") == $(pgrep -f "${SERVICE_NAME}.jar") ]]
+}
+
+get_config() {
+  if [[ -f "${CONFIG}" ]]; then
+    git config --file "${CONFIG}" "$1" "$2"
+  fi
+}
+
+start() {
+  echo "Starting ${SERVICE_NAME} ..."
+  if is_running; then
+    echo "${SERVICE_NAME} is already running"
+  else
+    java_home=$(get_config --get jvm.javaHome)
+    if [[ -z "${java_home}" ]]; then
+      java_home="/opt/gerrit/jdk8"
+    fi
+    java_options=($(get_config --get-all jvm.javaOptions))
+
+    nohup "${java_home}/bin/java" "${java_options[@]}" -DconfigFile="${CONFIG}" \
+      -jar "${PATH_TO_JAR}" > "${SERVICE_HOME}"/startup.log 2>&1&
+    echo "${!}" > "${PID_FILE}"
+    echo "${SERVICE_NAME} started"
+  fi
+}
+
+stop(){
+  if is_running; then
+    PID=$(cat "${PID_FILE}");
+    echo -n "${SERVICE_NAME} stopping ..."
+    kill "${kill_options}" "${PID}";
+    # wait for the process to die
+    while kill -0 "${PID}" >/dev/null 2>&1; do
+      sleep 1
+      echo -n "."
+    done
+    echo -e "\n${SERVICE_NAME} stopped"
+    rm -f "${PID_FILE}"
+  else
+    echo "${SERVICE_NAME} is not running"
+  fi
+}
+
+restart(){
+  stop
+  start
+}
+
+status(){
+  if is_running; then
+    echo "${SERVICE_NAME} is up."
+  else
+    echo "Looks like ${SERVICE_NAME} is down!"
+  fi
+}
+
+check(){
+  tail -100f /opt/gerrit/review_site/logs/gc/gc.log
+}
+
+main(){
+  action=$1
+  shift
+  kill_options="-TERM"
+
+  while [[ $# -gt 0 ]]; do
+    case "$1" in
+    --now)
+      kill_options="-KILL"
+      shift
+      ;;
+    *)
+      usage
+    esac
+  done
+
+  case "${action}" in
+    start)
+      start
+      ;;
+    stop)
+      stop
+      ;;
+    restart)
+      restart
+      ;;
+    status)
+      status
+      ;;
+    check)
+      check
+      ;;
+    *)
+      echo "${action} is not a known command."
+      usage
+  esac
+  exit 0
+}
+
+main "$@"
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..d98226d
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,115 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+    maven_jar(
+        name = "mockito",
+        artifact = "org.mockito:mockito-core:2.20.1",
+        sha1 = "a0f57a2efd118bd0f4735cb213f76874cb87b78b",
+    )
+
+    maven_jar(
+        name = "byte-buddy",
+        artifact = "net.bytebuddy:byte-buddy:1.8.15",
+        sha1 = "cb36fe3c70ead5fcd016856a7efff908402d86b8",
+    )
+
+    maven_jar(
+        name = "objenesis",
+        artifact = "org.objenesis:objenesis:2.6",
+        sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+    )
+
+    maven_jar(
+        name = "slf4j-api",
+        artifact = "org.slf4j:slf4j-api:1.7.25",
+        sha1 = "da76ca59f6a57ee3102f8f9bd9cee742973efa8a",
+    )
+
+    LOG4J2_VERS = "2.11.1"
+
+    maven_jar(
+        name = "log4j-slf4j-impl",
+        artifact = "org.apache.logging.log4j:log4j-slf4j-impl:" + LOG4J2_VERS,
+        sha1 = "4b41b53a3a2d299ce381a69d165381ca19f62912",
+    )
+
+    maven_jar(
+        name = "log4j-core",
+        artifact = "org.apache.logging.log4j:log4j-core:" + LOG4J2_VERS,
+        sha1 = "592a48674c926b01a9a747c7831bcd82a9e6d6e4",
+    )
+
+    maven_jar(
+        name = "log4j-api",
+        artifact = "org.apache.logging.log4j:log4j-api:" + LOG4J2_VERS,
+        sha1 = "268f0fe4df3eefe052b57c87ec48517d64fb2a10",
+    )
+
+    maven_jar(
+        name = "postgresql",
+        artifact = "org.postgresql:postgresql:42.2.4",
+        sha1 = "dff98730c28a4b3a3263f0cf4abb9a3392f815a7",
+    )
+
+    maven_jar(
+        name = "dbcp",
+        artifact = "commons-dbcp:commons-dbcp:1.4",
+        sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39",
+    )
+
+    maven_jar(
+        name = "pool",
+        artifact = "commons-pool:commons-pool:1.5.5",
+        sha1 = "7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b",
+    )
+
+    maven_jar(
+        name = "guava",
+        artifact = "com.google.guava:guava:25.1-jre",
+        sha1 = "6c57e4b22b44e89e548b5c9f70f0c45fe10fb0b4",
+    )
+
+    GUICE_VERS = "4.2.0"
+
+    maven_jar(
+        name = "guice",
+        artifact = "com.google.inject:guice:" + GUICE_VERS,
+        sha1 = "25e1f4c1d528a1cffabcca0d432f634f3132f6c8",
+    )
+
+    maven_jar(
+        name = "guice-assistedinject",
+        artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
+        sha1 = "e7270305960ad7db56f7e30cb9df6be9ff1cfb45",
+    )
+
+    maven_jar(
+        name = "aopalliance",
+        artifact = "aopalliance:aopalliance:1.0",
+        sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
+    )
+
+    maven_jar(
+        name = "javax_inject",
+        artifact = "javax.inject:javax.inject:1",
+        sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
+    )
+
+    maven_jar(
+        name = "jgit",
+        artifact =
+            "org.eclipse.jgit:org.eclipse.jgit:4.7.2.201807261330-r",
+        sha1 = "6c08ef848fa5f7d5d49776fa25ec24d738ee457d",
+    )
+
+    maven_jar(
+        name = "javaewah",
+        artifact = "com.googlecode.javaewah:JavaEWAH:1.1.6",
+        sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
+    )
+
+    maven_jar(
+        name = "retry",
+        artifact = "tech.huffman.re-retrying:re-retrying:3.0.0",
+        sha1 = "bd3ce1aaafc0f357354e76890f0a8199a0f42f3a",
+    )
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java
new file mode 100644
index 0000000..4081ed0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.base.Strings;
+import org.eclipse.jgit.lib.Config;
+
+/** Database configuration parameters. */
+public abstract class CommonConfig {
+
+  public static final String DB_URL_KEY = "databaseUrl";
+  public static final String DB_NAME_KEY = "databaseName";
+  public static final String DB_URL_OPTIONS_KEY = "databaseUrlOptions";
+  public static final String DB_USERNAME_KEY = "username";
+  public static final String DB_PASS_KEY = "password";
+  public static final String PACKED_KEY = "packed";
+  public static final String LOOSE_KEY = "loose";
+
+  public static final String DEFAULT_DB_URL = "jdbc:postgresql://localhost:5432/";
+  public static final String DEFAULT_DB_NAME = "gc";
+  public static final String DEFAULT_DB_USERNAME = DEFAULT_DB_NAME;
+  public static final String DEFAULT_DB_PASSWORD = DEFAULT_DB_NAME;
+  public static final int PACKED_DEFAULT = 40;
+  public static final int LOOSE_DEFAULT = 400;
+
+  private final String databaseUrl;
+  private final String databaseName;
+  private final String databaseUrlOptions;
+  private final String username;
+  private final String password;
+  private final int packed;
+  private final int loose;
+
+  /**
+   * Create CommonConfig from the specified parameters.
+   *
+   * @param databaseUrl The database server URL.
+   * @param databaseName The database name.
+   * @param databaseUrlOptions The database URL options.
+   * @param username The database server username.
+   * @param password The password of the database server user.
+   * @param loose The number of loose objects to consider a repo dirty
+   * @param packed The number of packs to consider a repo dirty
+   */
+  public CommonConfig(
+      String databaseUrl,
+      String databaseName,
+      String databaseUrlOptions,
+      String username,
+      String password,
+      int packed,
+      int loose) {
+    this.databaseUrl = databaseUrl.replaceFirst("/?$", "/");
+    this.databaseName = databaseName;
+    this.databaseUrlOptions = databaseUrlOptions;
+    this.username = username;
+    this.password = password;
+    this.packed = packed;
+    this.loose = loose;
+  }
+
+  /** @return the database server URL. */
+  public String getDatabaseUrl() {
+    return databaseUrl;
+  }
+
+  /** @return the database name. */
+  public String getDatabaseName() {
+    return databaseName;
+  }
+
+  /** @return the database URL options or empty if none. */
+  public String getDatabaseUrlOptions() {
+    return databaseUrlOptions;
+  }
+
+  /** @return the database server username. */
+  public String getUsername() {
+    return username;
+  }
+
+  /** @return the password of the database server user. */
+  public String getPassword() {
+    return password;
+  }
+
+  /** @return the number of pack file threshold. */
+  public int getPackedThreshold() {
+    return packed;
+  }
+
+  /** @return the number of loose objects threshold. */
+  public int getLooseThreshold() {
+    return loose;
+  }
+
+  protected static String getString(
+      Config config, String section, String subsection, String key, String defaultValue) {
+    String value = config.getString(section, subsection, key);
+    return Strings.isNullOrEmpty(value) ? defaultValue : value;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java
new file mode 100644
index 0000000..8606427
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.ericsson.gerrit.plugins.gcconductor.postgresqueue.PostgresModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Module containing common bindings to evaluator and executor */
+public class CommonModule extends AbstractModule {
+
+  private Class<? extends CommonConfig> commonConfig;
+
+  public CommonModule(Class<? extends CommonConfig> commonConfig) {
+    this.commonConfig = commonConfig;
+  }
+
+  @Override
+  protected void configure() {
+    install(new PostgresModule(commonConfig));
+    install(new FactoryModuleBuilder().build(EvaluationTask.Factory.class));
+  }
+
+  @Provides
+  @Singleton
+  @Hostname
+  String provideHostname() throws Exception {
+    ProcessBuilder builder = new ProcessBuilder("hostname", "-s");
+    builder.redirectErrorStream(true);
+    Process process = builder.start();
+    process.waitFor();
+    try (BufferedReader buffer =
+        new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+      return buffer.readLine();
+    }
+  }
+
+  @Provides
+  List<ShutdownListener> provideShutdownListeners(Injector injector) {
+    List<ShutdownListener> listeners = new ArrayList<>();
+    for (Binding<ShutdownListener> shutdownListenerBinding :
+        injector.findBindingsByType(new TypeLiteral<ShutdownListener>() {})) {
+      listeners.add(shutdownListenerBinding.getProvider().get());
+    }
+    return listeners;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java
new file mode 100644
index 0000000..2bbade5
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java
@@ -0,0 +1,230 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Evaluate the dirtiness of a repository. */
+public class EvaluationTask implements Runnable {
+  private static final Logger log = LoggerFactory.getLogger(EvaluationTask.class);
+
+  private final CommonConfig cfg;
+  private final GcQueue queue;
+  private final String hostname;
+
+  private String repositoryPath;
+
+  public interface Factory {
+    /**
+     * Instantiates EvaluationTask objects.
+     *
+     * @param repositoryPath path to the repository to consider.
+     * @return an instance of EvaluationTask.
+     */
+    EvaluationTask create(String repositoryPath);
+  }
+
+  /**
+   * Creates an EvaluationTask object.
+   *
+   * @param cfg The configuration where to read from dirtiness settings
+   * @param queue The queue to add the repository to be garbage collected
+   * @param hostname The hostname where the repository is evaluated.
+   * @param repositoryPath Path to the repository to evaluate.
+   */
+  @Inject
+  public EvaluationTask(
+      CommonConfig cfg, GcQueue queue, @Hostname String hostname, @Assisted String repositoryPath) {
+    this.cfg = cfg;
+    this.queue = queue;
+    this.hostname = hostname;
+    this.repositoryPath = repositoryPath;
+  }
+
+  @Override
+  public void run() {
+    if (!isAlreadyInQueue() && isDirty()) {
+      insertRepository();
+    }
+  }
+
+  private boolean isAlreadyInQueue() {
+    try {
+      return queue.contains(repositoryPath);
+    } catch (GcQueueException e) {
+      log.error("Error checking if repository is already in queue {}", repositoryPath, e);
+      return true;
+    }
+  }
+
+  private boolean isDirty() {
+    try (FileRepository repository =
+        (FileRepository)
+            RepositoryCache.open(FileKey.exact(new File(repositoryPath), FS.DETECTED))) {
+      RepoStatistics statistics = new GC(repository).getStatistics();
+      if (statistics.numberOfPackFiles >= cfg.getPackedThreshold()) {
+        log.debug(
+            "The number of packs ({}) exceeds the configured limit of {}",
+            statistics.numberOfPackFiles,
+            cfg.getPackedThreshold());
+        return true;
+      }
+      long looseObjects = statistics.numberOfLooseObjects;
+      int looseThreshold = cfg.getLooseThreshold();
+      if (looseObjects >= looseThreshold) {
+        long referencedLooseObjects = 0;
+        long unreferencedLooseObjects = 0;
+        long duration = 0;
+        long start = System.currentTimeMillis();
+        unreferencedLooseObjects = getUnreferencedLooseObjectsCount(repository);
+        duration = System.currentTimeMillis() - start;
+        referencedLooseObjects = looseObjects - unreferencedLooseObjects;
+        log.debug(
+            "{} of {} loose objects in repository {} were unreferenced. Evaluating unreferenced objects took {}ms.",
+            unreferencedLooseObjects,
+            looseObjects,
+            repositoryPath,
+            duration);
+        return referencedLooseObjects >= looseThreshold;
+      }
+    } catch (RepositoryNotFoundException rnfe) {
+      log.debug("Repository no longer exist, aborting evaluation.");
+    } catch (IOException e) {
+      log.error("Error gathering '{}' statistics.", repositoryPath, e);
+    }
+    return false;
+  }
+
+  @VisibleForTesting
+  int getUnreferencedLooseObjectsCount(FileRepository repo) throws IOException {
+    File objects = repo.getObjectsDirectory();
+    String[] fanout = objects.list();
+    if (fanout == null || fanout.length == 0) {
+      return 0;
+    }
+    Set<ObjectId> unreferencedCandidates = getUnreferencedCandidates(objects, fanout);
+    if (unreferencedCandidates.isEmpty()) {
+      return 0;
+    }
+    try (ObjectWalk walk = new ObjectWalk(repo)) {
+      for (Ref ref : getAllRefs(repo)) {
+        walk.markStart(walk.parseAny(ref.getObjectId()));
+      }
+      removeReferenced(unreferencedCandidates, walk);
+    }
+    return unreferencedCandidates.size();
+  }
+
+  private Set<ObjectId> getUnreferencedCandidates(File objects, String[] fanout) {
+    Set<ObjectId> candidates = new HashSet<>();
+    for (String dir : fanout) {
+      if (dir.length() != 2) {
+        continue;
+      }
+      File[] entries = new File(objects, dir).listFiles();
+      if (entries != null) {
+        addCandidates(candidates, dir, entries);
+      }
+    }
+    return candidates;
+  }
+
+  private void addCandidates(Set<ObjectId> candidates, String dir, File[] entries) {
+    for (File f : entries) {
+      String fileName = f.getName();
+      if (fileName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) {
+        continue;
+      }
+      try {
+        ObjectId id = ObjectId.fromString(dir + fileName);
+        candidates.add(id);
+      } catch (IllegalArgumentException notAnObject) {
+        // ignoring the file that does not represent loose object
+      }
+    }
+  }
+
+  private Collection<Ref> getAllRefs(FileRepository repo) throws IOException {
+    RefDatabase refdb = repo.getRefDatabase();
+    Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+    List<Ref> addl = refdb.getAdditionalRefs();
+    if (!addl.isEmpty()) {
+      List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+      all.addAll(refs);
+      // add additional refs which start with refs/
+      for (Ref r : addl) {
+        if (r.getName().startsWith(Constants.R_REFS)) {
+          all.add(r);
+        }
+      }
+      return all;
+    }
+    return refs;
+  }
+
+  private void removeReferenced(Set<ObjectId> id2File, ObjectWalk w) throws IOException {
+    RevObject ro = w.next();
+    while (ro != null) {
+      if (id2File.remove(ro.getId()) && id2File.isEmpty()) {
+        return;
+      }
+      ro = w.next();
+    }
+    ro = w.nextObject();
+    while (ro != null) {
+      if (id2File.remove(ro.getId()) && id2File.isEmpty()) {
+        return;
+      }
+      ro = w.nextObject();
+    }
+  }
+
+  private void insertRepository() {
+    try {
+      queue.add(repositoryPath, hostname);
+    } catch (GcQueueException e) {
+      log.error("Error adding repository in queue {}", repositoryPath, e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Evaluate if repository need GC: " + repositoryPath;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java
new file mode 100644
index 0000000..cc2560e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Queue that holds Git repositories to be Gc'ed.
+ *
+ * <p>Implementations of this interface should be registered in Guice as {@link Singleton}.
+ */
+public interface GcQueue {
+
+  /**
+   * Add repository to the queue.
+   *
+   * <p>Repositories are unique in the queue. This method is idempotent, adding an already existing
+   * repository will neither add it again nor throw an exception.
+   *
+   * @param repository The path to the repository.
+   * @param queuedFrom The hostname from which the repository is queued from.
+   * @throws GcQueueException if an error occur while adding the repository.
+   */
+  void add(String repository, String queuedFrom) throws GcQueueException;
+
+  /**
+   * Pick a repository from the queue.
+   *
+   * <p>If the queue contains an already picked repository by the specified executor, will return
+   * that repository.
+   *
+   * @param executor The name of the executor to assign repository to.
+   * @param queuedForLongerThan Only pick repository that were in the queue for longer than
+   *     specified number of seconds.
+   * @param queuedFrom If specified, only pick repository if queued from the specified hostname.
+   * @return RepositoryInfo representing the repository if any, otherwise return <code>null</code>.
+   * @throws GcQueueException if an error occur while picking a repository.
+   */
+  RepositoryInfo pick(String executor, long queuedForLongerThan, Optional<String> queuedFrom)
+      throws GcQueueException;
+
+  /**
+   * Unpick a repository from the queue.
+   *
+   * @param repository The path to the repository to unpick.
+   * @throws GcQueueException if an error occur while unpicking the repository.
+   */
+  void unpick(String repository) throws GcQueueException;
+
+  /**
+   * Remove a repository from the queue.
+   *
+   * @param repository The path to the repository to remove.
+   * @throws GcQueueException if an error occur while removing the repository.
+   */
+  void remove(String repository) throws GcQueueException;
+
+  /**
+   * Returns <code>true</code> if the queue contains the specified repository.
+   *
+   * @param repository The path to the repository.
+   * @return <code>true</code> if the queue contains the specified repository, otherwise <code>false
+   *     </code>.
+   * @throws GcQueueException if an error occur which checking if the repository is in the queue.
+   */
+  boolean contains(String repository) throws GcQueueException;
+
+  /**
+   * Reset all repositories queuedFrom to the specified hostname.
+   *
+   * @param queuedFrom The hostname to set queuedFrom to for every repository.
+   * @throws GcQueueException if an error occur while resetting queuedFrom.
+   */
+  void resetQueuedFrom(String queuedFrom) throws GcQueueException;
+
+  /**
+   * Returns list of repositories with their queuedAt timestamp, queuedFrom hostname and their
+   * associated executor, if any. The repositories are orderer from the oldest to the newest
+   * inserted.
+   *
+   * @return list of RepositoryInfo.
+   * @throws GcQueueException if an error occur while listing the repositories.
+   */
+  List<RepositoryInfo> list() throws GcQueueException;
+
+  /**
+   * Bump an existing repository to the top of the queue.
+   *
+   * @param repository The path to the repository.
+   * @throws GcQueueException if an error occurs while bumping the sequence of a repository.
+   */
+  void bumpToFirst(String repository) throws GcQueueException;
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java
new file mode 100644
index 0000000..90702e1
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+/**
+ * Indicates that an error happened while interacting with the GcQueue.
+ *
+ * <p>Specific GcQueue implementation errors can be retrieved by calling getCause().
+ */
+public class GcQueueException extends Exception {
+
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * Constructs an {@code GcQueueException} with the specified detail message and cause.
+   *
+   * @param message The detail message
+   * @param cause The cause (underlying exception) that explains why the error occurred.
+   */
+  public GcQueueException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java
new file mode 100644
index 0000000..fd76130
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Hostname {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java
new file mode 100644
index 0000000..a7eedec
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import java.sql.Timestamp;
+
+/** Information about a queued repository. */
+public class RepositoryInfo {
+
+  private final String path;
+  private final Timestamp queuedAt;
+  private final String executor;
+  private final String queuedFrom;
+
+  public RepositoryInfo(String path, Timestamp queuedAt, String executor, String queuedFrom) {
+    this.path = path;
+    this.queuedAt = queuedAt;
+    this.executor = executor;
+    this.queuedFrom = queuedFrom;
+  }
+
+  /** @return the path to the repository. */
+  public String getPath() {
+    return path;
+  }
+
+  /** @return the time the repository was queued at. */
+  public Timestamp getQueuedAt() {
+    return queuedAt;
+  }
+
+  /** @return the executor running gc on the repository or <code>null</code> if none. */
+  public String getExecutor() {
+    return executor;
+  }
+
+  /** @return the hostname that inserted the repository in the queue. */
+  public String getQueuedFrom() {
+    return queuedFrom;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java
new file mode 100644
index 0000000..e3db5e3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+/** Implement this interface to be notified on shutdown. */
+public interface ShutdownListener {
+
+  /** Called on shutdown. */
+  void onShutdown();
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java
new file mode 100644
index 0000000..f6bea89
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+
+/** Responsible to notify all {@link ShutdownListener} */
+@Singleton
+public class ShutdownNotifier {
+
+  private final List<ShutdownListener> listeners;
+
+  @Inject
+  public ShutdownNotifier(List<ShutdownListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  /** Notify all {@link ShutdownListener}. */
+  public void notifyAllListeners() {
+    for (ShutdownListener listener : Lists.reverse(listeners)) {
+      listener.onShutdown();
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java
new file mode 100644
index 0000000..d9fe107
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+    name = "add-to-queue",
+    description = "Add a repository to gc queue",
+    runsAt = MASTER_OR_SLAVE)
+final class AddToQueue extends SshCommand {
+  @Argument(index = 0, required = true, metaVar = "REPOSITORY")
+  private String repository;
+
+  @Option(name = "--first", usage = "add repository as first priority in GC queue")
+  private boolean first;
+
+  @Inject private GcQueue queue;
+
+  @Inject @Hostname private String hostName;
+
+  @Inject private GitRepositoryManager gitRepositoryManager;
+
+  @Inject private ProjectCache projectCache;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      Path repositoryPath = Paths.get(repository);
+      if (repositoryPath.toFile().exists()) {
+        repositoryPath = repositoryPath.toRealPath();
+      }
+      if (!FileKey.isGitRepository(repositoryPath.toFile(), FS.DETECTED)) {
+        repositoryPath = resolvePath();
+      }
+      repository = repositoryPath.toString();
+      queue.add(repository, hostName);
+      if (first) {
+        queue.bumpToFirst(repository);
+      }
+      stdout.println(String.format("%s was added to GC queue", repository));
+    } catch (IOException | GcQueueException e) {
+      throw die(e);
+    }
+  }
+
+  private Path resolvePath() throws UnloggedFailure {
+    if (!(gitRepositoryManager instanceof LocalDiskRepositoryManager)) {
+      throw die("Unable to resolve path to " + repository);
+    }
+    String projectName = extractFrom(repository);
+    Project.NameKey nameKey = new Project.NameKey(projectName);
+    if (projectCache.get(nameKey) == null) {
+      throw die(String.format("Repository %s not found", repository));
+    }
+    LocalDiskRepositoryManager localDiskRepositoryManager =
+        (LocalDiskRepositoryManager) gitRepositoryManager;
+    try {
+      return localDiskRepositoryManager
+          .getBasePath(nameKey)
+          .resolve(projectName.concat(Constants.DOT_GIT_EXT))
+          .toRealPath();
+    } catch (IOException e) {
+      throw die(e);
+    }
+  }
+
+  private String extractFrom(String path) {
+    String name = path;
+    if (name.startsWith("/")) {
+      name = name.substring(1);
+    }
+    if (name.endsWith("/")) {
+      name = name.substring(0, name.length() - 1);
+    }
+    if (name.endsWith(Constants.DOT_GIT_EXT)) {
+      name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT));
+    }
+    return name;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java
new file mode 100644
index 0000000..5f25fba
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.kohsuke.args4j.Argument;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+    name = "bump-to-first",
+    description = "Bumps a repository to first priority for GC",
+    runsAt = MASTER_OR_SLAVE)
+final class BumpToFirst extends SshCommand {
+  @Argument(index = 0, required = true, metaVar = "REPOSITORY")
+  private String repository;
+
+  @Inject private GcQueue queue;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      if (!queue.contains(repository)) {
+        throw die(String.format("%s is not in the queue", repository));
+      }
+      queue.bumpToFirst(repository);
+    } catch (GcQueueException e) {
+      throw die(e);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java
new file mode 100644
index 0000000..56b70b8
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.kohsuke.args4j.Argument;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+    name = "set-queued-from",
+    description = "Set queued from for all unassigned repositories",
+    runsAt = MASTER_OR_SLAVE)
+final class SetQueuedFrom extends SshCommand {
+
+  @Argument(index = 0, required = true, metaVar = "HOSTAME")
+  private String hostname;
+
+  @Inject private GcQueue queue;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      queue.resetQueuedFrom(hostname);
+    } catch (GcQueueException e) {
+      throw die(e);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java
new file mode 100644
index 0000000..fe41af0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "show-queue", description = "Show GC queue", runsAt = MASTER_OR_SLAVE)
+final class ShowQueue extends SshCommand {
+
+  @Inject private GcQueue queue;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    try {
+      List<RepositoryInfo> repositories = queue.list();
+
+      // Find width of executor and queuedFrom column. Typical executor names
+      // consist of 11 characters hostname suffixed by "-n" and typical queued
+      // from hostname consist of 11 characters
+      int executorColumnWidth = 13;
+      int queuedFromColumnWidth = 11;
+      for (RepositoryInfo repositoryInfo : repositories) {
+        if (repositoryInfo.getExecutor() != null) {
+          executorColumnWidth =
+              Math.max(executorColumnWidth, repositoryInfo.getExecutor().length());
+        }
+        queuedFromColumnWidth =
+            Math.max(queuedFromColumnWidth, repositoryInfo.getQueuedFrom().length());
+      }
+
+      String format = "%-12s %-" + executorColumnWidth + "s %-" + queuedFromColumnWidth + "s %s\n";
+      stdout.print(String.format(format, "Queued At", "Executor", "Queued From", "Repository"));
+      stdout.print(
+          "------------------------------------------------------------------------------\n");
+      for (RepositoryInfo repositoryInfo : repositories) {
+        stdout.print(
+            String.format(
+                format,
+                queuedAt(repositoryInfo.getQueuedAt()),
+                Strings.nullToEmpty(repositoryInfo.getExecutor()),
+                repositoryInfo.getQueuedFrom(),
+                repositoryInfo.getPath()));
+      }
+      stdout.print(
+          "------------------------------------------------------------------------------\n");
+      stdout.print(
+          "  "
+              + repositories.size()
+              + " repositor"
+              + (repositories.size() > 1 ? "ies" : "y")
+              + "\n");
+    } catch (Exception e) {
+      throw die(e);
+    }
+  }
+
+  private static String queuedAt(Date when) {
+    if (TimeUtil.nowMs() - when.getTime() < 24 * 60 * 60 * 1000L) {
+      return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
+    }
+    return new SimpleDateFormat("MMM-dd HH:mm").format(when);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java
new file mode 100644
index 0000000..7940bba
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+class SshModule extends PluginCommandModule {
+
+  @Override
+  protected void configureCommands() {
+    command(SetQueuedFrom.class);
+    command(ShowQueue.class);
+    command(AddToQueue.class);
+    command(BumpToFirst.class);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java
new file mode 100644
index 0000000..69e0a37
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.eclipse.jgit.transport.PostUploadHook;
+import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class Evaluator implements UploadValidationListener, PostUploadHook, GitReferenceUpdatedListener {
+  private static final Logger log = LoggerFactory.getLogger(Evaluator.class);
+  private static final ThreadLocal<String> uploadRepositoryPath = new ThreadLocal<String>() {};
+
+  private final ScheduledThreadPoolExecutor executor;
+  private final EvaluationTask.Factory evaluationTaskFactory;
+  private final GitRepositoryManager repoManager;
+  private final Map<String, Long> timestamps;
+
+  private long expireTime;
+
+  @Inject
+  Evaluator(
+      @EvaluatorExecutor ScheduledThreadPoolExecutor executor,
+      EvaluationTask.Factory evaluationTaskFactory,
+      GitRepositoryManager repoManager,
+      EvaluatorConfig config,
+      @GerritServerConfig Config gerritConfig) {
+    this.executor = executor;
+    this.evaluationTaskFactory = evaluationTaskFactory;
+    this.repoManager = repoManager;
+    this.expireTime = config.getExpireTimeRecheck();
+
+    int threads =
+        gerritConfig.getInt(
+            "receive", null, "threadPoolSize", Runtime.getRuntime().availableProcessors());
+    timestamps = new ConcurrentHashMap<>(10000, 0.75f, threads);
+  }
+
+  @Override
+  public void onPreUpload(
+      Repository repository,
+      Project project,
+      String remoteHost,
+      UploadPack up,
+      Collection<? extends ObjectId> wants,
+      Collection<? extends ObjectId> haves) {
+    uploadRepositoryPath.set(repository.getDirectory().getAbsolutePath());
+  }
+
+  @Override
+  public void onBeginNegotiate(
+      Repository repository,
+      Project project,
+      String remoteHost,
+      UploadPack up,
+      Collection<? extends ObjectId> wants,
+      int cntOffered) {
+    // Do nothing
+  }
+
+  @Override
+  public void onPostUpload(PackStatistics stats) {
+    String repositoryPath = uploadRepositoryPath.get();
+    if (repositoryPath != null && needsCheck(repositoryPath)) {
+      executor.execute(evaluationTaskFactory.create(repositoryPath));
+      uploadRepositoryPath.remove();
+    }
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    String projectName = event.getProjectName();
+    Project.NameKey projectNameKey = new Project.NameKey(projectName);
+    try (Repository repository = repoManager.openRepository(projectNameKey)) {
+      String repositoryPath = repository.getDirectory().getAbsolutePath();
+      if (needsCheck(repositoryPath)) {
+        executor.execute(evaluationTaskFactory.create(repositoryPath));
+      }
+    } catch (RepositoryNotFoundException e) {
+      log.error("Project not found {}", projectName, e);
+    } catch (IOException e) {
+      log.error("Error getting repository for project {}", projectName, e);
+    }
+  }
+
+  private boolean needsCheck(String repositoryPath) {
+    long now = System.currentTimeMillis();
+    if (!timestamps.containsKey(repositoryPath)
+        || now >= timestamps.get(repositoryPath) + expireTime) {
+      timestamps.put(repositoryPath, now);
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java
new file mode 100644
index 0000000..acb3600
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+
+/** Holds configuration values of the plugin. */
+@Singleton
+public class EvaluatorConfig extends CommonConfig {
+  static final String THREAD_POOL_KEY = "threadPoolSize";
+  static final String EXPIRE_TIME_RECHECK_KEY = "expireTimeRecheck";
+
+  static final int THREAD_POOL_DEFAULT = 4;
+  static final String EXPIRE_TIME_RECHECK_DEFAULT = "60s";
+
+  private final int threadPoolSize;
+  private final long expireTimeRecheck;
+
+  @Inject
+  EvaluatorConfig(PluginConfig cfg) {
+    super(
+        cfg.getString(DB_URL_KEY, DEFAULT_DB_URL),
+        cfg.getString(DB_NAME_KEY, DEFAULT_DB_NAME),
+        Strings.nullToEmpty(cfg.getString(DB_URL_OPTIONS_KEY)),
+        cfg.getString(DB_USERNAME_KEY, DEFAULT_DB_USERNAME),
+        cfg.getString(DB_PASS_KEY, DEFAULT_DB_PASSWORD),
+        cfg.getInt(PACKED_KEY, PACKED_DEFAULT),
+        cfg.getInt(LOOSE_KEY, LOOSE_DEFAULT));
+    threadPoolSize = cfg.getInt(THREAD_POOL_KEY, THREAD_POOL_DEFAULT);
+
+    String expireTimeRecheckString =
+        cfg.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT);
+    expireTimeRecheck = ConfigUtil.getTimeUnit(expireTimeRecheckString, -1, TimeUnit.MILLISECONDS);
+  }
+
+  /** @return the number of threads to use for the plugin evaluation tasks. */
+  public int getThreadPoolSize() {
+    return threadPoolSize;
+  }
+
+  /** @return the time to wait before re-evaluating the same repository. */
+  public long getExpireTimeRecheck() {
+    return expireTimeRecheck;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java
new file mode 100644
index 0000000..6f029f0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface EvaluatorExecutor {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java
new file mode 100644
index 0000000..7cf13af
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@Singleton
+class EvaluatorExecutorProvider implements Provider<ScheduledThreadPoolExecutor>, ShutdownListener {
+  private WorkQueue.Executor executor;
+
+  @Inject
+  EvaluatorExecutorProvider(
+      WorkQueue workQueue, @PluginName String pluginName, EvaluatorConfig config) {
+    executor = workQueue.createQueue(config.getThreadPoolSize(), "[" + pluginName + " plugin]");
+  }
+
+  @Override
+  public void onShutdown() {
+    executor.shutdownNow();
+    executor.unregisterWorkQueue();
+    executor = null;
+  }
+
+  @Override
+  public ScheduledThreadPoolExecutor get() {
+    return executor;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java
new file mode 100644
index 0000000..8ef2a6b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonModule;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.inject.Provides;
+import com.google.inject.internal.UniqueAnnotations;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.transport.PostUploadHook;
+
+/** Configures bindings of the evaluator. */
+public class EvaluatorModule extends LifecycleModule {
+  @Override
+  protected void configure() {
+    install(new CommonModule(EvaluatorConfig.class));
+    listener().to(OnPluginLoadUnload.class);
+
+    bind(ScheduledThreadPoolExecutor.class)
+        .annotatedWith(EvaluatorExecutor.class)
+        .toProvider(EvaluatorExecutorProvider.class);
+    bind(ShutdownListener.class)
+        .annotatedWith(UniqueAnnotations.create())
+        .to(EvaluatorExecutorProvider.class);
+
+    bind(Evaluator.class);
+    DynamicSet.bind(binder(), UploadValidationListener.class).to(Evaluator.class);
+    DynamicSet.bind(binder(), PostUploadHook.class).to(Evaluator.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(Evaluator.class);
+    bind(EvaluatorConfig.class);
+  }
+
+  @Provides
+  PluginConfig providePluginConfig(PluginConfigFactory config, @PluginName String pluginName) {
+    return config.getFromGerritConfig(pluginName, true);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java
new file mode 100644
index 0000000..d2de252
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+
+class OnPluginLoadUnload implements LifecycleListener {
+
+  private final ShutdownNotifier shutdownNotifier;
+
+  @Inject
+  OnPluginLoadUnload(ShutdownNotifier shutdownNotifier) {
+    this.shutdownNotifier = shutdownNotifier;
+  }
+
+  @Override
+  public void start() {
+    // nothing to do, only stop method is needed for now
+  }
+
+  @Override
+  public void stop() {
+    shutdownNotifier.notifyAllListeners();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java
new file mode 100644
index 0000000..3f5a419
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import org.eclipse.jgit.lib.EmptyProgressMonitor;
+
+class CancellableProgressMonitor extends EmptyProgressMonitor {
+
+  private boolean cancelled = false;
+
+  void cancel() {
+    cancelled = true;
+  }
+
+  @Override
+  public boolean isCancelled() {
+    return cancelled;
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java
new file mode 100644
index 0000000..03b8fd1
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
+
+class ConfigUtil {
+
+  private ConfigUtil() {}
+
+  /**
+   * Parse a numerical time unit, such as "1 minute", from the configuration.
+   *
+   * @param config the configuration file to read.
+   * @param section section the key is in.
+   * @param setting name of the setting to read.
+   * @param defaultValue default value to return if no value was set in the configuration file.
+   * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
+   *     assume if the value does not contain an indication of the units.
+   * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
+   */
+  public static long getTimeUnit(
+      Config config, String section, String setting, long defaultValue, final TimeUnit wantUnit) {
+    String value = config.getString(section, null, setting);
+    if (value == null) {
+      return defaultValue;
+    }
+
+    String s = value.trim();
+    if (s.length() == 0) {
+      return defaultValue;
+    }
+
+    if (s.startsWith("-") /* negative */) {
+      throw notTimeUnit(section, setting, value);
+    }
+
+    try {
+      return getTimeUnit(s, defaultValue, wantUnit);
+    } catch (IllegalArgumentException notTime) {
+      throw notTimeUnit(section, setting, value);
+    }
+  }
+
+  /**
+   * Parse a numerical time unit, such as "1 minute", from a string.
+   *
+   * @param valueString the string to parse.
+   * @param defaultValue default value to return if no value was set in the configuration file.
+   * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
+   *     assume if the value does not contain an indication of the units.
+   * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
+   */
+  public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) {
+    Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
+    if (!m.matches()) {
+      return defaultValue;
+    }
+
+    String digits = m.group(1);
+    String unitName = m.group(2).trim();
+
+    TimeUnit inputUnit;
+    int inputMul;
+
+    if (match(unitName, "h", "hour", "hours")) {
+      inputUnit = TimeUnit.HOURS;
+      inputMul = 1;
+    } else if ("".equals(unitName) || match(unitName, "d", "day", "days")) {
+      inputUnit = TimeUnit.DAYS;
+      inputMul = 1;
+    } else if (match(unitName, "w", "week", "weeks")) {
+      inputUnit = TimeUnit.DAYS;
+      inputMul = 7;
+    } else if (match(unitName, "mon", "month", "months")) {
+      inputUnit = TimeUnit.DAYS;
+      inputMul = 30;
+    } else {
+      throw notTimeUnit(valueString);
+    }
+
+    try {
+      return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit);
+    } catch (NumberFormatException nfe) {
+      throw notTimeUnit(valueString);
+    }
+  }
+
+  private static boolean match(String a, String... cases) {
+    for (String b : cases) {
+      if (b.equalsIgnoreCase(a)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static IllegalArgumentException notTimeUnit(
+      String section, String setting, String valueString) {
+    return new IllegalArgumentException(
+        "Invalid time unit value: " + section + "." + setting + " = " + valueString);
+  }
+
+  private static IllegalArgumentException notTimeUnit(String val) {
+    return new IllegalArgumentException("Invalid time unit value: " + val);
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java
new file mode 100644
index 0000000..f339302
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.time.ZoneId.systemDefault;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class ExecutorConfig extends CommonConfig {
+  private static final Logger log = LoggerFactory.getLogger(ExecutorConfig.class);
+
+  static final String CORE_SECTION = "core";
+  static final String DB_SECTION = "db";
+  static final String EVALUATION_SECTION = "evaluation";
+
+  static final String DELAY_KEY = "delay";
+  static final String EXECUTOR_KEY = "executors";
+  static final String PICK_OWN_HOST_KEY = "pickOwnHostOnly";
+  static final String REPOS_PATH_KEY = "repositoriesPath";
+  static final String INTERVAL_KEY = "interval";
+  static final String START_TIME_KEY = "startTime";
+
+  static final String EMPTY = "";
+  static final int DEFAULT_EXECUTORS = 2;
+  static final int DEFAULT_DELAY = 0;
+  static final String DEFAULT_REPOS_PATH = "/opt/gerrit/repos";
+  static final long DEFAULT_INTERVAL = -1;
+  static final long DEFAULT_INITIAL_DELAY = -1;
+
+  private final int delay;
+  private final int executors;
+  private final boolean pickOwnHostOnly;
+  private final String repositoriesPath;
+  private final long interval;
+  private final long initialDelay;
+
+  @Inject
+  ExecutorConfig(Config config) {
+    super(
+        getString(config, DB_SECTION, null, DB_URL_KEY, DEFAULT_DB_URL),
+        getString(config, DB_SECTION, null, DB_NAME_KEY, DEFAULT_DB_NAME),
+        Strings.nullToEmpty(getString(config, DB_SECTION, null, DB_URL_OPTIONS_KEY, EMPTY)),
+        getString(config, DB_SECTION, null, DB_USERNAME_KEY, DEFAULT_DB_USERNAME),
+        getString(config, DB_SECTION, null, DB_PASS_KEY, DEFAULT_DB_PASSWORD),
+        config.getInt(EVALUATION_SECTION, PACKED_KEY, PACKED_DEFAULT),
+        config.getInt(EVALUATION_SECTION, LOOSE_KEY, LOOSE_DEFAULT));
+    delay = config.getInt(CORE_SECTION, DELAY_KEY, DEFAULT_DELAY);
+    executors = config.getInt(CORE_SECTION, EXECUTOR_KEY, DEFAULT_EXECUTORS);
+    pickOwnHostOnly = config.getBoolean(CORE_SECTION, PICK_OWN_HOST_KEY, true);
+    repositoriesPath =
+        getString(config, EVALUATION_SECTION, null, REPOS_PATH_KEY, DEFAULT_REPOS_PATH);
+    interval = interval(config, EVALUATION_SECTION, INTERVAL_KEY);
+    initialDelay =
+        initialDelay(
+            config.getString(EVALUATION_SECTION, null, START_TIME_KEY),
+            ZonedDateTime.now(systemDefault()),
+            interval);
+  }
+
+  int getExecutors() {
+    return executors;
+  }
+
+  boolean isPickOwnHostOnly() {
+    return pickOwnHostOnly;
+  }
+
+  int getDelay() {
+    return delay;
+  }
+
+  String getRepositoriesPath() {
+    return repositoriesPath;
+  }
+
+  long getInitialDelay() {
+    return initialDelay;
+  }
+
+  long getInterval() {
+    return interval;
+  }
+
+  private long interval(Config rc, String section, String key) {
+    try {
+      return ConfigUtil.getTimeUnit(rc, section, key, DEFAULT_INTERVAL, TimeUnit.MILLISECONDS);
+    } catch (IllegalArgumentException e) {
+      log.debug("Invalid {}.{} setting. Periodic evaluation disabled", section, key, e);
+      return DEFAULT_INTERVAL;
+    }
+  }
+
+  @VisibleForTesting
+  long initialDelay(String start, ZonedDateTime now, long interval) {
+    if (start == null) {
+      return DEFAULT_INITIAL_DELAY;
+    }
+    try {
+      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
+      LocalTime firstStartTime = LocalTime.parse(start, formatter);
+      ZonedDateTime startTime = now.with(firstStartTime);
+      Optional<DayOfWeek> dayOfWeek = getDayOfWeek(start, formatter);
+      if (dayOfWeek.isPresent()) {
+        startTime = startTime.with(dayOfWeek.get());
+      }
+      startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
+      long firstDelay = Duration.between(now, startTime).toMillis() % interval;
+      if (firstDelay <= 0) {
+        firstDelay += interval;
+      }
+      return firstDelay;
+    } catch (DateTimeParseException e) {
+      log.debug(
+          "Invalid value {} for {} setting. Periodic evaluation disabled",
+          start,
+          START_TIME_KEY,
+          e);
+      return DEFAULT_INITIAL_DELAY;
+    }
+  }
+
+  private Optional<DayOfWeek> getDayOfWeek(String start, DateTimeFormatter formatter) {
+    try {
+      return Optional.of(formatter.parse(start, DayOfWeek::from));
+    } catch (DateTimeParseException ignored) {
+      // Day of week is an optional parameter.
+      return Optional.empty();
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java
new file mode 100644
index 0000000..9ca5c5c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonModule;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+
+/** Configures bindings of the executor. */
+class ExecutorModule extends AbstractModule {
+
+  private final Config config;
+
+  ExecutorModule(Config config) {
+    this.config = config;
+  }
+
+  @Override
+  protected void configure() {
+    install(new CommonModule(ExecutorConfig.class));
+    bind(RuntimeShutdown.class);
+    bind(Config.class).toInstance(config);
+    bind(ExecutorConfig.class);
+    install(new FactoryModuleBuilder().build(GcWorker.Factory.class));
+    bind(ShutdownListener.class).annotatedWith(UniqueAnnotations.create()).to(GcExecutor.class);
+    bind(CancellableProgressMonitor.class);
+    bind(GarbageCollector.class);
+    bind(ScheduledEvaluator.class);
+    bind(ScheduledEvaluationTask.class);
+  }
+
+  @Provides
+  @Singleton
+  @QueuedFrom
+  Optional<String> wasQueuedFrom(ExecutorConfig config, @Hostname String hostname) {
+    return config.isPickOwnHostOnly() ? Optional.of(hostname) : Optional.empty();
+  }
+
+  @Provides
+  @Singleton
+  @QueuedForLongerThan
+  int queuedForLongerThan(ExecutorConfig config) {
+    return config.getDelay();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java
new file mode 100644
index 0000000..bc825d0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ProgressMonitor;
+
+class GarbageCollector implements Callable<Boolean> {
+  private String repositoryPath;
+  private ProgressMonitor pm;
+
+  void setRepositoryPath(String repositoryPath) {
+    this.repositoryPath = repositoryPath;
+  }
+
+  void setPm(ProgressMonitor pm) {
+    this.pm = pm;
+  }
+
+  @Override
+  public Boolean call() throws Exception {
+    try (Git git = Git.open(new File(repositoryPath))) {
+      git.gc()
+          .setAggressive(true)
+          .setPreserveOldPacks(true)
+          .setPrunePreserved(true)
+          .setProgressMonitor(pm)
+          .call();
+      return true;
+    } catch (GitAPIException e) {
+      throw new IOException(e);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java
new file mode 100644
index 0000000..e0fb506
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+import com.google.inject.Stage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.logging.log4j.LogManager;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class GcExecutor implements ShutdownListener {
+  private static final Logger log = LoggerFactory.getLogger(GcExecutor.class);
+
+  static final String CONFIG_FILE_PROPERTY = "configFile";
+
+  private final List<GcWorker> workers = new ArrayList<>();
+
+  private final GcQueue queue;
+
+  private final String hostname;
+
+  @Inject
+  GcExecutor(
+      GcQueue queue,
+      ExecutorConfig config,
+      GcWorker.Factory gcWorkerFactory,
+      ScheduledEvaluator scheduledEvaluator,
+      @Hostname String hostname) {
+    this.hostname = hostname;
+    this.queue = queue;
+    unpickRepositories(queue, hostname);
+    startExecutors(config, gcWorkerFactory, hostname);
+    scheduleEvaluation(config, scheduledEvaluator);
+  }
+
+  private void unpickRepositories(GcQueue queue, String hostname) {
+    try {
+      for (RepositoryInfo queuedRepo : queue.list()) {
+        String executor = queuedRepo.getExecutor();
+        if (executor != null && executor.startsWith(hostname)) {
+          queue.unpick(queuedRepo.getPath());
+        }
+      }
+    } catch (GcQueueException e) {
+      log.error("Failed to clear assigned repositories {}", e.getMessage(), e);
+    }
+  }
+
+  private void startExecutors(
+      ExecutorConfig config, GcWorker.Factory gcWorkerFactory, String hostname) {
+    log.info("Starting executors...");
+    synchronized (this) {
+      for (int i = 0; i < config.getExecutors(); i++) {
+        GcWorker worker = gcWorkerFactory.create(hostname + "-" + i);
+        worker.start();
+        workers.add(worker);
+      }
+    }
+  }
+
+  private void scheduleEvaluation(ExecutorConfig config, ScheduledEvaluator scheduledEvaluator) {
+    if (shouldScheduleEvaluation(config)) {
+      scheduledEvaluator.scheduleWith(config.getInitialDelay(), config.getInterval());
+    }
+  }
+
+  private boolean shouldScheduleEvaluation(ExecutorConfig config) {
+    return config.getInitialDelay() > 0 && config.getInterval() > 0;
+  }
+
+  @Override
+  public void onShutdown() {
+    log.info("Shutting down executors...");
+    synchronized (this) {
+      for (GcWorker worker : workers) {
+        worker.shutdown();
+      }
+      for (GcWorker worker : workers) {
+        try {
+          worker.join(1_000);
+        } catch (InterruptedException e) {
+          log.warn("Wait for executors to shutdown interrupted");
+          Thread.currentThread().interrupt();
+        }
+      }
+      unpickRepositories(queue, hostname);
+    }
+    log.info("Executors shut down OK.");
+  }
+
+  public static void main(String[] args) {
+    try {
+      final Injector injector =
+          Guice.createInjector(Stage.PRODUCTION, new ExecutorModule(loadConfig()));
+      // get the GcExecutor class to force start up
+      injector.getInstance(GcExecutor.class);
+      injector.getInstance(RuntimeShutdown.class).waitFor();
+    } catch (Throwable t) {
+      log.error("Uncaught error:", t);
+    }
+    LogManager.shutdown();
+  }
+
+  @VisibleForTesting
+  static Config loadConfig() {
+    String configPath = System.getProperty(CONFIG_FILE_PROPERTY);
+    if (configPath != null) {
+      FileBasedConfig config = new FileBasedConfig(new File(configPath), FS.DETECTED);
+      try {
+        config.load();
+        return config;
+      } catch (IOException | ConfigInvalidException e) {
+        log.error(
+            "Unable to load configuration from file {}. Default values will be used.",
+            configPath,
+            e);
+      }
+    }
+    return new Config();
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java
new file mode 100644
index 0000000..14ea8fc
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.github.rholder.retry.Retryer;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class GcWorker extends Thread {
+  private static final Logger log = LoggerFactory.getLogger(GcWorker.class);
+
+  private final GcQueue queue;
+  private final GarbageCollector gc;
+  private final Optional<String> queuedFrom;
+  private final int queuedForLongerThan;
+  private final String name;
+  private final CancellableProgressMonitor cpm;
+  private final Retryer<Boolean> retryer =
+      RetryerBuilder.<Boolean>newBuilder()
+          .retryIfException()
+          .retryIfRuntimeException()
+          .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
+          .withStopStrategy(StopStrategies.stopAfterAttempt(3))
+          .build();
+
+  interface Factory {
+    GcWorker create(String name);
+  }
+
+  @Inject
+  GcWorker(
+      GcQueue queue,
+      GarbageCollector gc,
+      CancellableProgressMonitor cpm,
+      @QueuedFrom Optional<String> queuedFrom,
+      @QueuedForLongerThan int queuedForLongerThan,
+      @Assisted String name) {
+    this.queue = queue;
+    this.gc = gc;
+    this.cpm = cpm;
+    this.queuedFrom = queuedFrom;
+    this.queuedForLongerThan = queuedForLongerThan;
+    this.name = name;
+    setName(name);
+  }
+
+  @Override
+  public void run() {
+    while (!cpm.isCancelled()) {
+      String repoPath = pickRepository();
+      if (repoPath != null) {
+        runGc(repoPath);
+      } else {
+        log.debug("No repository picked, going to sleep");
+        try {
+          Thread.sleep(5000);
+        } catch (InterruptedException e) {
+          log.debug("Gc task was interrupted while waiting to pick a repository");
+          Thread.currentThread().interrupt();
+          return;
+        }
+      }
+    }
+  }
+
+  private String pickRepository() {
+    try {
+      RepositoryInfo repoInfo = queue.pick(name, queuedForLongerThan, queuedFrom);
+      if (repoInfo != null) {
+        return repoInfo.getPath();
+      }
+    } catch (GcQueueException e) {
+      log.error("Unable to pick repository from the queue", e);
+    }
+    return null;
+  }
+
+  private void runGc(String repoPath) {
+    try {
+      log.info("Starting gc on repository {}", repoPath);
+      gc.setRepositoryPath(repoPath);
+      gc.setPm(cpm);
+      retryer.call(gc);
+      log.info("Gc completed on repository {}", repoPath);
+    } catch (Throwable e) {
+      if (!cpm.isCancelled()) {
+        log.error(
+            "Gc failed on repository {}. Error Message: {} Cause: {}: {}",
+            repoPath,
+            e.getMessage(),
+            e.getCause(),
+            e.getCause().getStackTrace(),
+            e);
+      }
+    } finally {
+      if (cpm.isCancelled()) {
+        log.warn("Gc on repository {} was cancelled", repoPath);
+        unpickRepository(repoPath);
+      } else {
+        removeRepoFromQueue(repoPath);
+      }
+    }
+  }
+
+  void shutdown() {
+    cpm.cancel();
+    this.interrupt();
+  }
+
+  private void unpickRepository(String repoPath) {
+    try {
+      queue.unpick(repoPath);
+      log.debug("Executor was removed for repository {}", repoPath);
+    } catch (GcQueueException e) {
+      log.error("Unable to remove executor for repository {}", repoPath, e);
+    }
+  }
+
+  private void removeRepoFromQueue(String repoPath) {
+    try {
+      queue.remove(repoPath);
+      log.debug("Repository {} was removed", repoPath);
+    } catch (GcQueueException e) {
+      log.error("Unable to remove repository {} from the queue", repoPath, e);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java
new file mode 100644
index 0000000..48f636c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface QueuedForLongerThan {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java
new file mode 100644
index 0000000..ee4c7ed
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface QueuedFrom {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java
new file mode 100644
index 0000000..2766a1e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class RuntimeShutdown {
+  private static final Logger log = LoggerFactory.getLogger(RuntimeShutdown.class);
+
+  private final ShutdownHook hook;
+  private final ShutdownNotifier shutdownNotifier;
+
+  @Inject
+  RuntimeShutdown(ShutdownNotifier shutdownNotifier) {
+    this.shutdownNotifier = shutdownNotifier;
+    hook = new ShutdownHook();
+    Runtime.getRuntime().addShutdownHook(hook);
+  }
+
+  public void waitFor() {
+    hook.waitForShutdown();
+  }
+
+  private class ShutdownHook extends Thread {
+    private boolean completed;
+
+    ShutdownHook() {
+      setName("ShutdownCallback");
+    }
+
+    @Override
+    public void run() {
+      log.debug("Graceful shutdown requested");
+      shutdownNotifier.notifyAllListeners();
+      log.debug("Shutdown complete");
+      synchronized (this) {
+        completed = true;
+        notifyAll();
+      }
+    }
+
+    void waitForShutdown() {
+      synchronized (this) {
+        while (!completed) {
+          try {
+            wait();
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return;
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java
new file mode 100644
index 0000000..cc0e837
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+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.attribute.BasicFileAttributes;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class ScheduledEvaluationTask implements Runnable {
+  private static final Logger log = LoggerFactory.getLogger(ScheduledEvaluationTask.class);
+
+  private final EvaluationTask.Factory evaluationTaskFactory;
+  private final Path repositoriesPath;
+
+  @Inject
+  public ScheduledEvaluationTask(
+      EvaluationTask.Factory evaluationTaskFactory, ExecutorConfig config) {
+    this.evaluationTaskFactory = evaluationTaskFactory;
+    try {
+      repositoriesPath = Paths.get(config.getRepositoriesPath()).normalize().toRealPath();
+    } catch (IOException e) {
+      log.error("Failed to resolve repositoriesPath.", e);
+      throw new ProvisionException("Failed to resolve repositoriesPath: " + e.getMessage());
+    }
+  }
+
+  @Override
+  public void run() {
+    for (String repositoryPath : repositories()) {
+      if (Thread.currentThread().isInterrupted()) {
+        return;
+      }
+      evaluationTaskFactory.create(repositoryPath).run();
+    }
+  }
+
+  private Collection<String> repositories() {
+    ProjectVisitor visitor = new ProjectVisitor(repositoriesPath);
+    try {
+      Files.walkFileTree(
+          visitor.startFolder,
+          EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+          Integer.MAX_VALUE,
+          visitor);
+    } catch (IOException e) {
+      log.error("Error walking repository tree {}", visitor.startFolder.toAbsolutePath(), e);
+    }
+    return Collections.unmodifiableSortedSet(visitor.found);
+  }
+
+  class ProjectVisitor extends SimpleFileVisitor<Path> {
+    private final SortedSet<String> found = new TreeSet<>();
+    private Path startFolder;
+
+    private ProjectVisitor(Path startFolder) {
+      this.startFolder = startFolder;
+    }
+
+    @Override
+    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+      if (!dir.equals(startFolder) && isRepo(dir)) {
+        found.add(dir.toString());
+        return FileVisitResult.SKIP_SUBTREE;
+      }
+      return FileVisitResult.CONTINUE;
+    }
+
+    private boolean isRepo(Path p) {
+      return FileKey.isGitRepository(p.toFile(), FS.DETECTED);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java
new file mode 100644
index 0000000..7997e40
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class ScheduledEvaluator implements ShutdownListener {
+
+  private final ScheduledEvaluationTask evaluationTask;
+
+  private ScheduledThreadPoolExecutor scheduledExecutor;
+
+  @Inject
+  public ScheduledEvaluator(ScheduledEvaluationTask evaluationTask) {
+    this.evaluationTask = evaluationTask;
+  }
+
+  public void scheduleWith(long initialDelay, long interval) {
+    ThreadFactory threadFactory =
+        new ThreadFactoryBuilder().setNameFormat("ScheduledEvaluator-%d").build();
+    scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
+    scheduledExecutor.scheduleAtFixedRate(
+        evaluationTask, initialDelay, interval, TimeUnit.MILLISECONDS);
+  }
+
+  @Override
+  public void onShutdown() {
+    if (scheduledExecutor != null) {
+      scheduledExecutor.shutdownNow();
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java
new file mode 100644
index 0000000..0b66cf3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static java.lang.String.format;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.commons.dbcp.BasicDataSource;
+
+final class DatabaseConstants {
+
+  static final String DRIVER = "org.postgresql.Driver";
+  static final String INITIAL_DATABASE = "postgres";
+
+  private static final String REPOSITORIES_TABLE = "repositories";
+  static final String REPOSITORY = "repository";
+  static final String EXECUTOR = "executor";
+  private static final String SEQUENCE = "sequence";
+  static final String QUEUED_AT = "queued_at";
+  static final String HOSTNAME = "hostname";
+
+  static final String CREATE_OR_UPDATE_SCHEMA =
+      "DO"
+          + " $$"
+          + " BEGIN"
+          + " CREATE TABLE IF NOT EXISTS "
+          + REPOSITORIES_TABLE
+          + "("
+          + REPOSITORY
+          + " VARCHAR(4096) PRIMARY KEY, "
+          + SEQUENCE
+          + " BIGSERIAL UNIQUE, "
+          + QUEUED_AT
+          + " TIMESTAMP WITHOUT TIME ZONE DEFAULT localtimestamp, "
+          + EXECUTOR
+          + " VARCHAR(258), "
+          + HOSTNAME
+          + " VARCHAR(255) NOT NULL);"
+          // This section is temporary to support migrating live, next version will
+          // drop the executor tables, only drop the foreign key for now.
+          + " IF EXISTS ("
+          + "     SELECT constraint_name FROM information_schema.table_constraints"
+          + "     WHERE table_name='"
+          + REPOSITORIES_TABLE
+          + "'"
+          + "     AND constraint_name='repositories_executor_fkey')"
+          + " THEN"
+          + "     ALTER TABLE "
+          + REPOSITORIES_TABLE
+          + " DROP CONSTRAINT repositories_executor_fkey;"
+          + " END IF;"
+          + " END "
+          + " $$";
+
+  private static final String SELECT_REPOSITORIES = "SELECT * FROM " + REPOSITORIES_TABLE;
+
+  static final String SELECT_REPOSITORIES_ORDERED = SELECT_REPOSITORIES + " ORDER BY " + SEQUENCE;
+
+  private DatabaseConstants() {}
+
+  static final String databaseExists(String name) {
+    return "SELECT 1 from pg_database WHERE datname='" + name + "'";
+  }
+
+  static final String createDatabase(String name) {
+    return "CREATE DATABASE " + name;
+  }
+
+  static final String dropDatabase(String name) {
+    return "DROP DATABASE " + name;
+  }
+
+  static final String select(String repository) {
+    return SELECT_REPOSITORIES + " WHERE " + REPOSITORY + "='" + repository + "'";
+  }
+
+  static final String insert(String repository, String hostname) {
+    return format(
+        "INSERT INTO %s (%s,%s) SELECT '%s','%s' WHERE NOT EXISTS (%s)",
+        REPOSITORIES_TABLE, REPOSITORY, HOSTNAME, repository, hostname, select(repository));
+  }
+
+  static final String delete(String repository) {
+    return "DELETE FROM " + REPOSITORIES_TABLE + " WHERE " + REPOSITORY + "='" + repository + "'";
+  }
+
+  static final String updateExecutor(
+      String executor, long queuedForLongerThan, Optional<String> queuedFrom) {
+    return format(
+        "UPDATE %s SET %s='%s' WHERE %s IN (SELECT %s FROM %s WHERE (%s is null OR %s='' OR %s='%s') AND age(localtimestamp, queued_at) > '%s' %s ORDER BY %s LIMIT 1 FOR UPDATE OF %s) RETURNING *",
+        REPOSITORIES_TABLE,
+        EXECUTOR,
+        executor,
+        REPOSITORY,
+        REPOSITORY,
+        REPOSITORIES_TABLE,
+        EXECUTOR,
+        EXECUTOR,
+        EXECUTOR,
+        executor,
+        queuedForLongerThan,
+        (queuedFrom.isPresent() ? " AND '" + queuedFrom.get() + "' LIKE hostname||'%'" : ""),
+        SEQUENCE,
+        REPOSITORIES_TABLE);
+  }
+
+  static final String updateQueuedFrom(String hostname) {
+    return format(
+        "UPDATE %s SET %s='%s' WHERE %s is NULL", REPOSITORIES_TABLE, HOSTNAME, hostname, EXECUTOR);
+  }
+
+  static final String clearExecutor(String repository) {
+    return format(
+        "UPDATE %s SET %s=NULL WHERE %s='%s'",
+        REPOSITORIES_TABLE, EXECUTOR, REPOSITORY, repository);
+  }
+
+  static final String bumpRepository(String repository) {
+    return format(
+        "UPDATE %s SET %s=(SELECT min(%s) FROM %s) -1 WHERE %s='%s'",
+        REPOSITORIES_TABLE, SEQUENCE, SEQUENCE, REPOSITORIES_TABLE, REPOSITORY, repository);
+  }
+
+  static boolean executeStatement(BasicDataSource ds, String query) throws SQLException {
+    try (Connection conn = ds.getConnection();
+        Statement stat = conn.createStatement()) {
+      return stat.execute(query);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java
new file mode 100644
index 0000000..24ceba9
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.DRIVER;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.INITIAL_DATABASE;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.createDatabase;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.databaseExists;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.internal.UniqueAnnotations;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.apache.commons.dbcp.BasicDataSource;
+
+/** Configures bindings of the Postgres queue implementation. */
+public class PostgresModule extends AbstractModule {
+
+  private static final long EVICT_IDLE_TIME_MS = 1000L * 60;
+  private Class<? extends CommonConfig> commonConfig;
+
+  public PostgresModule(Class<? extends CommonConfig> commonConfig) {
+    this.commonConfig = commonConfig;
+  }
+
+  @Override
+  protected void configure() {
+    bind(CommonConfig.class).to(commonConfig);
+    bind(GcQueue.class).to(PostgresQueue.class);
+    bind(ShutdownListener.class)
+        .annotatedWith(UniqueAnnotations.create())
+        .to(DatabaseAccessCleanUp.class);
+  }
+
+  /**
+   * Provide access to the gc database. Database will be created if it does not exist.
+   *
+   * @param cfg The database configuration
+   * @return BasicDataSource to access gc database.
+   * @throws SQLException If an error occur while creating the database.
+   */
+  @Provides
+  @Singleton
+  BasicDataSource provideGcDatabaseAccess(CommonConfig cfg) throws SQLException {
+    BasicDataSource adminDataSource = createDataSource(cfg, INITIAL_DATABASE);
+    try (Connection conn = adminDataSource.getConnection();
+        Statement stat = conn.createStatement();
+        ResultSet resultSet = stat.executeQuery(databaseExists(cfg.getDatabaseName()))) {
+      if (!resultSet.next()) {
+        executeStatement(adminDataSource, createDatabase(cfg.getDatabaseName()));
+      }
+    } finally {
+      adminDataSource.close();
+    }
+    return createDataSource(cfg, cfg.getDatabaseName());
+  }
+
+  private static BasicDataSource createDataSource(CommonConfig cfg, String database) {
+    BasicDataSource ds = new BasicDataSource();
+    ds.setDriverClassName(DRIVER);
+    ds.setUrl(cfg.getDatabaseUrl() + database + cfg.getDatabaseUrlOptions());
+    ds.setUsername(cfg.getUsername());
+    ds.setPassword(cfg.getPassword());
+    ds.setMinEvictableIdleTimeMillis(EVICT_IDLE_TIME_MS);
+    ds.setTimeBetweenEvictionRunsMillis(EVICT_IDLE_TIME_MS / 2);
+    return ds;
+  }
+
+  /** Close the database access when plugin is unloaded */
+  @VisibleForTesting
+  static class DatabaseAccessCleanUp implements ShutdownListener {
+    private final BasicDataSource dataSource;
+
+    @Inject
+    public DatabaseAccessCleanUp(BasicDataSource dataSource) {
+      this.dataSource = dataSource;
+    }
+
+    @Override
+    public void onShutdown() {
+      try {
+        dataSource.close();
+      } catch (SQLException e) {
+        throw new RuntimeException("Failed to close database connection: " + e.getMessage(), e);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java
new file mode 100644
index 0000000..d2dca5a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.CREATE_OR_UPDATE_SCHEMA;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.EXECUTOR;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.HOSTNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.QUEUED_AT;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.REPOSITORY;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.SELECT_REPOSITORIES_ORDERED;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.bumpRepository;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.clearExecutor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.delete;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.insert;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.select;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.updateExecutor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.updateQueuedFrom;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.dbcp.BasicDataSource;
+
+@Singleton
+public class PostgresQueue implements GcQueue {
+
+  private final BasicDataSource dataSource;
+
+  @Inject
+  public PostgresQueue(BasicDataSource dataSource) throws SQLException {
+    this.dataSource = dataSource;
+    executeStatement(dataSource, CREATE_OR_UPDATE_SCHEMA);
+  }
+
+  @Override
+  public void add(String repository, String queuedFrom) throws GcQueueException {
+    try {
+      executeStatement(dataSource, insert(repository, queuedFrom));
+    } catch (SQLException e) {
+      if (!"23505".equals(e.getSQLState())) {
+        // UNIQUE CONSTRAINT violation means repository is already in the queue
+        throw new GcQueueException("Failed to add repository " + repository, e);
+      }
+    }
+  }
+
+  @Override
+  public RepositoryInfo pick(String executor, long queuedForLongerThan, Optional<String> queuedFrom)
+      throws GcQueueException {
+    try (Connection conn = dataSource.getConnection();
+        Statement stat = conn.createStatement();
+        ResultSet resultSet =
+            stat.executeQuery(updateExecutor(executor, queuedForLongerThan, queuedFrom))) {
+      if (resultSet.next()) {
+        return toRepositoryInfo(resultSet);
+      }
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to pick repository", e);
+    }
+    return null;
+  }
+
+  @Override
+  public void unpick(String repository) throws GcQueueException {
+    try {
+      executeStatement(dataSource, clearExecutor(repository));
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to unpick repository " + repository, e);
+    }
+  }
+
+  @Override
+  public void remove(String repository) throws GcQueueException {
+    try {
+      executeStatement(dataSource, delete(repository));
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to remove repository " + repository, e);
+    }
+  }
+
+  @Override
+  public boolean contains(String repository) throws GcQueueException {
+    try (Connection conn = dataSource.getConnection();
+        Statement stat = conn.createStatement();
+        ResultSet resultSet = stat.executeQuery(select(repository))) {
+      if (resultSet.next()) {
+        return true;
+      }
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to check if queue contains repository " + repository, e);
+    }
+    return false;
+  }
+
+  @Override
+  public void resetQueuedFrom(String queuedFrom) throws GcQueueException {
+    try {
+      executeStatement(dataSource, updateQueuedFrom(queuedFrom));
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to reset queuedFrom", e);
+    }
+  }
+
+  @Override
+  public List<RepositoryInfo> list() throws GcQueueException {
+    try (Connection conn = dataSource.getConnection();
+        Statement stat = conn.createStatement();
+        ResultSet resultSet = stat.executeQuery(SELECT_REPOSITORIES_ORDERED)) {
+      List<RepositoryInfo> repositories = new ArrayList<>();
+      while (resultSet.next()) {
+        repositories.add(toRepositoryInfo(resultSet));
+      }
+      return repositories;
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to list repositories ", e);
+    }
+  }
+
+  @Override
+  public void bumpToFirst(String repository) throws GcQueueException {
+    try {
+      executeStatement(dataSource, bumpRepository(repository));
+    } catch (SQLException e) {
+      throw new GcQueueException("Failed to update repository priority ", e);
+    }
+  }
+
+  private RepositoryInfo toRepositoryInfo(ResultSet resultSet) throws SQLException {
+    int repositoryColumn = resultSet.findColumn(REPOSITORY);
+    int queuedAtColumn = resultSet.findColumn(QUEUED_AT);
+    int executorColumn = resultSet.findColumn(EXECUTOR);
+    int hostnameColumn = resultSet.findColumn(HOSTNAME);
+    return new RepositoryInfo(
+        resultSet.getString(repositoryColumn),
+        resultSet.getTimestamp(queuedAtColumn),
+        resultSet.getString(executorColumn),
+        resultSet.getString(hostnameColumn));
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..870a2ad
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,37 @@
+This plugin provides an automated way of detecting, managing and cleaning up
+(garbage collecting) the 'dirty' repositories in a Gerrit instance.
+
+It has two components: gc-conductor and gc-executor.
+
+gc-conductor is a Gerrit plugin deployed in the plugins folder of a Gerrit site.
+Its main function is to evaluate the dirtiness of repositories and add them to a
+queue of repositories to be garbage collected. This queue is maintained as a
+database in a postgresql server.
+
+gc-executor is a runnable jar that picks up the repositories from the queue and
+performs the garbage collection operation on them. gc-executor can be deployed
+in the same machine that hosts the Gerrit application or in a different machine
+that has access to the repositories and the postgresql server holding the queue.
+
+Instructions to build gc-conductor and gc-executor can be found in the
+[build documentation][build]. For configuring the two components, see the
+[configuration file][config].
+
+The plugin also provides SSH commands to help managing the repositories queue:
+
+* _add-to-queue_ allows to manually add a repository to the queue.
+* _bump-to-first_ increases the priority of a repository by promoting it to the
+   front of the queue.
+* _show-queue_ shows the list of repositories in the queue.
+* _set-queued-from_ allows to change the identifier of the Gerrit instance that
+  added the repository to the queue. This command is mainly useful in the context
+  of an active-passive redundant configuration when the gc-executor runs in the
+  same machine as the Gerrit application: by default in these cases, gc-executor
+  only picks the repositories that have been added to the queue by the Gerrit
+  instance running in the same host. If one of the executors goes down, it can be
+  helpful to be able to change the _set_queued_from_ field, so that the running
+  one can pick up repositories that were not initially added to the queue by its
+  corresponding Gerrit instance.
+
+[build]: build.html
+[config]: config.html
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..e6e9ec1
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,113 @@
+Build
+=====
+
+This plugin is built with Bazel and two build modes are supported:
+
+* Standalone
+* In Gerrit tree.
+
+Standalone build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist.
+
+## Build standalone
+
+To build the plugin, issue the following command:
+
+```
+  bazel build @PLUGIN@
+```
+
+The output is created in
+
+```
+  bazel-genfiles/@PLUGIN@.jar
+```
+
+To package the plugin sources run:
+
+```
+  bazel build lib@PLUGIN@__plugin-src.jar
+```
+
+The output is created in:
+
+```
+  bazel-bin/lib@PLUGIN@__plugin-src.jar
+```
+
+To execute the tests run:
+
+```
+  bazel test //...
+```
+
+This project can be imported into the Eclipse IDE. Execute:
+
+```
+  ./tools/eclipse/project.py
+```
+
+to generate the required files and then import the project.
+
+
+To build the executor, issue the following command:
+
+```
+  bazel build gc-executor_deploy.jar
+```
+
+The output is created in:
+
+```
+  /bazel-bin/gc-executor_deploy.jar
+```
+
+This jar should be renamed to gc-executor.jar before deployment.
+
+## Build in Gerrit tree
+
+Clone or link this plugin to the plugins directory of Gerrit's
+source tree. Put the external dependency Bazel build file into
+the Gerrit /plugins directory, replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  rm external_plugin_deps.bzl
+  ln -s @PLUGIN@/external_plugin_deps.bzl .
+```
+
+From Gerrit source tree issue the command:
+
+```
+  bazel build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+To execute the tests run:
+
+```
+  bazel test --test_tag_filters=@PLUGIN@
+```
+
+or filtering using the comma separated tags:
+
+````
+  bazel test --test_tag_filters=@PLUGIN@ --strict_java_deps=off //...
+````
+
+This project can be imported into the Eclipse IDE.
+Add the plugin name to the `CUSTOM_PLUGINS` set in
+Gerrit core in `tools/bzl/plugins.bzl`, and execute:
+
+```
+  ./tools/eclipse/project.py
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/main/resources/Documentation/cmd-add-to-queue.md b/src/main/resources/Documentation/cmd-add-to-queue.md
new file mode 100644
index 0000000..326174f
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-add-to-queue.md
@@ -0,0 +1,58 @@
+add-to-queue
+=====================
+
+NAME
+----
+add-to-queue - Add a repository to the GC queue
+
+SYNOPSIS
+--------
+>     ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue <REPOSITORY> [--first]
+
+DESCRIPTION
+-----------
+Add a repository to the GC queue.
+
+An absolute path to the repository (including the .git suffix) or the project
+name are accepted. A symlink pointing to a repository is also admitted.
+
+Adding a repository to the GC queue is an idempotent operation, i.e., executing
+the command multiple times only add the repository to the queue once.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+OPTIONS
+---------
+`--first`
+:	Add repository as first priority in GC queue.
+
+EXAMPLES
+--------
+Absolute path to a repository:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue /repos/my/repo.git
+```
+
+Symlink pointing to a repository:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue /opt/gerrit/repos/my/repo.git
+```
+
+Name of the project:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue my/repo
+```
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-bump-to-first.md b/src/main/resources/Documentation/cmd-bump-to-first.md
new file mode 100644
index 0000000..f05b932
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-bump-to-first.md
@@ -0,0 +1,27 @@
+bump-to-first
+=====================
+
+NAME
+----
+bump-to-first - Update a repository to be first priority in GC queue.
+
+SYNOPSIS
+--------
+>     ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ bump-to-first <REPOSITORY>
+
+DESCRIPTION
+-----------
+Update a repository to be first priority in GC queue.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-set-queued-from.md b/src/main/resources/Documentation/cmd-set-queued-from.md
new file mode 100644
index 0000000..09293f2
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-set-queued-from.md
@@ -0,0 +1,27 @@
+set-queued-from
+=====================
+
+NAME
+----
+set-queued-from - Set queued from for all unassigned repositories
+
+SYNOPSIS
+--------
+>     ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ set-queued-from <HOSTNAME>
+
+DESCRIPTION
+-----------
+Set queued from for all unassigned repositories.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-show-queue.md b/src/main/resources/Documentation/cmd-show-queue.md
new file mode 100644
index 0000000..ab20a85
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-show-queue.md
@@ -0,0 +1,28 @@
+show-queue
+=====================
+
+NAME
+----
+show-queue - Show GC queue
+
+SYNOPSIS
+--------
+>     ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ show-queue
+
+DESCRIPTION
+-----------
+Display repositories in gc queue and the executor handling the repository.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used on a need basis by the admins but could also
+be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..e0049aa
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,190 @@
+@PLUGIN@ Configuration
+======================
+
+gc-conductor uses a postgresql database to manage the queue of repositories that
+need to be garbage collected. The following commands can be used on the postgresql
+database host to create the configured username/password; superuser is required:
+
+```
+  sudo /etc/init.d/postgresql start
+  sudo su - postgres
+  createuser -P -s -e username
+```
+
+File `gerrit.config`
+--------------------
+
+`plugin.@PLUGIN@.packed`
+:  Packed threshold. By default, `40`.
+
+`plugin.@PLUGIN@.loose`
+:  Loose threshold. By default, `400`.
+
+`plugin.@PLUGIN@.databaseUrl`
+:  Database url. By default, `jdbc:postgresql://localhost:5432/`.
+
+`plugin.@PLUGIN@.databaseName`
+:  Database name. By default, `gc`.
+
+`plugin.@PLUGIN@.databaseUrlOptions`
+:  jdbc option properties to append to the database URL, example
+`?ssl=true&loglevel=org.postgresql.Driver.DEBUG`. Empty by default.
+
+`plugin.@PLUGIN@.username`
+:  Database username. By default, `gc`.
+
+`plugin.@PLUGIN@.password`
+:  Database password. By default, `gc`.
+
+`plugin.@PLUGIN@.threadPoolSize`
+:  Thread pool size. By default, `4`.
+
+`plugin.@PLUGIN@.expireTimeRecheck`
+:  Time before a check is considered expired. By default, `60s`.
+
+GC executor
+--------------------
+
+GC executor is packaged as a runnable java jar. The [build documentation][build]
+details the steps to build gc-executor.jar. Once built, gc-executor.jar is deployed
+to the node charged of doing the garbage collection (GC) process.
+
+The configuration can be passed to the gc executor jar by using:
+
+```
+  java -DconfigFile=/path/to/config_file.config -jar executor.jar
+```
+
+### File `gc.config`
+
+The file `gc.config` is a Git-style config file that controls several settings for
+gc executor. The contents of the `gc.config` file are cached at startup. If this
+file is modified, gc executor needs to be restarted in order to be able to use the
+new values.
+
+
+#### Sample `gc.config`:
+
+```
+[jvm]
+  javaHome = /opt/gerrit/jdk8
+  javaOptions = -Xrunjdwp:transport=dt_socket,address=localhost:8788,server=y,suspend=n
+  javaOptions = -Xms1g
+  javaOptions = -Xmx32g
+  javaOptions = -XX:+UseG1GC
+  javaOptions = -XX:MaxGCPauseMillis=2000
+[core]
+  executors = 2
+  pickOwnHostOnly = false
+  delay = 0
+
+[db]
+  databaseUrl = jdbc:postgresql://same_host_as_plugin:5432/
+  databaseName = testDb
+  databaseUrlOptions = ?ssl=true
+  username = testUser
+  password = testPass
+
+[evaluation]
+  packed = 40
+  loose = 400
+  repositoriesPath = /path/to/repositories
+  startTime = Sat 22:00
+  interval = 1 week
+```
+
+#### Section `jvm`
+
+`jvm.javaHome`
+:       Location of java. By default /opt/gerrit/jdk8
+
+`jvm.javaOptions`
+:       Options to pass along to the Java runtime. If multiple values are
+configured, they are passed in order on the command line, separated by spaces.
+
+#### Section `core`
+
+`core.executors`
+:       Number of executors. By default, 2.
+
+`core.pickOwnHostOnly`
+:       Whether to pick repositories added to queue from same host only or not.
+By default, true.
+
+`core.delay`
+:       minimal delay in seconds a repository must be in queue before it can be
+picked. By default, 0.
+
+#### Section `db`
+
+`db.databaseUrl`
+:  Database URL. By default, `jdbc:postgresql://localhost:5432/`.
+
+`db.databaseName`
+:       Database name. By default, `gc`.
+
+`db.databaseUrlOptions`
+:  jdbc option properties to append to the database URL. For example,
+`?ssl=true&loglevel=org.postgresql.Driver.DEBUG`. Empty by default.
+
+`db.username`
+:       Username to connect to the database. By default, `gc`.
+
+`db.password`
+:       Password associated to that username. By default, `gc`.
+
+It is important to note here that the parameters used in this section should be
+the same used in `gerrit.config` to define the database settings.
+
+#### Section `evaluation`
+
+This section allows to configure dirtiness evaluation for a list of repositories.
+
+`evaluation.packed`
+:       number of pack files in a repository for it to be considered dirty. By
+default, 40.
+
+`evaluation.loose`
+:       number of loose objects in a repository for it to be considered dirty.
+By default, 400.
+
+`evaluation.repositoriesPath`
+:       path to the repositories to be evaluated for dirtiness. By default,
+/opt/gerrit/repos.
+
+`evaluation.startTime`
+:       start time to define the first execution of the repositories dirtiness
+evaluation. Expressed as &lt;day of week> &lt;hours>:&lt;minutes>. By default,
+disabled.
+
+This setting should be expressed using the following time units:
+
+  * &lt;day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+  * &lt;hours> : 00-23
+  * &lt;minutes> : 00-59
+
+`evaluation.interval`
+:       interval for periodic repetition of dirtiness evaluation. By default,
+disabled.
+
+The following suffixes are supported to define the time unit for the interval:
+
+ * h, hour, hours
+ * d, day, days
+ * w, week, weeks (1 week is treated as 7 days)
+ * mon, month, months (1 month is treated as 30 days)
+
+If no time unit is specified, days are assumed.
+
+### GC execution
+
+Executors can be started/stopped using gcctl.sh script.
+
+```
+  /opt/gerrit/gc-conductor/gcctl.sh {start|stop|restart|status|check}
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[build]: build.html
+[index]: index.html
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..7f63ec5
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" shutdownHook="disable">
+	<Properties>
+		<Property name="log.path">/opt/gerrit/review_site/logs/gc</Property>
+	</Properties>
+	<Appenders>
+		<RollingFile name="RollingFile" fileName="${log.path}/gc.log"
+			filePattern="${log.path}/gc.log.%d{yyyy-MM-dd}.gz"
+			ignoreExceptions="false">
+			<PatternLayout
+				pattern="%d{yyy-MM-dd HH:mm:ss.SSS} %-5level %logger{1} - %t - %msg%n" />
+			<Policies>
+				<TimeBasedTriggeringPolicy interval="1" modulate="true" />
+				<SizeBasedTriggeringPolicy size="30 MB" />
+			</Policies>
+		</RollingFile>
+	</Appenders>
+	<Loggers>
+		<Logger name="com.ericsson.gerrit.plugins.gcconductor" level="info" />
+		<Root level="error">
+			<AppenderRef ref="RollingFile" />
+		</Root>
+	</Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java
new file mode 100644
index 0000000..8106a71
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+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 EvaluationTaskTest {
+
+  @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+  private static final String SOME_HOSTNAME = "hostname";
+
+  @Mock private EvaluatorConfig cfg;
+  @Mock private GcQueue queue;
+
+  private EvaluationTask task;
+  private Repository repository;
+  private String repositoryPath;
+
+  @Before
+  public void setUp() throws Exception {
+    repository = createRepository("someRepo.git");
+    repositoryPath = repository.getDirectory().getAbsolutePath();
+    task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+  }
+
+  @Test
+  public void dirtyRepositoryObjectsShouldBeAddedToTheQueue() throws Exception {
+    when(cfg.getPackedThreshold()).thenReturn(1);
+    addFileTo(repository);
+    when(queue.contains(repositoryPath)).thenReturn(false);
+    task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+    task.run();
+    verify(queue).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void dirtyRepositoryPacksShouldBeAddedToTheQueue() throws Exception {
+    when(cfg.getPackedThreshold()).thenReturn(1);
+    when(queue.contains(repositoryPath)).thenReturn(false);
+    addFileTo(repository);
+    gc(repository);
+    task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+    task.run();
+    verify(queue).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void repositoryShouldNotBeAddedIfAlreadyInQueue() throws Exception {
+    when(queue.contains(repositoryPath)).thenReturn(true);
+    task.run();
+    verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void cleanRepositoryShouldNotBeAddedToQueue() throws Exception {
+    when(cfg.getLooseThreshold()).thenReturn(1);
+    when(cfg.getPackedThreshold()).thenReturn(1);
+    when(queue.contains(repositoryPath)).thenReturn(false);
+    task.run();
+    verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void queueThrowsErrorCheckingIfRepositoryExists() throws Exception {
+    doThrow(new GcQueueException("some message", new Throwable()))
+        .when(queue)
+        .contains(repositoryPath);
+    task.run();
+    verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void queueThrowsErrorInsertingRepository() throws Exception {
+    when(queue.contains(repositoryPath)).thenReturn(false);
+    doThrow(new GcQueueException("some message", new Throwable()))
+        .when(queue)
+        .add(repositoryPath, SOME_HOSTNAME);
+    task.run();
+  }
+
+  @Test
+  public void repositoryNoLongerExist() throws Exception {
+    when(queue.contains(repositoryPath)).thenReturn(false);
+    dir.delete();
+    task.run();
+    verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+  }
+
+  @Test
+  public void toStringReturnsProperMessage() {
+    assertThat(task.toString()).isEqualTo("Evaluate if repository need GC: " + repositoryPath);
+  }
+
+  @Test
+  public void noUnreferencedObjects() throws Exception {
+    addFileTo(repository);
+    FileRepository fileRepository = (FileRepository) repository;
+    assertThat(task.getUnreferencedLooseObjectsCount(fileRepository)).isEqualTo(0);
+  }
+
+  @Test
+  public void unreferencedObjectsCountShouldBeOne() throws Exception {
+    try (Git git = new Git(repository)) {
+      RevCommit initialCommit = git.commit().setMessage("initial commit").call();
+      addFileTo(repository);
+      git.reset().setMode(ResetType.HARD).setRef(initialCommit.getName()).call();
+    }
+    FileRepository fileRepository = (FileRepository) repository;
+    removeReflog(fileRepository.getDirectory());
+    assertThat(task.getUnreferencedLooseObjectsCount(fileRepository)).isEqualTo(1);
+  }
+
+  private void removeReflog(File directory) throws IOException {
+    Path logs = directory.toPath().resolve("logs");
+    Files.walk(logs).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+  }
+
+  private Repository createRepository(String repoName) throws Exception {
+    File repo = dir.newFolder(repoName);
+    try (Git git = Git.init().setDirectory(repo).call()) {
+      return git.getRepository();
+    }
+  }
+
+  private void addFileTo(Repository repository) throws Exception {
+    try (Git git = new Git(repository)) {
+      new File(repository.getDirectory().getParent(), "test");
+      git.add().addFilepattern("test").call();
+      git.commit().setMessage("Add test file").call();
+    }
+  }
+
+  private void gc(Repository repository) throws Exception {
+    try (Git git = new Git(repository)) {
+      git.gc().setAggressive(true).call();
+    }
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java
new file mode 100644
index 0000000..ba78afc
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.sql.Timestamp;
+import org.junit.Test;
+
+public class RepositoryInfoTest {
+
+  @Test
+  public void shouldReturnPath() {
+    String path = "/path/someRepo.git";
+    RepositoryInfo repoInfo = new RepositoryInfo(path, null, null, null);
+    assertThat(repoInfo.getPath()).isEqualTo(path);
+  }
+
+  @Test
+  public void shouldReturnQueuedAt() {
+    Timestamp time = new Timestamp(System.currentTimeMillis());
+    RepositoryInfo repoInfo = new RepositoryInfo(null, time, null, null);
+    assertThat(repoInfo.getQueuedAt()).isEqualTo(time);
+  }
+
+  @Test
+  public void shouldReturnExecutor() {
+    String executor = "someHost-1";
+    RepositoryInfo repoInfo = new RepositoryInfo(null, null, executor, null);
+    assertThat(repoInfo.getExecutor()).isEqualTo(executor);
+  }
+
+  @Test
+  public void shouldReturnQueuedFrom() {
+    String hostname = "someHost-2";
+    RepositoryInfo repoInfo = new RepositoryInfo(null, null, null, hostname);
+    assertThat(repoInfo.getQueuedFrom()).isEqualTo(hostname);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java
new file mode 100644
index 0000000..59f3498
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import org.junit.Test;
+
+public class ShutdownNotifierTest {
+
+  @Test
+  public void testNolistener() {
+    ShutdownNotifier notifier = new ShutdownNotifier(new ArrayList<>());
+    notifier.notifyAllListeners();
+  }
+
+  @Test
+  public void testWithlisteners() {
+    ShutdownListener listenerMock = mock(ShutdownListener.class);
+    ShutdownNotifier notifier = new ShutdownNotifier(Lists.newArrayList(listenerMock));
+    notifier.notifyAllListeners();
+    verify(listenerMock).onShutdown();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java
new file mode 100644
index 0000000..c63f248
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_NAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_PASS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_OPTIONS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_USERNAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_NAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_PASSWORD;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_URL;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_USERNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.EXPIRE_TIME_RECHECK_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.EXPIRE_TIME_RECHECK_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.THREAD_POOL_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.THREAD_POOL_KEY;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EvaluatorConfigTest {
+
+  private static final int PACKED_NOT_DEFAULT = 41;
+  private static final int LOOSE_NOT_DEFAULT = 401;
+  private static final String URL_NOT_DEFAULT = "jdbc:postgresql:test/";
+  private static final String DATABASE_NAME_NOT_DEFAULT = "jdbc:postgresql:test/";
+  private static final String URL_OPTION_NOT_DEFAULT = "?someOption=false";
+  private static final String USER_NOT_DEFAULT = "user";
+  private static final String PASS_NOT_DEFAULT = "pass";
+  private static final int THREAD_POOL_NOT_DEFAULT = 5;
+  private static final String EXPIRE_TIME_RECHECK_NOT_DEFAULT = "10s";
+  private static final boolean USING_DEFAULT_VALUES = true;
+  private static final boolean USING_CUSTOM_VALUES = false;
+
+  @Mock private PluginConfig pluginConfigMock;
+  private EvaluatorConfig configuration;
+
+  @Test
+  public void testValuesNotPresentInGerritConfig() {
+    buildMocks(USING_DEFAULT_VALUES);
+    assertThat(configuration.getPackedThreshold()).isEqualTo(PACKED_DEFAULT);
+    assertThat(configuration.getLooseThreshold()).isEqualTo(LOOSE_DEFAULT);
+    assertThat(configuration.getDatabaseUrl()).isEqualTo(DEFAULT_DB_URL);
+    assertThat(configuration.getDatabaseName()).isEqualTo(DEFAULT_DB_NAME);
+    assertThat(configuration.getDatabaseUrlOptions()).isEmpty();
+    assertThat(configuration.getUsername()).isEqualTo(DEFAULT_DB_USERNAME);
+    assertThat(configuration.getPassword()).isEqualTo(DEFAULT_DB_PASSWORD);
+    assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_DEFAULT);
+    assertThat(configuration.getExpireTimeRecheck())
+        .isEqualTo(convertTimeUnitStringToMilliseconds(EXPIRE_TIME_RECHECK_DEFAULT));
+  }
+
+  @Test
+  public void testValuesPresentInGerritConfig() {
+    buildMocks(USING_CUSTOM_VALUES);
+    assertThat(configuration.getPackedThreshold()).isEqualTo(PACKED_NOT_DEFAULT);
+    assertThat(configuration.getLooseThreshold()).isEqualTo(LOOSE_NOT_DEFAULT);
+    assertThat(configuration.getDatabaseUrl()).isEqualTo(URL_NOT_DEFAULT);
+    assertThat(configuration.getDatabaseName()).isEqualTo(DATABASE_NAME_NOT_DEFAULT);
+    assertThat(configuration.getDatabaseUrlOptions()).isEqualTo(URL_OPTION_NOT_DEFAULT);
+    assertThat(configuration.getUsername()).isEqualTo(USER_NOT_DEFAULT);
+    assertThat(configuration.getPassword()).isEqualTo(PASS_NOT_DEFAULT);
+    assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_NOT_DEFAULT);
+    assertThat(configuration.getExpireTimeRecheck())
+        .isEqualTo(convertTimeUnitStringToMilliseconds(EXPIRE_TIME_RECHECK_NOT_DEFAULT));
+  }
+
+  @Test
+  public void testDatabaseUrlAlwaysEndWithSlash() {
+    when(pluginConfigMock.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT))
+        .thenReturn(EXPIRE_TIME_RECHECK_DEFAULT);
+    when(pluginConfigMock.getString(DB_URL_KEY, DEFAULT_DB_URL)).thenReturn("someUrl");
+
+    configuration = new EvaluatorConfig(pluginConfigMock);
+    assertThat(configuration.getDatabaseUrl()).isEqualTo("someUrl/");
+  }
+
+  private void buildMocks(boolean useDefaults) {
+    when(pluginConfigMock.getInt(PACKED_KEY, PACKED_DEFAULT))
+        .thenReturn(useDefaults ? PACKED_DEFAULT : PACKED_NOT_DEFAULT);
+    when(pluginConfigMock.getInt(LOOSE_KEY, LOOSE_DEFAULT))
+        .thenReturn(useDefaults ? LOOSE_DEFAULT : LOOSE_NOT_DEFAULT);
+    when(pluginConfigMock.getString(DB_URL_KEY, DEFAULT_DB_URL))
+        .thenReturn(useDefaults ? DEFAULT_DB_URL : URL_NOT_DEFAULT);
+    when(pluginConfigMock.getString(DB_NAME_KEY, DEFAULT_DB_NAME))
+        .thenReturn(useDefaults ? DEFAULT_DB_NAME : DATABASE_NAME_NOT_DEFAULT);
+    when(pluginConfigMock.getString(DB_URL_OPTIONS_KEY))
+        .thenReturn(useDefaults ? null : URL_OPTION_NOT_DEFAULT);
+    when(pluginConfigMock.getString(DB_USERNAME_KEY, DEFAULT_DB_USERNAME))
+        .thenReturn(useDefaults ? DEFAULT_DB_USERNAME : USER_NOT_DEFAULT);
+    when(pluginConfigMock.getString(DB_PASS_KEY, DEFAULT_DB_PASSWORD))
+        .thenReturn(useDefaults ? DEFAULT_DB_PASSWORD : PASS_NOT_DEFAULT);
+    when(pluginConfigMock.getInt(THREAD_POOL_KEY, THREAD_POOL_DEFAULT))
+        .thenReturn(useDefaults ? THREAD_POOL_DEFAULT : THREAD_POOL_NOT_DEFAULT);
+    when(pluginConfigMock.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT))
+        .thenReturn(useDefaults ? EXPIRE_TIME_RECHECK_DEFAULT : EXPIRE_TIME_RECHECK_NOT_DEFAULT);
+
+    configuration = new EvaluatorConfig(pluginConfigMock);
+  }
+
+  private long convertTimeUnitStringToMilliseconds(String string) {
+    return ConfigUtil.getTimeUnit(string, -1, TimeUnit.MILLISECONDS);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java
new file mode 100644
index 0000000..356e8cf
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask.Factory;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EvaluatorTest {
+  private static final String REPOSITORY_PATH = "/path/someRepo.git";
+  private static final Project.NameKey NAME_KEY = new Project.NameKey("testProject");
+
+  @Mock private EvaluationTask task;
+  @Mock private GitReferenceUpdatedListener.Event event;
+  @Mock private GitRepositoryManager repoManager;
+  @Mock private Repository repository;
+  @Mock private ScheduledThreadPoolExecutor executor;
+  @Mock private EvaluatorConfig config;
+  @Mock private Config gerritConfig;
+
+  private Evaluator evaluator;
+
+  @Before
+  public void createEvaluator() {
+    when(event.getProjectName()).thenReturn(NAME_KEY.get());
+    task = new EvaluationTask(null, null, null, REPOSITORY_PATH);
+    when(repository.getDirectory()).thenReturn(new File(REPOSITORY_PATH));
+    Factory eventTaskFactory = mock(Factory.class);
+    when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+    when(config.getExpireTimeRecheck()).thenReturn(0L);
+    when(gerritConfig.getInt(
+            "receive", null, "threadPoolSize", Runtime.getRuntime().availableProcessors()))
+        .thenReturn(1);
+    evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+  }
+
+  @Test
+  public void onPostUploadShouldCreateTaskOnlyIfPreUploadCalled() {
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    verify(executor).execute(task);
+  }
+
+  @Test
+  public void onPostUploadShouldNotCreateTaskIfRepositoryIsNull() {
+    File fileMock = mock(File.class);
+    when(fileMock.getAbsolutePath()).thenReturn(null);
+    when(repository.getDirectory()).thenReturn(fileMock);
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    verify(executor, never()).execute(task);
+  }
+
+  @Test
+  public void onPostUploadShouldNotCreateTaskRepoIsNull() {
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    evaluator.onPostUpload(null);
+    verify(executor, times(1)).execute(task);
+  }
+
+  @Test
+  public void onPostUploadShouldCreateTaskExpired() {
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    verify(executor, times(2)).execute(task);
+  }
+
+  @Test
+  public void onPostUploadShouldNotCreateTaskNotExpired() {
+    when(config.getExpireTimeRecheck()).thenReturn(1000L);
+    Factory eventTaskFactory = mock(Factory.class);
+    when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+    evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    evaluator.onPreUpload(repository, null, null, null, null, null);
+    evaluator.onPostUpload(null);
+    verify(executor, times(1)).execute(task);
+  }
+
+  @Test
+  public void onGitReferenceUpdatedShouldCreateTaskExpired() throws Exception {
+    when(repoManager.openRepository(NAME_KEY)).thenReturn(repository);
+    evaluator.onGitReferenceUpdated(event);
+    evaluator.onGitReferenceUpdated(event);
+    verify(executor, times(2)).execute(task);
+  }
+
+  @Test
+  public void onGitReferenceUpdatedShouldNotCreateTaskNotExpired() throws Exception {
+    when(repoManager.openRepository(NAME_KEY)).thenReturn(repository);
+    when(config.getExpireTimeRecheck()).thenReturn(1000L);
+    Factory eventTaskFactory = mock(Factory.class);
+    when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+    evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+    evaluator.onGitReferenceUpdated(event);
+    evaluator.onGitReferenceUpdated(event);
+    verify(executor, times(1)).execute(task);
+  }
+
+  @Test
+  public void onGitReferenceUpdatedThrowsRepositoryNotFoundException() throws Exception {
+    doThrow(new RepositoryNotFoundException("")).when(repoManager).openRepository(NAME_KEY);
+    evaluator.onGitReferenceUpdated(event);
+    verify(executor, never()).execute(task);
+  }
+
+  @Test
+  public void onGitReferenceUpdatedThrowsIOException() throws Exception {
+    doThrow(new IOException()).when(repoManager).openRepository(NAME_KEY);
+    evaluator.onGitReferenceUpdated(event);
+    verify(executor, never()).execute(task);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java
new file mode 100644
index 0000000..17f92f2
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OnPluginLoadUnloadTest {
+
+  private OnPluginLoadUnload onPluginLoadUnload;
+  private ShutdownNotifier shutdownNotifierMock;
+
+  @Before
+  public void setUp() {
+    shutdownNotifierMock = mock(ShutdownNotifier.class);
+    onPluginLoadUnload = new OnPluginLoadUnload(shutdownNotifierMock);
+  }
+
+  @Test
+  public void testStart() {
+    onPluginLoadUnload.start();
+    verifyZeroInteractions(shutdownNotifierMock);
+  }
+
+  @Test
+  public void testStop() {
+    onPluginLoadUnload.stop();
+    verify(shutdownNotifierMock).notifyAllListeners();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java
new file mode 100644
index 0000000..b235a29
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CancellableProgressMonitorTest {
+
+  @Test
+  public void shouldReturnTrueIfThreadIsInterrupted() {
+    CancellableProgressMonitor cpm = new CancellableProgressMonitor();
+    cpm.cancel();
+    assertThat(cpm.isCancelled()).isTrue();
+  }
+
+  @Test
+  public void shouldReturnFalseIfThreadIsNotInterrupted() {
+    CancellableProgressMonitor cpm = new CancellableProgressMonitor();
+    assertThat(cpm.isCancelled()).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java
new file mode 100644
index 0000000..c99201a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ConfigUtilTest {
+
+  private static final int DEFAULT_VALUE = 1;
+
+  @Mock private Config config;
+
+  @Test
+  public void testGetTimeShouldReturnValue() {
+    when(config.getString("", null, "")).thenReturn("1 WEEK");
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS)).isEqualTo(7);
+  }
+
+  @Test
+  public void testGetTimeShouldReturnDefault() {
+    when(config.getString("", null, "")).thenReturn(null);
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+        .isEqualTo(DEFAULT_VALUE);
+
+    when(config.getString("", null, "")).thenReturn("");
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+        .isEqualTo(DEFAULT_VALUE);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGetTimeThrowsExceptionIfNegativeValue() {
+    when(config.getString("", null, "")).thenReturn("-1");
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+        .isEqualTo(DEFAULT_VALUE);
+
+    when(config.getString("", null, "")).thenReturn("");
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+        .isEqualTo(DEFAULT_VALUE);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGetTimeThrowsExceptionIfBadTimeUnit() {
+    when(config.getString("", null, "")).thenReturn("1s");
+    assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+        .isEqualTo(DEFAULT_VALUE);
+  }
+
+  @Test
+  public void testTimeUnit() {
+    assertEquals(ms(5, HOURS), parse("5h"));
+    assertEquals(ms(1, HOURS), parse("1hour"));
+    assertEquals(ms(48, HOURS), parse("48hours"));
+
+    assertEquals(ms(5, HOURS), parse("5 h"));
+    assertEquals(ms(1, HOURS), parse("1 hour"));
+    assertEquals(ms(48, HOURS), parse("48 hours"));
+    assertEquals(ms(48, HOURS), parse("48 \t \r hours"));
+
+    assertEquals(ms(4, DAYS), parse("4 d"));
+    assertEquals(ms(1, DAYS), parse("1day"));
+    assertEquals(ms(14, DAYS), parse("14days"));
+
+    assertEquals(ms(7, DAYS), parse("1 w"));
+    assertEquals(ms(7, DAYS), parse("1week"));
+    assertEquals(ms(14, DAYS), parse("2w"));
+    assertEquals(ms(14, DAYS), parse("2weeks"));
+
+    assertEquals(ms(30, DAYS), parse("1 mon"));
+    assertEquals(ms(30, DAYS), parse("1month"));
+    assertEquals(ms(60, DAYS), parse("2mon"));
+    assertEquals(ms(60, DAYS), parse("2months"));
+
+    assertEquals(ms(60, DAYS), parse("60"));
+    assertEquals(ms(1, MILLISECONDS), parse(""));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testUnsupportedTimeUnit() {
+    parse("1 min");
+  }
+
+  private static long ms(int cnt, TimeUnit unit) {
+    return MILLISECONDS.convert(cnt, unit);
+  }
+
+  private static long parse(String string) {
+    return ConfigUtil.getTimeUnit(string, 1, MILLISECONDS);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java
new file mode 100644
index 0000000..198eca9
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java
@@ -0,0 +1,169 @@
+package com.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_NAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_PASS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_OPTIONS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_USERNAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_NAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_PASSWORD;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_URL;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_USERNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.CORE_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DB_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_DELAY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_EXECUTORS;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_INITIAL_DELAY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_INTERVAL;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_REPOS_PATH;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DELAY_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EMPTY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EVALUATION_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EXECUTOR_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.PICK_OWN_HOST_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.REPOS_PATH_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.START_TIME_KEY;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ExecutorConfigTest {
+  private static final String CUSTOM_REPOS_PATH = "/other/path/to/repos";
+  private static final String CUSTOM_DB_URL = "customDbUrl/";
+  private static final String CUSTOM_DB_NAME = "customDbName";
+  private static final String URL_OPTIONS = "urlOptions";
+  private static final String CUSTOM_DB_USER = "customDbUser";
+  private static final String CUSTOM_DB_PASS = "customDbPass";
+  private static final int CUSTOM_DELAY = 5;
+  private static final int CUSTOM_EXECUTORS = 10;
+  private static final int CUSTOM_PACKED = 41;
+  private static final int CUSTOM_LOOSE = 401;
+  private static final int ONE_DAY_AS_MS = 86_400_000;
+
+  // Friday June 13, 2014 10:00 UTC
+  private static final ZonedDateTime NOW =
+      LocalDateTime.of(2014, Month.JUNE, 13, 10, 0, 0).atOffset(ZoneOffset.UTC).toZonedDateTime();
+
+  private Config config;
+  private ExecutorConfig executorConfig;
+
+  @Before
+  public void setUp() {
+    config = new Config();
+  }
+
+  @Test
+  public void shouldSetDefaultValues() {
+    executorConfig = new ExecutorConfig(config);
+
+    assertThat(executorConfig.getDatabaseUrl()).isEqualTo(DEFAULT_DB_URL);
+    assertThat(executorConfig.getDatabaseName()).isEqualTo(DEFAULT_DB_NAME);
+    assertThat(executorConfig.getDatabaseUrlOptions()).isEqualTo(EMPTY);
+    assertThat(executorConfig.getUsername()).isEqualTo(DEFAULT_DB_USERNAME);
+    assertThat(executorConfig.getPassword()).isEqualTo(DEFAULT_DB_PASSWORD);
+    assertThat(executorConfig.getDelay()).isEqualTo(DEFAULT_DELAY);
+    assertThat(executorConfig.getExecutors()).isEqualTo(DEFAULT_EXECUTORS);
+    assertThat(executorConfig.isPickOwnHostOnly()).isEqualTo(true);
+    assertThat(executorConfig.getLooseThreshold()).isEqualTo(LOOSE_DEFAULT);
+    assertThat(executorConfig.getPackedThreshold()).isEqualTo(PACKED_DEFAULT);
+    assertThat(executorConfig.getRepositoriesPath()).isEqualTo(DEFAULT_REPOS_PATH);
+    assertThat(executorConfig.getInterval()).isEqualTo(DEFAULT_INTERVAL);
+    assertThat(executorConfig.getInitialDelay()).isEqualTo(DEFAULT_INITIAL_DELAY);
+  }
+
+  @Test
+  public void shouldReadValuesFromConfig() {
+    config.setString(DB_SECTION, null, DB_URL_KEY, CUSTOM_DB_URL);
+    config.setString(DB_SECTION, null, DB_NAME_KEY, CUSTOM_DB_NAME);
+    config.setString(DB_SECTION, null, DB_URL_OPTIONS_KEY, URL_OPTIONS);
+    config.setString(DB_SECTION, null, DB_USERNAME_KEY, CUSTOM_DB_USER);
+    config.setString(DB_SECTION, null, DB_PASS_KEY, CUSTOM_DB_PASS);
+    config.setInt(CORE_SECTION, null, DELAY_KEY, CUSTOM_DELAY);
+    config.setInt(CORE_SECTION, null, EXECUTOR_KEY, CUSTOM_EXECUTORS);
+    config.setBoolean(CORE_SECTION, null, PICK_OWN_HOST_KEY, false);
+    config.setInt(EVALUATION_SECTION, null, LOOSE_KEY, CUSTOM_LOOSE);
+    config.setInt(EVALUATION_SECTION, null, PACKED_KEY, CUSTOM_PACKED);
+    config.setString(EVALUATION_SECTION, null, REPOS_PATH_KEY, CUSTOM_REPOS_PATH);
+    config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 day");
+    config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "Sun 00:00");
+    executorConfig = new ExecutorConfig(config);
+
+    assertThat(executorConfig.getDatabaseUrl()).isEqualTo(CUSTOM_DB_URL);
+    assertThat(executorConfig.getDatabaseName()).isEqualTo(CUSTOM_DB_NAME);
+    assertThat(executorConfig.getDatabaseUrlOptions()).isEqualTo(URL_OPTIONS);
+    assertThat(executorConfig.getUsername()).isEqualTo(CUSTOM_DB_USER);
+    assertThat(executorConfig.getPassword()).isEqualTo(CUSTOM_DB_PASS);
+    assertThat(executorConfig.getDelay()).isEqualTo(CUSTOM_DELAY);
+    assertThat(executorConfig.getExecutors()).isEqualTo(CUSTOM_EXECUTORS);
+    assertThat(executorConfig.isPickOwnHostOnly()).isEqualTo(false);
+    assertThat(executorConfig.getLooseThreshold()).isEqualTo(CUSTOM_LOOSE);
+    assertThat(executorConfig.getPackedThreshold()).isEqualTo(CUSTOM_PACKED);
+    assertThat(executorConfig.getRepositoriesPath()).isEqualTo(CUSTOM_REPOS_PATH);
+    assertThat(executorConfig.getInterval()).isEqualTo(ONE_DAY_AS_MS);
+    assertThat(executorConfig.getInitialDelay()).isAtLeast(1L);
+  }
+
+  @Test
+  public void shouldParseTimeOnly() {
+    config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 hour");
+    config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "15:00");
+    executorConfig = new ExecutorConfig(config);
+
+    assertThat(executorConfig.getInitialDelay()).isAtLeast(1L);
+  }
+
+  @Test
+  public void shouldUseDefaultValuesIfConfigInvalid() {
+    config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 x");
+    config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "123 ab:cd");
+    executorConfig = new ExecutorConfig(config);
+
+    assertThat(executorConfig.getInterval()).isEqualTo(DEFAULT_INTERVAL);
+    assertThat(executorConfig.getInitialDelay()).isEqualTo(DEFAULT_INITIAL_DELAY);
+  }
+
+  @Test
+  public void checkInitialDelayGivesExpectedTime() {
+    assertThat(initialDelayFor("11:00", "1h")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelayFor("05:30", "1h")).isEqualTo(ms(30, MINUTES));
+    assertThat(initialDelayFor("13:59", "1h")).isEqualTo(ms(59, MINUTES));
+
+    assertThat(initialDelayFor("11:00", "1d")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelayFor("05:30", "1d")).isEqualTo(ms(19, HOURS) + ms(30, MINUTES));
+    assertThat(initialDelayFor("Sat 10:00", "1d")).isEqualTo(ms(1, DAYS));
+    assertThat(initialDelayFor("Mon 09:00", "1d")).isEqualTo(ms(23, HOURS));
+
+    assertThat(initialDelayFor("11:00", "1w")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelayFor("05:30", "1w"))
+        .isEqualTo(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES));
+    assertThat(initialDelayFor("Fri 11:00", "1w")).isEqualTo(ms(1, HOURS));
+    assertThat(initialDelayFor("Mon 11:00", "1w")).isEqualTo(ms(3, DAYS) + ms(1, HOURS));
+  }
+
+  private long initialDelayFor(String startTime, String interval) {
+    config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, interval);
+    config.setString(EVALUATION_SECTION, null, START_TIME_KEY, startTime);
+    executorConfig = new ExecutorConfig(config);
+    return executorConfig.initialDelay(startTime, NOW, executorConfig.getInterval());
+  }
+
+  private long ms(int cnt, TimeUnit unit) {
+    return MILLISECONDS.convert(cnt, unit);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java
new file mode 100644
index 0000000..f4d3cef
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import com.google.inject.Binder;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExecutorModuleTest {
+  @Mock private Binder binder;
+  @Mock private ExecutorConfig execCfg;
+  @Mock private Config config;
+
+  private ExecutorModule executorModule;
+
+  @Before
+  public void setUp() {
+    executorModule = new ExecutorModule(config);
+  }
+
+  @Test
+  public void testWasQueuedFrom() {
+    String host = "hostname";
+    when(execCfg.isPickOwnHostOnly()).thenReturn(true);
+    Optional<String> wasQueuedFrom = executorModule.wasQueuedFrom(execCfg, host);
+    if (!wasQueuedFrom.isPresent()) {
+      fail("wasQueued from is empty");
+    }
+    assertThat(wasQueuedFrom.get()).isEqualTo(host);
+
+    when(execCfg.isPickOwnHostOnly()).thenReturn(false);
+    wasQueuedFrom = executorModule.wasQueuedFrom(execCfg, host);
+    assertThat(wasQueuedFrom.isPresent()).isFalse();
+  }
+
+  @Test
+  public void testQueuedForLongerThan() {
+    when(execCfg.getDelay()).thenReturn(5);
+    assertThat(executorModule.queuedForLongerThan(execCfg)).isEqualTo(5);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java
new file mode 100644
index 0000000..6d08f55
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class GCTest {
+
+  @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+  @Test
+  public void testGc() throws Exception {
+    Repository repository = createRepository("repoTest");
+    RepoStatistics stats = computeFor(repository);
+    assertThat(stats.numberOfLooseObjects).isEqualTo(0);
+
+    addFileTo(repository);
+    stats = computeFor(repository);
+    assertThat(stats.numberOfLooseObjects).isEqualTo(2);
+    ProgressMonitor cpm = new CancellableProgressMonitor();
+    GarbageCollector gc = new GarbageCollector();
+    gc.setRepositoryPath(repository.getDirectory().getAbsolutePath());
+    gc.setPm(cpm);
+
+    assertThat(gc.call()).isTrue();
+    stats = computeFor(repository);
+    assertThat(stats.numberOfLooseObjects).isEqualTo(0);
+    assertThat(stats.numberOfPackedRefs).isEqualTo(1);
+  }
+
+  private Repository createRepository(String repoName) throws Exception {
+    File repo = dir.newFolder(repoName);
+    try (Git git = Git.init().setDirectory(repo).call()) {
+      return git.getRepository();
+    }
+  }
+
+  private void addFileTo(Repository repository) throws Exception {
+    try (Git git = new Git(repository)) {
+      new File(repository.getDirectory().getParent(), "test");
+      git.add().addFilepattern("test").call();
+      git.commit().setMessage("Add test file").call();
+    }
+  }
+
+  private RepoStatistics computeFor(Repository repository) throws IOException {
+    return new GC((FileRepository) repository).getStatistics();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java
new file mode 100644
index 0000000..4c74510
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java
@@ -0,0 +1,183 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.ericsson.gerrit.plugins.gcconductor.executor.GcExecutor.CONFIG_FILE_PROPERTY;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+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 GcExecutorTest {
+
+  private static final String HOSTNAME = "hostname";
+  private static final String EXECUTOR = "hostname-0";
+  private static final String REPOSITORY = "repository";
+
+  @Mock private ExecutorConfig config;
+  @Mock GcWorker.Factory gcWorkerFactory;
+  @Mock private GcWorker gcWorker;
+  @Mock private GcQueue gcQueue;
+  @Mock private ScheduledEvaluator scheduledEvaluator;
+
+  @Rule public TemporaryFolder testTempFolder = new TemporaryFolder();
+
+  @Test
+  public void testGcExecutor() {
+    when(config.getExecutors()).thenReturn(1);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcWorker).start();
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testLeftOverReposAreUnpickedWhenStarting() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    when(gcQueue.list())
+        .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, EXECUTOR, HOSTNAME)));
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcQueue).unpick(REPOSITORY);
+    verify(gcWorker).start();
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testUnassignedReposAreNotUnpickedWhenStarting() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    when(gcQueue.list())
+        .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcQueue, never()).unpick(REPOSITORY);
+    verify(gcWorker).start();
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testExecutorDoesNotUnpickNotOwnedRepo() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    when(gcQueue.list())
+        .thenReturn(
+            ImmutableList.of(
+                new RepositoryInfo(REPOSITORY, null, "another executor", "another hostname")));
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcQueue, never()).unpick(REPOSITORY);
+    verify(gcWorker).start();
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testQueueThrowsExceptionUnpickingAtStart() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    doThrow(new GcQueueException("", new Throwable())).when(gcQueue).list();
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcQueue, never()).unpick(REPOSITORY);
+    verify(gcWorker).start();
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testScheduledEvaluationIsConfigured() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(config.getInitialDelay()).thenReturn(1L);
+    when(config.getInterval()).thenReturn(1L);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    when(gcQueue.list())
+        .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcWorker).start();
+    verify(scheduledEvaluator).scheduleWith(1L, 1L);
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testScheduledEvaluationBothParametersRequired() throws Exception {
+    when(config.getExecutors()).thenReturn(1);
+    when(config.getInitialDelay()).thenReturn(1L);
+    when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+    when(gcQueue.list())
+        .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+    GcExecutor gcExecutor =
+        new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+    verify(gcWorker).start();
+    verifyZeroInteractions(scheduledEvaluator);
+    gcExecutor.onShutdown();
+    verify(gcWorker).shutdown();
+  }
+
+  @Test
+  public void testloadConfigWhenNotSpecified() {
+    Config config = GcExecutor.loadConfig();
+    assertThat(config.toText()).isEmpty();
+  }
+
+  @Test
+  public void testloadConfigFromWhenSpecifiedByProperty() throws Exception {
+    File configFile = testTempFolder.newFile();
+    FileBasedConfig specifiedConfig = new FileBasedConfig(configFile, FS.DETECTED);
+    specifiedConfig.setString("otherSection", null, "otherKey", "otherValue");
+    specifiedConfig.save();
+    System.setProperty(CONFIG_FILE_PROPERTY, configFile.getAbsolutePath());
+    Config config = GcExecutor.loadConfig();
+    assertThat(config.toText()).isEqualTo(specifiedConfig.toText());
+    System.clearProperty(CONFIG_FILE_PROPERTY);
+  }
+
+  @Test
+  public void shouldReturnEmptyConfigIfAnErrorOccur() throws Exception {
+    File file = testTempFolder.newFile();
+    Files.write(file.toPath(), "[section]\n invalid!@$@#%#$".getBytes(), StandardOpenOption.CREATE);
+    System.setProperty(CONFIG_FILE_PROPERTY, file.getAbsolutePath());
+    Config config = GcExecutor.loadConfig();
+    assertThat(config.toText()).isEmpty();
+    System.clearProperty(CONFIG_FILE_PROPERTY);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java
new file mode 100644
index 0000000..c9b8e5d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import java.io.IOException;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GcWorkerTest {
+  private static final String EXEC_NAME = Thread.currentThread().getName();
+  private static final String REPO_PATH = "repo";
+  private static final String HOSTNAME = "hostname";
+  private static final Optional<String> QUEUED_FROM = Optional.empty();
+
+  @Mock private GcQueue queue;
+  @Mock private GarbageCollector garbageCollector;
+  @Mock private CancellableProgressMonitor cpm;
+
+  private RepositoryInfo repoInfo;
+
+  private GcWorker gcTask;
+
+  @Before
+  public void setUp() {
+    Thread.interrupted(); // reset the flag
+    repoInfo = new RepositoryInfo(REPO_PATH, null, EXEC_NAME, HOSTNAME);
+    gcTask = new GcWorker(queue, garbageCollector, cpm, QUEUED_FROM, 0, EXEC_NAME);
+  }
+
+  @Test
+  public void shouldPickAndRemoveRepository() throws Exception {
+    when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(true);
+    gcTask.run();
+    verify(garbageCollector).call();
+    verify(queue).remove(REPO_PATH);
+    cpm.cancel();
+  }
+
+  @Test
+  public void noRepositoryPicked() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+    gcTask.run();
+    verifyZeroInteractions(garbageCollector);
+    verify(queue, never()).remove(any(String.class));
+    verify(queue, never()).unpick(any(String.class));
+  }
+
+  @Test
+  public void interruptedWhenWaitingToPickRepository() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+    Thread t = new Thread(() -> gcTask.run());
+    t.start();
+    t.interrupt();
+    verifyZeroInteractions(garbageCollector);
+    verify(queue, never()).remove(any(String.class));
+    verify(queue, never()).unpick(any(String.class));
+  }
+
+  @Test
+  public void gcFailsOnRepository() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(false).thenReturn(true);
+    when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+    doThrow(new IOException()).when(garbageCollector).call();
+    gcTask.run();
+    verify(queue).remove(REPO_PATH);
+    verify(queue, never()).unpick(any(String.class));
+  }
+
+  @Test
+  public void queueThrowsExceptionWhenPickingRepository() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+    doThrow(new GcQueueException("", new Throwable())).when(queue).pick(EXEC_NAME, 0, QUEUED_FROM);
+    gcTask.run();
+    verifyZeroInteractions(garbageCollector);
+    verify(queue, never()).remove(any(String.class));
+    verify(queue, never()).unpick(any(String.class));
+  }
+
+  @Test
+  public void gcRepositoryIsInterrupted() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(true).thenReturn(true).thenReturn(true);
+    when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+    doThrow(new IOException()).when(garbageCollector).call();
+    gcTask.run();
+    verify(queue).unpick(REPO_PATH);
+    verify(queue, never()).remove(REPO_PATH);
+  }
+
+  @Test
+  public void queueThrowsExceptionWhenRemovingRepository() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(true);
+    when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+    doThrow(new GcQueueException("", new Throwable())).when(queue).remove(REPO_PATH);
+    gcTask.run();
+    verify(garbageCollector).call();
+    verify(queue).remove(REPO_PATH);
+    verify(queue, never()).unpick(REPO_PATH);
+  }
+
+  @Test
+  public void queueThrowsExceptionWhenUnpickingRepository() throws Exception {
+    when(cpm.isCancelled()).thenReturn(false).thenReturn(true).thenReturn(true).thenReturn(true);
+    when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+    doThrow(new IOException()).when(garbageCollector).call();
+    doThrow(new GcQueueException("", new Throwable())).when(queue).unpick(REPO_PATH);
+    gcTask.run();
+    verify(queue).unpick(REPO_PATH);
+    verify(queue, never()).remove(REPO_PATH);
+  }
+
+  @Test
+  public void callingShutdownSetsCancellableToTrue() {
+    gcTask.shutdown();
+    verify(cpm).cancel();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java
new file mode 100644
index 0000000..c560fbf
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask.Factory;
+import com.google.inject.ProvisionException;
+import java.io.File;
+import org.eclipse.jgit.api.Git;
+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 ScheduledEvaluationTaskTest {
+
+  @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+  @Mock private ExecutorConfig config;
+  @Mock private Factory evaluationTaskFactory;
+  @Mock private EvaluationTask evaluationTask;
+
+  private ScheduledEvaluationTask periodicEvaluator;
+
+  @Before
+  public void setUp() {
+    when(config.getRepositoriesPath()).thenReturn(dir.getRoot().getAbsolutePath());
+    periodicEvaluator = new ScheduledEvaluationTask(evaluationTaskFactory, config);
+  }
+
+  @Test(expected = ProvisionException.class)
+  public void shouldThrowAnExceptionOnInvalidrepositoryPath() {
+    when(config.getRepositoriesPath()).thenReturn("unexistingPath");
+    periodicEvaluator = new ScheduledEvaluationTask(evaluationTaskFactory, config);
+  }
+
+  @Test
+  public void shouldAddRepository() throws Exception {
+    Repository repository = createRepository("repoTest");
+    when(evaluationTaskFactory.create(repository.getDirectory().getAbsolutePath()))
+        .thenReturn(evaluationTask);
+    periodicEvaluator.run();
+    verify(evaluationTask).run();
+  }
+
+  @Test
+  public void shouldNotAddFolderIfNotRepository() throws Exception {
+    File notARepo = dir.newFolder("notRepository");
+    periodicEvaluator.run();
+    verify(evaluationTaskFactory, never()).create(notARepo.getAbsolutePath());
+  }
+
+  @Test
+  public void shouldHonorInterruption() throws Exception {
+    createRepository("repoTest");
+    Thread t = new Thread(() -> periodicEvaluator.run());
+    t.start();
+    t.interrupt();
+    verifyZeroInteractions(evaluationTaskFactory);
+  }
+
+  private Repository createRepository(String repoName) throws Exception {
+    File repo = dir.newFolder(repoName);
+    try (Git git = Git.init().setDirectory(repo).call()) {
+      return git.getRepository();
+    }
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java
new file mode 100644
index 0000000..f04cdf3
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ScheduledEvaluatorTest {
+
+  @Mock private ScheduledEvaluationTask evaluationTask;
+
+  private ScheduledEvaluator scheduledEvaluator;
+
+  @Test
+  public void testScheduleWith() throws Exception {
+    scheduledEvaluator = new ScheduledEvaluator(evaluationTask);
+    scheduledEvaluator.scheduleWith(1, 2000);
+    TimeUnit.MILLISECONDS.sleep(100);
+    verify(evaluationTask).run();
+    scheduledEvaluator.onShutdown();
+  }
+
+  @Test
+  public void testOnShutdown() {
+    scheduledEvaluator = new ScheduledEvaluator(evaluationTask);
+    scheduledEvaluator.onShutdown();
+    verifyZeroInteractions(evaluationTask);
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java
new file mode 100644
index 0000000..fa2593f
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.configMockFor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.deleteDatabase;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import com.ericsson.gerrit.plugins.gcconductor.postgresqueue.PostgresModule.DatabaseAccessCleanUp;
+import java.sql.SQLException;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostgresModuleTest {
+
+  private static final String TEST_DATABASE_NAME = "gc_test_module";
+  private EvaluatorConfig configMock;
+  private PostgresModule module;
+  private BasicDataSource dataSource;
+
+  @Before
+  public void setUp() throws SQLException {
+    configMock = configMockFor(TEST_DATABASE_NAME);
+    module = new PostgresModule(null);
+    dataSource = module.provideGcDatabaseAccess(configMock);
+  }
+
+  @After
+  public void tearDown() throws SQLException {
+    if (dataSource != null) {
+      dataSource.close();
+    }
+    deleteDatabase(TEST_DATABASE_NAME);
+  }
+
+  @Test
+  public void shouldCreateGcDatabase() {
+    assertThat(dataSource).isNotNull();
+    assertThat(dataSource.isClosed()).isFalse();
+  }
+
+  @Test
+  public void shouldNotComplainsIfGcDatabaseAlreadyExists() throws SQLException {
+    dataSource.close();
+    dataSource = module.provideGcDatabaseAccess(configMock);
+    assertThat(dataSource).isNotNull();
+    assertThat(dataSource.isClosed()).isFalse();
+  }
+
+  @Test(expected = SQLException.class)
+  public void shouldFailIfDatabaseNameIsInvalid() throws SQLException {
+    module.provideGcDatabaseAccess(configMockFor("invalid!@#$@$%^-name"));
+  }
+
+  @Test
+  public void shouldCloseDatabaseAccessOnStop() {
+    DatabaseAccessCleanUp dbCleanUp = new DatabaseAccessCleanUp(dataSource);
+    assertThat(dataSource).isNotNull();
+    assertThat(dataSource.isClosed()).isFalse();
+    dbCleanUp.onShutdown();
+    assertThat(dataSource.isClosed()).isTrue();
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void shouldThrowExceptionIfFailsToCloseDatabaseAccess() throws SQLException {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    doThrow(new SQLException("somme error")).when(dataSouceMock).close();
+    DatabaseAccessCleanUp dbCleanUp = new DatabaseAccessCleanUp(dataSouceMock);
+    dbCleanUp.onShutdown();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java
new file mode 100644
index 0000000..2b60076
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java
@@ -0,0 +1,496 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.configMockFor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.deleteDatabase;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostgresQueueTest {
+
+  private static final String TEST_DATABASE_NAME = "gc_test_queue";
+  private BasicDataSource dataSource;
+  private PostgresQueue queue;
+
+  @Before
+  public void setUp() throws SQLException {
+    dataSource =
+        new PostgresModule(null).provideGcDatabaseAccess(configMockFor(TEST_DATABASE_NAME));
+    queue = new PostgresQueue(dataSource);
+  }
+
+  @After
+  public void tearDown() throws SQLException {
+    if (dataSource != null) {
+      dataSource.close();
+    }
+    deleteDatabase(TEST_DATABASE_NAME);
+  }
+
+  @Test
+  public void shouldCreateSchemaOnInit() throws GcQueueException {
+    assertThat(queue.list()).isEmpty();
+  }
+
+  @Test(expected = SQLException.class)
+  public void shouldThrowExceptionIfFailsToCreateSchemaOnInit() throws Exception {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    when(dataSouceMock.getConnection()).thenThrow(new SQLException("some message"));
+    queue = new PostgresQueue(dataSouceMock);
+  }
+
+  @Test
+  public void testAddContainsAndRemove() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String hostname = "someHostname";
+
+    assertThat(queue.list()).isEmpty();
+    assertThat(queue.contains(repoPath)).isFalse();
+
+    queue.add(repoPath, hostname);
+    assertThat(queue.list().size()).isEqualTo(1);
+    assertThat(queue.contains(repoPath)).isTrue();
+
+    queue.add(repoPath, hostname);
+    assertThat(queue.list().size()).isEqualTo(1);
+    assertThat(queue.contains(repoPath)).isTrue();
+
+    String repoPath2 = "/some/path/to/some/repository2";
+    String hostname2 = "someHostname2";
+
+    queue.add(repoPath2, hostname2);
+    assertThat(queue.list().size()).isEqualTo(2);
+    assertThat(queue.contains(repoPath)).isTrue();
+    assertThat(queue.contains(repoPath2)).isTrue();
+
+    queue.remove(repoPath2);
+    assertThat(queue.list().size()).isEqualTo(1);
+    assertThat(queue.contains(repoPath)).isTrue();
+    assertThat(queue.contains(repoPath2)).isFalse();
+
+    queue.remove(repoPath);
+    assertThat(queue.list().size()).isEqualTo(0);
+    assertThat(queue.contains(repoPath)).isFalse();
+
+    queue.remove(repoPath);
+    assertThat(queue.list().size()).isEqualTo(0);
+    assertThat(queue.contains(repoPath)).isFalse();
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testAddThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.add("repo", "hostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testAddThatFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.add("repo", "hostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testAddThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.add("repo", "hostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testContainsThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.contains("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testContainsThatFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.contains("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testContainsThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.contains("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testContainsThatFailsWhenIteratingResults() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+    queue.contains("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testRemoveThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.remove("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testRemoveThatFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.remove("repo");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testRemoveThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.remove("repo");
+  }
+
+  @Test
+  public void testList() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository.git";
+    String repoPath2 = "/some/path/to/some/repository2.git";
+    String hostname = "hostname";
+    String executor = "hostname-1";
+
+    assertThat(queue.list()).isEmpty();
+    Timestamp before = new Timestamp(System.currentTimeMillis());
+    queue.add(repoPath, hostname);
+    queue.add(repoPath2, hostname);
+    queue.pick(executor, 0, Optional.empty());
+
+    assertThat(queue.list().size()).isEqualTo(2);
+
+    assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+    assertThat(queue.list().get(0).getExecutor()).isEqualTo(executor);
+    assertThat(queue.list().get(0).getQueuedAt()).isAtLeast(before);
+    assertThat(queue.list().get(0).getQueuedAt())
+        .isAtMost(new Timestamp(System.currentTimeMillis()));
+    assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(hostname);
+
+    assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath2);
+    assertThat(queue.list().get(1).getExecutor()).isNull();
+    assertThat(queue.list().get(1).getQueuedAt()).isAtLeast(queue.list().get(0).getQueuedAt());
+    assertThat(queue.list().get(1).getQueuedAt())
+        .isAtMost(new Timestamp(System.currentTimeMillis()));
+    assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(hostname);
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testListThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.list();
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testListThatFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.list();
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testListThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.list();
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testListThatFailsWhenIteratingResults() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+    queue.list();
+  }
+
+  @Test
+  public void testPick() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String hostname = "someHostname";
+    String executor = "someExecutor";
+    String executor2 = "someExecutor2";
+
+    // queue is empty nothing to pick
+    assertThat(queue.list()).isEmpty();
+    assertThat(queue.pick(executor, 0, Optional.empty())).isNull();
+
+    // queue contains 1 repository, should pick that one
+    queue.add(repoPath, hostname);
+    RepositoryInfo picked = queue.pick(executor, 0, Optional.empty());
+    assertThat(picked).isNotNull();
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+
+    // queue contains 1 already picked repository, should pick same one
+    picked = queue.pick(executor, 0, Optional.empty());
+    assertThat(picked).isNotNull();
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+
+    // queue contains 1 already picked repository, nothing to pick for other
+    // executors
+    assertThat(queue.pick(executor2, 0, Optional.empty())).isNull();
+  }
+
+  @Test
+  public void testPickRepositoriesInOrder() throws GcQueueException {
+    String repositoryFormat = "my/path%s.git";
+    for (int i = 0; i < 100; i++) {
+      queue.add(String.format(repositoryFormat, i), "someHostname");
+    }
+    for (int i = 0; i < 100; i++) {
+      String pickedRepo = queue.pick("someExecutor", 0, Optional.empty()).getPath();
+      assertThat(pickedRepo).isEqualTo(String.format(repositoryFormat, i));
+      queue.remove(pickedRepo);
+    }
+  }
+
+  @Test
+  public void testPickInQueueForLongerThan() throws GcQueueException, InterruptedException {
+    String repoPath = "/some/path/to/some/repository";
+    String hostname = "someHostname";
+    String executor = "someExecutor";
+
+    // pick repository older than 10 seconds, nothing to pick
+    queue.add(repoPath, hostname);
+    assertThat(queue.pick(executor, 10, Optional.empty())).isNull();
+    assertThat(queue.list().get(0).getExecutor()).isNull();
+
+    // make 2 seconds elapse and pick repository older than 1 second, should pick one
+    TimeUnit.SECONDS.sleep((2));
+    RepositoryInfo picked = queue.pick(executor, 1, Optional.empty());
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+  }
+
+  @Test
+  public void testPickQueuedFrom() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String hostname = "hostname";
+    String otherHostname = "otherHostname";
+    String executor = "hostname-1";
+
+    // pick repository queued from otherHostname, nothing to pick
+    queue.add(repoPath, hostname);
+    assertThat(queue.pick(executor, 0, Optional.of(otherHostname))).isNull();
+    assertThat(queue.list().get(0).getExecutor()).isNull();
+
+    // pick repository queued from hostname, should pick one
+    RepositoryInfo picked = queue.pick(executor, 0, Optional.of(hostname));
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testPickThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.pick("executor", 0, Optional.empty());
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testPickThatFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.pick("executor", 0, Optional.empty());
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testPickThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.pick("executor", 0, Optional.empty());
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testPickThatFailsWhenIteratingResults() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+    queue.pick("executor", 0, Optional.empty());
+  }
+
+  @Test
+  public void testUnpick() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String hostname = "someHostname";
+    String executor = "someExecutor";
+
+    // queue contains 1 repository, should pick that one
+    queue.add(repoPath, hostname);
+    RepositoryInfo picked = queue.pick(executor, 0, Optional.empty());
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+
+    queue.unpick(repoPath);
+    // unpick repo so should pick that one again
+    queue.unpick(repoPath);
+    picked = queue.pick(executor, 0, Optional.empty());
+    assertThat(picked.getPath()).isEqualTo(repoPath);
+    assertThat(picked.getExecutor()).isEqualTo(executor);
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testUnpickThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.unpick("/some/path/to/some/repository.git");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testUnpickFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.unpick("/some/path/to/some/repository.git");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testUnpickThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.unpick("/some/path/to/some/repository.git");
+  }
+
+  @Test
+  public void testResetQueuedFrom() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String repoPath2 = "/some/path/to/some/repository2";
+    String hostname = "hostname";
+    String otherHostname = "otherHostname";
+
+    queue.add(repoPath, hostname);
+    queue.add(repoPath2, hostname);
+    assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(hostname);
+    assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(hostname);
+
+    queue.resetQueuedFrom(otherHostname);
+    assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(otherHostname);
+    assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(otherHostname);
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testResetQueuedFromThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.resetQueuedFrom("someHostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testResetQueuedFromFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.resetQueuedFrom("someHostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testResetQueuedFromThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.resetQueuedFrom("someHostname");
+  }
+
+  @Test
+  public void testBumpToFirst() throws GcQueueException {
+    String repoPath = "/some/path/to/some/repository";
+    String repoPath2 = "/some/path/to/some/repository2";
+    String repoPath3 = "/some/path/to/some/repository3";
+    String hostname = "hostname";
+
+    // Queue contains 1 repository, bumping should have no effect
+    queue.add(repoPath, hostname);
+    assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+    queue.bumpToFirst(repoPath);
+    assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+
+    // Queue has 3 repositories, should be able to change their order
+    queue.add(repoPath2, hostname);
+    queue.add(repoPath3, hostname);
+    assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath2);
+    assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath3);
+
+    // repoPath3 should be first, all other repositories should be shifted down
+    queue.bumpToFirst(repoPath3);
+    assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath3);
+    assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath);
+    assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath2);
+
+    // Bumping a repository that is already first priority should have no effect
+    queue.bumpToFirst(repoPath3);
+    assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath3);
+    assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath);
+    assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath2);
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testBumpToFirstThatFailsWhenGettingConnection() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+    queue.bumpToFirst("someHostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testBumpToFirstFailsWhenCreatingStatement() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+    queue.bumpToFirst("someHostname");
+  }
+
+  @Test(expected = GcQueueException.class)
+  public void testBumpToFirstThatFailsWhenExecutingQuery() throws Exception {
+    queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+    queue.bumpToFirst("someHostname");
+  }
+
+  private BasicDataSource createDataSourceThatFailsWhenGettingConnection() throws SQLException {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    Connection connectionMock = mock(Connection.class);
+    Statement statementMock = mock(Statement.class);
+
+    when(dataSouceMock.getConnection()).thenReturn(connectionMock).thenThrow(new SQLException());
+    when(connectionMock.createStatement()).thenReturn(statementMock);
+
+    return dataSouceMock;
+  }
+
+  private BasicDataSource createDataSourceThatFailsWhenCreatingStatement() throws SQLException {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    Connection connectionMock = mock(Connection.class);
+    Statement statementMock = mock(Statement.class);
+
+    when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+    when(connectionMock.createStatement()).thenReturn(statementMock).thenThrow(new SQLException());
+
+    return dataSouceMock;
+  }
+
+  private BasicDataSource createDataSourceThatFailsWhenExecutingQuery() throws SQLException {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    Connection connectionMock = mock(Connection.class);
+    Statement statementMock = mock(Statement.class);
+
+    when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+    when(connectionMock.createStatement()).thenReturn(statementMock);
+    when(statementMock.execute(anyString())).thenReturn(true).thenThrow(new SQLException());
+    when(statementMock.executeQuery(anyString())).thenThrow(new SQLException());
+
+    return dataSouceMock;
+  }
+
+  private BasicDataSource createDataSourceThatFailsWhenIteratingResults() throws SQLException {
+    BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+    Connection connectionMock = mock(Connection.class);
+    Statement statementMock = mock(Statement.class);
+    ResultSet resultSetMock = mock(ResultSet.class);
+
+    when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+    when(connectionMock.createStatement()).thenReturn(statementMock);
+    when(statementMock.executeQuery(anyString())).thenReturn(resultSetMock);
+    when(resultSetMock.next()).thenThrow(new SQLException());
+
+    return dataSouceMock;
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java
new file mode 100644
index 0000000..023f9cd
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.DRIVER;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.INITIAL_DATABASE;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.dropDatabase;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import java.sql.SQLException;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Test;
+
+public class TestUtil {
+
+  private static final String DATABASE_SERVER_URL = "jdbc:postgresql://localhost:5432/";
+  private static final String DEFAULT_USER_AND_PASSWORD = "gc";
+
+  static EvaluatorConfig configMockFor(String databaseName) {
+    EvaluatorConfig configMock = mock(EvaluatorConfig.class);
+    when(configMock.getDatabaseUrl()).thenReturn(DATABASE_SERVER_URL);
+    when(configMock.getDatabaseUrlOptions()).thenReturn("");
+    when(configMock.getDatabaseName()).thenReturn(databaseName);
+    when(configMock.getUsername()).thenReturn(DEFAULT_USER_AND_PASSWORD);
+    when(configMock.getPassword()).thenReturn(DEFAULT_USER_AND_PASSWORD);
+    return configMock;
+  }
+
+  static void deleteDatabase(String databaseName) throws SQLException {
+    BasicDataSource ds = new BasicDataSource();
+    try {
+      ds.setDriverClassName(DRIVER);
+      ds.setUrl(DATABASE_SERVER_URL + INITIAL_DATABASE);
+      ds.setUsername(DEFAULT_USER_AND_PASSWORD);
+      ds.setPassword(DEFAULT_USER_AND_PASSWORD);
+      executeStatement(ds, dropDatabase(databaseName));
+    } finally {
+      ds.close();
+    }
+  }
+
+  @Test
+  public void fakeTest() {
+    // Hackish way of avoiding bazel test mark this class as failed
+    // because of the lack of executable methods
+    return;
+  }
+}
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..d5764f7
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+    "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..0b25d23
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..35a7598
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+    name = "main_classpath_collect",
+    testonly = 1,
+    deps = [
+        "//:gc-conductor__plugin_test_deps",
+    ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..2c64166
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright (C) 2017 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.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n gc-conductor -r .
diff --git a/tools/sonar/sonar.sh b/tools/sonar/sonar.sh
new file mode 100755
index 0000000..8df06d3
--- /dev/null
+++ b/tools/sonar/sonar.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# 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.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/sonar:sonar --output location | sed s/BUILD:.*//`sonar.py
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..00630d2
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+  cd $1 && git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_GC-CONDUCTOR_LABEL "$(rev .)"