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