org.openrewrite.jgit.gitrepo.RepoCommand Maven / Gradle / Ivy
Show all versions of jgit Show documentation
/*
* 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.openrewrite.jgit.gitrepo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.openrewrite.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
import static org.openrewrite.jgit.lib.Constants.R_REMOTES;
import static org.openrewrite.jgit.lib.Constants.R_TAGS;
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.openrewrite.jgit.annotations.NonNull;
import org.openrewrite.jgit.annotations.Nullable;
import org.openrewrite.jgit.api.Git;
import org.openrewrite.jgit.api.GitCommand;
import org.openrewrite.jgit.api.SubmoduleAddCommand;
import org.openrewrite.jgit.api.errors.ConcurrentRefUpdateException;
import org.openrewrite.jgit.api.errors.GitAPIException;
import org.openrewrite.jgit.api.errors.InvalidRefNameException;
import org.openrewrite.jgit.api.errors.JGitInternalException;
import org.openrewrite.jgit.dircache.DirCache;
import org.openrewrite.jgit.dircache.DirCacheBuilder;
import org.openrewrite.jgit.dircache.DirCacheEntry;
import org.openrewrite.jgit.gitrepo.ManifestParser.IncludedFileReader;
import org.openrewrite.jgit.gitrepo.RepoProject.CopyFile;
import org.openrewrite.jgit.gitrepo.RepoProject.LinkFile;
import org.openrewrite.jgit.gitrepo.internal.RepoText;
import org.openrewrite.jgit.internal.JGitText;
import org.openrewrite.jgit.lib.CommitBuilder;
import org.openrewrite.jgit.lib.Config;
import org.openrewrite.jgit.lib.Constants;
import org.openrewrite.jgit.lib.FileMode;
import org.openrewrite.jgit.lib.ObjectId;
import org.openrewrite.jgit.lib.ObjectInserter;
import org.openrewrite.jgit.lib.PersonIdent;
import org.openrewrite.jgit.lib.ProgressMonitor;
import org.openrewrite.jgit.lib.Ref;
import org.openrewrite.jgit.lib.RefDatabase;
import org.openrewrite.jgit.lib.RefUpdate;
import org.openrewrite.jgit.lib.RefUpdate.Result;
import org.openrewrite.jgit.lib.Repository;
import org.openrewrite.jgit.revwalk.RevCommit;
import org.openrewrite.jgit.revwalk.RevWalk;
import org.openrewrite.jgit.treewalk.TreeWalk;
import org.openrewrite.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 git-repo project page
* @since 3.4
*/
public class RepoCommand extends GitCommand {
private static final int LOCK_FAILURE_MAX_RETRIES = 5;
// Retry exponentially with delays in this range
private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50;
private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000;
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.
*
* 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 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.openrewrite.jgit.lib.Repository}
*/
public RepoCommand(Repository repo) {
super(repo);
}
/**
* Set path to the manifest XML file.
*
* Calling {@link #setInputStream} will ignore the path set here.
*
* @param path
* (with /
as separator)
* @return this command
*/
public RepoCommand setPath(String path) {
this.manifestPath = path;
return this;
}
/**
* Set the input stream to the manifest XML.
*
* 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.
*
* 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.
*
* This is the target branch of the super project to be updated. If not set,
* default is HEAD.
*
* 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.
*
* Submodule entries in .gitmodules can include a "branch" field
* to indicate what remote branch each submodule tracks.
*
* 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.
*
* Subprojects that request a specific commit or tag will not have
* a branch name recorded.
*
* 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.
*
* 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.
*
* 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 NullProgressMonitor
*
* @see org.openrewrite.jgit.lib.NullProgressMonitor
* @param monitor
* a {@link org.openrewrite.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.
*
* When set to true, we'll just skip the manifest entry and continue
* on to the next one.
*
* When set to false (default), we'll throw an error when remote
* failures occur.
*
* 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.
*
* For non-bare repositories, the current user will be used and this will be
* ignored.
*
* @param author
* the author's {@link org.openrewrite.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.openrewrite.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.openrewrite.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 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 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) {
// "branch" field is only for non-tag references.
// Keep tags in "ref" field as hint for other tools.
String field = proj.getRevision().startsWith(
R_TAGS) ? "ref" : "branch"; //$NON-NLS-1$ //$NON-NLS-2$
cfg.setString("submodule", name, field, //$NON-NLS-1$
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);
long prevDelay = 0;
for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) {
try {
return commitTreeOnCurrentTip(
inserter, rw, treeId);
} catch (ConcurrentRefUpdateException e) {
prevDelay = FileUtils.delay(prevDelay,
LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS,
LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS);
Thread.sleep(prevDelay);
repo.getRefDatabase().refresh();
}
}
// In the last try, just propagate the exceptions
return commitTreeOnCurrentTip(inserter, rw, treeId);
} catch (GitAPIException | IOException | InterruptedException 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 RevCommit commitTreeOnCurrentTip(ObjectInserter inserter,
RevWalk rw, ObjectId treeId)
throws IOException, ConcurrentRefUpdateException {
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);
}
private void addSubmodule(String name, String url, String path,
String revision, List copyfiles, List 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 renameProjects(List projects) {
Map> m = new TreeMap<>();
for (RepoProject proj : projects) {
List l = m.get(proj.getName());
if (l == null) {
l = new ArrayList<>();
m.put(proj.getName(), l);
}
l.add(proj);
}
List ret = new ArrayList<>();
for (List 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;
}
}