SubmoduleWalk.java
- /*
- * Copyright (C) 2011, GitHub Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.submodule;
- import java.io.File;
- import java.io.IOException;
- import java.text.MessageFormat;
- import java.util.HashMap;
- import java.util.Map;
- import org.eclipse.jgit.dircache.DirCache;
- import org.eclipse.jgit.dircache.DirCacheIterator;
- import org.eclipse.jgit.errors.ConfigInvalidException;
- import org.eclipse.jgit.errors.CorruptObjectException;
- import org.eclipse.jgit.errors.IncorrectObjectTypeException;
- import org.eclipse.jgit.errors.MissingObjectException;
- import org.eclipse.jgit.errors.RepositoryNotFoundException;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.AnyObjectId;
- import org.eclipse.jgit.lib.BaseRepositoryBuilder;
- import org.eclipse.jgit.lib.BlobBasedConfig;
- import org.eclipse.jgit.lib.Config;
- import org.eclipse.jgit.lib.ConfigConstants;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.lib.RepositoryBuilder;
- import org.eclipse.jgit.lib.RepositoryBuilderFactory;
- import org.eclipse.jgit.lib.StoredConfig;
- import org.eclipse.jgit.storage.file.FileBasedConfig;
- import org.eclipse.jgit.treewalk.AbstractTreeIterator;
- import org.eclipse.jgit.treewalk.CanonicalTreeParser;
- import org.eclipse.jgit.treewalk.TreeWalk;
- import org.eclipse.jgit.treewalk.filter.PathFilter;
- import org.eclipse.jgit.treewalk.filter.TreeFilter;
- import org.eclipse.jgit.util.FS;
- /**
- * Walker that visits all submodule entries found in a tree
- */
- public class SubmoduleWalk implements AutoCloseable {
- /**
- * The values for the config parameter submodule.<name>.ignore
- *
- * @since 3.6
- */
- public enum IgnoreSubmoduleMode {
- /**
- * Ignore all modifications to submodules
- */
- ALL,
- /**
- * Ignore changes to the working tree of a submodule
- */
- DIRTY,
- /**
- * Ignore changes to untracked files in the working tree of a submodule
- */
- UNTRACKED,
- /**
- * Ignore nothing. That's the default
- */
- NONE;
- }
- /**
- * Create a generator to walk over the submodule entries currently in the
- * index
- *
- * The {@code .gitmodules} file is read from the index.
- *
- * @param repository
- * a {@link org.eclipse.jgit.lib.Repository} object.
- * @return generator over submodule index entries. The caller is responsible
- * for calling {@link #close()}.
- * @throws java.io.IOException
- */
- public static SubmoduleWalk forIndex(Repository repository)
- throws IOException {
- @SuppressWarnings("resource") // The caller closes it
- SubmoduleWalk generator = new SubmoduleWalk(repository);
- try {
- DirCache index = repository.readDirCache();
- generator.setTree(new DirCacheIterator(index));
- } catch (IOException e) {
- generator.close();
- throw e;
- }
- return generator;
- }
- /**
- * Create a generator and advance it to the submodule entry at the given
- * path
- *
- * @param repository
- * a {@link org.eclipse.jgit.lib.Repository} object.
- * @param treeId
- * the root of a tree containing both a submodule at the given
- * path and .gitmodules at the root.
- * @param path
- * a {@link java.lang.String} object.
- * @return generator at given path. The caller is responsible for calling
- * {@link #close()}. Null if no submodule at given path.
- * @throws java.io.IOException
- */
- public static SubmoduleWalk forPath(Repository repository,
- AnyObjectId treeId, String path) throws IOException {
- SubmoduleWalk generator = new SubmoduleWalk(repository);
- try {
- generator.setTree(treeId);
- PathFilter filter = PathFilter.create(path);
- generator.setFilter(filter);
- generator.setRootTree(treeId);
- while (generator.next())
- if (filter.isDone(generator.walk))
- return generator;
- } catch (IOException e) {
- generator.close();
- throw e;
- }
- generator.close();
- return null;
- }
- /**
- * Create a generator and advance it to the submodule entry at the given
- * path
- *
- * @param repository
- * a {@link org.eclipse.jgit.lib.Repository} object.
- * @param iterator
- * the root of a tree containing both a submodule at the given
- * path and .gitmodules at the root.
- * @param path
- * a {@link java.lang.String} object.
- * @return generator at given path. The caller is responsible for calling
- * {@link #close()}. Null if no submodule at given path.
- * @throws java.io.IOException
- */
- public static SubmoduleWalk forPath(Repository repository,
- AbstractTreeIterator iterator, String path) throws IOException {
- SubmoduleWalk generator = new SubmoduleWalk(repository);
- try {
- generator.setTree(iterator);
- PathFilter filter = PathFilter.create(path);
- generator.setFilter(filter);
- generator.setRootTree(iterator);
- while (generator.next())
- if (filter.isDone(generator.walk))
- return generator;
- } catch (IOException e) {
- generator.close();
- throw e;
- }
- generator.close();
- return null;
- }
- /**
- * Get submodule directory
- *
- * @param parent
- * the {@link org.eclipse.jgit.lib.Repository}.
- * @param path
- * submodule path
- * @return directory
- */
- public static File getSubmoduleDirectory(final Repository parent,
- final String path) {
- return new File(parent.getWorkTree(), path);
- }
- /**
- * Get submodule repository
- *
- * @param parent
- * the {@link org.eclipse.jgit.lib.Repository}.
- * @param path
- * submodule path
- * @return repository or null if repository doesn't exist
- * @throws java.io.IOException
- */
- public static Repository getSubmoduleRepository(final Repository parent,
- final String path) throws IOException {
- return getSubmoduleRepository(parent.getWorkTree(), path,
- parent.getFS());
- }
- /**
- * Get submodule repository at path
- *
- * @param parent
- * the parent
- * @param path
- * submodule path
- * @return repository or null if repository doesn't exist
- * @throws java.io.IOException
- */
- public static Repository getSubmoduleRepository(final File parent,
- final String path) throws IOException {
- return getSubmoduleRepository(parent, path, FS.DETECTED);
- }
- /**
- * Get submodule repository at path, using the specified file system
- * abstraction
- *
- * @param parent
- * @param path
- * @param fs
- * the file system abstraction to be used
- * @return repository or null if repository doesn't exist
- * @throws IOException
- * @since 4.10
- */
- public static Repository getSubmoduleRepository(final File parent,
- final String path, FS fs) throws IOException {
- return getSubmoduleRepository(parent, path, fs,
- new RepositoryBuilder());
- }
- /**
- * Get submodule repository at path, using the specified file system
- * abstraction and the specified builder
- *
- * @param parent
- * {@link Repository} that contains the submodule
- * @param path
- * of the working tree of the submodule
- * @param fs
- * {@link FS} to use
- * @param builder
- * {@link BaseRepositoryBuilder} to use to build the submodule
- * repository
- * @return the {@link Repository} of the submodule, or {@code null} if it
- * doesn't exist
- * @throws IOException
- * on errors
- * @since 5.6
- */
- public static Repository getSubmoduleRepository(File parent, String path,
- FS fs, BaseRepositoryBuilder<?, ? extends Repository> builder)
- throws IOException {
- File subWorkTree = new File(parent, path);
- if (!subWorkTree.isDirectory()) {
- return null;
- }
- try {
- return builder //
- .setMustExist(true) //
- .setFS(fs) //
- .setWorkTree(subWorkTree) //
- .build();
- } catch (RepositoryNotFoundException e) {
- return null;
- }
- }
- /**
- * Resolve submodule repository URL.
- * <p>
- * This handles relative URLs that are typically specified in the
- * '.gitmodules' file by resolving them against the remote URL of the parent
- * repository.
- * <p>
- * Relative URLs will be resolved against the parent repository's working
- * directory if the parent repository has no configured remote URL.
- *
- * @param parent
- * parent repository
- * @param url
- * absolute or relative URL of the submodule repository
- * @return resolved URL
- * @throws java.io.IOException
- */
- public static String getSubmoduleRemoteUrl(final Repository parent,
- final String url) throws IOException {
- if (!url.startsWith("./") && !url.startsWith("../")) //$NON-NLS-1$ //$NON-NLS-2$
- return url;
- String remoteName = null;
- // Look up remote URL associated wit HEAD ref
- Ref ref = parent.exactRef(Constants.HEAD);
- if (ref != null) {
- if (ref.isSymbolic())
- ref = ref.getLeaf();
- remoteName = parent.getConfig().getString(
- ConfigConstants.CONFIG_BRANCH_SECTION,
- Repository.shortenRefName(ref.getName()),
- ConfigConstants.CONFIG_KEY_REMOTE);
- }
- // Fall back to 'origin' if current HEAD ref has no remote URL
- if (remoteName == null)
- remoteName = Constants.DEFAULT_REMOTE_NAME;
- String remoteUrl = parent.getConfig().getString(
- ConfigConstants.CONFIG_REMOTE_SECTION, remoteName,
- ConfigConstants.CONFIG_KEY_URL);
- // Fall back to parent repository's working directory if no remote URL
- if (remoteUrl == null) {
- remoteUrl = parent.getWorkTree().getAbsolutePath();
- // Normalize slashes to '/'
- if ('\\' == File.separatorChar)
- remoteUrl = remoteUrl.replace('\\', '/');
- }
- // Remove trailing '/'
- if (remoteUrl.charAt(remoteUrl.length() - 1) == '/')
- remoteUrl = remoteUrl.substring(0, remoteUrl.length() - 1);
- char separator = '/';
- String submoduleUrl = url;
- while (submoduleUrl.length() > 0) {
- if (submoduleUrl.startsWith("./")) //$NON-NLS-1$
- submoduleUrl = submoduleUrl.substring(2);
- else if (submoduleUrl.startsWith("../")) { //$NON-NLS-1$
- int lastSeparator = remoteUrl.lastIndexOf('/');
- if (lastSeparator < 1) {
- lastSeparator = remoteUrl.lastIndexOf(':');
- separator = ':';
- }
- if (lastSeparator < 1)
- throw new IOException(MessageFormat.format(
- JGitText.get().submoduleParentRemoteUrlInvalid,
- remoteUrl));
- remoteUrl = remoteUrl.substring(0, lastSeparator);
- submoduleUrl = submoduleUrl.substring(3);
- } else
- break;
- }
- return remoteUrl + separator + submoduleUrl;
- }
- private final Repository repository;
- private final TreeWalk walk;
- private StoredConfig repoConfig;
- private AbstractTreeIterator rootTree;
- private Config modulesConfig;
- private String path;
- private Map<String, String> pathToName;
- private RepositoryBuilderFactory factory;
- /**
- * Create submodule generator
- *
- * @param repository
- * the {@link org.eclipse.jgit.lib.Repository}.
- * @throws java.io.IOException
- */
- public SubmoduleWalk(Repository repository) throws IOException {
- this.repository = repository;
- repoConfig = repository.getConfig();
- walk = new TreeWalk(repository);
- walk.setRecursive(true);
- }
- /**
- * Set the config used by this walk.
- *
- * This method need only be called if constructing a walk manually instead of
- * with one of the static factory methods above.
- *
- * @param config
- * .gitmodules config object
- * @return this generator
- */
- public SubmoduleWalk setModulesConfig(Config config) {
- modulesConfig = config;
- loadPathNames();
- return this;
- }
- /**
- * Set the tree used by this walk for finding {@code .gitmodules}.
- * <p>
- * The root tree is not read until the first submodule is encountered by the
- * walk.
- * <p>
- * This method need only be called if constructing a walk manually instead of
- * with one of the static factory methods above.
- *
- * @param tree
- * tree containing .gitmodules
- * @return this generator
- */
- public SubmoduleWalk setRootTree(AbstractTreeIterator tree) {
- rootTree = tree;
- modulesConfig = null;
- pathToName = null;
- return this;
- }
- /**
- * Set the tree used by this walk for finding {@code .gitmodules}.
- * <p>
- * The root tree is not read until the first submodule is encountered by the
- * walk.
- * <p>
- * This method need only be called if constructing a walk manually instead of
- * with one of the static factory methods above.
- *
- * @param id
- * ID of a tree containing .gitmodules
- * @return this generator
- * @throws java.io.IOException
- */
- public SubmoduleWalk setRootTree(AnyObjectId id) throws IOException {
- final CanonicalTreeParser p = new CanonicalTreeParser();
- p.reset(walk.getObjectReader(), id);
- rootTree = p;
- modulesConfig = null;
- pathToName = null;
- return this;
- }
- /**
- * Load the config for this walk from {@code .gitmodules}.
- * <p>
- * Uses the root tree if {@link #setRootTree(AbstractTreeIterator)} was
- * previously called, otherwise uses the working tree.
- * <p>
- * If no submodule config is found, loads an empty config.
- *
- * @return this generator
- * @throws java.io.IOException
- * if an error occurred, or if the repository is bare
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- */
- public SubmoduleWalk loadModulesConfig() throws IOException, ConfigInvalidException {
- if (rootTree == null) {
- File modulesFile = new File(repository.getWorkTree(),
- Constants.DOT_GIT_MODULES);
- FileBasedConfig config = new FileBasedConfig(modulesFile,
- repository.getFS());
- config.load();
- modulesConfig = config;
- loadPathNames();
- } else {
- try (TreeWalk configWalk = new TreeWalk(repository)) {
- configWalk.addTree(rootTree);
- // The root tree may be part of the submodule walk, so we need to revert
- // it after this walk.
- int idx;
- for (idx = 0; !rootTree.first(); idx++) {
- rootTree.back(1);
- }
- try {
- configWalk.setRecursive(false);
- PathFilter filter = PathFilter.create(Constants.DOT_GIT_MODULES);
- configWalk.setFilter(filter);
- while (configWalk.next()) {
- if (filter.isDone(configWalk)) {
- modulesConfig = new BlobBasedConfig(null, repository,
- configWalk.getObjectId(0));
- loadPathNames();
- return this;
- }
- }
- modulesConfig = new Config();
- pathToName = null;
- } finally {
- if (idx > 0)
- rootTree.next(idx);
- }
- }
- }
- return this;
- }
- private void loadPathNames() {
- pathToName = null;
- if (modulesConfig != null) {
- HashMap<String, String> pathNames = new HashMap<>();
- for (String name : modulesConfig
- .getSubsections(ConfigConstants.CONFIG_SUBMODULE_SECTION)) {
- pathNames.put(modulesConfig.getString(
- ConfigConstants.CONFIG_SUBMODULE_SECTION, name,
- ConfigConstants.CONFIG_KEY_PATH), name);
- }
- pathToName = pathNames;
- }
- }
- /**
- * Checks whether the working tree contains a .gitmodules file. That's a
- * hint that the repo contains submodules.
- *
- * @param repository
- * the repository to check
- * @return <code>true</code> if the working tree contains a .gitmodules file,
- * <code>false</code> otherwise. Always returns <code>false</code>
- * for bare repositories.
- * @throws java.io.IOException
- * @throws CorruptObjectException if any.
- * @since 3.6
- */
- public static boolean containsGitModulesFile(Repository repository)
- throws IOException {
- if (repository.isBare()) {
- return false;
- }
- File modulesFile = new File(repository.getWorkTree(),
- Constants.DOT_GIT_MODULES);
- return (modulesFile.exists());
- }
- private void lazyLoadModulesConfig() throws IOException, ConfigInvalidException {
- if (modulesConfig == null) {
- loadModulesConfig();
- }
- }
- private String getModuleName(String modulePath) {
- String name = pathToName != null ? pathToName.get(modulePath) : null;
- return name != null ? name : modulePath;
- }
- /**
- * Set tree filter
- *
- * @param filter
- * a {@link org.eclipse.jgit.treewalk.filter.TreeFilter} object.
- * @return this generator
- */
- public SubmoduleWalk setFilter(TreeFilter filter) {
- walk.setFilter(filter);
- return this;
- }
- /**
- * Set the tree iterator used for finding submodule entries
- *
- * @param iterator
- * an {@link org.eclipse.jgit.treewalk.AbstractTreeIterator}
- * object.
- * @return this generator
- * @throws org.eclipse.jgit.errors.CorruptObjectException
- */
- public SubmoduleWalk setTree(AbstractTreeIterator iterator)
- throws CorruptObjectException {
- walk.addTree(iterator);
- return this;
- }
- /**
- * Set the tree used for finding submodule entries
- *
- * @param treeId
- * an {@link org.eclipse.jgit.lib.AnyObjectId} object.
- * @return this generator
- * @throws java.io.IOException
- * @throws IncorrectObjectTypeException
- * if any.
- * @throws MissingObjectException
- * if any.
- */
- public SubmoduleWalk setTree(AnyObjectId treeId) throws IOException {
- walk.addTree(treeId);
- return this;
- }
- /**
- * Reset generator and start new submodule walk
- *
- * @return this generator
- */
- public SubmoduleWalk reset() {
- repoConfig = repository.getConfig();
- modulesConfig = null;
- pathToName = null;
- walk.reset();
- return this;
- }
- /**
- * Get directory that will be the root of the submodule's local repository
- *
- * @return submodule repository directory
- */
- public File getDirectory() {
- return getSubmoduleDirectory(repository, path);
- }
- /**
- * Advance to next submodule in the index tree.
- *
- * The object id and path of the next entry can be obtained by calling
- * {@link #getObjectId()} and {@link #getPath()}.
- *
- * @return true if entry found, false otherwise
- * @throws java.io.IOException
- */
- public boolean next() throws IOException {
- while (walk.next()) {
- if (FileMode.GITLINK != walk.getFileMode(0))
- continue;
- path = walk.getPathString();
- return true;
- }
- path = null;
- return false;
- }
- /**
- * Get path of current submodule entry
- *
- * @return path
- */
- public String getPath() {
- return path;
- }
- /**
- * Sets the {@link RepositoryBuilderFactory} to use for creating submodule
- * repositories. If none is set, a plain {@link RepositoryBuilder} is used.
- *
- * @param factory
- * to set
- * @since 5.6
- */
- public void setBuilderFactory(RepositoryBuilderFactory factory) {
- this.factory = factory;
- }
- private BaseRepositoryBuilder<?, ? extends Repository> getBuilder() {
- return factory != null ? factory.get() : new RepositoryBuilder();
- }
- /**
- * The module name for the current submodule entry (used for the section
- * name of .git/config)
- *
- * @since 4.10
- * @return name
- * @throws ConfigInvalidException
- * @throws IOException
- */
- public String getModuleName() throws IOException, ConfigInvalidException {
- lazyLoadModulesConfig();
- return getModuleName(path);
- }
- /**
- * Get object id of current submodule entry
- *
- * @return object id
- */
- public ObjectId getObjectId() {
- return walk.getObjectId(0);
- }
- /**
- * Get the configured path for current entry. This will be the value from
- * the .gitmodules file in the current repository's working tree.
- *
- * @return configured path
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- */
- public String getModulesPath() throws IOException, ConfigInvalidException {
- lazyLoadModulesConfig();
- return modulesConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
- getModuleName(), ConfigConstants.CONFIG_KEY_PATH);
- }
- /**
- * Get the configured remote URL for current entry. This will be the value
- * from the repository's config.
- *
- * @return configured URL
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- */
- public String getConfigUrl() throws IOException, ConfigInvalidException {
- return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
- getModuleName(), ConfigConstants.CONFIG_KEY_URL);
- }
- /**
- * Get the configured remote URL for current entry. This will be the value
- * from the .gitmodules file in the current repository's working tree.
- *
- * @return configured URL
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- */
- public String getModulesUrl() throws IOException, ConfigInvalidException {
- lazyLoadModulesConfig();
- return modulesConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
- getModuleName(), ConfigConstants.CONFIG_KEY_URL);
- }
- /**
- * Get the configured update field for current entry. This will be the value
- * from the repository's config.
- *
- * @return update value
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- */
- public String getConfigUpdate() throws IOException, ConfigInvalidException {
- return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
- getModuleName(), ConfigConstants.CONFIG_KEY_UPDATE);
- }
- /**
- * Get the configured update field for current entry. This will be the value
- * from the .gitmodules file in the current repository's working tree.
- *
- * @return update value
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- */
- public String getModulesUpdate() throws IOException, ConfigInvalidException {
- lazyLoadModulesConfig();
- return modulesConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
- getModuleName(), ConfigConstants.CONFIG_KEY_UPDATE);
- }
- /**
- * Get the configured ignore field for the current entry. This will be the
- * value from the .gitmodules file in the current repository's working tree.
- *
- * @return ignore value
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- * @throws java.io.IOException
- * @since 3.6
- */
- public IgnoreSubmoduleMode getModulesIgnore() throws IOException,
- ConfigInvalidException {
- IgnoreSubmoduleMode mode = repoConfig.getEnum(
- IgnoreSubmoduleMode.values(),
- ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
- ConfigConstants.CONFIG_KEY_IGNORE, null);
- if (mode != null) {
- return mode;
- }
- lazyLoadModulesConfig();
- return modulesConfig.getEnum(IgnoreSubmoduleMode.values(),
- ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(),
- ConfigConstants.CONFIG_KEY_IGNORE, IgnoreSubmoduleMode.NONE);
- }
- /**
- * Get repository for current submodule entry
- *
- * @return repository or null if non-existent
- * @throws java.io.IOException
- */
- public Repository getRepository() throws IOException {
- return getSubmoduleRepository(repository.getWorkTree(), path,
- repository.getFS(), getBuilder());
- }
- /**
- * Get commit id that HEAD points to in the current submodule's repository
- *
- * @return object id of HEAD reference
- * @throws java.io.IOException
- */
- public ObjectId getHead() throws IOException {
- try (Repository subRepo = getRepository()) {
- if (subRepo == null) {
- return null;
- }
- return subRepo.resolve(Constants.HEAD);
- }
- }
- /**
- * Get ref that HEAD points to in the current submodule's repository
- *
- * @return ref name, null on failures
- * @throws java.io.IOException
- */
- public String getHeadRef() throws IOException {
- try (Repository subRepo = getRepository()) {
- if (subRepo == null) {
- return null;
- }
- Ref head = subRepo.exactRef(Constants.HEAD);
- return head != null ? head.getLeaf().getName() : null;
- }
- }
- /**
- * Get the resolved remote URL for the current submodule.
- * <p>
- * This method resolves the value of {@link #getModulesUrl()} to an absolute
- * URL
- *
- * @return resolved remote URL
- * @throws java.io.IOException
- * @throws org.eclipse.jgit.errors.ConfigInvalidException
- */
- public String getRemoteUrl() throws IOException, ConfigInvalidException {
- String url = getModulesUrl();
- return url != null ? getSubmoduleRemoteUrl(repository, url) : null;
- }
- /**
- * {@inheritDoc}
- * <p>
- * Release any resources used by this walker's reader.
- *
- * @since 4.0
- */
- @Override
- public void close() {
- walk.close();
- }
- }