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();
	}
}