BareSuperprojectWriter.java
/*
* Copyright (C) 2021, Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gitrepo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.List;
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.dircache.InvalidPathException;
import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException;
import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile;
import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader;
import org.eclipse.jgit.gitrepo.RepoCommand.RemoteUnavailableException;
import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
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.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FileUtils;
/**
* Writes .gitmodules and gitlinks of parsed manifest projects into a bare
* repository.
*
* To write on a regular repository, see {@link RegularSuperprojectWriter}.
*/
class BareSuperprojectWriter {
private static final int LOCK_FAILURE_MAX_RETRIES = 5;
// Retry exponentially with delays in this range
private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50;
private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000;
private final Repository repo;
private final URI targetUri;
private final String targetBranch;
private final RemoteReader callback;
private final BareWriterConfig config;
private final PersonIdent author;
private List<ExtraContent> extraContents;
static class BareWriterConfig {
boolean ignoreRemoteFailures = false;
boolean recordRemoteBranch = true;
boolean recordSubmoduleLabels = true;
boolean recordShallowSubmodules = true;
static BareWriterConfig getDefault() {
return new BareWriterConfig();
}
private BareWriterConfig() {
}
}
static class ExtraContent {
final String path;
final String content;
ExtraContent(String path, String content) {
this.path = path;
this.content = content;
}
}
BareSuperprojectWriter(Repository repo, URI targetUri,
String targetBranch,
PersonIdent author, RemoteReader callback,
BareWriterConfig config,
List<ExtraContent> extraContents) {
assert (repo.isBare());
this.repo = repo;
this.targetUri = targetUri;
this.targetBranch = targetBranch;
this.author = author;
this.callback = callback;
this.config = config;
this.extraContents = extraContents;
}
RevCommit write(List<RepoProject> repoProjects)
throws GitAPIException {
DirCache index = DirCache.newInCore();
ObjectInserter inserter = repo.newObjectInserter();
try (RevWalk rw = new RevWalk(repo)) {
prepareIndex(repoProjects, index, inserter);
ObjectId treeId = index.writeTree(inserter);
long prevDelay = 0;
for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) {
try {
return commitTreeOnCurrentTip(inserter, rw, treeId);
} catch (ConcurrentRefUpdateException e) {
prevDelay = FileUtils.delay(prevDelay,
LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS,
LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS);
Thread.sleep(prevDelay);
repo.getRefDatabase().refresh();
}
}
// In the last try, just propagate the exceptions
return commitTreeOnCurrentTip(inserter, rw, treeId);
} catch (IOException | InterruptedException | InvalidPathException e) {
throw new ManifestErrorException(e);
}
}
private void prepareIndex(List<RepoProject> projects, DirCache index,
ObjectInserter inserter) throws IOException, GitAPIException {
Config cfg = new Config();
StringBuilder attributes = new StringBuilder();
DirCacheBuilder builder = index.builder();
for (RepoProject proj : projects) {
String name = proj.getName();
String path = proj.getPath();
String url = proj.getUrl();
ObjectId objectId;
if (ObjectId.isId(proj.getRevision())) {
objectId = ObjectId.fromString(proj.getRevision());
} else {
objectId = callback.sha1(url, proj.getRevision());
if (objectId == null && !config.ignoreRemoteFailures) {
throw new RemoteUnavailableException(url);
}
if (config.recordRemoteBranch) {
// "branch" field is only for non-tag references.
// Keep tags in "ref" field as hint for other tools.
String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$
: "branch"; //$NON-NLS-1$
cfg.setString("submodule", name, field, //$NON-NLS-1$
proj.getRevision());
}
if (config.recordShallowSubmodules
&& proj.getRecommendShallow() != null) {
// The shallow recommendation is losing information.
// As the repo manifests stores the recommended
// depth in the 'clone-depth' field, while
// git core only uses a binary 'shallow = true/false'
// hint, we'll map any depth to 'shallow = true'
cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
true);
}
}
if (config.recordSubmoduleLabels) {
StringBuilder rec = new StringBuilder();
rec.append("/"); //$NON-NLS-1$
rec.append(path);
for (String group : proj.getGroups()) {
rec.append(" "); //$NON-NLS-1$
rec.append(group);
}
rec.append("\n"); //$NON-NLS-1$
attributes.append(rec.toString());
}
URI submodUrl = URI.create(url);
if (targetUri != null) {
submodUrl = RepoCommand.relativize(targetUri, submodUrl);
}
cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
submodUrl.toString());
// create gitlink
if (objectId != null) {
DirCacheEntry dcEntry = new DirCacheEntry(path);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.GITLINK);
builder.add(dcEntry);
for (CopyFile copyfile : proj.getCopyFiles()) {
RemoteFile rf = callback.readFileWithMode(url,
proj.getRevision(), copyfile.src);
objectId = inserter.insert(Constants.OBJ_BLOB,
rf.getContents());
dcEntry = new DirCacheEntry(copyfile.dest);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(rf.getFileMode());
builder.add(dcEntry);
}
for (LinkFile linkfile : proj.getLinkFiles()) {
String link;
if (linkfile.dest.contains("/")) { //$NON-NLS-1$
link = FileUtils.relativizeGitPath(
linkfile.dest.substring(0,
linkfile.dest.lastIndexOf('/')),
proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
} else {
link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
}
objectId = inserter.insert(Constants.OBJ_BLOB,
link.getBytes(UTF_8));
dcEntry = new DirCacheEntry(linkfile.dest);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.SYMLINK);
builder.add(dcEntry);
}
}
}
String content = cfg.toText();
// create a new DirCacheEntry for .gitmodules file.
DirCacheEntry dcEntry = new DirCacheEntry(
Constants.DOT_GIT_MODULES);
ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
content.getBytes(UTF_8));
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.REGULAR_FILE);
builder.add(dcEntry);
if (config.recordSubmoduleLabels) {
// create a new DirCacheEntry for .gitattributes file.
DirCacheEntry dcEntryAttr = new DirCacheEntry(
Constants.DOT_GIT_ATTRIBUTES);
ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
attributes.toString().getBytes(UTF_8));
dcEntryAttr.setObjectId(attrId);
dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
builder.add(dcEntryAttr);
}
for (ExtraContent ec : extraContents) {
DirCacheEntry extraDcEntry = new DirCacheEntry(ec.path);
ObjectId oid = inserter.insert(Constants.OBJ_BLOB,
ec.content.getBytes(UTF_8));
extraDcEntry.setObjectId(oid);
extraDcEntry.setFileMode(FileMode.REGULAR_FILE);
builder.add(extraDcEntry);
}
builder.finish();
}
private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter,
RevWalk rw, ObjectId treeId)
throws IOException, ConcurrentRefUpdateException {
ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
if (headId != null
&& rw.parseCommit(headId).getTree().getId().equals(treeId)) {
// No change. Do nothing.
return rw.parseCommit(headId);
}
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
if (headId != null) {
commit.setParentIds(headId);
}
commit.setAuthor(author);
commit.setCommitter(author);
commit.setMessage(RepoText.get().repoCommitMessage);
ObjectId commitId = inserter.insert(commit);
inserter.flush();
RefUpdate ru = repo.updateRef(targetBranch);
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, targetBranch), ru.getRef(), rc);
default:
throw new JGitInternalException(
MessageFormat.format(JGitText.get().updatingRefFailed,
targetBranch, commitId.name(), rc));
}
return rw.parseCommit(commitId);
}
}