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