/*
 * Copyright (C) 2014, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
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 Also:
Since:3.4
/** * 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 * &quot;foo&quot; and another project's manifestPath is &quot;foo/bar&quot;. This won't * work in git submodule, so we'll skip all the sub projects * (&quot;foo/bar&quot; 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 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
/** * 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.
Params:
  • uri – The URI of the remote repository
  • 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.
Throws:
Returns:the sha1 of the remote repository, or null if the ref does not exist.
/** * 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.
Params:
  • uri – The URI of the remote repository
  • ref – The ref (branch/tag/etc.) to read
  • path – The relative path (inside the repo) to the file to read
Throws:
Returns:the file content.
Since:3.5
Deprecated:Use readFileWithMode(String, String, String) instead
/** * 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.
Params:
  • uri – The URI of the remote repository
  • 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.
  • path – The relative path (inside the repo) to the file to read
Throws:
  • GitAPIException – If the ref have an invalid or ambiguous name, or it does not exist in the repository,
  • IOException – If the object does not exist or is too large
Returns:The contents and file mode of the file in the given repository and branch. Never null.
Since:5.2
/** * 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
/** * 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;
Params:
  • contents – Raw contents of the file.
  • fileMode – Git file mode for this file (e.g. executable or regular)
/** * @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).

Returns:Raw contents of the file. Do not modify it.
/** * 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; }
Returns:Git file mode for this file (e.g. executable or regular)
/** * @return Git file mode for this file (e.g. executable or regular) */
@NonNull public FileMode getFileMode() { return fileMode; } }
A default implementation of RemoteReader callback.
/** 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
Params:
/** * 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.

Calling setInputStream will ignore the path set here.

Params:
  • path – (with / as separator)
Returns:this command
/** * 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.

Setting inputStream will ignore the path set. It will be closed in call.

Params:
Returns:this command
Since:3.5
/** * 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.
Params:
  • uri – the base URI
Returns:this command
/** * 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.
Params:
  • uri – the URI of the repository holding the superproject.
Returns:this command
Since:4.8
/** * 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
Params:
  • groups – groups separated by comma, examples: default|all|G1,-G2,-G3
Returns:this command
/** * 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.

Params:
  • branch – a branch name
Returns:this command
/** * 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.

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.

Params:
  • branch – branch name
Returns:this command
Since:4.1
/** * 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.

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.

Params:
  • enable – Whether to record the branch name
Returns:this command
Since:4.2
/** * 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.

Not implemented for non-bare repositories.

Params:
  • enable – Whether to record the labels in the .gitattributes
Returns:this command
Since:4.4
/** * 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.

Not implemented for non-bare repositories.

Params:
  • enable – Whether to record the shallow recommendation.
Returns:this command
Since:4.4
/** * 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 NullProgressMonitor
Params:
See Also:
  • NullProgressMonitor
Returns:this command
/** * 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.

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.

Params:
  • ignore – Whether to ignore the remote failures.
Returns:this command
Since:4.3
/** * 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.

For non-bare repositories, the current user will be used and this will be ignored.

Params:
Returns:this command
/** * 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.
Params:
Returns:this command
/** * 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.
Params:
Returns:this command
Since:4.0
/** * 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}
/** {@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); } } 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); } } else { 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.
Params:
  • projects – parsed projects
Returns:projects that are renamed if necessary
/** * 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; } }