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