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 java.io.File;
  12. import java.io.FileInputStream;
  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.net.URI;
  16. import java.text.MessageFormat;
  17. import java.util.ArrayList;
  18. import java.util.List;
  19. import java.util.Map;
  20. import java.util.Objects;
  21. import java.util.StringJoiner;
  22. import java.util.TreeMap;

  23. import org.eclipse.jgit.annotations.NonNull;
  24. import org.eclipse.jgit.annotations.Nullable;
  25. import org.eclipse.jgit.api.Git;
  26. import org.eclipse.jgit.api.GitCommand;
  27. import org.eclipse.jgit.api.errors.GitAPIException;
  28. import org.eclipse.jgit.api.errors.InvalidRefNameException;
  29. import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.ExtraContent;
  30. import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
  31. import org.eclipse.jgit.gitrepo.internal.RepoText;
  32. import org.eclipse.jgit.internal.JGitText;
  33. import org.eclipse.jgit.lib.Constants;
  34. import org.eclipse.jgit.lib.FileMode;
  35. import org.eclipse.jgit.lib.ObjectId;
  36. import org.eclipse.jgit.lib.PersonIdent;
  37. import org.eclipse.jgit.lib.ProgressMonitor;
  38. import org.eclipse.jgit.lib.Ref;
  39. import org.eclipse.jgit.lib.RefDatabase;
  40. import org.eclipse.jgit.lib.Repository;
  41. import org.eclipse.jgit.revwalk.RevCommit;
  42. import org.eclipse.jgit.treewalk.TreeWalk;
  43. import org.eclipse.jgit.util.FileUtils;

  44. /**
  45.  * A class used to execute a repo command.
  46.  *
  47.  * This will parse a repo XML manifest, convert it into .gitmodules file and the
  48.  * repository config file.
  49.  *
  50.  * If called against a bare repository, it will replace all the existing content
  51.  * of the repository with the contents populated from the manifest.
  52.  *
  53.  * repo manifest allows projects overlapping, e.g. one project's manifestPath is
  54.  * "foo" and another project's manifestPath is "foo/bar". This won't
  55.  * work in git submodule, so we'll skip all the sub projects
  56.  * ("foo/bar" in the example) while converting.
  57.  *
  58.  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
  59.  * @since 3.4
  60.  */
  61. public class RepoCommand extends GitCommand<RevCommit> {


  62.     private String manifestPath;
  63.     private String baseUri;
  64.     private URI targetUri;
  65.     private String groupsParam;
  66.     private String branch;
  67.     private String targetBranch = Constants.HEAD;
  68.     private PersonIdent author;
  69.     private RemoteReader callback;
  70.     private InputStream inputStream;
  71.     private IncludedFileReader includedReader;

  72.     private BareSuperprojectWriter.BareWriterConfig bareWriterConfig = BareSuperprojectWriter.BareWriterConfig
  73.             .getDefault();

  74.     private ProgressMonitor monitor;

  75.     private final List<ExtraContent> extraContents = new ArrayList<>();

  76.     /**
  77.      * A callback to get ref sha1 of a repository from its uri.
  78.      *
  79.      * We provided a default implementation {@link DefaultRemoteReader} to
  80.      * use ls-remote command to read the sha1 from the repository and clone the
  81.      * repository to read the file. Callers may have their own quicker
  82.      * implementation.
  83.      *
  84.      * @since 3.4
  85.      */
  86.     public interface RemoteReader {
  87.         /**
  88.          * Read a remote ref sha1.
  89.          *
  90.          * @param uri
  91.          *            The URI of the remote repository
  92.          * @param ref
  93.          *            Name of the ref to lookup. May be a short-hand form, e.g.
  94.          *            "master" which is automatically expanded to
  95.          *            "refs/heads/master" if "refs/heads/master" already exists.
  96.          * @return the sha1 of the remote repository, or null if the ref does
  97.          *         not exist.
  98.          * @throws GitAPIException
  99.          */
  100.         @Nullable
  101.         public ObjectId sha1(String uri, String ref) throws GitAPIException;

  102.         /**
  103.          * Read a file from a remote repository.
  104.          *
  105.          * @param uri
  106.          *            The URI of the remote repository
  107.          * @param ref
  108.          *            The ref (branch/tag/etc.) to read
  109.          * @param path
  110.          *            The relative path (inside the repo) to the file to read
  111.          * @return the file content.
  112.          * @throws GitAPIException
  113.          * @throws IOException
  114.          * @since 3.5
  115.          *
  116.          * @deprecated Use {@link #readFileWithMode(String, String, String)}
  117.          *             instead
  118.          */
  119.         @Deprecated
  120.         public default byte[] readFile(String uri, String ref, String path)
  121.                 throws GitAPIException, IOException {
  122.             return readFileWithMode(uri, ref, path).getContents();
  123.         }

  124.         /**
  125.          * Read contents and mode (i.e. permissions) of the file from a remote
  126.          * repository.
  127.          *
  128.          * @param uri
  129.          *            The URI of the remote repository
  130.          * @param ref
  131.          *            Name of the ref to lookup. May be a short-hand form, e.g.
  132.          *            "master" which is automatically expanded to
  133.          *            "refs/heads/master" if "refs/heads/master" already exists.
  134.          * @param path
  135.          *            The relative path (inside the repo) to the file to read
  136.          * @return The contents and file mode of the file in the given
  137.          *         repository and branch. Never null.
  138.          * @throws GitAPIException
  139.          *             If the ref have an invalid or ambiguous name, or it does
  140.          *             not exist in the repository,
  141.          * @throws IOException
  142.          *             If the object does not exist or is too large
  143.          * @since 5.2
  144.          */
  145.         @NonNull
  146.         public RemoteFile readFileWithMode(String uri, String ref, String path)
  147.                 throws GitAPIException, IOException;
  148.     }

  149.     /**
  150.      * Read-only view of contents and file mode (i.e. permissions) for a file in
  151.      * a remote repository.
  152.      *
  153.      * @since 5.2
  154.      */
  155.     public static final class RemoteFile {
  156.         @NonNull
  157.         private final byte[] contents;

  158.         @NonNull
  159.         private final FileMode fileMode;

  160.         /**
  161.          * @param contents
  162.          *            Raw contents of the file.
  163.          * @param fileMode
  164.          *            Git file mode for this file (e.g. executable or regular)
  165.          */
  166.         public RemoteFile(@NonNull byte[] contents,
  167.                 @NonNull FileMode fileMode) {
  168.             this.contents = Objects.requireNonNull(contents);
  169.             this.fileMode = Objects.requireNonNull(fileMode);
  170.         }

  171.         /**
  172.          * Contents of the file.
  173.          * <p>
  174.          * Callers who receive this reference must not modify its contents (as
  175.          * it can point to internal cached data).
  176.          *
  177.          * @return Raw contents of the file. Do not modify it.
  178.          */
  179.         @NonNull
  180.         public byte[] getContents() {
  181.             return contents;
  182.         }

  183.         /**
  184.          * @return Git file mode for this file (e.g. executable or regular)
  185.          */
  186.         @NonNull
  187.         public FileMode getFileMode() {
  188.             return fileMode;
  189.         }

  190.     }

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

  193.         @Override
  194.         public ObjectId sha1(String uri, String ref) throws GitAPIException {
  195.             Map<String, Ref> map = Git
  196.                     .lsRemoteRepository()
  197.                     .setRemote(uri)
  198.                     .callAsMap();
  199.             Ref r = RefDatabase.findRef(map, ref);
  200.             return r != null ? r.getObjectId() : null;
  201.         }

  202.         @Override
  203.         public RemoteFile readFileWithMode(String uri, String ref, String path)
  204.                 throws GitAPIException, IOException {
  205.             File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
  206.             try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
  207.                     .setURI(uri).call()) {
  208.                 Repository repo = git.getRepository();
  209.                 ObjectId refCommitId = sha1(uri, ref);
  210.                 if (refCommitId == null) {
  211.                     throw new InvalidRefNameException(MessageFormat
  212.                             .format(JGitText.get().refNotResolved, ref));
  213.                 }
  214.                 RevCommit commit = repo.parseCommit(refCommitId);
  215.                 TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());

  216.                 // TODO(ifrade): Cope better with big files (e.g. using
  217.                 // InputStream instead of byte[])
  218.                 return new RemoteFile(
  219.                         tw.getObjectReader().open(tw.getObjectId(0))
  220.                                 .getCachedBytes(Integer.MAX_VALUE),
  221.                         tw.getFileMode(0));
  222.             } finally {
  223.                 FileUtils.delete(dir, FileUtils.RECURSIVE);
  224.             }
  225.         }
  226.     }

  227.     @SuppressWarnings("serial")
  228.     static class ManifestErrorException extends GitAPIException {
  229.         ManifestErrorException(Throwable cause) {
  230.             super(RepoText.get().invalidManifest, cause);
  231.         }
  232.     }

  233.     @SuppressWarnings("serial")
  234.     static class RemoteUnavailableException extends GitAPIException {
  235.         RemoteUnavailableException(String uri) {
  236.             super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
  237.         }
  238.     }

  239.     /**
  240.      * Constructor for RepoCommand
  241.      *
  242.      * @param repo
  243.      *            the {@link org.eclipse.jgit.lib.Repository}
  244.      */
  245.     public RepoCommand(Repository repo) {
  246.         super(repo);
  247.     }

  248.     /**
  249.      * Set path to the manifest XML file.
  250.      * <p>
  251.      * Calling {@link #setInputStream} will ignore the path set here.
  252.      *
  253.      * @param path
  254.      *            (with <code>/</code> as separator)
  255.      * @return this command
  256.      */
  257.     public RepoCommand setPath(String path) {
  258.         this.manifestPath = path;
  259.         return this;
  260.     }

  261.     /**
  262.      * Set the input stream to the manifest XML.
  263.      * <p>
  264.      * Setting inputStream will ignore the path set. It will be closed in
  265.      * {@link #call}.
  266.      *
  267.      * @param inputStream a {@link java.io.InputStream} object.
  268.      * @return this command
  269.      * @since 3.5
  270.      */
  271.     public RepoCommand setInputStream(InputStream inputStream) {
  272.         this.inputStream = inputStream;
  273.         return this;
  274.     }

  275.     /**
  276.      * Set base URI of the paths inside the XML. This is typically the name of
  277.      * the directory holding the manifest repository, eg. for
  278.      * https://android.googlesource.com/platform/manifest, this should be
  279.      * /platform (if you would run this on android.googlesource.com) or
  280.      * https://android.googlesource.com/platform elsewhere.
  281.      *
  282.      * @param uri
  283.      *            the base URI
  284.      * @return this command
  285.      */
  286.     public RepoCommand setURI(String uri) {
  287.         this.baseUri = uri;
  288.         return this;
  289.     }

  290.     /**
  291.      * Set the URI of the superproject (this repository), so the .gitmodules
  292.      * file can specify the submodule URLs relative to the superproject.
  293.      *
  294.      * @param uri
  295.      *            the URI of the repository holding the superproject.
  296.      * @return this command
  297.      * @since 4.8
  298.      */
  299.     public RepoCommand setTargetURI(String uri) {
  300.         // The repo name is interpreted as a directory, for example
  301.         // Gerrit (http://gerrit.googlesource.com/gerrit) has a
  302.         // .gitmodules referencing ../plugins/hooks, which is
  303.         // on http://gerrit.googlesource.com/plugins/hooks,
  304.         this.targetUri = URI.create(uri + "/"); //$NON-NLS-1$
  305.         return this;
  306.     }

  307.     /**
  308.      * Set groups to sync
  309.      *
  310.      * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
  311.      * @return this command
  312.      */
  313.     public RepoCommand setGroups(String groups) {
  314.         this.groupsParam = groups;
  315.         return this;
  316.     }

  317.     /**
  318.      * Set default branch.
  319.      * <p>
  320.      * This is generally the name of the branch the manifest file was in. If
  321.      * there's no default revision (branch) specified in manifest and no
  322.      * revision specified in project, this branch will be used.
  323.      *
  324.      * @param branch
  325.      *            a branch name
  326.      * @return this command
  327.      */
  328.     public RepoCommand setBranch(String branch) {
  329.         this.branch = branch;
  330.         return this;
  331.     }

  332.     /**
  333.      * Set target branch.
  334.      * <p>
  335.      * This is the target branch of the super project to be updated. If not set,
  336.      * default is HEAD.
  337.      * <p>
  338.      * For non-bare repositories, HEAD will always be used and this will be
  339.      * ignored.
  340.      *
  341.      * @param branch
  342.      *            branch name
  343.      * @return this command
  344.      * @since 4.1
  345.      */
  346.     public RepoCommand setTargetBranch(String branch) {
  347.         this.targetBranch = Constants.R_HEADS + branch;
  348.         return this;
  349.     }

  350.     /**
  351.      * Set whether the branch name should be recorded in .gitmodules.
  352.      * <p>
  353.      * Submodule entries in .gitmodules can include a "branch" field
  354.      * to indicate what remote branch each submodule tracks.
  355.      * <p>
  356.      * That field is used by "git submodule update --remote" to update
  357.      * to the tip of the tracked branch when asked and by Gerrit to
  358.      * update the superproject when a change on that branch is merged.
  359.      * <p>
  360.      * Subprojects that request a specific commit or tag will not have
  361.      * a branch name recorded.
  362.      * <p>
  363.      * Not implemented for non-bare repositories.
  364.      *
  365.      * @param enable Whether to record the branch name
  366.      * @return this command
  367.      * @since 4.2
  368.      */
  369.     public RepoCommand setRecordRemoteBranch(boolean enable) {
  370.         this.bareWriterConfig.recordRemoteBranch = enable;
  371.         return this;
  372.     }

  373.     /**
  374.      * Set whether the labels field should be recorded as a label in
  375.      * .gitattributes.
  376.      * <p>
  377.      * Not implemented for non-bare repositories.
  378.      *
  379.      * @param enable Whether to record the labels in the .gitattributes
  380.      * @return this command
  381.      * @since 4.4
  382.      */
  383.     public RepoCommand setRecordSubmoduleLabels(boolean enable) {
  384.         this.bareWriterConfig.recordSubmoduleLabels = enable;
  385.         return this;
  386.     }

  387.     /**
  388.      * Set whether the clone-depth field should be recorded as a shallow
  389.      * recommendation in .gitmodules.
  390.      * <p>
  391.      * Not implemented for non-bare repositories.
  392.      *
  393.      * @param enable Whether to record the shallow recommendation.
  394.      * @return this command
  395.      * @since 4.4
  396.      */
  397.     public RepoCommand setRecommendShallow(boolean enable) {
  398.         this.bareWriterConfig.recordShallowSubmodules = enable;
  399.         return this;
  400.     }

  401.     /**
  402.      * The progress monitor associated with the clone operation. By default,
  403.      * this is set to <code>NullProgressMonitor</code>
  404.      *
  405.      * @see org.eclipse.jgit.lib.NullProgressMonitor
  406.      * @param monitor
  407.      *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
  408.      * @return this command
  409.      */
  410.     public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
  411.         this.monitor = monitor;
  412.         return this;
  413.     }

  414.     /**
  415.      * Set whether to skip projects whose commits don't exist remotely.
  416.      * <p>
  417.      * When set to true, we'll just skip the manifest entry and continue
  418.      * on to the next one.
  419.      * <p>
  420.      * When set to false (default), we'll throw an error when remote
  421.      * failures occur.
  422.      * <p>
  423.      * Not implemented for non-bare repositories.
  424.      *
  425.      * @param ignore Whether to ignore the remote failures.
  426.      * @return this command
  427.      * @since 4.3
  428.      */
  429.     public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
  430.         this.bareWriterConfig.ignoreRemoteFailures = ignore;
  431.         return this;
  432.     }

  433.     /**
  434.      * Set the author/committer for the bare repository commit.
  435.      * <p>
  436.      * For non-bare repositories, the current user will be used and this will be
  437.      * ignored.
  438.      *
  439.      * @param author
  440.      *            the author's {@link org.eclipse.jgit.lib.PersonIdent}
  441.      * @return this command
  442.      */
  443.     public RepoCommand setAuthor(PersonIdent author) {
  444.         this.author = author;
  445.         return this;
  446.     }

  447.     /**
  448.      * Set the GetHeadFromUri callback.
  449.      *
  450.      * This is only used in bare repositories.
  451.      *
  452.      * @param callback
  453.      *            a {@link org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader}
  454.      *            object.
  455.      * @return this command
  456.      */
  457.     public RepoCommand setRemoteReader(RemoteReader callback) {
  458.         this.callback = callback;
  459.         return this;
  460.     }

  461.     /**
  462.      * Set the IncludedFileReader callback.
  463.      *
  464.      * @param reader
  465.      *            a
  466.      *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
  467.      *            object.
  468.      * @return this command
  469.      * @since 4.0
  470.      */
  471.     public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
  472.         this.includedReader = reader;
  473.         return this;
  474.     }

  475.     /**
  476.      * Create a file with the given content in the destination repository
  477.      *
  478.      * @param path
  479.      *            where to create the file in the destination repository
  480.      * @param contents
  481.      *            content for the create file
  482.      * @return this command
  483.      *
  484.      * @since 6.1
  485.      */
  486.     public RepoCommand addToDestination(String path, String contents) {
  487.         this.extraContents.add(new ExtraContent(path, contents));
  488.         return this;
  489.     }

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

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

  523.         if (repo.isBare()) {
  524.             List<RepoProject> renamedProjects = renameProjects(filteredProjects);
  525.             BareSuperprojectWriter writer = new BareSuperprojectWriter(repo, targetUri,
  526.                     targetBranch,
  527.                     author == null ? new PersonIdent(repo) : author,
  528.                     callback == null ? new DefaultRemoteReader() : callback,
  529.                     bareWriterConfig, extraContents);
  530.             return writer.write(renamedProjects);
  531.         }


  532.         RegularSuperprojectWriter writer = new RegularSuperprojectWriter(repo, monitor);
  533.         return writer.write(filteredProjects);
  534.     }

  535.     /**
  536.      * Rename the projects if there's a conflict when converted to submodules.
  537.      *
  538.      * @param projects
  539.      *            parsed projects
  540.      * @return projects that are renamed if necessary
  541.      */
  542.     private List<RepoProject> renameProjects(List<RepoProject> projects) {
  543.         Map<String, List<RepoProject>> m = new TreeMap<>();
  544.         for (RepoProject proj : projects) {
  545.             List<RepoProject> l = m.get(proj.getName());
  546.             if (l == null) {
  547.                 l = new ArrayList<>();
  548.                 m.put(proj.getName(), l);
  549.             }
  550.             l.add(proj);
  551.         }

  552.         List<RepoProject> ret = new ArrayList<>();
  553.         for (List<RepoProject> ps : m.values()) {
  554.             boolean nameConflict = ps.size() != 1;
  555.             for (RepoProject proj : ps) {
  556.                 String name = proj.getName();
  557.                 if (nameConflict) {
  558.                     name += SLASH + proj.getPath();
  559.                 }
  560.                 RepoProject p = new RepoProject(name,
  561.                         proj.getPath(), proj.getRevision(), null,
  562.                         proj.getGroups(), proj.getRecommendShallow());
  563.                 p.setUrl(proj.getUrl());
  564.                 p.addCopyFiles(proj.getCopyFiles());
  565.                 p.addLinkFiles(proj.getLinkFiles());
  566.                 ret.add(p);
  567.             }
  568.         }
  569.         return ret;
  570.     }

  571.     /*
  572.      * Assume we are document "a/b/index.html", what should we put in a href to get to "a/" ?
  573.      * Returns the child if either base or child is not a bare path. This provides a missing feature in
  574.      * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
  575.      */
  576.     private static final String SLASH = "/"; //$NON-NLS-1$
  577.     static URI relativize(URI current, URI target) {
  578.         if (!Objects.equals(current.getHost(), target.getHost())) {
  579.             return target;
  580.         }

  581.         String cur = current.normalize().getPath();
  582.         String dest = target.normalize().getPath();

  583.         // TODO(hanwen): maybe (absolute, relative) should throw an exception.
  584.         if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
  585.             return target;
  586.         }

  587.         while (cur.startsWith(SLASH)) {
  588.             cur = cur.substring(1);
  589.         }
  590.         while (dest.startsWith(SLASH)) {
  591.             dest = dest.substring(1);
  592.         }

  593.         if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
  594.             // Avoid having to special-casing in the next two ifs.
  595.             String prefix = "prefix/"; //$NON-NLS-1$
  596.             cur = prefix + cur;
  597.             dest = prefix + dest;
  598.         }

  599.         if (!cur.endsWith(SLASH)) {
  600.             // The current file doesn't matter.
  601.             int lastSlash = cur.lastIndexOf('/');
  602.             cur = cur.substring(0, lastSlash);
  603.         }
  604.         String destFile = ""; //$NON-NLS-1$
  605.         if (!dest.endsWith(SLASH)) {
  606.             // We always have to provide the destination file.
  607.             int lastSlash = dest.lastIndexOf('/');
  608.             destFile = dest.substring(lastSlash + 1, dest.length());
  609.             dest = dest.substring(0, dest.lastIndexOf('/'));
  610.         }

  611.         String[] cs = cur.split(SLASH);
  612.         String[] ds = dest.split(SLASH);

  613.         int common = 0;
  614.         while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
  615.             common++;
  616.         }

  617.         StringJoiner j = new StringJoiner(SLASH);
  618.         for (int i = common; i < cs.length; i++) {
  619.             j.add(".."); //$NON-NLS-1$
  620.         }
  621.         for (int i = common; i < ds.length; i++) {
  622.             j.add(ds[i]);
  623.         }

  624.         j.add(destFile);
  625.         return URI.create(j.toString());
  626.     }

  627. }