RepoCommand.java
- /*
- * Copyright (C) 2014, 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 java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.net.URI;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
- import java.util.Objects;
- import java.util.StringJoiner;
- import java.util.TreeMap;
- import org.eclipse.jgit.annotations.NonNull;
- import org.eclipse.jgit.annotations.Nullable;
- import org.eclipse.jgit.api.Git;
- import org.eclipse.jgit.api.GitCommand;
- import org.eclipse.jgit.api.errors.GitAPIException;
- import org.eclipse.jgit.api.errors.InvalidRefNameException;
- import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.ExtraContent;
- import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
- import org.eclipse.jgit.gitrepo.internal.RepoText;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.ProgressMonitor;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.RefDatabase;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.treewalk.TreeWalk;
- import org.eclipse.jgit.util.FileUtils;
- /**
- * A class used to execute a repo command.
- *
- * This will parse a repo XML manifest, convert it into .gitmodules file and the
- * repository config file.
- *
- * If called against a bare repository, it will replace all the existing content
- * of the repository with the contents populated from the manifest.
- *
- * repo manifest allows projects overlapping, e.g. one project's manifestPath is
- * "foo" and another project's manifestPath is "foo/bar". This won't
- * work in git submodule, so we'll skip all the sub projects
- * ("foo/bar" in the example) while converting.
- *
- * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
- * @since 3.4
- */
- public class RepoCommand extends GitCommand<RevCommit> {
- private String manifestPath;
- private String baseUri;
- private URI targetUri;
- private String groupsParam;
- private String branch;
- private String targetBranch = Constants.HEAD;
- private PersonIdent author;
- private RemoteReader callback;
- private InputStream inputStream;
- private IncludedFileReader includedReader;
- private BareSuperprojectWriter.BareWriterConfig bareWriterConfig = BareSuperprojectWriter.BareWriterConfig
- .getDefault();
- private ProgressMonitor monitor;
- private final List<ExtraContent> extraContents = new ArrayList<>();
- /**
- * A callback to get ref sha1 of a repository from its uri.
- *
- * We provided a default implementation {@link DefaultRemoteReader} to
- * use ls-remote command to read the sha1 from the repository and clone the
- * repository to read the file. Callers may have their own quicker
- * implementation.
- *
- * @since 3.4
- */
- public interface RemoteReader {
- /**
- * Read a remote ref sha1.
- *
- * @param uri
- * The URI of the remote repository
- * @param ref
- * Name of the ref to lookup. May be a short-hand form, e.g.
- * "master" which is automatically expanded to
- * "refs/heads/master" if "refs/heads/master" already exists.
- * @return the sha1 of the remote repository, or null if the ref does
- * not exist.
- * @throws GitAPIException
- */
- @Nullable
- public ObjectId sha1(String uri, String ref) throws GitAPIException;
- /**
- * Read a file from a remote repository.
- *
- * @param uri
- * The URI of the remote repository
- * @param ref
- * The ref (branch/tag/etc.) to read
- * @param path
- * The relative path (inside the repo) to the file to read
- * @return the file content.
- * @throws GitAPIException
- * @throws IOException
- * @since 3.5
- *
- * @deprecated Use {@link #readFileWithMode(String, String, String)}
- * instead
- */
- @Deprecated
- public default byte[] readFile(String uri, String ref, String path)
- throws GitAPIException, IOException {
- return readFileWithMode(uri, ref, path).getContents();
- }
- /**
- * Read contents and mode (i.e. permissions) of the file from a remote
- * repository.
- *
- * @param uri
- * The URI of the remote repository
- * @param ref
- * Name of the ref to lookup. May be a short-hand form, e.g.
- * "master" which is automatically expanded to
- * "refs/heads/master" if "refs/heads/master" already exists.
- * @param path
- * The relative path (inside the repo) to the file to read
- * @return The contents and file mode of the file in the given
- * repository and branch. Never null.
- * @throws GitAPIException
- * If the ref have an invalid or ambiguous name, or it does
- * not exist in the repository,
- * @throws IOException
- * If the object does not exist or is too large
- * @since 5.2
- */
- @NonNull
- public RemoteFile readFileWithMode(String uri, String ref, String path)
- throws GitAPIException, IOException;
- }
- /**
- * Read-only view of contents and file mode (i.e. permissions) for a file in
- * a remote repository.
- *
- * @since 5.2
- */
- public static final class RemoteFile {
- @NonNull
- private final byte[] contents;
- @NonNull
- private final FileMode fileMode;
- /**
- * @param contents
- * Raw contents of the file.
- * @param fileMode
- * Git file mode for this file (e.g. executable or regular)
- */
- public RemoteFile(@NonNull byte[] contents,
- @NonNull FileMode fileMode) {
- this.contents = Objects.requireNonNull(contents);
- this.fileMode = Objects.requireNonNull(fileMode);
- }
- /**
- * Contents of the file.
- * <p>
- * Callers who receive this reference must not modify its contents (as
- * it can point to internal cached data).
- *
- * @return Raw contents of the file. Do not modify it.
- */
- @NonNull
- public byte[] getContents() {
- return contents;
- }
- /**
- * @return Git file mode for this file (e.g. executable or regular)
- */
- @NonNull
- public FileMode getFileMode() {
- return fileMode;
- }
- }
- /** A default implementation of {@link RemoteReader} callback. */
- public static class DefaultRemoteReader implements RemoteReader {
- @Override
- public ObjectId sha1(String uri, String ref) throws GitAPIException {
- Map<String, Ref> map = Git
- .lsRemoteRepository()
- .setRemote(uri)
- .callAsMap();
- Ref r = RefDatabase.findRef(map, ref);
- return r != null ? r.getObjectId() : null;
- }
- @Override
- public RemoteFile readFileWithMode(String uri, String ref, String path)
- throws GitAPIException, IOException {
- File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
- try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
- .setURI(uri).call()) {
- Repository repo = git.getRepository();
- ObjectId refCommitId = sha1(uri, ref);
- if (refCommitId == null) {
- throw new InvalidRefNameException(MessageFormat
- .format(JGitText.get().refNotResolved, ref));
- }
- RevCommit commit = repo.parseCommit(refCommitId);
- TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());
- // TODO(ifrade): Cope better with big files (e.g. using
- // InputStream instead of byte[])
- return new RemoteFile(
- tw.getObjectReader().open(tw.getObjectId(0))
- .getCachedBytes(Integer.MAX_VALUE),
- tw.getFileMode(0));
- } finally {
- FileUtils.delete(dir, FileUtils.RECURSIVE);
- }
- }
- }
- @SuppressWarnings("serial")
- static class ManifestErrorException extends GitAPIException {
- ManifestErrorException(Throwable cause) {
- super(RepoText.get().invalidManifest, cause);
- }
- }
- @SuppressWarnings("serial")
- static class RemoteUnavailableException extends GitAPIException {
- RemoteUnavailableException(String uri) {
- super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
- }
- }
- /**
- * Constructor for RepoCommand
- *
- * @param repo
- * the {@link org.eclipse.jgit.lib.Repository}
- */
- public RepoCommand(Repository repo) {
- super(repo);
- }
- /**
- * Set path to the manifest XML file.
- * <p>
- * Calling {@link #setInputStream} will ignore the path set here.
- *
- * @param path
- * (with <code>/</code> as separator)
- * @return this command
- */
- public RepoCommand setPath(String path) {
- this.manifestPath = path;
- return this;
- }
- /**
- * Set the input stream to the manifest XML.
- * <p>
- * Setting inputStream will ignore the path set. It will be closed in
- * {@link #call}.
- *
- * @param inputStream a {@link java.io.InputStream} object.
- * @return this command
- * @since 3.5
- */
- public RepoCommand setInputStream(InputStream inputStream) {
- this.inputStream = inputStream;
- return this;
- }
- /**
- * Set base URI of the paths inside the XML. This is typically the name of
- * the directory holding the manifest repository, eg. for
- * https://android.googlesource.com/platform/manifest, this should be
- * /platform (if you would run this on android.googlesource.com) or
- * https://android.googlesource.com/platform elsewhere.
- *
- * @param uri
- * the base URI
- * @return this command
- */
- public RepoCommand setURI(String uri) {
- this.baseUri = uri;
- return this;
- }
- /**
- * Set the URI of the superproject (this repository), so the .gitmodules
- * file can specify the submodule URLs relative to the superproject.
- *
- * @param uri
- * the URI of the repository holding the superproject.
- * @return this command
- * @since 4.8
- */
- public RepoCommand setTargetURI(String uri) {
- // The repo name is interpreted as a directory, for example
- // Gerrit (http://gerrit.googlesource.com/gerrit) has a
- // .gitmodules referencing ../plugins/hooks, which is
- // on http://gerrit.googlesource.com/plugins/hooks,
- this.targetUri = URI.create(uri + "/"); //$NON-NLS-1$
- return this;
- }
- /**
- * Set groups to sync
- *
- * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
- * @return this command
- */
- public RepoCommand setGroups(String groups) {
- this.groupsParam = groups;
- return this;
- }
- /**
- * Set default branch.
- * <p>
- * This is generally the name of the branch the manifest file was in. If
- * there's no default revision (branch) specified in manifest and no
- * revision specified in project, this branch will be used.
- *
- * @param branch
- * a branch name
- * @return this command
- */
- public RepoCommand setBranch(String branch) {
- this.branch = branch;
- return this;
- }
- /**
- * Set target branch.
- * <p>
- * This is the target branch of the super project to be updated. If not set,
- * default is HEAD.
- * <p>
- * For non-bare repositories, HEAD will always be used and this will be
- * ignored.
- *
- * @param branch
- * branch name
- * @return this command
- * @since 4.1
- */
- public RepoCommand setTargetBranch(String branch) {
- this.targetBranch = Constants.R_HEADS + branch;
- return this;
- }
- /**
- * Set whether the branch name should be recorded in .gitmodules.
- * <p>
- * Submodule entries in .gitmodules can include a "branch" field
- * to indicate what remote branch each submodule tracks.
- * <p>
- * That field is used by "git submodule update --remote" to update
- * to the tip of the tracked branch when asked and by Gerrit to
- * update the superproject when a change on that branch is merged.
- * <p>
- * Subprojects that request a specific commit or tag will not have
- * a branch name recorded.
- * <p>
- * Not implemented for non-bare repositories.
- *
- * @param enable Whether to record the branch name
- * @return this command
- * @since 4.2
- */
- public RepoCommand setRecordRemoteBranch(boolean enable) {
- this.bareWriterConfig.recordRemoteBranch = enable;
- return this;
- }
- /**
- * Set whether the labels field should be recorded as a label in
- * .gitattributes.
- * <p>
- * Not implemented for non-bare repositories.
- *
- * @param enable Whether to record the labels in the .gitattributes
- * @return this command
- * @since 4.4
- */
- public RepoCommand setRecordSubmoduleLabels(boolean enable) {
- this.bareWriterConfig.recordSubmoduleLabels = enable;
- return this;
- }
- /**
- * Set whether the clone-depth field should be recorded as a shallow
- * recommendation in .gitmodules.
- * <p>
- * Not implemented for non-bare repositories.
- *
- * @param enable Whether to record the shallow recommendation.
- * @return this command
- * @since 4.4
- */
- public RepoCommand setRecommendShallow(boolean enable) {
- this.bareWriterConfig.recordShallowSubmodules = enable;
- return this;
- }
- /**
- * The progress monitor associated with the clone operation. By default,
- * this is set to <code>NullProgressMonitor</code>
- *
- * @see org.eclipse.jgit.lib.NullProgressMonitor
- * @param monitor
- * a {@link org.eclipse.jgit.lib.ProgressMonitor}
- * @return this command
- */
- public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
- this.monitor = monitor;
- return this;
- }
- /**
- * Set whether to skip projects whose commits don't exist remotely.
- * <p>
- * When set to true, we'll just skip the manifest entry and continue
- * on to the next one.
- * <p>
- * When set to false (default), we'll throw an error when remote
- * failures occur.
- * <p>
- * Not implemented for non-bare repositories.
- *
- * @param ignore Whether to ignore the remote failures.
- * @return this command
- * @since 4.3
- */
- public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
- this.bareWriterConfig.ignoreRemoteFailures = ignore;
- return this;
- }
- /**
- * Set the author/committer for the bare repository commit.
- * <p>
- * For non-bare repositories, the current user will be used and this will be
- * ignored.
- *
- * @param author
- * the author's {@link org.eclipse.jgit.lib.PersonIdent}
- * @return this command
- */
- public RepoCommand setAuthor(PersonIdent author) {
- this.author = author;
- return this;
- }
- /**
- * Set the GetHeadFromUri callback.
- *
- * This is only used in bare repositories.
- *
- * @param callback
- * a {@link org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader}
- * object.
- * @return this command
- */
- public RepoCommand setRemoteReader(RemoteReader callback) {
- this.callback = callback;
- return this;
- }
- /**
- * Set the IncludedFileReader callback.
- *
- * @param reader
- * a
- * {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
- * object.
- * @return this command
- * @since 4.0
- */
- public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
- this.includedReader = reader;
- return this;
- }
- /**
- * Create a file with the given content in the destination repository
- *
- * @param path
- * where to create the file in the destination repository
- * @param contents
- * content for the create file
- * @return this command
- *
- * @since 6.1
- */
- public RepoCommand addToDestination(String path, String contents) {
- this.extraContents.add(new ExtraContent(path, contents));
- return this;
- }
- /** {@inheritDoc} */
- @Override
- public RevCommit call() throws GitAPIException {
- checkCallable();
- if (baseUri == null) {
- baseUri = ""; //$NON-NLS-1$
- }
- if (inputStream == null) {
- if (manifestPath == null || manifestPath.length() == 0)
- throw new IllegalArgumentException(
- JGitText.get().pathNotConfigured);
- try {
- inputStream = new FileInputStream(manifestPath);
- } catch (IOException e) {
- throw new IllegalArgumentException(
- JGitText.get().pathNotConfigured, e);
- }
- }
- List<RepoProject> filteredProjects;
- try {
- ManifestParser parser = new ManifestParser(includedReader,
- manifestPath, branch, baseUri, groupsParam, repo);
- parser.read(inputStream);
- filteredProjects = parser.getFilteredProjects();
- } catch (IOException e) {
- throw new ManifestErrorException(e);
- } finally {
- try {
- inputStream.close();
- } catch (IOException e) {
- // Just ignore it, it's not important.
- }
- }
- if (repo.isBare()) {
- List<RepoProject> renamedProjects = renameProjects(filteredProjects);
- BareSuperprojectWriter writer = new BareSuperprojectWriter(repo, targetUri,
- targetBranch,
- author == null ? new PersonIdent(repo) : author,
- callback == null ? new DefaultRemoteReader() : callback,
- bareWriterConfig, extraContents);
- return writer.write(renamedProjects);
- }
- RegularSuperprojectWriter writer = new RegularSuperprojectWriter(repo, monitor);
- return writer.write(filteredProjects);
- }
- /**
- * Rename the projects if there's a conflict when converted to submodules.
- *
- * @param projects
- * parsed projects
- * @return projects that are renamed if necessary
- */
- private List<RepoProject> renameProjects(List<RepoProject> projects) {
- Map<String, List<RepoProject>> m = new TreeMap<>();
- for (RepoProject proj : projects) {
- List<RepoProject> l = m.get(proj.getName());
- if (l == null) {
- l = new ArrayList<>();
- m.put(proj.getName(), l);
- }
- l.add(proj);
- }
- List<RepoProject> ret = new ArrayList<>();
- for (List<RepoProject> ps : m.values()) {
- boolean nameConflict = ps.size() != 1;
- for (RepoProject proj : ps) {
- String name = proj.getName();
- if (nameConflict) {
- name += SLASH + proj.getPath();
- }
- RepoProject p = new RepoProject(name,
- proj.getPath(), proj.getRevision(), null,
- proj.getGroups(), proj.getRecommendShallow());
- p.setUrl(proj.getUrl());
- p.addCopyFiles(proj.getCopyFiles());
- p.addLinkFiles(proj.getLinkFiles());
- ret.add(p);
- }
- }
- return ret;
- }
- /*
- * Assume we are document "a/b/index.html", what should we put in a href to get to "a/" ?
- * Returns the child if either base or child is not a bare path. This provides a missing feature in
- * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
- */
- private static final String SLASH = "/"; //$NON-NLS-1$
- static URI relativize(URI current, URI target) {
- if (!Objects.equals(current.getHost(), target.getHost())) {
- return target;
- }
- String cur = current.normalize().getPath();
- String dest = target.normalize().getPath();
- // TODO(hanwen): maybe (absolute, relative) should throw an exception.
- if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
- return target;
- }
- while (cur.startsWith(SLASH)) {
- cur = cur.substring(1);
- }
- while (dest.startsWith(SLASH)) {
- dest = dest.substring(1);
- }
- if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
- // Avoid having to special-casing in the next two ifs.
- String prefix = "prefix/"; //$NON-NLS-1$
- cur = prefix + cur;
- dest = prefix + dest;
- }
- if (!cur.endsWith(SLASH)) {
- // The current file doesn't matter.
- int lastSlash = cur.lastIndexOf('/');
- cur = cur.substring(0, lastSlash);
- }
- String destFile = ""; //$NON-NLS-1$
- if (!dest.endsWith(SLASH)) {
- // We always have to provide the destination file.
- int lastSlash = dest.lastIndexOf('/');
- destFile = dest.substring(lastSlash + 1, dest.length());
- dest = dest.substring(0, dest.lastIndexOf('/'));
- }
- String[] cs = cur.split(SLASH);
- String[] ds = dest.split(SLASH);
- int common = 0;
- while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
- common++;
- }
- StringJoiner j = new StringJoiner(SLASH);
- for (int i = common; i < cs.length; i++) {
- j.add(".."); //$NON-NLS-1$
- }
- for (int i = common; i < ds.length; i++) {
- j.add(ds[i]);
- }
- j.add(destFile);
- return URI.create(j.toString());
- }
- }