aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java323
1 files changed, 323 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
new file mode 100644
index 0000000000..e511a68d2e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
@@ -0,0 +1,323 @@
+/*
+ * 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());
+ if (config.recordRemoteBranch && proj.getUpstream() != null) {
+ cfg.setString("submodule", name, "ref", proj.getUpstream()); //$NON-NLS-1$//$NON-NLS-2$
+ }
+ } 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);
+ }
+}