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;
+ }
+ }
+}