| /* |
| * 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 <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); |
| } |
| } |
| |
| 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. |
| * |
| * @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; |
| } |
| } |