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