Initial commit

Initial version of repository-usage plugin, tested against H2 and
PostgreSQL.  Currently able to scan submodule updates and manifest
files in the root tree.

Change-Id: I4988b4215211817d2af8ca8b64c9d013a72fce95
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..dd4f476
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,15 @@
+[alias]
+  repository-usage = //:repository-usage
+  plugin = //:repository-usage
+  src = //:repository-usage-sources
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..525de60
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.project
+.classpath
+buck-out
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..d894755
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,66 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+include_defs('//lib/maven.defs')
+
+DEPS = [
+  ':digester3',
+  ':beanutils',
+  ':logging',
+]
+PROVIDED_DEPS = [
+  '//lib/commons:dbcp',
+  '//lib:gson',
+]
+TEST_DEPS = GERRIT_PLUGIN_API + [
+  ':repository-usage__plugin',
+  '//lib:junit',
+  '//lib:truth',
+]
+
+gerrit_plugin(
+  name = 'repository-usage',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/resources/**/*']),
+  manifest_entries = [
+    'Gerrit-PluginName: repository-usage',
+    'Gerrit-Module: com.googlesource.gerrit.plugins.repositoryuse.Module',
+  ],
+  deps = DEPS,
+  provided_deps = PROVIDED_DEPS,
+)
+
+java_library(
+  name = 'classpath',
+  deps = [':repository-usage__plugin'],
+)
+
+java_test(
+  name = 'repository-usage_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  labels = ['repository-usage'],
+  source_under_test = [':repository-usage__plugin'],
+  deps = TEST_DEPS,
+)
+
+maven_jar(
+  name = 'digester3',
+  id = 'org.apache.commons:commons-digester3:3.2',
+  sha1 = 'c3f68c5ff25ec5204470fd8fdf4cb8feff5e8a79',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'beanutils',
+  id = 'commons-beanutils:commons-beanutils:1.8.3',
+  sha1 = '686ef3410bcf4ab8ce7fd0b899e832aaba5facf7',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+  name = 'logging',
+  id = 'commons-logging:commons-logging:1.1.1',
+  sha1 = '5043bfebc3db072ed80fbd362e7caf00e885d8ae',
+  license = 'Apache2.0',
+)
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..f501067
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,27 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+maven_jar(
+  name = 'junit',
+  id = 'junit:junit:4.10',
+  sha1 = 'e4f1766ce7404a08f45d859fb9c226fc9e41a861',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [':hamcrest-core'],
+)
+
+maven_jar(
+  name = 'hamcrest-core',
+  id = 'org.hamcrest:hamcrest-core:1.3',
+  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = ['//lib:junit'],
+)
+
+maven_jar(
+  name = 'truth',
+  id = 'com.google.truth:truth:0.26',
+  sha1 = 'b5802815625d82f39c33219299771f3d64301b06',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':junit',
+  ],
+)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
new file mode 100644
index 0000000..96d016a
--- /dev/null
+++ b/lib/gerrit/BUCK
@@ -0,0 +1,13 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '2.11'
+REPO = MAVEN_CENTRAL
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  sha1 = 'be80ff991f7b9f8669b7a2a399003ec1ae69ed31',
+  license = 'Apache2.0',
+  attach_source = False,
+  repository = REPO,
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java
new file mode 100644
index 0000000..3722cd8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Config.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+public class Config {
+  public enum Database {
+    H2, POSTGRESQL
+  }
+
+  @Inject
+  private static PluginConfigFactory cfg;
+
+  @Inject
+  @PluginName
+  private static String pluginName;
+
+  @Inject
+  private static SitePaths sitePaths;
+
+  private static boolean configParsed = false;
+  private static boolean refreshAllSubmodules;
+  private static boolean parseManifests;
+  private static Database databaseType;
+  private static String database;
+  private static String databaseHost;
+  private static String databaseUser;
+  private static String databasePassword;
+
+  private static void readConfig() {
+    PluginConfig pc = cfg.getFromGerritConfig(pluginName);
+    refreshAllSubmodules = pc.getBoolean("refreshAllSubmodules", false);
+    parseManifests = pc.getBoolean("parseManifests", true);
+    databaseType = pc.getEnum("databaseType", Database.H2);
+    database = pc.getString("database",
+        sitePaths.site_path.resolve("db/UsageDB").toString());
+    databaseHost = pc.getString("databaseHost", "");
+    databaseUser = pc.getString("databaseUser", "");
+    databasePassword = pc.getString("databasePassword", "");
+    configParsed = true;
+  }
+
+  public static boolean refreshAllSubmodules() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return refreshAllSubmodules;
+  }
+
+  public static boolean parseManifests() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return parseManifests;
+  }
+
+  public static Database getDatabaseType() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return databaseType;
+  }
+
+  public static String getDatabase() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return database;
+  }
+
+  public static String getDatabaseHost() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return databaseHost;
+  }
+
+  public static String getDatabaseUser() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return databaseUser;
+  }
+
+  public static String getDatabasePassword() {
+    if (!configParsed) {
+      readConfig();
+    }
+    return databasePassword;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java
new file mode 100644
index 0000000..acaa3ee
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/ManifestParser.java
@@ -0,0 +1,173 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import org.apache.commons.digester3.Digester;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.SAXException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ManifestParser {
+  private static final Logger log =
+      LoggerFactory.getLogger(ManifestParser.class);
+  private HashMap<String, String> remotes;
+  private Project defaultProject;
+  private ArrayList<Project> projects;
+
+  public ManifestParser() {
+    remotes = new HashMap<>();
+    defaultProject = new Project(null, null, null);
+    projects = new ArrayList<>();
+  }
+
+  public Map<String, String> parseManifest(byte[] contents) {
+    Digester digester = new Digester();
+    digester.push(this);
+
+    // Add all remote handlers
+    digester.addCallMethod("manifest/remote", "addRemote", 2);
+    digester.addCallParam("manifest/remote", 0, "name");
+    digester.addCallParam("manifest/remote", 1, "fetch");
+
+    // Add all default handlers. This handles both repo format
+    // attributes (remote, revision) and non-standard attributes
+    // (branch, tag, commit-id).
+    digester.addCallMethod("manifest/default", "addDefault", 5);
+    digester.addCallParam("manifest/default", 0, "remote");
+    digester.addCallParam("manifest/default", 1, "revision");
+    digester.addCallParam("manifest/default", 2, "branch");
+    digester.addCallParam("manifest/default", 3, "tag");
+    digester.addCallParam("manifest/default", 4, "commit-id");
+
+    // Add all project handlers. This handles both repo format
+    // attributes (remote, revision) and non-standard attributes
+    // (branch, tag, commit-id).
+    digester.addCallMethod("manifest/project", "addProject", 6);
+    digester.addCallParam("manifest/project", 0, "remote");
+    digester.addCallParam("manifest/project", 1, "name");
+    digester.addCallParam("manifest/project", 2, "revision");
+    digester.addCallParam("manifest/project", 3, "branch");
+    digester.addCallParam("manifest/project", 4, "tag");
+    digester.addCallParam("manifest/project", 5, "commit-id");
+
+
+    InputStream input = new ByteArrayInputStream(contents);
+    try {
+      digester.parse(input);
+    } catch (IOException | SAXException e) {
+      log.warn("Unable to parse manifest", e);
+    }
+    HashMap<String, String> resolvedProjects = new HashMap<>(projects.size());
+    for (Project p : projects) {
+      String uri = null;
+      String revision = null;
+
+      if (p.getRemote() != null) {
+        uri = remotes.get(p.getRemote());
+      }
+
+      if (uri == null && defaultProject.getRemote() != null) {
+        uri = remotes.get(defaultProject.getRemote());
+      }
+
+      if (uri == null && p.getRemote() != null) {
+        uri = p.getRemote();
+      }
+
+      if (uri != null) {
+        uri += "/" + p.getName();
+      }
+
+      if (p.getRevision() != null) {
+        revision = p.getRevision();
+      } else if (defaultProject.getRevision() != null) {
+        revision = defaultProject.getRevision();
+      }
+
+      if (uri != null && revision != null) {
+        resolvedProjects.put(uri, revision);
+      } else {
+        log.warn("Invalid project description in manifest");
+      }
+    }
+    return resolvedProjects;
+  }
+
+  public void addRemote(String name, String fetch) {
+    remotes.put(name, fetch);
+  }
+
+  public void addDefault(String remote, String revision, String branch,
+      String tag, String commitId) {
+    defaultProject =
+        new Project(remote, null, getRevision(revision, branch, tag, commitId));
+  }
+
+  public void addProject(String remote, String name, String revision,
+      String branch, String tag, String commitId) {
+    if (name != null) {
+      projects.add(new Project(remote, name,
+          getRevision(revision, branch, tag, commitId)));
+    } else {
+      log.warn("Project name not specified in manifest");
+    }
+  }
+
+  private String getRevision(String revision, String branch, String tag,
+      String commitId) {
+    if (revision != null) {
+      return revision;
+    } else if (branch != null) {
+      return branch;
+    } else if (tag != null) {
+      return tag;
+    } else if (commitId != null) {
+      return commitId;
+    }
+    return null;
+  }
+
+  public static class Project {
+    private String remote;
+    private String name;
+    private String revision;
+
+    public Project(String remote, String name, String revision) {
+      this.remote = remote;
+      this.name = name;
+      this.revision = revision;
+    }
+
+    public String getRemote() {
+      return remote;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public String getRevision() {
+      return revision;
+    }
+  }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
new file mode 100644
index 0000000..4c7b822
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Module.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.internal.UniqueAnnotations;
+
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+        .to(RefUpdateHandler.class);
+    requestStaticInjection(Config.class);
+    requestStaticInjection(Ref.Table.class);
+    requestStaticInjection(Usage.Table.class);
+    bind(LifecycleListener.class).annotatedWith(UniqueAnnotations.create())
+        .to(SQLDriver.class);
+  }
+
+  @Provides
+  @Singleton
+  SQLDriver provideSqlDriver() {
+    return new SQLDriver();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java
new file mode 100644
index 0000000..58c74d2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Ref.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class Ref {
+  private static final Logger log = LoggerFactory.getLogger(Ref.class);
+  private static Table table = new Table();
+
+  private String project;
+  private String ref;
+  private String commit;
+  private Date lastUpdated;
+
+  public Ref(String project, String ref, String commit) {
+    init(project, ref, commit, new Date());
+  }
+
+  public Ref(String project, String ref, String commit, Date date) {
+    init(project, ref, commit, date);
+  }
+
+  private void init(String project, String ref, String commit, Date date) {
+    this.project = project;
+    this.ref = ref;
+    this.commit = commit;
+    this.lastUpdated = date;
+  }
+
+  public String getProject() {
+    return project;
+  }
+
+  public String getRef() {
+    return ref;
+  }
+
+  public String getCommit() {
+    return commit;
+  }
+
+  public void setCommit(String commit) {
+    this.commit = commit;
+  }
+
+  public Date getLastUpdated() {
+    return lastUpdated;
+  }
+
+  public void save() {
+    lastUpdated = new Date();
+    table.insertOrUpdate(this);
+    log.info(String.format("Saving Ref: %s, %s, %s", project, ref, commit));
+  }
+
+  public void delete() {
+    log.info(String.format("Deleting Ref: %s, %s", project, ref));
+    table.delete(this);
+  }
+
+  public static List<Ref> fetchByProject(String project) {
+    return table.fetchByProject(project);
+  }
+
+  public static Ref fetchByRef(String project, String ref) {
+    List<Ref> tmp = table.fetchByRef(project, ref);
+    if (tmp.size() == 1) {
+      return tmp.get(0);
+    }
+    return null;
+  }
+
+  /*
+   * static interface Table { public void insertOrUpdate(Ref u); public
+   * List<Ref> fetchByProject(String project); public List<Ref>
+   * fetchByRef(String project, String ref); }
+   *
+   * public interface TableFactory { Table create(); }
+   */
+
+  static class Table {
+    private static final String TABLE_NAME = "RefStatus";
+    private static final String PROJECT = "project";
+    private static final String REF = "ref";
+    private static final String COMMIT = "commit";
+    private static final String DATE = "last_update";
+    @Inject
+    private static SQLDriver sql;
+
+    public Table() {
+      // Create the table if it doesn't exist
+      createTable();
+    }
+
+    private void createTable() {
+      StringBuilder query = new StringBuilder();
+      query.append(String.format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME));
+      query.append(String.format("%s VARCHAR(1023),", PROJECT));
+      query.append(String.format("%s VARCHAR(255),", REF));
+      query.append(String.format("%s VARCHAR(40),", COMMIT));
+      query.append(String.format("%s TIMESTAMP DEFAULT NOW(),", DATE));
+      query.append(String.format("PRIMARY KEY (%s, %s))", PROJECT, REF));
+      try {
+        sql.execute(query.toString());
+      } catch (SQLException e) {
+        log.error("Unable to create Ref table", e);
+      }
+    }
+
+    public void insertOrUpdate(Ref r) {
+      if (fetchByRef(r.getProject(), r.getRef()).isEmpty()) {
+        String query = "INSERT INTO " + TABLE_NAME + "(" + PROJECT + ", " + REF
+            + ", " + COMMIT + ", " + DATE + ") VALUES (?, ?, ?, "
+            + sql.getDateFormat() + ")";
+        try {
+          sql.execute(query, r.getProject(), r.getRef(), r.getCommit(),
+              sql.getDateAsString(r.getLastUpdated()));
+        } catch (SQLException e) {
+          log.error("Unable to insert reference", e);
+        }
+      } else {
+        String query = "UPDATE " + TABLE_NAME + " SET " + COMMIT + "=?, " + DATE
+            + "=" + sql.getDateFormat() + " WHERE " + PROJECT + "=? AND " + REF
+            + "=?";
+        try {
+          sql.execute(query, r.getCommit(),
+              sql.getDateAsString(r.getLastUpdated()), r.getProject(),
+              r.getRef());
+        } catch (SQLException e) {
+          log.error("Unable to update reference", e);
+        }
+      }
+    }
+
+    public void delete(Ref r) {
+      String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT
+          + "=? AND " + REF + "=?";
+      try {
+        sql.execute(query, r.getProject(), r.getRef());
+      } catch (SQLException e) {
+        log.error("Unable to delete reference", e);
+      }
+    }
+
+
+    public List<Ref> fetchByProject(String project) {
+      String query = "SELECT " + PROJECT + ", " + REF + ", " + COMMIT + ", "
+          + DATE + " FROM " + TABLE_NAME + " WHERE " + PROJECT + "=?";
+      try {
+        return loadRefs(sql.fetchRows(query, project));
+
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    public List<Ref> fetchByRef(String project, String ref) {
+      String query = "SELECT " + PROJECT + ", " + REF + ", " + COMMIT + ", "
+          + DATE + " FROM " + TABLE_NAME + " WHERE " + PROJECT + "=? AND " + REF
+          + "=?";
+      try {
+        return loadRefs(sql.fetchRows(query, project, ref));
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    private List<Ref> loadRefs(List<Map<String, String>> rows) {
+      List<Ref> result = new ArrayList<>();
+      for (Map<String, String> row : rows) {
+        Ref tmp = new Ref(row.get(PROJECT), row.get(REF), row.get(COMMIT),
+            sql.getStringAsDate(row.get(DATE)));
+        result.add(tmp);
+      }
+      return result;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
new file mode 100644
index 0000000..f3685b2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/RefUpdateHandler.java
@@ -0,0 +1,292 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.submodule.SubmoduleWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RefUpdateHandler implements GitReferenceUpdatedListener {
+
+  private static final Logger log =
+      LoggerFactory.getLogger(RefUpdateHandler.class);
+  private final GitRepositoryManager repoManager;
+  private final String serverName;
+
+  @Inject
+  RefUpdateHandler(GitRepositoryManager repoManager,
+      @CanonicalWebUrl String canonicalWebUrl) {
+    this.repoManager = repoManager;
+    if (canonicalWebUrl != null) {
+      try {
+        URL url = new URL(canonicalWebUrl);
+        canonicalWebUrl = url.getHost();
+      } catch (MalformedURLException e) {
+        log.warn("Could not parse canonicalWebUrl", e);
+      }
+    }
+    this.serverName = canonicalWebUrl;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(Event event) {
+    if (event.isDelete() && event.getRefName().startsWith(Constants.R_HEADS)
+        || event.getRefName().startsWith(Constants.R_TAGS)) {
+      // Ref was deleted... clean up any references
+      Ref ref = Ref.fetchByRef(event.getProjectName(), event.getRefName());
+      if (ref != null) {
+        ref.delete();
+      }
+      if (event.getRefName().startsWith(Constants.R_HEADS)) {
+        // Also clean up uses from this ref
+        Usage.deleteByBranch(getCanonicalProject(event.getProjectName()),
+            event.getRefName());
+      }
+    } else if (event.getRefName().startsWith(Constants.R_TAGS)) {
+      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
+          event.getNewObjectId());
+      updatedRef.save();
+    } else if (event.getRefName().startsWith(Constants.R_HEADS)) {
+      Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
+          event.getNewObjectId());
+      updatedRef.save();
+      Project.NameKey nameKey = new Project.NameKey(event.getProjectName());
+      try {
+        if (Config.refreshAllSubmodules() || event.isCreate()
+            || isSubmoduleUpdate(event, nameKey)) {
+          Map<String, String> submodules = getSubmodules(event, nameKey);
+          updateProjects(event.getProjectName(), event.getRefName(),
+              submodules);
+        }
+        if (Config.parseManifests()) {
+          parseManifests(event, nameKey);
+        }
+      } catch (IOException e) {
+        log.error(e.getMessage(), e);
+      }
+    }
+  }
+
+  private void parseManifests(Event event, Project.NameKey project)
+      throws RepositoryNotFoundException, IOException {
+    if (event.isDelete()) {
+      return;
+    }
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo); TreeWalk tw = new TreeWalk(repo)) {
+        RevCommit commit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+
+        tw.setRecursive(false);
+        tw.addTree(commit.getTree());
+        ObjectReader or = tw.getObjectReader();
+        while (tw.next()) {
+          String path = tw.getPathString();
+          if (path.endsWith(".xml")) {
+            ManifestParser mp = new ManifestParser();
+            ObjectLoader ol = or.open(tw.getObjectId(0));
+            if (!ol.isLarge()) {
+              Map<String, String> tmp = mp.parseManifest(ol.getBytes());
+              HashMap<String, String> projects = new HashMap<>();
+              for (String key : tmp.keySet()) {
+                projects
+                    .put(
+                        normalizePath(String.format("%s:%s",
+                            event.getProjectName(), path), key, true),
+                    tmp.get(key));
+              }
+              updateProjects(
+                  String.format("%s:%s", event.getProjectName(), path),
+                  event.getRefName(), projects);
+            } else {
+              log.warn(String.format("%s is too large, skipping manifest parse",
+                  tw.getPathString()));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Has a submodule been updated?
+   *
+   * @param event the Event
+   * @return True if a submodule update occurred, otherwise False.
+   */
+  private boolean isSubmoduleUpdate(Event event, Project.NameKey project)
+      throws RepositoryNotFoundException, IOException {
+    if (event.isDelete()) {
+      return false;
+    }
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo);
+          DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+        RevTree aTree = null;
+        if (!event.isCreate()) {
+          // If this is a new ref, we can't get the original commit.
+          // We can still use the DiffFormatter to give us what changed
+          // by passing null, however.
+          RevCommit aCommit =
+              walk.parseCommit(repo.resolve(event.getOldObjectId()));
+          aTree = aCommit.getTree();
+        }
+        RevCommit bCommit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+        RevTree bTree = bCommit.getTree();
+
+        df.setRepository(repo);
+        df.setDiffComparator(RawTextComparator.DEFAULT);
+        df.setDetectRenames(true);
+        List<DiffEntry> diffEntries = df.scan(aTree, bTree);
+        for (DiffEntry de : diffEntries) {
+          FileMode oldMode = de.getOldMode();
+          FileMode newMode = de.getNewMode();
+          if ((oldMode != null && oldMode == FileMode.GITLINK)
+              || (newMode != null && newMode == FileMode.GITLINK)) {
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  private Map<String, String> getSubmodules(Event event,
+      Project.NameKey project) throws RepositoryNotFoundException, IOException {
+    HashMap<String, String> submodules = new HashMap<>();
+    try (Repository repo = repoManager.openRepository(project)) {
+      try (RevWalk walk = new RevWalk(repo);
+          SubmoduleWalk sw = new SubmoduleWalk(repo)) {
+        RevCommit commit =
+            walk.parseCommit(repo.resolve(event.getNewObjectId()));
+        sw.setTree(commit.getTree());
+        sw.setRootTree(commit.getTree());
+        while (sw.next()) {
+          submodules.put(
+              normalizePath(event.getProjectName(), sw.getModulesUrl(), false),
+              sw.getObjectId().name());
+        }
+      } catch (ConfigInvalidException e) {
+        log.warn("Invalid .gitmodules configuration while parsing "
+            + event.getProjectName());
+      }
+    }
+    return submodules;
+  }
+
+  private void updateProjects(String project, String branch,
+      Map<String, String> projects) {
+    String canonicalProject = getCanonicalProject(project);
+    List<Usage> uses = Usage.fetchByProject(canonicalProject, branch);
+    for (Usage use : uses) {
+      if (!projects.containsKey(use.getDestination())) {
+        // No longer exists; delete.
+        use.delete();
+      } else {
+        // Update SHA1 here.
+        use.setRef(projects.get(use.getDestination()));
+        use.save();
+        projects.remove(use.getDestination());
+      }
+    }
+    // At this point, submodules only contains new elements.
+    // Create them.
+    for (String key : projects.keySet()) {
+      Usage use = new Usage(canonicalProject, branch, key, projects.get(key));
+      use.save();
+    }
+  }
+
+  private String getCanonicalProject(String project) {
+    String canonicalProject =
+        String.format("https://%s/%s", serverName, project);
+    try {
+      URL url = new URL(canonicalProject);
+      canonicalProject = url.getHost() + url.getPath();
+    } catch (MalformedURLException e) {
+      log.warn("Could not parse project as URL: " + canonicalProject);
+    }
+    return canonicalProject;
+  }
+
+  private String normalizePath(String project, String destination,
+      boolean isManifest) {
+    String originalProject =
+        isManifest ? project.substring(0, project.lastIndexOf(":")) : project;
+
+    // Handle relative and absolute paths on the same server
+    if (destination.startsWith("/")) {
+      if (serverName != null) {
+        destination = serverName + destination;
+      } else {
+        log.warn("Could not parse absolute path; canonicalWebUrl not set");
+      }
+    } else if (destination.startsWith(".")) {
+      if (serverName != null) {
+        Path path = Paths.get(String.format("/%s/%s", project, destination));
+        destination = serverName + path.normalize().toString();
+      } else {
+        log.warn("Could not parse relative path; canonicalWebUrl not set");
+      }
+    } else if (!destination.matches("^[^:]+://.*")) {
+      if (serverName != null) {
+        destination = serverName + "/" + originalProject + "/" + destination;
+      } else {
+        log.warn("Could not parse relative path; canonicalWebURl not set");
+      }
+    }
+
+    try {
+      // Replace the protocol with a known scheme, to avoid angering URL
+      destination = destination.replaceFirst("^[^:]+://", "");
+      URL url = new URL("https://" + destination);
+      destination = url.getHost() + url.getPath();
+    } catch (MalformedURLException e) {
+      log.warn("Could not parse destination as URL: " + destination);
+    }
+    return destination;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java
new file mode 100644
index 0000000..fdc6252
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/SQLDriver.java
@@ -0,0 +1,158 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+
+import com.googlesource.gerrit.plugins.repositoryuse.Config.Database;
+
+import org.apache.commons.dbcp.BasicDataSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SQLDriver implements LifecycleListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(ManifestParser.class);
+  private static final int POOL_SIZE = 5;
+  private static SimpleDateFormat sdf =
+      new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+  private BasicDataSource ds;
+
+  public SQLDriver() {
+    ds = new BasicDataSource();
+    try {
+      ds.setDriverClassName(getDriver());
+      ds.setUrl(getDatabaseUrl());
+      ds.setUsername(Config.getDatabaseUser());
+      ds.setPassword(Config.getDatabasePassword());
+      ds.setInitialSize(POOL_SIZE);
+    } catch (Exception e) {
+      log.error("Unable to create database connection", e);
+    }
+  }
+
+  @Override
+  public void start() {
+    // no-op
+  }
+
+  @Override
+  public void stop() {
+    if (ds != null) {
+      try {
+        ds.close();
+      } catch (SQLException e) {
+        log.error("Unable to close connection", e);
+      }
+    }
+  }
+
+  public List<Map<String, String>> fetchRows(String query, String... parameters)
+      throws SQLException {
+    ArrayList<Map<String, String>> result = new ArrayList<>();
+    try (Connection c = ds.getConnection();
+        PreparedStatement s = c.prepareStatement(query)) {
+      int i = 1;
+      for (String param : parameters) {
+        s.setString(i, param);
+        i++;
+      }
+      ResultSet r = s.executeQuery();
+      ResultSetMetaData rsmd = r.getMetaData();
+      while (r.next()) {
+        HashMap<String, String> row = new HashMap<>(rsmd.getColumnCount());
+        for (i = 1; i <= rsmd.getColumnCount(); i++) {
+          row.put(rsmd.getColumnLabel(i).toLowerCase(), r.getString(i));
+        }
+        result.add(row);
+      }
+    }
+    return result;
+  }
+
+  public void execute(String query, String... parameters) throws SQLException {
+    try (Connection c = ds.getConnection();
+        PreparedStatement s = c.prepareStatement(query)) {
+      int i = 1;
+      for (String param : parameters) {
+        s.setString(i, param);
+        i++;
+      }
+      if (!s.execute() && s.getUpdateCount() > 0) {
+        if (c.getAutoCommit() == false) {
+          c.commit();
+        }
+      }
+    }
+  }
+
+  public String getDateAsString(Date date) {
+    if (date != null) {
+      return sdf.format(date);
+    }
+    return sdf.format(new Date());
+  }
+
+  public Date getStringAsDate(String date) {
+    if (date != null) {
+      try {
+        return sdf.parse(date);
+      } catch (ParseException e) {
+        log.warn("Unable to parse date", e);
+      }
+    }
+    return new Date();
+  }
+
+  public String getDateFormat() {
+    if (Config.getDatabaseType() == Database.POSTGRESQL) {
+      return "TO_TIMESTAMP(?, 'YYYY-MM-DD HH24:MI:SS')";
+    }
+    return "?";
+  }
+
+  private String getDriver() throws Exception {
+    if (Config.getDatabaseType() == Database.H2) {
+      return "org.h2.Driver";
+    } else if (Config.getDatabaseType() == Database.POSTGRESQL) {
+      return "org.postgresql.Driver";
+    }
+    throw new Exception("Unsupported database engine");
+  }
+
+  private String getDatabaseUrl() throws Exception {
+    if (Config.getDatabaseType() == Database.H2) {
+      return "jdbc:h2:" + Config.getDatabase();
+    } else if (Config.getDatabaseType() == Database.POSTGRESQL) {
+      return "jdbc:postgresql://" + Config.getDatabaseHost() + "/"
+          + Config.getDatabase();
+    }
+    throw new Exception("Unsupported database engine");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java
new file mode 100644
index 0000000..5ca49a4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/repositoryuse/Usage.java
@@ -0,0 +1,272 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.repositoryuse;
+
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class Usage {
+  private static final Logger log = LoggerFactory.getLogger(Usage.class);
+  private static Table table = new Table();
+
+
+  private String project;
+  private String branch;
+  private String destination;
+  private String ref;
+  private String info;
+  private Date lastUpdated;
+
+  public Usage(String project, String branch, String destination, String ref) {
+    init(project, branch, destination, ref, null, new Date());
+  }
+
+  public Usage(String project, String branch, String destination, String ref,
+      String info) {
+    init(project, branch, destination, ref, info, new Date());
+  }
+
+  public Usage(String project, String branch, String destination, String ref,
+      String info, Date date) {
+    init(project, branch, destination, ref, info, date);
+  }
+
+  private void init(String project, String branch, String destination,
+      String ref, String info, Date date) {
+    this.project = project;
+    this.branch = branch;
+    this.destination = destination;
+    this.ref = ref;
+    this.info = info;
+    this.lastUpdated = date;
+  }
+
+  public String getProject() {
+    return project;
+  }
+
+  public String getBranch() {
+    return branch;
+  }
+
+  public String getDestination() {
+    return destination;
+  }
+
+  public String getRef() {
+    return ref;
+  }
+
+  public void setRef(String ref) {
+    this.ref = ref;
+  }
+
+  public String getInfo() {
+    return info;
+  }
+
+  public void setInfo(String info) {
+    this.info = info;
+  }
+
+  public Date getLastUpdated() {
+    return lastUpdated;
+  }
+
+  public void save() {
+    lastUpdated = new Date();
+    table.insertOrUpdate(this);
+    log.info(String.format("Saving Usage: %s, %s, %s, %s", project, branch,
+        destination, ref));
+  }
+
+  public void delete() {
+    table.delete(this);
+    log.info(String.format("Deleting Usage: %s, %s, %s", project, branch,
+        destination));
+  }
+
+  public static List<Usage> fetchByProject(String project) {
+    return table.fetchByProject(project);
+  }
+
+  public static List<Usage> fetchByProject(String project, String branch) {
+    return table.fetchByProject(project, branch);
+  }
+
+  public static List<Usage> fetchByDependency(String dependency) {
+    return table.fetchByDependency(dependency);
+  }
+
+  public static void deleteByBranch(String project, String branch) {
+    table.deleteByBranch(project, branch);
+    log.info(String.format("Deleting all uses: %s, %s", project, branch));
+  }
+
+  static class Table {
+    private static final String TABLE_NAME = "RepoUsage";
+    private static final String PROJECT = "project";
+    private static final String BRANCH = "branch";
+    private static final String DESTINATION = "destination";
+    private static final String REF = "ref";
+    private static final String INFO = "info";
+    private static final String DATE = "last_update";
+    @Inject
+    private static SQLDriver sql;
+
+    public Table() {
+      // Create the table if it doesn't exist
+      createTable();
+    }
+
+    private void createTable() {
+      StringBuilder query = new StringBuilder();
+      query.append(String.format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME));
+      query.append(String.format("%s VARCHAR(1023),", PROJECT));
+      query.append(String.format("%s VARCHAR(255),", BRANCH));
+      query.append(String.format("%s VARCHAR(1023),", DESTINATION));
+      query.append(String.format("%s VARCHAR(255),", REF));
+      query.append(String.format("%s VARCHAR(255),", INFO));
+      query.append(String.format("%s TIMESTAMP DEFAULT NOW(),", DATE));
+      query.append(String.format("PRIMARY KEY (%s, %s, %s))", PROJECT, BRANCH,
+          DESTINATION));
+      try {
+        sql.execute(query.toString());
+      } catch (SQLException e) {
+        log.error("Unable to create Usage table", e);
+      }
+    }
+
+    public void insertOrUpdate(Usage u) {
+      if (fetchByProject(u.getProject(), u.getBranch(), u.getDestination())
+          .isEmpty()) {
+        String query = "INSERT INTO " + TABLE_NAME + "(" + PROJECT + ", "
+            + BRANCH + ", " + DESTINATION + ", " + REF + ", " + INFO + ", "
+            + DATE + ") VALUES (?, ?, ?, ?, ?, " + sql.getDateFormat() + ")";
+        try {
+          sql.execute(query, u.getProject(), u.getBranch(), u.getDestination(),
+              u.getRef(), u.getInfo(), sql.getDateAsString(u.getLastUpdated()));
+        } catch (SQLException e) {
+          log.error("Unable to insert usage", e);
+        }
+      } else {
+        String query = "UPDATE " + TABLE_NAME + " SET " + REF + "=?, " + INFO
+            + "=?, " + DATE + "=" + sql.getDateFormat() + " WHERE " + PROJECT
+            + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?";
+        try {
+          sql.execute(query, u.getRef(), u.getInfo(),
+              sql.getDateAsString(u.getLastUpdated()), u.getProject(),
+              u.getBranch(), u.getDestination());
+        } catch (SQLException e) {
+          log.error("Unable to update usage", e);
+        }
+      }
+    }
+
+    public void delete(Usage u) {
+      String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT
+          + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?";
+      try {
+        sql.execute(query, u.getProject(), u.getBranch(), u.getDestination());
+      } catch (SQLException e) {
+        log.error("Unable to delete usage", e);
+      }
+    }
+
+    public void deleteByBranch(String project, String branch) {
+      String query = "DELETE FROM " + TABLE_NAME + " WHERE " + PROJECT
+          + "=? AND " + BRANCH + "=?";
+      try {
+        sql.execute(query, project, branch);
+      } catch (SQLException e) {
+        log.error("Unable to delete usage", e);
+      }
+    }
+
+    public List<Usage> fetchByProject(String project) {
+      String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION
+          + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME
+          + " WHERE " + PROJECT + "=?";
+      try {
+        return loadUsage(sql.fetchRows(query, project));
+
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    public List<Usage> fetchByProject(String project, String branch) {
+      String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION
+          + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME
+          + " WHERE " + PROJECT + "=? AND " + BRANCH + "=?";
+      try {
+        return loadUsage(sql.fetchRows(query, project, branch));
+
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    public List<Usage> fetchByProject(String project, String branch,
+        String destination) {
+      String query =
+          "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION + ", " + REF
+              + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME + " WHERE "
+              + PROJECT + "=? AND " + BRANCH + "=? AND " + DESTINATION + "=?";
+      try {
+        return loadUsage(sql.fetchRows(query, project, branch, destination));
+
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    public List<Usage> fetchByDependency(String dependency) {
+      String query = "SELECT " + PROJECT + ", " + BRANCH + ", " + DESTINATION
+          + ", " + REF + ", " + INFO + ", " + DATE + " FROM " + TABLE_NAME
+          + " WHERE " + DESTINATION + "=?";
+      try {
+        return loadUsage(sql.fetchRows(query, dependency));
+
+      } catch (SQLException e) {
+        log.error("Unable to execute query", e);
+      }
+      return Collections.emptyList();
+    }
+
+    private List<Usage> loadUsage(List<Map<String, String>> rows) {
+      List<Usage> result = new ArrayList<>();
+      for (Map<String, String> row : rows) {
+        Usage tmp = new Usage(row.get(PROJECT), row.get(BRANCH),
+            row.get(DESTINATION), row.get(REF), row.get(INFO),
+            sql.getStringAsDate(row.get(DATE)));
+        result.add(tmp);
+      }
+      return result;
+    }
+  }
+}