blob: 365561a3e611e5db9c9e4077c11f92ce366a740b [file] [log] [blame]
package com.googlesource.gerrit.plugins.supermanifest;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.server.plugincontext.PluginMapContext;
import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import java.util.StringJoiner;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.gitrepo.internal.RepoText;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class JiriUpdater implements SubModuleUpdater {
PersonIdent serverIdent;
URI canonicalWebUrl;
private final PluginMapContext<DownloadScheme> downloadScheme;
public JiriUpdater(
PersonIdent serverIdent,
URI canonicalWebUrl,
PluginMapContext<DownloadScheme> downloadScheme) {
this.serverIdent = serverIdent;
this.canonicalWebUrl = canonicalWebUrl;
this.downloadScheme = downloadScheme;
}
private static final Logger log = LoggerFactory.getLogger(SuperManifestRefUpdatedListener.class);
private void warn(String formatStr, Object... args) {
// The docs claim that log.warn() uses format strings, but it doesn't seem to work, so we do it
// explicitly.
log.warn(canonicalWebUrl + " : " + String.format(formatStr, args));
}
private void updateSubmodules(
Repository repo,
String targetRef,
URI targetURI,
JiriProjects projects,
GerritRemoteReader reader)
throws IOException, GitAPIException {
DirCache index = DirCache.newInCore();
DirCacheBuilder builder = index.builder();
ObjectInserter inserter = repo.newObjectInserter();
try (RevWalk rw = new RevWalk(repo)) {
Config cfg = new Config();
projects.sortByPath();
String parent = null;
for (JiriProjects.Project proj : projects.getProjects()) {
String path = proj.getPath();
String nameUri = proj.getRemote();
if (parent != null) {
String p1 = StringUtil.stripAndAddCharsAtEnd(path, "/");
String p2 = StringUtil.stripAndAddCharsAtEnd(parent, "/");
if (p1.startsWith(p2)) {
warn(
"Skipping project %s(%s) as git doesn't support nested submodules",
proj.getName(), path);
continue;
}
}
ObjectId objectId;
String ref = proj.getRef();
if (ObjectId.isId(ref)) {
objectId = ObjectId.fromString(ref);
} else {
objectId = reader.sha1(nameUri, ref);
if (objectId == null) {
warn("failed to get ref '%s' for '%s', skipping", ref, nameUri);
continue;
}
}
// can be branch or tag
cfg.setString("submodule", path, "branch", ref);
if (proj.getHistorydepth() > 0) {
cfg.setBoolean("submodule", path, "shallow", true);
if (proj.getHistorydepth() != 1) {
warn(
"Project %s(%s) has historydepth other than 1. Submodule only support shallow of depth 1.",
proj.getName(), proj.getPath());
}
}
URI submodUrl = URI.create(nameUri);
// check if repo is local by matching hostnames
String repoName = submodUrl.getPath();
while (repoName.startsWith("/")) {
repoName = repoName.substring(1);
}
URI localURI = getLocalURI(repoName);
if (localURI != null && localURI.getHost().equals(submodUrl.getHost())) {
submodUrl = relativize(targetURI, URI.create(repoName));
}
cfg.setString("submodule", path, "path", path);
cfg.setString("submodule", path, "url", submodUrl.toString());
// create gitlink
DirCacheEntry dcEntry = new DirCacheEntry(path);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.GITLINK);
builder.add(dcEntry);
parent = path;
}
String content = cfg.toText();
// create a new DirCacheEntry for .gitmodules file.
final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
ObjectId objectId =
inserter.insert(Constants.OBJ_BLOB, content.getBytes(Constants.CHARACTER_ENCODING));
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.REGULAR_FILE);
builder.add(dcEntry);
builder.finish();
ObjectId treeId = index.writeTree(inserter);
// Create a Commit object, populate it and write it
ObjectId headId = repo.resolve(targetRef + "^{commit}");
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
if (headId != null) commit.setParentIds(headId);
PersonIdent author =
new PersonIdent(
serverIdent.getName(),
serverIdent.getEmailAddress(),
new Date(),
serverIdent.getTimeZone());
commit.setAuthor(author);
commit.setCommitter(author);
commit.setMessage(RepoText.get().repoCommitMessage);
ObjectId commitId = inserter.insert(commit);
inserter.flush();
RefUpdate ru = repo.updateRef(targetRef);
ru.setNewObjectId(commitId);
ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
Result rc = ru.update(rw);
switch (rc) {
case NEW:
case FORCED:
case FAST_FORWARD:
// Successful. Do nothing.
break;
case REJECTED:
case LOCK_FAILURE:
throw new ConcurrentRefUpdateException(
MessageFormat.format(JGitText.get().cannotLock, targetRef), ru.getRef(), rc);
default:
throw new JGitInternalException(
MessageFormat.format(
JGitText.get().updatingRefFailed, targetRef, commitId.name(), rc));
}
}
}
private URI getLocalURI(String projectName) {
List<URI> uriList = Lists.newArrayList();
downloadScheme.runEach(extension -> {
DownloadScheme scheme = extension.get();
if (scheme.isEnabled() && scheme.getUrl(projectName) != null) {
String url = scheme.getUrl(projectName);
URI localURI = URI.create(url);
// Discard URIs whose scheme are not "http" or "https" as they may not contain full hostname
if (localURI.getScheme().equals("https") || localURI.getScheme().equals("http")) {
uriList.add(localURI);
}
}
});
if (uriList.isEmpty()) {
return null;
}
return uriList.get(0);
}
private static final String SLASH = "/";
/*
* Copied from https://github.com/eclipse/jgit/blob/e9fb111182b55cc82c530d82f13176c7a85cd958/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java#L729
*/
static URI relativize(URI current, URI target) {
// We only handle bare paths for now.
if (!target.toString().equals(target.getPath())) {
return target;
}
if (!current.toString().equals(current.getPath())) {
return target;
}
String cur = current.normalize().getPath();
String dest = target.normalize().getPath();
if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
return target;
}
while (cur.startsWith(SLASH)) {
cur = cur.substring(1);
}
while (dest.startsWith(SLASH)) {
dest = dest.substring(1);
}
if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
// Avoid having to special-casing in the next two ifs.
String prefix = "prefix/";
cur = prefix + cur;
dest = prefix + dest;
}
if (!cur.endsWith(SLASH)) {
// The current file doesn't matter.
int lastSlash = cur.lastIndexOf('/');
cur = cur.substring(0, lastSlash);
}
String destFile = "";
if (!dest.endsWith(SLASH)) {
// We always have to provide the destination file.
int lastSlash = dest.lastIndexOf('/');
destFile = dest.substring(lastSlash + 1, dest.length());
dest = dest.substring(0, dest.lastIndexOf('/'));
}
String[] cs = cur.split(SLASH);
String[] ds = dest.split(SLASH);
int common = 0;
while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
common++;
}
StringJoiner j = new StringJoiner(SLASH);
for (int i = common; i < cs.length; i++) {
j.add("..");
}
for (int i = common; i < ds.length; i++) {
j.add(ds[i]);
}
j.add(destFile);
return URI.create(j.toString());
}
@Override
public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef)
throws IOException, GitAPIException, ConfigInvalidException {
try (Repository destRepo = reader.openRepository(c.getDestRepoKey().toString())) {
JiriProjects projects =
JiriManifestParser.getProjects(
reader, c.getSrcRepoKey().toString(), srcRef, c.getXmlPath());
String targetRef = c.getDestBranch().equals("*") ? srcRef : REFS_HEADS + c.getDestBranch();
updateSubmodules(
destRepo, targetRef, URI.create(c.getDestRepoKey().toString() + "/"), projects, reader);
}
}
}