Supermanifest plugin.
The supermanifest plugin watches for ref update events, and updates a
superproject in response to a submitted change to the manifest.
This works by running JGit's RepoCommand, and therefore ignores
submodule subscription settings.
Change-Id: Ic3f6ee32a2f6a434b5cbabcec9cc46df22767db6
Reviewed-on: https://gerrit-review.googlesource.com/92293
Tested-by: Han-Wen Nienhuys <hanwen@google.com>
Reviewed-by: ekempin <ekempin@google.com>
Reviewed-by: Han-Wen Nienhuys <hanwen@google.com>
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..c5a9919
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,34 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+
+gerrit_plugin(
+ name = "supermanifest",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: supermanifest",
+ "Gerrit-Module: com.googlesource.gerrit.plugins.supermanifest.SuperManifestModule",
+ "Implementation-Title: Supermanifest plugin",
+ "Implementation-URL: https://gerrit-review.googlesource.com/#/todo",
+ ],
+ resources = glob(["src/main/**/*"]),
+)
+
+junit_tests(
+ name = "supermanifest_tests",
+ size = "large",
+ srcs = glob(["src/test/java/**/*IT.java"]),
+ tags = [
+ "supermanifest-plugin",
+ ],
+ data = [
+ # plugin test handling is broken; kludge it here.
+ ":supermanifest.jar",
+ ],
+ resources = glob(["src/test/resources/**"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":supermanifest__plugin",
+ "//gerrit-acceptance-framework:lib",
+ "//gerrit-plugin-api:lib",
+ ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
new file mode 100644
index 0000000..066a603
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestModule.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.supermanifest;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+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;
+
+public class SuperManifestModule extends AbstractModule {
+ SuperManifestModule() {
+ }
+
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+ .to(SuperManifestRefUpdatedListener.class)
+ .in(SINGLETON);
+ DynamicSet.bind(binder(), LifecycleListener.class)
+ .to(SuperManifestRefUpdatedListener.class)
+ .in(SINGLETON);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
new file mode 100644
index 0000000..00bbdc1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestRefUpdatedListener.java
@@ -0,0 +1,494 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.supermanifest;
+
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidRemoteException;
+import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.gitrepo.ManifestParser;
+import org.eclipse.jgit.gitrepo.RepoCommand;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This plugin will listen for changes to XML files in manifest repositories. When it finds such
+ * changes, it will trigger an update of the associated superproject.
+ */
+@Singleton
+class SuperManifestRefUpdatedListener implements GitReferenceUpdatedListener, LifecycleListener {
+ private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class);
+
+ private static final String SECTION_NAME = "superproject";
+
+ private final GitRepositoryManager repoManager;
+ private final String canonicalWebUrl;
+ private final PluginConfigFactory cfgFactory;
+ private final String pluginName;
+ private final AllProjectsName allProjectsName;
+ private final ProjectCache projectCache;
+ private final PersonIdent serverIdent;
+
+ // Mutable.
+ private Set<ConfigEntry> config;
+
+ @Inject
+ SuperManifestRefUpdatedListener(
+ AllProjectsName allProjectsName,
+ @CanonicalWebUrl String canonicalWebUrl,
+ @PluginName String pluginName,
+ PluginConfigFactory cfgFactory,
+ ProjectCache projectCache,
+ @GerritPersonIdent PersonIdent serverIdent,
+ GitRepositoryManager repoManager) {
+ this.pluginName = pluginName;
+ this.serverIdent = serverIdent;
+ this.allProjectsName = allProjectsName;
+ this.repoManager = repoManager;
+ this.canonicalWebUrl = canonicalWebUrl;
+ this.cfgFactory = cfgFactory;
+ this.projectCache = projectCache;
+ }
+
+ private static byte[] readBlob(Repository repo, String idStr) throws IOException {
+ try (ObjectReader reader = repo.newObjectReader()) {
+ ObjectId id = repo.resolve(idStr);
+ if (id == null) {
+ throw new RevisionSyntaxException(
+ String.format("repo %s does not have %s", repo.toString(), idStr), idStr);
+ }
+ return reader.open(id).getCachedBytes(Integer.MAX_VALUE);
+ }
+ }
+
+ private static class ConfigEntry {
+ Project.NameKey srcRepoKey;
+ String srcRef;
+ String xmlPath;
+ Project.NameKey destRepoKey;
+ boolean recordSubmoduleLabels;
+
+ // destBranch can be "*" in which case srcRef is ignored.
+ String destBranch;
+
+ public String src() {
+ return srcRepoKey + ":" + srcRef + ":" + xmlPath;
+ }
+
+ public String dest() {
+ return destRepoKey + ":" + destBranch;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s => %s", src(), dest());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ConfigEntry that = (ConfigEntry) o;
+ if (!destRepoKey.equals(that.destRepoKey)) return false;
+ return destBranch.equals(that.destBranch);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(destRepoKey, destBranch);
+ }
+ }
+
+ private static boolean hasDiff(
+ GitRepositoryManager repoManager, String repoName, String oldId, String newId, String path)
+ throws IOException {
+ if (oldId.equals(ObjectId.toString(null))) {
+ return true;
+ }
+
+ Project.NameKey projectName = new Project.NameKey(repoName);
+
+ try (Repository repo = repoManager.openRepository(projectName);
+ RevWalk rw = new RevWalk(repo)) {
+
+ RevCommit c1 = rw.parseCommit(ObjectId.fromString(oldId));
+ if (c1 == null) {
+ return true;
+ }
+ RevCommit c2 = rw.parseCommit(ObjectId.fromString(newId));
+
+ try (TreeWalk tw = TreeWalk.forPath(repo, path, c1.getTree().getId(), c2.getTree().getId())) {
+
+ return !tw.getObjectId(0).equals(tw.getObjectId(1));
+ }
+ }
+ }
+
+ /*
+ [superproject "submodules:refs/heads/nyc"]
+ srcRepo = platforms/manifest
+ srcRef = refs/heads/nyc
+ srcPath = manifest.xml
+
+ */
+ private Set<ConfigEntry> parseConfiguration(PluginConfigFactory cfgFactory, String name) {
+ Config cfg = null;
+ try {
+ cfg = cfgFactory.getProjectPluginConfig(allProjectsName, name);
+ } catch (NoSuchProjectException e) {
+ Preconditions.checkState(false);
+ }
+
+ Set<ConfigEntry> newConf = new HashSet<>();
+ Set<String> destinations = new HashSet<>();
+ Set<String> wildcardDestinations = new HashSet<>();
+ Set<String> sources = new HashSet<>();
+
+ for (String subsect : cfg.getSubsections(SECTION_NAME)) {
+ try {
+ ConfigEntry e = newConfigEntry(cfg, subsect);
+ if (destinations.contains(e.srcRepoKey.get()) || sources.contains(e.destRepoKey.get())) {
+ // Don't want cyclic dependencies.
+ throw new ConfigInvalidException(
+ String.format("repo in entry %s cannot be both source and destination", e));
+ }
+ if (e.destBranch.equals("*")) {
+ if (wildcardDestinations.contains(e.destRepoKey.get())) {
+ throw new ConfigInvalidException(
+ String.format("repo %s already has a wildcard destination branch.", e.destRepoKey));
+ }
+ wildcardDestinations.add(e.destRepoKey.get());
+ }
+ sources.add(e.srcRepoKey.get());
+ destinations.add(e.destRepoKey.get());
+
+ newConf.add(e);
+
+ } catch (ConfigInvalidException e) {
+ log.error("ConfigInvalidException: " + e.toString());
+ }
+ }
+
+ return newConf;
+ }
+
+ private static ConfigEntry newConfigEntry(Config cfg, String name) throws ConfigInvalidException {
+ String[] parts = name.split(":");
+ if (parts.length != 2) {
+ throw new ConfigInvalidException(
+ String.format("pluginName '%s' must have form REPO:BRANCH", name));
+ }
+
+ String destRepo = parts[0];
+ String destRef = parts[1];
+
+ if (!destRef.startsWith(REFS_HEADS)) {
+ throw new ConfigInvalidException(
+ String.format("invalid destination '%s'. Must specify refs/heads/", destRef));
+ }
+
+ if (destRef.contains("*") && !destRef.equals(REFS_HEADS + "*")) {
+ throw new ConfigInvalidException(
+ String.format("invalid destination '%s'. Use just '*' for all branches.", destRef));
+ }
+
+ ConfigEntry e = new ConfigEntry();
+ String srcRepo = cfg.getString(SECTION_NAME, name, "srcRepo");
+ if (srcRepo == null) {
+ throw new ConfigInvalidException(String.format("entry %s did not specify srcRepo", name));
+ }
+
+ // TODO(hanwen): sanity check repo names.
+ e.srcRepoKey = new Project.NameKey(srcRepo);
+
+ if (destRef.equals(REFS_HEADS + "*")) {
+ e.srcRef = "";
+ } else {
+ if (!Repository.isValidRefName(destRef)) {
+ throw new ConfigInvalidException(String.format("destination branch '%s' invalid", destRef));
+ }
+
+ e.srcRef = cfg.getString(SECTION_NAME, name, "srcRef");
+ if (!Repository.isValidRefName(e.srcRef)) {
+ throw new ConfigInvalidException(String.format("source ref '%s' invalid", e.srcRef));
+ }
+
+ if (e.srcRef == null) {
+ throw new ConfigInvalidException(String.format("entry %s did not specify srcRef", name));
+ }
+ }
+
+ e.xmlPath = cfg.getString(SECTION_NAME, name, "srcPath");
+ if (e.xmlPath == null) {
+ throw new ConfigInvalidException(String.format("entry %s did not specify srcPath", name));
+ }
+
+ e.destRepoKey = new Project.NameKey(destRepo);
+
+ // The external format is chosen so we can support copying over tags as well.
+ e.destBranch = destRef.substring(REFS_HEADS.length());
+
+ e.recordSubmoduleLabels = cfg.getBoolean(SECTION_NAME, name, "recordSubmoduleLabels", false);
+ return e;
+ }
+
+ private boolean checkRepoExists(Project.NameKey id) {
+ return projectCache.get(id) != null;
+ }
+
+ @Override
+ public void stop() {}
+
+ @Override
+ public void start() {
+ updateConfiguration();
+ }
+
+ /** for debugging. */
+ private String printConfiguration() {
+ StringBuilder b = new StringBuilder();
+ b.append("# configuration entries: " + config.size() + "\n");
+ for (ConfigEntry c : config) {
+ b.append(c.toString() + "\n");
+ }
+ return b.toString();
+ }
+
+ private void updateConfiguration() {
+ Set<ConfigEntry> entries = parseConfiguration(cfgFactory, pluginName);
+
+ Set<ConfigEntry> filtered = new HashSet<>();
+ for (ConfigEntry e : entries) {
+ if (!checkRepoExists(e.srcRepoKey)) {
+ log.error(String.format("source repo '%s' does not exist", e.srcRepoKey));
+ } else if (!checkRepoExists(e.destRepoKey)) {
+ log.error(String.format("destination repo '%s' does not exist", e.destRepoKey));
+ } else {
+ filtered.add(e);
+ }
+ }
+
+ config = filtered;
+ log.info("SuperManifest: new configuration" + printConfiguration());
+ }
+
+ @Override
+ public synchronized void onGitReferenceUpdated(Event event) {
+ if (event.getProjectName().equals(allProjectsName.get())) {
+ if (event.getRefName().equals("refs/meta/config")) {
+ updateConfiguration();
+ }
+ return;
+ }
+
+ for (ConfigEntry c : config) {
+ if (!c.srcRepoKey.get().equals(event.getProjectName())) {
+ continue;
+ }
+
+ if (!(c.destBranch.equals("*") || c.srcRef.equals(event.getRefName()))) {
+ continue;
+ }
+
+ if (c.destBranch.equals("*") && !event.getRefName().startsWith(REFS_HEADS)) {
+ continue;
+ }
+
+ try {
+ if (!hasDiff(
+ repoManager,
+ event.getProjectName(),
+ event.getOldObjectId(),
+ event.getNewObjectId(),
+ c.xmlPath)) {
+ continue;
+ }
+ } catch (IOException e) {
+ log.error("ignoring hasDiff error for" + c.toString() + ":", e.toString());
+ }
+
+ try {
+ update(c, event.getRefName());
+ } catch (IOException | GitAPIException e) {
+ // We are in an asynchronously called listener, so there is no user action
+ // to give feedback to. We log the error, but it would be nice if we could surface
+ // these logs somewhere.
+ // Perhaps we could store these as commits in some special branch (but in what repo?).
+ log.error("update for " + c.toString() + " failed: ", e);
+ }
+ }
+ }
+
+ private static class GerritIncludeReader implements ManifestParser.IncludedFileReader {
+ private final Repository repo;
+ private final String ref;
+
+ GerritIncludeReader(Repository repo, String ref) {
+ this.repo = repo;
+ this.ref = ref;
+ }
+
+ @Override
+ public InputStream readIncludeFile(String path) throws IOException {
+ String blobRef = ref + ":" + path;
+ return new ByteArrayInputStream(readBlob(repo, blobRef));
+ }
+ }
+
+ private void update(ConfigEntry c, String srcRef) throws IOException, GitAPIException {
+ try (GerritRemoteReader reader = new GerritRemoteReader()) {
+ Repository destRepo = reader.openRepository(c.destRepoKey.toString());
+ Repository srcRepo = reader.openRepository(c.srcRepoKey.toString());
+
+ RepoCommand cmd = new RepoCommand(destRepo);
+
+ if (c.destBranch.equals("*")) {
+ cmd.setTargetBranch(srcRef.substring(REFS_HEADS.length()));
+ } else {
+ cmd.setTargetBranch(c.destBranch);
+ }
+
+ InputStream manifestStream =
+ new ByteArrayInputStream(readBlob(srcRepo, srcRef + ":" + c.xmlPath));
+
+ cmd.setAuthor(serverIdent);
+ cmd.setRecordRemoteBranch(true);
+ cmd.setRecordSubmoduleLabels(c.recordSubmoduleLabels);
+ cmd.setInputStream(manifestStream);
+ cmd.setRecommendShallow(true);
+ cmd.setRemoteReader(reader);
+
+ // Is this the right URL?
+ cmd.setURI(canonicalWebUrl);
+
+ // Must setup a included file reader; the default is to read the file from the filesystem
+ // otherwise, which would leak data from the serving machine.
+ cmd.setIncludedFileReader(new GerritIncludeReader(srcRepo, srcRef));
+
+ RevCommit commit = cmd.call();
+ }
+ }
+
+ // GerritRemoteReader is for injecting Gerrit's Git implementation into JGit.
+ private class GerritRemoteReader implements RepoCommand.RemoteReader, Closeable {
+ private final Map<String, Repository> repos;
+
+ GerritRemoteReader() {
+ this.repos = new HashMap<>();
+ }
+
+ @Override
+ public ObjectId sha1(String uriStr, String refName) throws GitAPIException {
+ URI url;
+ try {
+ url = new URI(uriStr);
+ } catch (URISyntaxException e) {
+ // TODO(hanwen): is there a better exception for this?
+ throw new InvalidRemoteException(e.getMessage());
+ }
+
+ String repoName = url.getPath();
+
+ // The path starts with '/'.
+ repoName = repoName.substring(1);
+
+ try {
+ Repository repo = openRepository(repoName);
+ Ref ref = repo.findRef(refName);
+ if (ref == null || ref.getObjectId() == null) {
+ log.warn("%s: repo %s cannot resolve %s", repo, refName);
+ return null;
+ }
+
+ ref = repo.peel(ref);
+ ObjectId id = ref.getPeeledObjectId();
+ return id != null ? id : ref.getObjectId();
+ } catch (RepositoryNotFoundException e) {
+ log.warn("failed to open repository: " + repoName, e);
+ return null;
+ } catch (IOException io) {
+ RefNotFoundException e =
+ new RefNotFoundException(String.format("cannot open %s to read %s", repoName, refName));
+ e.initCause(io);
+ throw e;
+ }
+ }
+
+ @Override
+ public byte[] readFile(String repoName, String ref, String path)
+ throws GitAPIException, IOException {
+ Repository repo;
+ repo = openRepository(repoName);
+ return readBlob(repo, ref + ":" + path);
+ }
+
+ private Repository openRepository(String name) throws IOException {
+ if (repos.containsKey(name)) {
+ return repos.get(name);
+ }
+
+ Repository repo = repoManager.openRepository(new Project.NameKey(name));
+ repos.put(name, repo);
+ return repo;
+ }
+
+ @Override
+ public void close() {
+ for (Repository repo : repos.values()) {
+ repo.close();
+ }
+ repos.clear();
+ }
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..b013c09
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,29 @@
+This plugin updates a submodule superproject based on a manifest repository.
+
+It should be configured by adding `supermanifest.config` to the
+`All-Projects` project. The format for configuration is as follows:
+
+
+```
+[superproject "submodules:refs/heads/nyc"]
+ srcRepo = platforms/manifest
+ srcRef = refs/heads/nyc
+ srcPath = manifest.xml
+```
+
+this configures a repository called `submodules` to have a branch
+`nyc`, for which the contents corresponds to the manifest file
+`manifest.xml` on branch `refs/heads/nyc` in project `platforms/manifest`.
+
+For the destination branch, you may also specify `*` to copy all
+branches in the manifest repository.
+
+```
+[superproject "submodules:*"]
+ srcRepo = platforms/manifest
+ srcPath = manifest.xml
+```
+
+This plugin bypasses visibility restrictions, so edits to the manifest
+repo can be used to reveal existence of hidden repositories or
+branches.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
new file mode 100644
index 0000000..55b6104
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/SuperManifestIT.java
@@ -0,0 +1,297 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.supermanifest;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.Test;
+
+@TestPlugin(
+ name = "supermanifest",
+ sysModule = "com.googlesource.gerrit.plugins.supermanifest.SuperManifestModule"
+)
+public class SuperManifestIT extends LightweightPluginDaemonTest {
+ Project.NameKey testRepoKeys[];
+
+ void setupTestRepos() throws Exception {
+ testRepoKeys = new Project.NameKey[2];
+ for (int i = 0; i < 2; i++) {
+ testRepoKeys[i] = createProject("project" + i);
+
+ TestRepository<InMemoryRepository> repo = cloneProject(testRepoKeys[i], admin);
+
+ PushOneCommit push =
+ pushFactory.create(db, admin.getIdent(), repo, "Subject", "file" + i, "file");
+ push.to("refs/heads/master").assertOkStatus();
+ }
+ }
+
+ void pushConfig(String config) throws Exception {
+ // This will trigger a configuration reload.
+ TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
+ GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
+ allProjectRepo.reset("config");
+ PushOneCommit push =
+ pushFactory.create(
+ db, admin.getIdent(), allProjectRepo, "Subject", "supermanifest.config", config);
+ PushOneCommit.Result res = push.to("refs/meta/config");
+ res.assertOkStatus();
+ }
+
+ @Test
+ public void basicFunctionalityWorks() throws Exception {
+ setupTestRepos();
+
+ // Make sure the manifest exists so the configuration loads successfully.
+ Project.NameKey manifestKey = createProject("manifest");
+ TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+
+ Project.NameKey superKey = createProject("superproject");
+
+ TestRepository<InMemoryRepository> superRepo = cloneProject(superKey, admin);
+
+ pushConfig(
+ "[superproject \""
+ + superKey.get()
+ + ":refs/heads/destbranch\"]\n"
+ + " srcRepo = "
+ + manifestKey.get()
+ + "\n"
+ + " srcRef = refs/heads/srcbranch\n"
+ + " srcPath = default.xml\n");
+
+ String remoteXml = " <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
+ String originXml = " <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
+ // XML change will trigger commit to superproject.
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[0].get()
+ + "\" path=\"project1\" />\n"
+ + "</manifest>\n";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/srcbranch")
+ .assertOkStatus();
+
+ BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+ assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+ try {
+ branch.file("project2");
+ fail("wanted exception");
+ } catch (ResourceNotFoundException e) {
+ // all fine.
+ }
+
+ xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[0].get()
+ + "\" path=\"project1\" />\n"
+ + " <project name=\""
+ + testRepoKeys[1].get()
+ + "\" path=\"project2\" />\n"
+ + "</manifest>\n";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/srcbranch")
+ .assertOkStatus();
+
+ branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+ assertThat(branch.file("project2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+
+ // Make sure config change gets picked up.
+ pushConfig(
+ "[superproject \""
+ + superKey.get()
+ + ":refs/heads/other\"]\n"
+ + " srcRepo = "
+ + manifestKey.get()
+ + "\n"
+ + " srcRef = refs/heads/srcbranch\n"
+ + " srcPath = default.xml\n");
+
+ // Push another XML change; this should trigger a commit using the new config.
+ xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[1].get()
+ + "\" path=\"project3\" />\n"
+ + "</manifest>\n";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/srcbranch")
+ .assertOkStatus();
+
+ branch = gApi.projects().name(superKey.get()).branch("refs/heads/other");
+ assertThat(branch.file("project3").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+ }
+
+ @Test
+ public void wildcardDestBranchWorks() throws Exception {
+ setupTestRepos();
+
+ // Make sure the manifest exists so the configuration loads successfully.
+ Project.NameKey manifestKey = createProject("manifest");
+ TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+
+ Project.NameKey superKey = createProject("superproject");
+ TestRepository<InMemoryRepository> superRepo = cloneProject(superKey, admin);
+
+ pushConfig(
+ "[superproject \""
+ + superKey.get()
+ + ":refs/heads/*\"]\n"
+ + " srcRepo = "
+ + manifestKey.get()
+ + "\n"
+ + " srcRef = blablabla\n"
+ + " srcPath = default.xml\n");
+
+ String remoteXml = " <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
+ String originXml = " <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
+
+ // XML change will trigger commit to superproject.
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[0].get()
+ + "\" path=\"project1\" />\n"
+ + "</manifest>\n";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/src1")
+ .assertOkStatus();
+
+ xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[1].get()
+ + "\" path=\"project2\" />\n"
+ + "</manifest>\n";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/src2")
+ .assertOkStatus();
+
+ BranchApi branch1 = gApi.projects().name(superKey.get()).branch("refs/heads/src1");
+ assertThat(branch1.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+ try {
+ branch1.file("project2");
+ fail("wanted exception");
+ } catch (ResourceNotFoundException e) {
+ // all fine.
+ }
+
+ BranchApi branch2 = gApi.projects().name(superKey.get()).branch("refs/heads/src2");
+ assertThat(branch2.file("project2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+ try {
+ branch2.file("project1");
+ fail("wanted exception");
+ } catch (ResourceNotFoundException e) {
+ // all fine.
+ }
+ }
+
+ @Test
+ public void manifestIncludesOtherManifest() throws Exception {
+ setupTestRepos();
+
+ String remoteXml = " <remote name=\"origin\" fetch=\"" + canonicalWebUrl.get() + "\" />\n";
+ String originXml = " <default remote=\"origin\" revision=\"refs/heads/master\" />\n";
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<manifest>\n"
+ + remoteXml
+ + originXml
+ + " <project name=\""
+ + testRepoKeys[0].get()
+ + "\" path=\"project1\" />\n"
+ + "</manifest>\n";
+
+ Project.NameKey manifestKey = createProject("manifest");
+ TestRepository<InMemoryRepository> manifestRepo = cloneProject(manifestKey, admin);
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "default.xml", xml)
+ .to("refs/heads/master")
+ .assertOkStatus();
+
+ String superXml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<manifest>"
+ + " <include name=\"default.xml\"/>"
+ + "</manifest>";
+
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "super.xml", superXml)
+ .to("refs/heads/master")
+ .assertOkStatus();
+
+ Project.NameKey superKey = createProject("superproject");
+ TestRepository<InMemoryRepository> superRepo = cloneProject(superKey, admin);
+
+ pushConfig(
+ "[superproject \""
+ + superKey.get()
+ + ":refs/heads/master\"]\n"
+ + " srcRepo = "
+ + manifestKey.get()
+ + "\n"
+ + " srcRef = refs/heads/master\n"
+ + " srcPath = super.xml\n");
+
+ // trigger XML change to verify that it worked.
+ pushFactory
+ .create(db, admin.getIdent(), manifestRepo, "Subject", "super.xml", superXml + " ")
+ .to("refs/heads/master")
+ .assertOkStatus();
+
+ BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/master");
+ assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+ }
+
+ // TODO - should add tests for all the error handling in configuration parsing?
+}
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
new file mode 100644
index 0000000..7ae9afc
--- /dev/null
+++ b/src/test/resources/log4j.properties
@@ -0,0 +1,59 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+log4j.rootCategory=INFO, stderr
+log4j.appender.stderr=org.apache.log4j.ConsoleAppender
+log4j.appender.stderr.target=System.err
+log4j.appender.stderr.layout=org.apache.log4j.PatternLayout
+log4j.appender.stderr.layout.ConversionPattern=[%d] [%t] %-5p %c %x: %m%n
+
+log4j.logger.com.google.gerrit.sshd.CachingPublicKeyAuthenticator=ERROR
+log4j.logger.org.eclipse.jgit.lib.Repository=ERROR
+log4j.logger.com.google.gerrit.server.git.BatchUpdate=INFO
+log4j.logger.com.google.gerrit.server.git.ReceiveCommits=INFO
+log4j.logger.com.google.gerrit.server.git=INFO
+
+# Silence non-critical messages from MINA SSHD.
+#
+log4j.logger.org.apache.mina=WARN
+log4j.logger.org.apache.sshd.common=WARN
+log4j.logger.org.apache.sshd.server=WARN
+log4j.logger.com.google.gerrit.sshd.GerritServerSession=WARN
+
+# Silence non-critical messages from mime-util.
+#
+log4j.logger.eu.medsea.mimeutil=WARN
+
+# Silence non-critical messages from openid4java
+#
+log4j.logger.org.apache.http=WARN
+log4j.logger.org.apache.xml=WARN
+log4j.logger.org.openid4java=WARN
+log4j.logger.org.openid4java.consumer.ConsumerManager=FATAL
+log4j.logger.org.openid4java.discovery.Discovery=ERROR
+log4j.logger.org.openid4java.server.RealmVerifier=ERROR
+log4j.logger.org.openid4java.message.AuthSuccess=ERROR
+
+# Silence non-critical messages from c3p0 (if used).
+#
+log4j.logger.com.mchange.v2.c3p0=WARN
+log4j.logger.com.mchange.v2.resourcepool=WARN
+log4j.logger.com.mchange.v2.sql=WARN
+
+# Silence non-critical messages from Velocity
+#
+log4j.logger.velocity=WARN
+
+# Silence non-critical messages from apache.http
+log4j.logger.org.apache.http=WARN