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);
- }
- }