Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

https://github.com/git-commit-id/git-commit-id-maven-plugin/issues/701: Fix an issue with submodules #106

Merged
merged 6 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions src/main/java/pl/project13/core/GitCommitIdPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import pl.project13.core.git.GitDescribeConfig;
import pl.project13.core.log.LogInterface;
import pl.project13.core.util.BuildFileChangeListener;
import pl.project13.core.util.GitDirLocator;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -279,6 +280,8 @@ default Map<String, String> getSystemEnv() {
Charset getPropertiesSourceCharset();

boolean shouldPropertiesEscapeUnicode();

boolean shouldFailOnNoGitDirectory();
}

protected static final Pattern allowedCharactersForEvaluateOnCommit = Pattern.compile("[a-zA-Z0-9\\_\\-\\^\\/\\.]+");
Expand Down Expand Up @@ -341,16 +344,31 @@ protected static void loadGitData(@Nonnull Callback cb, @Nonnull Properties prop
throw new GitCommitIdExecutionException("suspicious argument for evaluateOnCommit, aborting execution!");
}

File dotGitDirectory = new GitDirLocator(
cb.getProjectBaseDir(),
cb.useNativeGit(),
cb.shouldFailOnNoGitDirectory()
).lookupGitDirectory(cb.getDotGitDirectory());
if (dotGitDirectory != null) {
cb.getLogInterface().info("dotGitDirectory '" + dotGitDirectory.getAbsolutePath() + "'");
} else {
cb.getLogInterface().info("dotGitDirectory is null, aborting execution!");
return;
}

if (cb.useNativeGit()) {
loadGitDataWithNativeGit(cb, properties);
loadGitDataWithNativeGit(cb, dotGitDirectory, properties);
} else {
loadGitDataWithJGit(cb, properties);
loadGitDataWithJGit(cb, dotGitDirectory, properties);
}
}

private static void loadGitDataWithNativeGit(@Nonnull Callback cb, @Nonnull Properties properties) throws GitCommitIdExecutionException {
private static void loadGitDataWithNativeGit(
@Nonnull Callback cb,
@Nonnull File dotGitDirectory,
@Nonnull Properties properties) throws GitCommitIdExecutionException {
GitDataProvider nativeGitProvider = NativeGitProvider
.on(cb.getDotGitDirectory().getParentFile(), cb.getNativeGitTimeoutInMs(), cb.getLogInterface())
.on(dotGitDirectory, cb.getNativeGitTimeoutInMs(), cb.getLogInterface())
.setPrefixDot(cb.getPrefixDot())
.setAbbrevLength(cb.getAbbrevLength())
.setDateFormat(cb.getDateFormat())
Expand All @@ -365,9 +383,12 @@ private static void loadGitDataWithNativeGit(@Nonnull Callback cb, @Nonnull Prop
nativeGitProvider.loadGitData(cb.getEvaluateOnCommit(), cb.getSystemEnv(), properties);
}

private static void loadGitDataWithJGit(@Nonnull Callback cb, @Nonnull Properties properties) throws GitCommitIdExecutionException {
private static void loadGitDataWithJGit(
@Nonnull Callback cb,
@Nonnull File dotGitDirectory,
@Nonnull Properties properties) throws GitCommitIdExecutionException {
GitDataProvider jGitProvider = JGitProvider
.on(cb.getDotGitDirectory(), cb.getLogInterface())
.on(dotGitDirectory, cb.getLogInterface())
.setPrefixDot(cb.getPrefixDot())
.setAbbrevLength(cb.getAbbrevLength())
.setDateFormat(cb.getDateFormat())
Expand Down
248 changes: 248 additions & 0 deletions src/main/java/pl/project13/core/util/GitDirLocator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
* This file is part of git-commit-id-plugin-core by Konrad 'ktoso' Malawski <[email protected]>
*
* git-commit-id-plugin-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* git-commit-id-plugin-core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with git-commit-id-plugin-core. If not, see <http://www.gnu.org/licenses/>.
*/

package pl.project13.core.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Path;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.eclipse.jgit.lib.Constants;
import pl.project13.core.GitCommitIdExecutionException;

/**
* This class encapsulates logic to locate a valid .git directory of the currently used project. If
* it's not already specified, this logic will try to find it.
*/
public class GitDirLocator {
final File projectBasedir;
final boolean useNativeGit;
final boolean shouldFailOnNoGitDirectory;

/**
* Constructor to encapsulates all references required to locate a valid .git directory
*
* @param projectBasedir The project basedir that will be used as last resort to search
* the parent project hierarchy until a .git directory is found.
* @param useNativeGit Boolean that indicates if we use the native git implementation or the
* jGit Implementation. For the native git we usually need to
* use the parent "git"-Folder, as git can not run commands
* in "your-project/.git".
* @param shouldFailOnNoGitDirectory Boolean that indicates if the process should fail if no
* git directory can be found.
*/
public GitDirLocator(
File projectBasedir,
boolean useNativeGit,
boolean shouldFailOnNoGitDirectory) {
this.projectBasedir = projectBasedir;
this.useNativeGit = useNativeGit;
this.shouldFailOnNoGitDirectory = shouldFailOnNoGitDirectory;
}

/**
* Attempts to lookup a valid .git directory of the currently used project.
*
* @param manuallyConfiguredDir A user has the ability to configure a git-directory with the
* {@code dotGitDirectory} configuration setting. By default it should be simply {@code
* ${project.basedir}/.git}
* @return A valid .git directory, or {@code null} if none could be found under the user specified
* location or within the project or it's reactor projects.
*/
@Nullable
public File lookupGitDirectory(@Nonnull File manuallyConfiguredDir) throws GitCommitIdExecutionException {
File dotGitDirectory = runSearch(manuallyConfiguredDir, true);
if (shouldFailOnNoGitDirectory && !directoryExists(dotGitDirectory)) {
throw new GitCommitIdExecutionException(
".git directory is not found! Please specify a valid [dotGitDirectory] in your"
+ " project");
}
// dotGitDirectory can be null here, when shouldFailOnNoGitDirectory == true
if (useNativeGit) {
// Check if the resolved directory structure looks like it is a submodule
// path like `your-project/.git/modules/remote-module`.
if (dotGitDirectory != null) {
File parent = dotGitDirectory.getParentFile();
if (parent != null) {
File parentParent = parent.getParentFile();
if (parentParent != null && parentParent.getName().equals(".git") && parent.getName().equals("modules")) {
// Yes, we have a submodule, so this becomes a bit more tricky!
// First what we need to find is the unresolvedGitDir
File unresolvedGitDir = runSearch(manuallyConfiguredDir, false);
// Now to be extra sure, check if the unresolved
// ".git" we have found is actually a file, which is the case for submodules
if (unresolvedGitDir != null && unresolvedGitDir.isFile()) {
// Yes, it's a submodule!
// For the native git executable we can not use the resolved
// dotGitDirectory which looks like `your-project/.git/modules/remote-module`.
// The main reason seems that some git commands like `git config`
// consume the relative worktree configuration like
// `worktree = ../../../remote-module` from that location.
// When running `git config` in `your-project/.git/modules/remote-module`
// it would fail with an error since the relative worktree location is
// only valid from the original location (`your-project/remote-module/.git`).
//
// Hence instead of using the resolved git dir location we need to use the
// unresolvedGitDir, but we need to keep in mind that we initially have pointed to
// a `git`-File like `your-project/remote-module/.git`
dotGitDirectory = unresolvedGitDir;
}
}
}
}
// The directory is likely an actual .dot-dir like `your-project/.git`.
// In such a directory we can not run any git commands so we need to use the parent.
if (dotGitDirectory != null) {
dotGitDirectory = dotGitDirectory.getParentFile();
}
}
return dotGitDirectory;
}

private static boolean directoryExists(@Nullable File fileLocation) {
return fileLocation != null && fileLocation.exists() && fileLocation.isDirectory();
}

@Nullable
private File runSearch(@Nonnull File manuallyConfiguredDir, boolean resolveGitReferenceFile) {
if (manuallyConfiguredDir.exists()) {

// If manuallyConfiguredDir is a directory then we can use it as the git path.
if (manuallyConfiguredDir.isDirectory()) {
return manuallyConfiguredDir;
}

if (manuallyConfiguredDir.isFile() && !resolveGitReferenceFile) {
return manuallyConfiguredDir;
}
// If the path exists but is not a directory it might be a git submodule "gitdir" link.
File gitDirLinkPath = processGitDirFile(manuallyConfiguredDir);

// If the linkPath was found from the file and it exists then use it.
if (isExistingDirectory(gitDirLinkPath)) {
return gitDirLinkPath;
}

/*
* FIXME: I think we should fail here because a manual path was set and it was not found
* but I'm leaving it falling back to searching for the git path because that is the current
* behaviour - Unluckypixie.
*/
}

return findProjectGitDirectory(resolveGitReferenceFile);
}

/**
* Search up all the parent project hierarchy until a .git directory is found.
*
* @return File which represents the location of the .git directory or NULL if none found.
*/
@Nullable
private File findProjectGitDirectory(boolean resolveGitReferenceFile) {
File basedir = this.projectBasedir;
while (basedir != null) {
File gitdir = new File(basedir, Constants.DOT_GIT);
if (gitdir.exists()) {
if (gitdir.isDirectory()) {
return gitdir;
} else if (gitdir.isFile()) {
if (resolveGitReferenceFile) {
return processGitDirFile(gitdir);
} else {
return gitdir;
}
} else {
return null;
}
}
basedir = basedir.getParentFile();
}
return null;
}

/**
* Load a ".git" git submodule file and read the gitdir path from it.
*
* @return File object with path loaded or null
*/
private File processGitDirFile(@Nonnull File file) {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// There should be just one line in the file, e.g.
// "gitdir: /usr/local/src/parentproject/.git/modules/submodule"
String line = reader.readLine();
if (line == null) {
return null;
}
// Separate the key and the value in the string.
String[] parts = line.split(": ");

// If we don't have 2 parts or if the key is not gitdir then give up.
if (parts.length != 2 || !parts[0].equals("gitdir")) {
return null;
}

// All seems ok so return the "gitdir" value read from the file.
String extractFromConfig = parts[1];
File gitDir = resolveWorktree(new File(extractFromConfig));
if (gitDir.isAbsolute()) {
// gitdir value is an absolute path. Return as-is
return gitDir;
} else {
// gitdir value is relative.
return new File(file.getParentFile(), extractFromConfig);
}
} catch (IOException e) {
return null;
}
}

/**
* Attempts to resolve the actual location of the .git folder for a given
* worktree.
* For example for a worktree like {@code a/.git/worktrees/X} structure would
* return {@code a/.git}.
*
* If the conditions for a git worktree like file structure are met simply return the provided
* argument as is.
*/
static File resolveWorktree(File fileLocation) {
Path parent = fileLocation.toPath().getParent();
if (parent == null) {
return fileLocation;
}
if (parent.endsWith(Path.of(".git", "worktrees"))) {
return parent.getParent().toFile();
}
return fileLocation;
}

/**
* Helper method to validate that the specified {@code File} is an existing directory.
*
* @param fileLocation The {@code File} that should be checked if it's actually an existing
* directory.
* @return {@code true} if the specified {@code File} is an existing directory, {@false}
* otherwise.
*/
private static boolean isExistingDirectory(@Nullable File fileLocation) {
return fileLocation != null && fileLocation.exists() && fileLocation.isDirectory();
}
}
34 changes: 34 additions & 0 deletions src/test/java/pl/project13/core/AvailableGitTestRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,41 @@ public enum AvailableGitTestRepo {
*/
WITH_COMMIT_THAT_HAS_TWO_TAGS("src/test/resources/_git_with_commit_that_has_two_tags"),
ON_A_TAG_DIRTY("src/test/resources/_git_on_a_tag_dirty"),
/**
* <pre>
* * 01ed93c - (11 years ago) any commit, just a readme - Konrad Malawski (HEAD -> master)
* * 4ce26eb - (11 years ago) my submodules, yay - Konrad Malawski
* </pre>
* <pre>
* $ git submodule status
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module1
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module2
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module3
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module4
*
* $ git config --file .gitmodules --get-regexp '\.url$'
* submodule.module1.url /tmp/module1
* submodule.module2.url /tmp/module1
* submodule.module3.url /tmp/module1
* submodule.module4.url /tmp/module1
* </pre>
*/
WITH_SUBMODULES("src/test/resources/_git_with_submodules"),

/**
* <pre>
* 6455ccd - (3 minutes ago) init (HEAD -> master)
* </pre>
* <pre>
* $ git submodule status
* 945bfe60e8a3eff168e915c7ba5bac37c9d0165b remote-module (heads/empty-branch)
*
* $ git submodule foreach --recursive git remote get-url origin
* Entering 'remote-module'
* [email protected]:git-commit-id/git-test-resources.git
* </pre>
*/
WITH_REMOTE_SUBMODULES("src/test/resources/_git_with_remote_submodules"),
/**
* <pre>
* b6a73ed - (HEAD, master) third addition (4 minutes ago) <p>Konrad Malawski</p>
Expand Down
Loading