RepoCommand.java

  1. /*
  2.  * Copyright (C) 2014, 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.DEFAULT_REMOTE_NAME;
  13. import static org.eclipse.jgit.lib.Constants.R_REMOTES;

  14. import java.io.File;
  15. import java.io.FileInputStream;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.net.URI;
  19. import java.text.MessageFormat;
  20. import java.util.ArrayList;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Objects;
  24. import java.util.StringJoiner;
  25. import java.util.TreeMap;

  26. import org.eclipse.jgit.annotations.NonNull;
  27. import org.eclipse.jgit.annotations.Nullable;
  28. import org.eclipse.jgit.api.Git;
  29. import org.eclipse.jgit.api.GitCommand;
  30. import org.eclipse.jgit.api.SubmoduleAddCommand;
  31. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  32. import org.eclipse.jgit.api.errors.GitAPIException;
  33. import org.eclipse.jgit.api.errors.InvalidRefNameException;
  34. import org.eclipse.jgit.api.errors.JGitInternalException;
  35. import org.eclipse.jgit.dircache.DirCache;
  36. import org.eclipse.jgit.dircache.DirCacheBuilder;
  37. import org.eclipse.jgit.dircache.DirCacheEntry;
  38. import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
  39. import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
  40. import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
  41. import org.eclipse.jgit.gitrepo.internal.RepoText;
  42. import org.eclipse.jgit.internal.JGitText;
  43. import org.eclipse.jgit.lib.CommitBuilder;
  44. import org.eclipse.jgit.lib.Config;
  45. import org.eclipse.jgit.lib.Constants;
  46. import org.eclipse.jgit.lib.FileMode;
  47. import org.eclipse.jgit.lib.ObjectId;
  48. import org.eclipse.jgit.lib.ObjectInserter;
  49. import org.eclipse.jgit.lib.PersonIdent;
  50. import org.eclipse.jgit.lib.ProgressMonitor;
  51. import org.eclipse.jgit.lib.Ref;
  52. import org.eclipse.jgit.lib.RefDatabase;
  53. import org.eclipse.jgit.lib.RefUpdate;
  54. import org.eclipse.jgit.lib.RefUpdate.Result;
  55. import org.eclipse.jgit.lib.Repository;
  56. import org.eclipse.jgit.revwalk.RevCommit;
  57. import org.eclipse.jgit.revwalk.RevWalk;
  58. import org.eclipse.jgit.treewalk.TreeWalk;
  59. import org.eclipse.jgit.util.FileUtils;

  60. /**
  61.  * A class used to execute a repo command.
  62.  *
  63.  * This will parse a repo XML manifest, convert it into .gitmodules file and the
  64.  * repository config file.
  65.  *
  66.  * If called against a bare repository, it will replace all the existing content
  67.  * of the repository with the contents populated from the manifest.
  68.  *
  69.  * repo manifest allows projects overlapping, e.g. one project's manifestPath is
  70.  * "foo" and another project's manifestPath is "foo/bar". This won't
  71.  * work in git submodule, so we'll skip all the sub projects
  72.  * ("foo/bar" in the example) while converting.
  73.  *
  74.  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
  75.  * @since 3.4
  76.  */
  77. public class RepoCommand extends GitCommand<RevCommit> {
  78.     private String manifestPath;
  79.     private String baseUri;
  80.     private URI targetUri;
  81.     private String groupsParam;
  82.     private String branch;
  83.     private String targetBranch = Constants.HEAD;
  84.     private boolean recordRemoteBranch = true;
  85.     private boolean recordSubmoduleLabels = true;
  86.     private boolean recordShallowSubmodules = true;
  87.     private PersonIdent author;
  88.     private RemoteReader callback;
  89.     private InputStream inputStream;
  90.     private IncludedFileReader includedReader;
  91.     private boolean ignoreRemoteFailures = false;

  92.     private ProgressMonitor monitor;

  93.     /**
  94.      * A callback to get ref sha1 of a repository from its uri.
  95.      *
  96.      * We provided a default implementation {@link DefaultRemoteReader} to
  97.      * use ls-remote command to read the sha1 from the repository and clone the
  98.      * repository to read the file. Callers may have their own quicker
  99.      * implementation.
  100.      *
  101.      * @since 3.4
  102.      */
  103.     public interface RemoteReader {
  104.         /**
  105.          * Read a remote ref sha1.
  106.          *
  107.          * @param uri
  108.          *            The URI of the remote repository
  109.          * @param ref
  110.          *            Name of the ref to lookup. May be a short-hand form, e.g.
  111.          *            "master" which is automatically expanded to
  112.          *            "refs/heads/master" if "refs/heads/master" already exists.
  113.          * @return the sha1 of the remote repository, or null if the ref does
  114.          *         not exist.
  115.          * @throws GitAPIException
  116.          */
  117.         @Nullable
  118.         public ObjectId sha1(String uri, String ref) throws GitAPIException;

  119.         /**
  120.          * Read a file from a remote repository.
  121.          *
  122.          * @param uri
  123.          *            The URI of the remote repository
  124.          * @param ref
  125.          *            The ref (branch/tag/etc.) to read
  126.          * @param path
  127.          *            The relative path (inside the repo) to the file to read
  128.          * @return the file content.
  129.          * @throws GitAPIException
  130.          * @throws IOException
  131.          * @since 3.5
  132.          *
  133.          * @deprecated Use {@link #readFileWithMode(String, String, String)}
  134.          *             instead
  135.          */
  136.         @Deprecated
  137.         public default byte[] readFile(String uri, String ref, String path)
  138.                 throws GitAPIException, IOException {
  139.             return readFileWithMode(uri, ref, path).getContents();
  140.         }

  141.         /**
  142.          * Read contents and mode (i.e. permissions) of the file from a remote
  143.          * repository.
  144.          *
  145.          * @param uri
  146.          *            The URI of the remote repository
  147.          * @param ref
  148.          *            Name of the ref to lookup. May be a short-hand form, e.g.
  149.          *            "master" which is automatically expanded to
  150.          *            "refs/heads/master" if "refs/heads/master" already exists.
  151.          * @param path
  152.          *            The relative path (inside the repo) to the file to read
  153.          * @return The contents and file mode of the file in the given
  154.          *         repository and branch. Never null.
  155.          * @throws GitAPIException
  156.          *             If the ref have an invalid or ambiguous name, or it does
  157.          *             not exist in the repository,
  158.          * @throws IOException
  159.          *             If the object does not exist or is too large
  160.          * @since 5.2
  161.          */
  162.         @NonNull
  163.         public RemoteFile readFileWithMode(String uri, String ref, String path)
  164.                 throws GitAPIException, IOException;
  165.     }

  166.     /**
  167.      * Read-only view of contents and file mode (i.e. permissions) for a file in
  168.      * a remote repository.
  169.      *
  170.      * @since 5.2
  171.      */
  172.     public static final class RemoteFile {
  173.         @NonNull
  174.         private final byte[] contents;

  175.         @NonNull
  176.         private final FileMode fileMode;

  177.         /**
  178.          * @param contents
  179.          *            Raw contents of the file.
  180.          * @param fileMode
  181.          *            Git file mode for this file (e.g. executable or regular)
  182.          */
  183.         public RemoteFile(@NonNull byte[] contents,
  184.                 @NonNull FileMode fileMode) {
  185.             this.contents = Objects.requireNonNull(contents);
  186.             this.fileMode = Objects.requireNonNull(fileMode);
  187.         }

  188.         /**
  189.          * Contents of the file.
  190.          * <p>
  191.          * Callers who receive this reference must not modify its contents (as
  192.          * it can point to internal cached data).
  193.          *
  194.          * @return Raw contents of the file. Do not modify it.
  195.          */
  196.         @NonNull
  197.         public byte[] getContents() {
  198.             return contents;
  199.         }

  200.         /**
  201.          * @return Git file mode for this file (e.g. executable or regular)
  202.          */
  203.         @NonNull
  204.         public FileMode getFileMode() {
  205.             return fileMode;
  206.         }

  207.     }

  208.     /** A default implementation of {@link RemoteReader} callback. */
  209.     public static class DefaultRemoteReader implements RemoteReader {

  210.         @Override
  211.         public ObjectId sha1(String uri, String ref) throws GitAPIException {
  212.             Map<String, Ref> map = Git
  213.                     .lsRemoteRepository()
  214.                     .setRemote(uri)
  215.                     .callAsMap();
  216.             Ref r = RefDatabase.findRef(map, ref);
  217.             return r != null ? r.getObjectId() : null;
  218.         }

  219.         @Override
  220.         public RemoteFile readFileWithMode(String uri, String ref, String path)
  221.                 throws GitAPIException, IOException {
  222.             File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
  223.             try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
  224.                     .setURI(uri).call()) {
  225.                 Repository repo = git.getRepository();
  226.                 ObjectId refCommitId = sha1(uri, ref);
  227.                 if (refCommitId == null) {
  228.                     throw new InvalidRefNameException(MessageFormat
  229.                             .format(JGitText.get().refNotResolved, ref));
  230.                 }
  231.                 RevCommit commit = repo.parseCommit(refCommitId);
  232.                 TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());

  233.                 // TODO(ifrade): Cope better with big files (e.g. using
  234.                 // InputStream instead of byte[])
  235.                 return new RemoteFile(
  236.                         tw.getObjectReader().open(tw.getObjectId(0))
  237.                                 .getCachedBytes(Integer.MAX_VALUE),
  238.                         tw.getFileMode(0));
  239.             } finally {
  240.                 FileUtils.delete(dir, FileUtils.RECURSIVE);
  241.             }
  242.         }
  243.     }

  244.     @SuppressWarnings("serial")
  245.     private static class ManifestErrorException extends GitAPIException {
  246.         ManifestErrorException(Throwable cause) {
  247.             super(RepoText.get().invalidManifest, cause);
  248.         }
  249.     }

  250.     @SuppressWarnings("serial")
  251.     private static class RemoteUnavailableException extends GitAPIException {
  252.         RemoteUnavailableException(String uri) {
  253.             super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
  254.         }
  255.     }

  256.     /**
  257.      * Constructor for RepoCommand
  258.      *
  259.      * @param repo
  260.      *            the {@link org.eclipse.jgit.lib.Repository}
  261.      */
  262.     public RepoCommand(Repository repo) {
  263.         super(repo);
  264.     }

  265.     /**
  266.      * Set path to the manifest XML file.
  267.      * <p>
  268.      * Calling {@link #setInputStream} will ignore the path set here.
  269.      *
  270.      * @param path
  271.      *            (with <code>/</code> as separator)
  272.      * @return this command
  273.      */
  274.     public RepoCommand setPath(String path) {
  275.         this.manifestPath = path;
  276.         return this;
  277.     }

  278.     /**
  279.      * Set the input stream to the manifest XML.
  280.      * <p>
  281.      * Setting inputStream will ignore the path set. It will be closed in
  282.      * {@link #call}.
  283.      *
  284.      * @param inputStream a {@link java.io.InputStream} object.
  285.      * @return this command
  286.      * @since 3.5
  287.      */
  288.     public RepoCommand setInputStream(InputStream inputStream) {
  289.         this.inputStream = inputStream;
  290.         return this;
  291.     }

  292.     /**
  293.      * Set base URI of the paths inside the XML. This is typically the name of
  294.      * the directory holding the manifest repository, eg. for
  295.      * https://android.googlesource.com/platform/manifest, this should be
  296.      * /platform (if you would run this on android.googlesource.com) or
  297.      * https://android.googlesource.com/platform elsewhere.
  298.      *
  299.      * @param uri
  300.      *            the base URI
  301.      * @return this command
  302.      */
  303.     public RepoCommand setURI(String uri) {
  304.         this.baseUri = uri;
  305.         return this;
  306.     }

  307.     /**
  308.      * Set the URI of the superproject (this repository), so the .gitmodules
  309.      * file can specify the submodule URLs relative to the superproject.
  310.      *
  311.      * @param uri
  312.      *            the URI of the repository holding the superproject.
  313.      * @return this command
  314.      * @since 4.8
  315.      */
  316.     public RepoCommand setTargetURI(String uri) {
  317.         // The repo name is interpreted as a directory, for example
  318.         // Gerrit (http://gerrit.googlesource.com/gerrit) has a
  319.         // .gitmodules referencing ../plugins/hooks, which is
  320.         // on http://gerrit.googlesource.com/plugins/hooks,
  321.         this.targetUri = URI.create(uri + "/"); //$NON-NLS-1$
  322.         return this;
  323.     }

  324.     /**
  325.      * Set groups to sync
  326.      *
  327.      * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
  328.      * @return this command
  329.      */
  330.     public RepoCommand setGroups(String groups) {
  331.         this.groupsParam = groups;
  332.         return this;
  333.     }

  334.     /**
  335.      * Set default branch.
  336.      * <p>
  337.      * This is generally the name of the branch the manifest file was in. If
  338.      * there's no default revision (branch) specified in manifest and no
  339.      * revision specified in project, this branch will be used.
  340.      *
  341.      * @param branch
  342.      *            a branch name
  343.      * @return this command
  344.      */
  345.     public RepoCommand setBranch(String branch) {
  346.         this.branch = branch;
  347.         return this;
  348.     }

  349.     /**
  350.      * Set target branch.
  351.      * <p>
  352.      * This is the target branch of the super project to be updated. If not set,
  353.      * default is HEAD.
  354.      * <p>
  355.      * For non-bare repositories, HEAD will always be used and this will be
  356.      * ignored.
  357.      *
  358.      * @param branch
  359.      *            branch name
  360.      * @return this command
  361.      * @since 4.1
  362.      */
  363.     public RepoCommand setTargetBranch(String branch) {
  364.         this.targetBranch = Constants.R_HEADS + branch;
  365.         return this;
  366.     }

  367.     /**
  368.      * Set whether the branch name should be recorded in .gitmodules.
  369.      * <p>
  370.      * Submodule entries in .gitmodules can include a "branch" field
  371.      * to indicate what remote branch each submodule tracks.
  372.      * <p>
  373.      * That field is used by "git submodule update --remote" to update
  374.      * to the tip of the tracked branch when asked and by Gerrit to
  375.      * update the superproject when a change on that branch is merged.
  376.      * <p>
  377.      * Subprojects that request a specific commit or tag will not have
  378.      * a branch name recorded.
  379.      * <p>
  380.      * Not implemented for non-bare repositories.
  381.      *
  382.      * @param enable Whether to record the branch name
  383.      * @return this command
  384.      * @since 4.2
  385.      */
  386.     public RepoCommand setRecordRemoteBranch(boolean enable) {
  387.         this.recordRemoteBranch = enable;
  388.         return this;
  389.     }

  390.     /**
  391.      * Set whether the labels field should be recorded as a label in
  392.      * .gitattributes.
  393.      * <p>
  394.      * Not implemented for non-bare repositories.
  395.      *
  396.      * @param enable Whether to record the labels in the .gitattributes
  397.      * @return this command
  398.      * @since 4.4
  399.      */
  400.     public RepoCommand setRecordSubmoduleLabels(boolean enable) {
  401.         this.recordSubmoduleLabels = enable;
  402.         return this;
  403.     }

  404.     /**
  405.      * Set whether the clone-depth field should be recorded as a shallow
  406.      * recommendation in .gitmodules.
  407.      * <p>
  408.      * Not implemented for non-bare repositories.
  409.      *
  410.      * @param enable Whether to record the shallow recommendation.
  411.      * @return this command
  412.      * @since 4.4
  413.      */
  414.     public RepoCommand setRecommendShallow(boolean enable) {
  415.         this.recordShallowSubmodules = enable;
  416.         return this;
  417.     }

  418.     /**
  419.      * The progress monitor associated with the clone operation. By default,
  420.      * this is set to <code>NullProgressMonitor</code>
  421.      *
  422.      * @see org.eclipse.jgit.lib.NullProgressMonitor
  423.      * @param monitor
  424.      *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
  425.      * @return this command
  426.      */
  427.     public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
  428.         this.monitor = monitor;
  429.         return this;
  430.     }

  431.     /**
  432.      * Set whether to skip projects whose commits don't exist remotely.
  433.      * <p>
  434.      * When set to true, we'll just skip the manifest entry and continue
  435.      * on to the next one.
  436.      * <p>
  437.      * When set to false (default), we'll throw an error when remote
  438.      * failures occur.
  439.      * <p>
  440.      * Not implemented for non-bare repositories.
  441.      *
  442.      * @param ignore Whether to ignore the remote failures.
  443.      * @return this command
  444.      * @since 4.3
  445.      */
  446.     public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
  447.         this.ignoreRemoteFailures = ignore;
  448.         return this;
  449.     }

  450.     /**
  451.      * Set the author/committer for the bare repository commit.
  452.      * <p>
  453.      * For non-bare repositories, the current user will be used and this will be
  454.      * ignored.
  455.      *
  456.      * @param author
  457.      *            the author's {@link org.eclipse.jgit.lib.PersonIdent}
  458.      * @return this command
  459.      */
  460.     public RepoCommand setAuthor(PersonIdent author) {
  461.         this.author = author;
  462.         return this;
  463.     }

  464.     /**
  465.      * Set the GetHeadFromUri callback.
  466.      *
  467.      * This is only used in bare repositories.
  468.      *
  469.      * @param callback
  470.      *            a {@link org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader}
  471.      *            object.
  472.      * @return this command
  473.      */
  474.     public RepoCommand setRemoteReader(RemoteReader callback) {
  475.         this.callback = callback;
  476.         return this;
  477.     }

  478.     /**
  479.      * Set the IncludedFileReader callback.
  480.      *
  481.      * @param reader
  482.      *            a
  483.      *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
  484.      *            object.
  485.      * @return this command
  486.      * @since 4.0
  487.      */
  488.     public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
  489.         this.includedReader = reader;
  490.         return this;
  491.     }

  492.     /** {@inheritDoc} */
  493.     @Override
  494.     public RevCommit call() throws GitAPIException {
  495.         checkCallable();
  496.         if (baseUri == null) {
  497.             baseUri = ""; //$NON-NLS-1$
  498.         }
  499.         if (inputStream == null) {
  500.             if (manifestPath == null || manifestPath.length() == 0)
  501.                 throw new IllegalArgumentException(
  502.                         JGitText.get().pathNotConfigured);
  503.             try {
  504.                 inputStream = new FileInputStream(manifestPath);
  505.             } catch (IOException e) {
  506.                 throw new IllegalArgumentException(
  507.                         JGitText.get().pathNotConfigured, e);
  508.             }
  509.         }

  510.         List<RepoProject> filteredProjects;
  511.         try {
  512.             ManifestParser parser = new ManifestParser(includedReader,
  513.                     manifestPath, branch, baseUri, groupsParam, repo);
  514.             parser.read(inputStream);
  515.             filteredProjects = parser.getFilteredProjects();
  516.         } catch (IOException e) {
  517.             throw new ManifestErrorException(e);
  518.         } finally {
  519.             try {
  520.                 inputStream.close();
  521.             } catch (IOException e) {
  522.                 // Just ignore it, it's not important.
  523.             }
  524.         }

  525.         if (repo.isBare()) {
  526.             if (author == null)
  527.                 author = new PersonIdent(repo);
  528.             if (callback == null)
  529.                 callback = new DefaultRemoteReader();
  530.             List<RepoProject> renamedProjects = renameProjects(filteredProjects);

  531.             DirCache index = DirCache.newInCore();
  532.             DirCacheBuilder builder = index.builder();
  533.             ObjectInserter inserter = repo.newObjectInserter();
  534.             try (RevWalk rw = new RevWalk(repo)) {
  535.                 Config cfg = new Config();
  536.                 StringBuilder attributes = new StringBuilder();
  537.                 for (RepoProject proj : renamedProjects) {
  538.                     String name = proj.getName();
  539.                     String path = proj.getPath();
  540.                     String url = proj.getUrl();
  541.                     ObjectId objectId;
  542.                     if (ObjectId.isId(proj.getRevision())) {
  543.                         objectId = ObjectId.fromString(proj.getRevision());
  544.                     } else {
  545.                         objectId = callback.sha1(url, proj.getRevision());
  546.                         if (objectId == null && !ignoreRemoteFailures) {
  547.                             throw new RemoteUnavailableException(url);
  548.                         }
  549.                         if (recordRemoteBranch) {
  550.                             // can be branch or tag
  551.                             cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
  552.                                     proj.getRevision());
  553.                         }

  554.                         if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
  555.                             // The shallow recommendation is losing information.
  556.                             // As the repo manifests stores the recommended
  557.                             // depth in the 'clone-depth' field, while
  558.                             // git core only uses a binary 'shallow = true/false'
  559.                             // hint, we'll map any depth to 'shallow = true'
  560.                             cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
  561.                                     true);
  562.                         }
  563.                     }
  564.                     if (recordSubmoduleLabels) {
  565.                         StringBuilder rec = new StringBuilder();
  566.                         rec.append("/"); //$NON-NLS-1$
  567.                         rec.append(path);
  568.                         for (String group : proj.getGroups()) {
  569.                             rec.append(" "); //$NON-NLS-1$
  570.                             rec.append(group);
  571.                         }
  572.                         rec.append("\n"); //$NON-NLS-1$
  573.                         attributes.append(rec.toString());
  574.                     }

  575.                     URI submodUrl = URI.create(url);
  576.                     if (targetUri != null) {
  577.                         submodUrl = relativize(targetUri, submodUrl);
  578.                     }
  579.                     cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
  580.                     cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
  581.                             submodUrl.toString());

  582.                     // create gitlink
  583.                     if (objectId != null) {
  584.                         DirCacheEntry dcEntry = new DirCacheEntry(path);
  585.                         dcEntry.setObjectId(objectId);
  586.                         dcEntry.setFileMode(FileMode.GITLINK);
  587.                         builder.add(dcEntry);

  588.                         for (CopyFile copyfile : proj.getCopyFiles()) {
  589.                             RemoteFile rf = callback.readFileWithMode(
  590.                                 url, proj.getRevision(), copyfile.src);
  591.                             objectId = inserter.insert(Constants.OBJ_BLOB,
  592.                                     rf.getContents());
  593.                             dcEntry = new DirCacheEntry(copyfile.dest);
  594.                             dcEntry.setObjectId(objectId);
  595.                             dcEntry.setFileMode(rf.getFileMode());
  596.                             builder.add(dcEntry);
  597.                         }
  598.                         for (LinkFile linkfile : proj.getLinkFiles()) {
  599.                             String link;
  600.                             if (linkfile.dest.contains("/")) { //$NON-NLS-1$
  601.                                 link = FileUtils.relativizeGitPath(
  602.                                     linkfile.dest.substring(0,
  603.                                         linkfile.dest.lastIndexOf('/')),
  604.                                     proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
  605.                             } else {
  606.                                 link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
  607.                             }

  608.                             objectId = inserter.insert(Constants.OBJ_BLOB,
  609.                                     link.getBytes(UTF_8));
  610.                             dcEntry = new DirCacheEntry(linkfile.dest);
  611.                             dcEntry.setObjectId(objectId);
  612.                             dcEntry.setFileMode(FileMode.SYMLINK);
  613.                             builder.add(dcEntry);
  614.                         }
  615.                     }
  616.                 }
  617.                 String content = cfg.toText();

  618.                 // create a new DirCacheEntry for .gitmodules file.
  619.                 final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
  620.                 ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
  621.                         content.getBytes(UTF_8));
  622.                 dcEntry.setObjectId(objectId);
  623.                 dcEntry.setFileMode(FileMode.REGULAR_FILE);
  624.                 builder.add(dcEntry);

  625.                 if (recordSubmoduleLabels) {
  626.                     // create a new DirCacheEntry for .gitattributes file.
  627.                     final DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
  628.                     ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
  629.                             attributes.toString().getBytes(UTF_8));
  630.                     dcEntryAttr.setObjectId(attrId);
  631.                     dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
  632.                     builder.add(dcEntryAttr);
  633.                 }

  634.                 builder.finish();
  635.                 ObjectId treeId = index.writeTree(inserter);

  636.                 // Create a Commit object, populate it and write it
  637.                 ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
  638.                 if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
  639.                     // No change. Do nothing.
  640.                     return rw.parseCommit(headId);
  641.                 }

  642.                 CommitBuilder commit = new CommitBuilder();
  643.                 commit.setTreeId(treeId);
  644.                 if (headId != null)
  645.                     commit.setParentIds(headId);
  646.                 commit.setAuthor(author);
  647.                 commit.setCommitter(author);
  648.                 commit.setMessage(RepoText.get().repoCommitMessage);

  649.                 ObjectId commitId = inserter.insert(commit);
  650.                 inserter.flush();

  651.                 RefUpdate ru = repo.updateRef(targetBranch);
  652.                 ru.setNewObjectId(commitId);
  653.                 ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
  654.                 Result rc = ru.update(rw);

  655.                 switch (rc) {
  656.                     case NEW:
  657.                     case FORCED:
  658.                     case FAST_FORWARD:
  659.                         // Successful. Do nothing.
  660.                         break;
  661.                     case REJECTED:
  662.                     case LOCK_FAILURE:
  663.                         throw new ConcurrentRefUpdateException(
  664.                                 MessageFormat.format(
  665.                                         JGitText.get().cannotLock, targetBranch),
  666.                                 ru.getRef(),
  667.                                 rc);
  668.                     default:
  669.                         throw new JGitInternalException(MessageFormat.format(
  670.                                 JGitText.get().updatingRefFailed,
  671.                                 targetBranch, commitId.name(), rc));
  672.                 }

  673.                 return rw.parseCommit(commitId);
  674.             } catch (GitAPIException | IOException e) {
  675.                 throw new ManifestErrorException(e);
  676.             }
  677.         }
  678.         try (Git git = new Git(repo)) {
  679.             for (RepoProject proj : filteredProjects) {
  680.                 addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(),
  681.                         proj.getRevision(), proj.getCopyFiles(),
  682.                         proj.getLinkFiles(), git);
  683.             }
  684.             return git.commit().setMessage(RepoText.get().repoCommitMessage)
  685.                     .call();
  686.         } catch (GitAPIException | IOException e) {
  687.             throw new ManifestErrorException(e);
  688.         }
  689.     }

  690.     private void addSubmodule(String name, String url, String path,
  691.             String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
  692.             Git git) throws GitAPIException, IOException {
  693.         assert (!repo.isBare());
  694.         assert (git != null);
  695.         if (!linkfiles.isEmpty()) {
  696.             throw new UnsupportedOperationException(
  697.                     JGitText.get().nonBareLinkFilesNotSupported);
  698.         }

  699.         SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path)
  700.                 .setURI(url);
  701.         if (monitor != null)
  702.             add.setProgressMonitor(monitor);

  703.         Repository subRepo = add.call();
  704.         if (revision != null) {
  705.             try (Git sub = new Git(subRepo)) {
  706.                 sub.checkout().setName(findRef(revision, subRepo)).call();
  707.             }
  708.             subRepo.close();
  709.             git.add().addFilepattern(path).call();
  710.         }
  711.         for (CopyFile copyfile : copyfiles) {
  712.             copyfile.copy();
  713.             git.add().addFilepattern(copyfile.dest).call();
  714.         }
  715.     }

  716.     /**
  717.      * Rename the projects if there's a conflict when converted to submodules.
  718.      *
  719.      * @param projects
  720.      *            parsed projects
  721.      * @return projects that are renamed if necessary
  722.      */
  723.     private List<RepoProject> renameProjects(List<RepoProject> projects) {
  724.         Map<String, List<RepoProject>> m = new TreeMap<>();
  725.         for (RepoProject proj : projects) {
  726.             List<RepoProject> l = m.get(proj.getName());
  727.             if (l == null) {
  728.                 l = new ArrayList<>();
  729.                 m.put(proj.getName(), l);
  730.             }
  731.             l.add(proj);
  732.         }

  733.         List<RepoProject> ret = new ArrayList<>();
  734.         for (List<RepoProject> ps : m.values()) {
  735.             boolean nameConflict = ps.size() != 1;
  736.             for (RepoProject proj : ps) {
  737.                 String name = proj.getName();
  738.                 if (nameConflict) {
  739.                     name += SLASH + proj.getPath();
  740.                 }
  741.                 RepoProject p = new RepoProject(name,
  742.                         proj.getPath(), proj.getRevision(), null,
  743.                         proj.getGroups(), proj.getRecommendShallow());
  744.                 p.setUrl(proj.getUrl());
  745.                 p.addCopyFiles(proj.getCopyFiles());
  746.                 p.addLinkFiles(proj.getLinkFiles());
  747.                 ret.add(p);
  748.             }
  749.         }
  750.         return ret;
  751.     }

  752.     /*
  753.      * Assume we are document "a/b/index.html", what should we put in a href to get to "a/" ?
  754.      * Returns the child if either base or child is not a bare path. This provides a missing feature in
  755.      * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
  756.      */
  757.     private static final String SLASH = "/"; //$NON-NLS-1$
  758.     static URI relativize(URI current, URI target) {
  759.         if (!Objects.equals(current.getHost(), target.getHost())) {
  760.             return target;
  761.         }

  762.         String cur = current.normalize().getPath();
  763.         String dest = target.normalize().getPath();

  764.         // TODO(hanwen): maybe (absolute, relative) should throw an exception.
  765.         if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
  766.             return target;
  767.         }

  768.         while (cur.startsWith(SLASH)) {
  769.             cur = cur.substring(1);
  770.         }
  771.         while (dest.startsWith(SLASH)) {
  772.             dest = dest.substring(1);
  773.         }

  774.         if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
  775.             // Avoid having to special-casing in the next two ifs.
  776.             String prefix = "prefix/"; //$NON-NLS-1$
  777.             cur = prefix + cur;
  778.             dest = prefix + dest;
  779.         }

  780.         if (!cur.endsWith(SLASH)) {
  781.             // The current file doesn't matter.
  782.             int lastSlash = cur.lastIndexOf('/');
  783.             cur = cur.substring(0, lastSlash);
  784.         }
  785.         String destFile = ""; //$NON-NLS-1$
  786.         if (!dest.endsWith(SLASH)) {
  787.             // We always have to provide the destination file.
  788.             int lastSlash = dest.lastIndexOf('/');
  789.             destFile = dest.substring(lastSlash + 1, dest.length());
  790.             dest = dest.substring(0, dest.lastIndexOf('/'));
  791.         }

  792.         String[] cs = cur.split(SLASH);
  793.         String[] ds = dest.split(SLASH);

  794.         int common = 0;
  795.         while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
  796.             common++;
  797.         }

  798.         StringJoiner j = new StringJoiner(SLASH);
  799.         for (int i = common; i < cs.length; i++) {
  800.             j.add(".."); //$NON-NLS-1$
  801.         }
  802.         for (int i = common; i < ds.length; i++) {
  803.             j.add(ds[i]);
  804.         }

  805.         j.add(destFile);
  806.         return URI.create(j.toString());
  807.     }

  808.     private static String findRef(String ref, Repository repo)
  809.             throws IOException {
  810.         if (!ObjectId.isId(ref)) {
  811.             Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
  812.             if (r != null)
  813.                 return r.getName();
  814.         }
  815.         return ref;
  816.     }
  817. }