All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.jgit.gitrepo.RepoCommand Maven / Gradle / Ivy

The newest version!
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy