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 static java.nio.charset.StandardCharsets.UTF_8;
- import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
- import static org.eclipse.jgit.lib.Constants.R_REMOTES;
- 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.SubmoduleAddCommand;
- import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
- import org.eclipse.jgit.api.errors.GitAPIException;
- import org.eclipse.jgit.api.errors.InvalidRefNameException;
- import org.eclipse.jgit.api.errors.JGitInternalException;
- import org.eclipse.jgit.dircache.DirCache;
- import org.eclipse.jgit.dircache.DirCacheBuilder;
- import org.eclipse.jgit.dircache.DirCacheEntry;
- import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
- import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
- import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
- import org.eclipse.jgit.gitrepo.internal.RepoText;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.CommitBuilder;
- import org.eclipse.jgit.lib.Config;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.ObjectInserter;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.ProgressMonitor;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.RefDatabase;
- import org.eclipse.jgit.lib.RefUpdate;
- import org.eclipse.jgit.lib.RefUpdate.Result;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.revwalk.RevWalk;
- import org.eclipse.jgit.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 boolean recordRemoteBranch = true;
- private boolean recordSubmoduleLabels = true;
- private boolean recordShallowSubmodules = true;
- private PersonIdent author;
- private RemoteReader callback;
- private InputStream inputStream;
- private IncludedFileReader includedReader;
- private boolean ignoreRemoteFailures = false;
- private ProgressMonitor monitor;
- /**
- * 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")
- private static class ManifestErrorException extends GitAPIException {
- ManifestErrorException(Throwable cause) {
- super(RepoText.get().invalidManifest, cause);
- }
- }
- @SuppressWarnings("serial")
- private 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.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.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.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.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;
- }
- /** {@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()) {
- if (author == null)
- author = new PersonIdent(repo);
- if (callback == null)
- callback = new DefaultRemoteReader();
- List<RepoProject> renamedProjects = renameProjects(filteredProjects);
- DirCache index = DirCache.newInCore();
- DirCacheBuilder builder = index.builder();
- ObjectInserter inserter = repo.newObjectInserter();
- try (RevWalk rw = new RevWalk(repo)) {
- Config cfg = new Config();
- StringBuilder attributes = new StringBuilder();
- for (RepoProject proj : renamedProjects) {
- String name = proj.getName();
- String path = proj.getPath();
- String url = proj.getUrl();
- ObjectId objectId;
- if (ObjectId.isId(proj.getRevision())) {
- objectId = ObjectId.fromString(proj.getRevision());
- } else {
- objectId = callback.sha1(url, proj.getRevision());
- if (objectId == null && !ignoreRemoteFailures) {
- throw new RemoteUnavailableException(url);
- }
- if (recordRemoteBranch) {
- // can be branch or tag
- cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
- proj.getRevision());
- }
- if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
- // The shallow recommendation is losing information.
- // As the repo manifests stores the recommended
- // depth in the 'clone-depth' field, while
- // git core only uses a binary 'shallow = true/false'
- // hint, we'll map any depth to 'shallow = true'
- cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
- true);
- }
- }
- if (recordSubmoduleLabels) {
- StringBuilder rec = new StringBuilder();
- rec.append("/"); //$NON-NLS-1$
- rec.append(path);
- for (String group : proj.getGroups()) {
- rec.append(" "); //$NON-NLS-1$
- rec.append(group);
- }
- rec.append("\n"); //$NON-NLS-1$
- attributes.append(rec.toString());
- }
- URI submodUrl = URI.create(url);
- if (targetUri != null) {
- submodUrl = relativize(targetUri, submodUrl);
- }
- cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
- cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
- submodUrl.toString());
- // create gitlink
- if (objectId != null) {
- DirCacheEntry dcEntry = new DirCacheEntry(path);
- dcEntry.setObjectId(objectId);
- dcEntry.setFileMode(FileMode.GITLINK);
- builder.add(dcEntry);
- for (CopyFile copyfile : proj.getCopyFiles()) {
- RemoteFile rf = callback.readFileWithMode(
- url, proj.getRevision(), copyfile.src);
- objectId = inserter.insert(Constants.OBJ_BLOB,
- rf.getContents());
- dcEntry = new DirCacheEntry(copyfile.dest);
- dcEntry.setObjectId(objectId);
- dcEntry.setFileMode(rf.getFileMode());
- builder.add(dcEntry);
- }
- for (LinkFile linkfile : proj.getLinkFiles()) {
- String link;
- if (linkfile.dest.contains("/")) { //$NON-NLS-1$
- link = FileUtils.relativizeGitPath(
- linkfile.dest.substring(0,
- linkfile.dest.lastIndexOf('/')),
- proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
- } else {
- link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
- }
- objectId = inserter.insert(Constants.OBJ_BLOB,
- link.getBytes(UTF_8));
- dcEntry = new DirCacheEntry(linkfile.dest);
- dcEntry.setObjectId(objectId);
- dcEntry.setFileMode(FileMode.SYMLINK);
- builder.add(dcEntry);
- }
- }
- }
- String content = cfg.toText();
- // create a new DirCacheEntry for .gitmodules file.
- final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
- ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
- content.getBytes(UTF_8));
- dcEntry.setObjectId(objectId);
- dcEntry.setFileMode(FileMode.REGULAR_FILE);
- builder.add(dcEntry);
- if (recordSubmoduleLabels) {
- // create a new DirCacheEntry for .gitattributes file.
- final DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
- ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
- attributes.toString().getBytes(UTF_8));
- dcEntryAttr.setObjectId(attrId);
- dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
- builder.add(dcEntryAttr);
- }
- builder.finish();
- ObjectId treeId = index.writeTree(inserter);
- // Create a Commit object, populate it and write it
- ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
- if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
- // No change. Do nothing.
- return rw.parseCommit(headId);
- }
- CommitBuilder commit = new CommitBuilder();
- commit.setTreeId(treeId);
- if (headId != null)
- commit.setParentIds(headId);
- commit.setAuthor(author);
- commit.setCommitter(author);
- commit.setMessage(RepoText.get().repoCommitMessage);
- ObjectId commitId = inserter.insert(commit);
- inserter.flush();
- RefUpdate ru = repo.updateRef(targetBranch);
- ru.setNewObjectId(commitId);
- ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
- Result rc = ru.update(rw);
- switch (rc) {
- case NEW:
- case FORCED:
- case FAST_FORWARD:
- // Successful. Do nothing.
- break;
- case REJECTED:
- case LOCK_FAILURE:
- throw new ConcurrentRefUpdateException(
- MessageFormat.format(
- JGitText.get().cannotLock, targetBranch),
- ru.getRef(),
- rc);
- default:
- throw new JGitInternalException(MessageFormat.format(
- JGitText.get().updatingRefFailed,
- targetBranch, commitId.name(), rc));
- }
- return rw.parseCommit(commitId);
- } catch (GitAPIException | IOException e) {
- throw new ManifestErrorException(e);
- }
- }
- try (Git git = new Git(repo)) {
- for (RepoProject proj : filteredProjects) {
- addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(),
- proj.getRevision(), proj.getCopyFiles(),
- proj.getLinkFiles(), git);
- }
- return git.commit().setMessage(RepoText.get().repoCommitMessage)
- .call();
- } catch (GitAPIException | IOException e) {
- throw new ManifestErrorException(e);
- }
- }
- private void addSubmodule(String name, String url, String path,
- String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
- Git git) throws GitAPIException, IOException {
- assert (!repo.isBare());
- assert (git != null);
- if (!linkfiles.isEmpty()) {
- throw new UnsupportedOperationException(
- JGitText.get().nonBareLinkFilesNotSupported);
- }
- SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path)
- .setURI(url);
- if (monitor != null)
- add.setProgressMonitor(monitor);
- Repository subRepo = add.call();
- if (revision != null) {
- try (Git sub = new Git(subRepo)) {
- sub.checkout().setName(findRef(revision, subRepo)).call();
- }
- subRepo.close();
- git.add().addFilepattern(path).call();
- }
- for (CopyFile copyfile : copyfiles) {
- copyfile.copy();
- git.add().addFilepattern(copyfile.dest).call();
- }
- }
- /**
- * 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());
- }
- private static String findRef(String ref, Repository repo)
- throws IOException {
- if (!ObjectId.isId(ref)) {
- Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
- if (r != null)
- return r.getName();
- }
- return ref;
- }
- }