BareSuperprojectWriter.java

  1. /*
  2.  * Copyright (C) 2021, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.gitrepo;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static org.eclipse.jgit.lib.Constants.R_TAGS;

  13. import java.io.IOException;
  14. import java.net.URI;
  15. import java.text.MessageFormat;
  16. import java.util.List;

  17. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  18. import org.eclipse.jgit.api.errors.GitAPIException;
  19. import org.eclipse.jgit.api.errors.JGitInternalException;
  20. import org.eclipse.jgit.dircache.DirCache;
  21. import org.eclipse.jgit.dircache.DirCacheBuilder;
  22. import org.eclipse.jgit.dircache.DirCacheEntry;
  23. import org.eclipse.jgit.dircache.InvalidPathException;
  24. import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException;
  25. import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile;
  26. import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader;
  27. import org.eclipse.jgit.gitrepo.RepoCommand.RemoteUnavailableException;
  28. import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
  29. import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
  30. import org.eclipse.jgit.gitrepo.internal.RepoText;
  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.lib.CommitBuilder;
  33. import org.eclipse.jgit.lib.Config;
  34. import org.eclipse.jgit.lib.Constants;
  35. import org.eclipse.jgit.lib.FileMode;
  36. import org.eclipse.jgit.lib.ObjectId;
  37. import org.eclipse.jgit.lib.ObjectInserter;
  38. import org.eclipse.jgit.lib.PersonIdent;
  39. import org.eclipse.jgit.lib.RefUpdate;
  40. import org.eclipse.jgit.lib.RefUpdate.Result;
  41. import org.eclipse.jgit.lib.Repository;
  42. import org.eclipse.jgit.revwalk.RevCommit;
  43. import org.eclipse.jgit.revwalk.RevWalk;
  44. import org.eclipse.jgit.util.FileUtils;

  45. /**
  46.  * Writes .gitmodules and gitlinks of parsed manifest projects into a bare
  47.  * repository.
  48.  *
  49.  * To write on a regular repository, see {@link RegularSuperprojectWriter}.
  50.  */
  51. class BareSuperprojectWriter {
  52.     private static final int LOCK_FAILURE_MAX_RETRIES = 5;

  53.     // Retry exponentially with delays in this range
  54.     private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50;

  55.     private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000;

  56.     private final Repository repo;

  57.     private final URI targetUri;

  58.     private final String targetBranch;

  59.     private final RemoteReader callback;

  60.     private final BareWriterConfig config;

  61.     private final PersonIdent author;

  62.     private List<ExtraContent> extraContents;

  63.     static class BareWriterConfig {
  64.         boolean ignoreRemoteFailures = false;

  65.         boolean recordRemoteBranch = true;

  66.         boolean recordSubmoduleLabels = true;

  67.         boolean recordShallowSubmodules = true;

  68.         static BareWriterConfig getDefault() {
  69.             return new BareWriterConfig();
  70.         }

  71.         private BareWriterConfig() {
  72.         }
  73.     }

  74.     static class ExtraContent {
  75.         final String path;

  76.         final String content;

  77.         ExtraContent(String path, String content) {
  78.             this.path = path;
  79.             this.content = content;
  80.         }
  81.     }

  82.     BareSuperprojectWriter(Repository repo, URI targetUri,
  83.             String targetBranch,
  84.             PersonIdent author, RemoteReader callback,
  85.             BareWriterConfig config,
  86.             List<ExtraContent> extraContents) {
  87.         assert (repo.isBare());
  88.         this.repo = repo;
  89.         this.targetUri = targetUri;
  90.         this.targetBranch = targetBranch;
  91.         this.author = author;
  92.         this.callback = callback;
  93.         this.config = config;
  94.         this.extraContents = extraContents;
  95.     }

  96.     RevCommit write(List<RepoProject> repoProjects)
  97.             throws GitAPIException {
  98.         DirCache index = DirCache.newInCore();
  99.         ObjectInserter inserter = repo.newObjectInserter();

  100.         try (RevWalk rw = new RevWalk(repo)) {
  101.             prepareIndex(repoProjects, index, inserter);
  102.             ObjectId treeId = index.writeTree(inserter);
  103.             long prevDelay = 0;
  104.             for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) {
  105.                 try {
  106.                     return commitTreeOnCurrentTip(inserter, rw, treeId);
  107.                 } catch (ConcurrentRefUpdateException e) {
  108.                     prevDelay = FileUtils.delay(prevDelay,
  109.                             LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS,
  110.                             LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS);
  111.                     Thread.sleep(prevDelay);
  112.                     repo.getRefDatabase().refresh();
  113.                 }
  114.             }
  115.             // In the last try, just propagate the exceptions
  116.             return commitTreeOnCurrentTip(inserter, rw, treeId);
  117.         } catch (IOException | InterruptedException | InvalidPathException e) {
  118.             throw new ManifestErrorException(e);
  119.         }
  120.     }

  121.     private void prepareIndex(List<RepoProject> projects, DirCache index,
  122.             ObjectInserter inserter) throws IOException, GitAPIException {
  123.         Config cfg = new Config();
  124.         StringBuilder attributes = new StringBuilder();
  125.         DirCacheBuilder builder = index.builder();
  126.         for (RepoProject proj : projects) {
  127.             String name = proj.getName();
  128.             String path = proj.getPath();
  129.             String url = proj.getUrl();
  130.             ObjectId objectId;
  131.             if (ObjectId.isId(proj.getRevision())) {
  132.                 objectId = ObjectId.fromString(proj.getRevision());
  133.             } else {
  134.                 objectId = callback.sha1(url, proj.getRevision());
  135.                 if (objectId == null && !config.ignoreRemoteFailures) {
  136.                     throw new RemoteUnavailableException(url);
  137.                 }
  138.                 if (config.recordRemoteBranch) {
  139.                     // "branch" field is only for non-tag references.
  140.                     // Keep tags in "ref" field as hint for other tools.
  141.                     String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$
  142.                             : "branch"; //$NON-NLS-1$
  143.                     cfg.setString("submodule", name, field, //$NON-NLS-1$
  144.                             proj.getRevision());
  145.                 }

  146.                 if (config.recordShallowSubmodules
  147.                         && proj.getRecommendShallow() != null) {
  148.                     // The shallow recommendation is losing information.
  149.                     // As the repo manifests stores the recommended
  150.                     // depth in the 'clone-depth' field, while
  151.                     // git core only uses a binary 'shallow = true/false'
  152.                     // hint, we'll map any depth to 'shallow = true'
  153.                     cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
  154.                             true);
  155.                 }
  156.             }
  157.             if (config.recordSubmoduleLabels) {
  158.                 StringBuilder rec = new StringBuilder();
  159.                 rec.append("/"); //$NON-NLS-1$
  160.                 rec.append(path);
  161.                 for (String group : proj.getGroups()) {
  162.                     rec.append(" "); //$NON-NLS-1$
  163.                     rec.append(group);
  164.                 }
  165.                 rec.append("\n"); //$NON-NLS-1$
  166.                 attributes.append(rec.toString());
  167.             }

  168.             URI submodUrl = URI.create(url);
  169.             if (targetUri != null) {
  170.                 submodUrl = RepoCommand.relativize(targetUri, submodUrl);
  171.             }
  172.             cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
  173.             cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
  174.                     submodUrl.toString());

  175.             // create gitlink
  176.             if (objectId != null) {
  177.                 DirCacheEntry dcEntry = new DirCacheEntry(path);
  178.                 dcEntry.setObjectId(objectId);
  179.                 dcEntry.setFileMode(FileMode.GITLINK);
  180.                 builder.add(dcEntry);

  181.                 for (CopyFile copyfile : proj.getCopyFiles()) {
  182.                     RemoteFile rf = callback.readFileWithMode(url,
  183.                             proj.getRevision(), copyfile.src);
  184.                     objectId = inserter.insert(Constants.OBJ_BLOB,
  185.                             rf.getContents());
  186.                     dcEntry = new DirCacheEntry(copyfile.dest);
  187.                     dcEntry.setObjectId(objectId);
  188.                     dcEntry.setFileMode(rf.getFileMode());
  189.                     builder.add(dcEntry);
  190.                 }
  191.                 for (LinkFile linkfile : proj.getLinkFiles()) {
  192.                     String link;
  193.                     if (linkfile.dest.contains("/")) { //$NON-NLS-1$
  194.                         link = FileUtils.relativizeGitPath(
  195.                                 linkfile.dest.substring(0,
  196.                                         linkfile.dest.lastIndexOf('/')),
  197.                                 proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
  198.                     } else {
  199.                         link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
  200.                     }

  201.                     objectId = inserter.insert(Constants.OBJ_BLOB,
  202.                             link.getBytes(UTF_8));
  203.                     dcEntry = new DirCacheEntry(linkfile.dest);
  204.                     dcEntry.setObjectId(objectId);
  205.                     dcEntry.setFileMode(FileMode.SYMLINK);
  206.                     builder.add(dcEntry);
  207.                 }
  208.             }
  209.         }
  210.         String content = cfg.toText();

  211.         // create a new DirCacheEntry for .gitmodules file.
  212.         DirCacheEntry dcEntry = new DirCacheEntry(
  213.                 Constants.DOT_GIT_MODULES);
  214.         ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
  215.                 content.getBytes(UTF_8));
  216.         dcEntry.setObjectId(objectId);
  217.         dcEntry.setFileMode(FileMode.REGULAR_FILE);
  218.         builder.add(dcEntry);

  219.         if (config.recordSubmoduleLabels) {
  220.             // create a new DirCacheEntry for .gitattributes file.
  221.             DirCacheEntry dcEntryAttr = new DirCacheEntry(
  222.                     Constants.DOT_GIT_ATTRIBUTES);
  223.             ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
  224.                     attributes.toString().getBytes(UTF_8));
  225.             dcEntryAttr.setObjectId(attrId);
  226.             dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
  227.             builder.add(dcEntryAttr);
  228.         }

  229.         for (ExtraContent ec : extraContents) {
  230.             DirCacheEntry extraDcEntry = new DirCacheEntry(ec.path);

  231.             ObjectId oid = inserter.insert(Constants.OBJ_BLOB,
  232.                     ec.content.getBytes(UTF_8));
  233.             extraDcEntry.setObjectId(oid);
  234.             extraDcEntry.setFileMode(FileMode.REGULAR_FILE);
  235.             builder.add(extraDcEntry);
  236.         }

  237.         builder.finish();
  238.     }

  239.     private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter,
  240.             RevWalk rw, ObjectId treeId)
  241.             throws IOException, ConcurrentRefUpdateException {
  242.         ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
  243.         if (headId != null
  244.                 && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
  245.             // No change. Do nothing.
  246.             return rw.parseCommit(headId);
  247.         }

  248.         CommitBuilder commit = new CommitBuilder();
  249.         commit.setTreeId(treeId);
  250.         if (headId != null) {
  251.             commit.setParentIds(headId);
  252.         }
  253.         commit.setAuthor(author);
  254.         commit.setCommitter(author);
  255.         commit.setMessage(RepoText.get().repoCommitMessage);

  256.         ObjectId commitId = inserter.insert(commit);
  257.         inserter.flush();

  258.         RefUpdate ru = repo.updateRef(targetBranch);
  259.         ru.setNewObjectId(commitId);
  260.         ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
  261.         Result rc = ru.update(rw);
  262.         switch (rc) {
  263.         case NEW:
  264.         case FORCED:
  265.         case FAST_FORWARD:
  266.             // Successful. Do nothing.
  267.             break;
  268.         case REJECTED:
  269.         case LOCK_FAILURE:
  270.             throw new ConcurrentRefUpdateException(MessageFormat.format(
  271.                     JGitText.get().cannotLock, targetBranch), ru.getRef(), rc);
  272.         default:
  273.             throw new JGitInternalException(
  274.                     MessageFormat.format(JGitText.get().updatingRefFailed,
  275.                             targetBranch, commitId.name(), rc));
  276.         }

  277.         return rw.parseCommit(commitId);
  278.     }
  279. }