blob: ac20e4e653f28682874eb41868daacb1f9c2eb73 [file] [log] [blame]
// Copyright (C) 2017 Google Inc
//
// 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.entities.RefNames.REFS_HEADS;
import com.google.gerrit.entities.Project;
import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
class JiriManifestParser {
static class ManifestItem {
public ManifestItem(
String repoKey, String manifest, String ref, String pKey, boolean revisionPinned) {
this.repoKey = repoKey;
this.manifest = manifest;
this.ref = ref;
this.revisionPinned = revisionPinned;
this.projectKey = pKey;
}
String repoKey;
String manifest;
String ref;
boolean revisionPinned;
// In jiri if import is pinned to a revision and if
// we have a corresponding project in the manifest, jiri would
// pin that project to same revision. So passing key to match
// project to import tag.
// For Eg, if you have manifest in manifest2 repo
// <manifest><projects><project name="manifest2" .../>
// And If you import this from your main manifest
// <manifest><imports><import name="manifest2" revision="A"... />
// jiri will pin manifest2 project to A as well.
String projectKey;
}
static class RepoMap<K, V extends Repository> extends HashMap<K, V> implements AutoCloseable {
private static final long serialVersionUID = 1L;
@Override
public void close() {
for (Repository repo : this.values()) {
repo.close();
}
}
}
public static JiriProjects getProjects(
GerritRemoteReader reader, String repoKey, String ref, String manifest)
throws ConfigInvalidException, IOException {
try (RepoMap<String, Repository> repoMap = new RepoMap<>()) {
repoMap.put(repoKey, reader.openRepository(repoKey));
Queue<ManifestItem> q = new LinkedList<>();
q.add(new ManifestItem(repoKey, manifest, ref, "", false));
HashMap<String, HashSet<String>> processedRepoFiles = new HashMap<>();
HashMap<String, JiriProjects.Project> projectMap = new HashMap<>();
while (q.size() != 0) {
ManifestItem mi = q.remove();
Repository repo = repoMap.get(mi.repoKey);
if (repo == null) {
repo = reader.openRepository(mi.repoKey);
repoMap.put(mi.repoKey, repo);
}
HashSet<String> processedFiles = processedRepoFiles.get(mi.repoKey);
if (processedFiles == null) {
processedFiles = new HashSet<>();
processedRepoFiles.put(mi.repoKey, processedFiles);
}
if (processedFiles.contains(mi.manifest)) {
continue;
}
processedFiles.add(mi.manifest);
JiriManifest m;
try {
m = parseManifest(repo, mi.ref, mi.manifest);
} catch (JAXBException | XMLStreamException e) {
throw new ConfigInvalidException("XML parse error", e);
}
for (JiriProjects.Project project : m.projects.getProjects()) {
project.fillDefault();
if (mi.revisionPinned && project.Key().equals(mi.projectKey)) {
project.setRevision(mi.ref);
}
if (projectMap.containsKey(project.Key())) {
if (!projectMap.get(project.Key()).equals(project))
throw new ConfigInvalidException(
String.format(
"Duplicate conflicting project %s in manifest %s\n%s\n%s",
project.Key(),
mi.manifest,
project.toString(),
projectMap.get(project.Key()).toString()));
} else {
projectMap.put(project.Key(), project);
}
}
URI parentURI;
try {
parentURI = new URI(mi.manifest);
} catch (URISyntaxException e) {
throw new ConfigInvalidException("Invalid parent URI", e);
}
for (JiriManifest.LocalImport l : m.imports.getLocalImports()) {
ManifestItem tw =
new ManifestItem(
mi.repoKey,
parentURI.resolve(l.getFile()).getPath(),
mi.ref,
mi.projectKey,
mi.revisionPinned);
q.add(tw);
}
for (JiriManifest.Import i : m.imports.getImports()) {
i.fillDefault();
URI uri;
try {
uri = new URI(i.getRemote());
} catch (URISyntaxException e) {
throw new ConfigInvalidException("Invalid URI", e);
}
String iRepoKey = Project.nameKey(StringUtils.strip(uri.getPath(), "/")).toString();
String iRef = i.getRevision();
boolean revisionPinned = true;
if (iRef.isEmpty()) {
iRef = REFS_HEADS + i.getRemotebranch();
revisionPinned = false;
}
ManifestItem tmi =
new ManifestItem(iRepoKey, i.getManifest(), iRef, i.Key(), revisionPinned);
q.add(tmi);
}
}
return new JiriProjects(projectMap.values().toArray(new JiriProjects.Project[0]));
}
}
private static JiriManifest parseManifest(Repository repo, String ref, String file)
throws JAXBException, IOException, XMLStreamException {
byte[] b = Utils.readBlob(repo, ref + ":" + file);
JAXBContext jc = JAXBContext.newInstance(JiriManifest.class);
XMLInputFactory inf = XMLInputFactory.newFactory();
inf.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
inf.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader sr = inf.createXMLStreamReader(new StreamSource(new ByteArrayInputStream(b)));
return (JiriManifest) jc.createUnmarshaller().unmarshal(sr);
}
}