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