Skip to content

Commit d92eba9

Browse files
authored
Merge pull request #106 from git-commit-id/701
git-commit-id/git-commit-id-maven-plugin#701: Fix an issue with submodules
2 parents 0dcee41 + 6352d44 commit d92eba9

File tree

7 files changed

+470
-7
lines changed

7 files changed

+470
-7
lines changed

src/main/java/pl/project13/core/GitCommitIdPlugin.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pl.project13.core.git.GitDescribeConfig;
2222
import pl.project13.core.log.LogInterface;
2323
import pl.project13.core.util.BuildFileChangeListener;
24+
import pl.project13.core.util.GitDirLocator;
2425

2526
import javax.annotation.Nonnull;
2627
import javax.annotation.Nullable;
@@ -279,6 +280,8 @@ default Map<String, String> getSystemEnv() {
279280
Charset getPropertiesSourceCharset();
280281

281282
boolean shouldPropertiesEscapeUnicode();
283+
284+
boolean shouldFailOnNoGitDirectory();
282285
}
283286

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

347+
File dotGitDirectory = new GitDirLocator(
348+
cb.getProjectBaseDir(),
349+
cb.useNativeGit(),
350+
cb.shouldFailOnNoGitDirectory()
351+
).lookupGitDirectory(cb.getDotGitDirectory());
352+
if (dotGitDirectory != null) {
353+
cb.getLogInterface().info("dotGitDirectory '" + dotGitDirectory.getAbsolutePath() + "'");
354+
} else {
355+
cb.getLogInterface().info("dotGitDirectory is null, aborting execution!");
356+
return;
357+
}
358+
344359
if (cb.useNativeGit()) {
345-
loadGitDataWithNativeGit(cb, properties);
360+
loadGitDataWithNativeGit(cb, dotGitDirectory, properties);
346361
} else {
347-
loadGitDataWithJGit(cb, properties);
362+
loadGitDataWithJGit(cb, dotGitDirectory, properties);
348363
}
349364
}
350365

351-
private static void loadGitDataWithNativeGit(@Nonnull Callback cb, @Nonnull Properties properties) throws GitCommitIdExecutionException {
366+
private static void loadGitDataWithNativeGit(
367+
@Nonnull Callback cb,
368+
@Nonnull File dotGitDirectory,
369+
@Nonnull Properties properties) throws GitCommitIdExecutionException {
352370
GitDataProvider nativeGitProvider = NativeGitProvider
353-
.on(cb.getDotGitDirectory().getParentFile(), cb.getNativeGitTimeoutInMs(), cb.getLogInterface())
371+
.on(dotGitDirectory, cb.getNativeGitTimeoutInMs(), cb.getLogInterface())
354372
.setPrefixDot(cb.getPrefixDot())
355373
.setAbbrevLength(cb.getAbbrevLength())
356374
.setDateFormat(cb.getDateFormat())
@@ -365,9 +383,12 @@ private static void loadGitDataWithNativeGit(@Nonnull Callback cb, @Nonnull Prop
365383
nativeGitProvider.loadGitData(cb.getEvaluateOnCommit(), cb.getSystemEnv(), properties);
366384
}
367385

368-
private static void loadGitDataWithJGit(@Nonnull Callback cb, @Nonnull Properties properties) throws GitCommitIdExecutionException {
386+
private static void loadGitDataWithJGit(
387+
@Nonnull Callback cb,
388+
@Nonnull File dotGitDirectory,
389+
@Nonnull Properties properties) throws GitCommitIdExecutionException {
369390
GitDataProvider jGitProvider = JGitProvider
370-
.on(cb.getDotGitDirectory(), cb.getLogInterface())
391+
.on(dotGitDirectory, cb.getLogInterface())
371392
.setPrefixDot(cb.getPrefixDot())
372393
.setAbbrevLength(cb.getAbbrevLength())
373394
.setDateFormat(cb.getDateFormat())
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* This file is part of git-commit-id-plugin-core by Konrad 'ktoso' Malawski <konrad.malawski@java.pl>
3+
*
4+
* git-commit-id-plugin-core is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* git-commit-id-plugin-core is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with git-commit-id-plugin-core. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package pl.project13.core.util;
19+
20+
import java.io.BufferedReader;
21+
import java.io.File;
22+
import java.io.FileReader;
23+
import java.io.IOException;
24+
import java.nio.file.Path;
25+
import javax.annotation.Nonnull;
26+
import javax.annotation.Nullable;
27+
import org.eclipse.jgit.lib.Constants;
28+
import pl.project13.core.GitCommitIdExecutionException;
29+
30+
/**
31+
* This class encapsulates logic to locate a valid .git directory of the currently used project. If
32+
* it's not already specified, this logic will try to find it.
33+
*/
34+
public class GitDirLocator {
35+
final File projectBasedir;
36+
final boolean useNativeGit;
37+
final boolean shouldFailOnNoGitDirectory;
38+
39+
/**
40+
* Constructor to encapsulates all references required to locate a valid .git directory
41+
*
42+
* @param projectBasedir The project basedir that will be used as last resort to search
43+
* the parent project hierarchy until a .git directory is found.
44+
* @param useNativeGit Boolean that indicates if we use the native git implementation or the
45+
* jGit Implementation. For the native git we usually need to
46+
* use the parent "git"-Folder, as git can not run commands
47+
* in "your-project/.git".
48+
* @param shouldFailOnNoGitDirectory Boolean that indicates if the process should fail if no
49+
* git directory can be found.
50+
*/
51+
public GitDirLocator(
52+
File projectBasedir,
53+
boolean useNativeGit,
54+
boolean shouldFailOnNoGitDirectory) {
55+
this.projectBasedir = projectBasedir;
56+
this.useNativeGit = useNativeGit;
57+
this.shouldFailOnNoGitDirectory = shouldFailOnNoGitDirectory;
58+
}
59+
60+
/**
61+
* Attempts to lookup a valid .git directory of the currently used project.
62+
*
63+
* @param manuallyConfiguredDir A user has the ability to configure a git-directory with the
64+
* {@code dotGitDirectory} configuration setting. By default it should be simply {@code
65+
* ${project.basedir}/.git}
66+
* @return A valid .git directory, or {@code null} if none could be found under the user specified
67+
* location or within the project or it's reactor projects.
68+
*/
69+
@Nullable
70+
public File lookupGitDirectory(@Nonnull File manuallyConfiguredDir) throws GitCommitIdExecutionException {
71+
File dotGitDirectory = runSearch(manuallyConfiguredDir, true);
72+
if (shouldFailOnNoGitDirectory && !directoryExists(dotGitDirectory)) {
73+
throw new GitCommitIdExecutionException(
74+
".git directory is not found! Please specify a valid [dotGitDirectory] in your"
75+
+ " project");
76+
}
77+
// dotGitDirectory can be null here, when shouldFailOnNoGitDirectory == true
78+
if (useNativeGit) {
79+
// Check if the resolved directory structure looks like it is a submodule
80+
// path like `your-project/.git/modules/remote-module`.
81+
if (dotGitDirectory != null) {
82+
File parent = dotGitDirectory.getParentFile();
83+
if (parent != null) {
84+
File parentParent = parent.getParentFile();
85+
if (parentParent != null && parentParent.getName().equals(".git") && parent.getName().equals("modules")) {
86+
// Yes, we have a submodule, so this becomes a bit more tricky!
87+
// First what we need to find is the unresolvedGitDir
88+
File unresolvedGitDir = runSearch(manuallyConfiguredDir, false);
89+
// Now to be extra sure, check if the unresolved
90+
// ".git" we have found is actually a file, which is the case for submodules
91+
if (unresolvedGitDir != null && unresolvedGitDir.isFile()) {
92+
// Yes, it's a submodule!
93+
// For the native git executable we can not use the resolved
94+
// dotGitDirectory which looks like `your-project/.git/modules/remote-module`.
95+
// The main reason seems that some git commands like `git config`
96+
// consume the relative worktree configuration like
97+
// `worktree = ../../../remote-module` from that location.
98+
// When running `git config` in `your-project/.git/modules/remote-module`
99+
// it would fail with an error since the relative worktree location is
100+
// only valid from the original location (`your-project/remote-module/.git`).
101+
//
102+
// Hence instead of using the resolved git dir location we need to use the
103+
// unresolvedGitDir, but we need to keep in mind that we initially have pointed to
104+
// a `git`-File like `your-project/remote-module/.git`
105+
dotGitDirectory = unresolvedGitDir;
106+
}
107+
}
108+
}
109+
}
110+
// The directory is likely an actual .dot-dir like `your-project/.git`.
111+
// In such a directory we can not run any git commands so we need to use the parent.
112+
if (dotGitDirectory != null) {
113+
dotGitDirectory = dotGitDirectory.getParentFile();
114+
}
115+
}
116+
return dotGitDirectory;
117+
}
118+
119+
private static boolean directoryExists(@Nullable File fileLocation) {
120+
return fileLocation != null && fileLocation.exists() && fileLocation.isDirectory();
121+
}
122+
123+
@Nullable
124+
private File runSearch(@Nonnull File manuallyConfiguredDir, boolean resolveGitReferenceFile) {
125+
if (manuallyConfiguredDir.exists()) {
126+
127+
// If manuallyConfiguredDir is a directory then we can use it as the git path.
128+
if (manuallyConfiguredDir.isDirectory()) {
129+
return manuallyConfiguredDir;
130+
}
131+
132+
if (manuallyConfiguredDir.isFile() && !resolveGitReferenceFile) {
133+
return manuallyConfiguredDir;
134+
}
135+
// If the path exists but is not a directory it might be a git submodule "gitdir" link.
136+
File gitDirLinkPath = processGitDirFile(manuallyConfiguredDir);
137+
138+
// If the linkPath was found from the file and it exists then use it.
139+
if (isExistingDirectory(gitDirLinkPath)) {
140+
return gitDirLinkPath;
141+
}
142+
143+
/*
144+
* FIXME: I think we should fail here because a manual path was set and it was not found
145+
* but I'm leaving it falling back to searching for the git path because that is the current
146+
* behaviour - Unluckypixie.
147+
*/
148+
}
149+
150+
return findProjectGitDirectory(resolveGitReferenceFile);
151+
}
152+
153+
/**
154+
* Search up all the parent project hierarchy until a .git directory is found.
155+
*
156+
* @return File which represents the location of the .git directory or NULL if none found.
157+
*/
158+
@Nullable
159+
private File findProjectGitDirectory(boolean resolveGitReferenceFile) {
160+
File basedir = this.projectBasedir;
161+
while (basedir != null) {
162+
File gitdir = new File(basedir, Constants.DOT_GIT);
163+
if (gitdir.exists()) {
164+
if (gitdir.isDirectory()) {
165+
return gitdir;
166+
} else if (gitdir.isFile()) {
167+
if (resolveGitReferenceFile) {
168+
return processGitDirFile(gitdir);
169+
} else {
170+
return gitdir;
171+
}
172+
} else {
173+
return null;
174+
}
175+
}
176+
basedir = basedir.getParentFile();
177+
}
178+
return null;
179+
}
180+
181+
/**
182+
* Load a ".git" git submodule file and read the gitdir path from it.
183+
*
184+
* @return File object with path loaded or null
185+
*/
186+
private File processGitDirFile(@Nonnull File file) {
187+
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
188+
// There should be just one line in the file, e.g.
189+
// "gitdir: /usr/local/src/parentproject/.git/modules/submodule"
190+
String line = reader.readLine();
191+
if (line == null) {
192+
return null;
193+
}
194+
// Separate the key and the value in the string.
195+
String[] parts = line.split(": ");
196+
197+
// If we don't have 2 parts or if the key is not gitdir then give up.
198+
if (parts.length != 2 || !parts[0].equals("gitdir")) {
199+
return null;
200+
}
201+
202+
// All seems ok so return the "gitdir" value read from the file.
203+
String extractFromConfig = parts[1];
204+
File gitDir = resolveWorktree(new File(extractFromConfig));
205+
if (gitDir.isAbsolute()) {
206+
// gitdir value is an absolute path. Return as-is
207+
return gitDir;
208+
} else {
209+
// gitdir value is relative.
210+
return new File(file.getParentFile(), extractFromConfig);
211+
}
212+
} catch (IOException e) {
213+
return null;
214+
}
215+
}
216+
217+
/**
218+
* Attempts to resolve the actual location of the .git folder for a given
219+
* worktree.
220+
* For example for a worktree like {@code a/.git/worktrees/X} structure would
221+
* return {@code a/.git}.
222+
*
223+
* If the conditions for a git worktree like file structure are met simply return the provided
224+
* argument as is.
225+
*/
226+
static File resolveWorktree(File fileLocation) {
227+
Path parent = fileLocation.toPath().getParent();
228+
if (parent == null) {
229+
return fileLocation;
230+
}
231+
if (parent.endsWith(Path.of(".git", "worktrees"))) {
232+
return parent.getParent().toFile();
233+
}
234+
return fileLocation;
235+
}
236+
237+
/**
238+
* Helper method to validate that the specified {@code File} is an existing directory.
239+
*
240+
* @param fileLocation The {@code File} that should be checked if it's actually an existing
241+
* directory.
242+
* @return {@code true} if the specified {@code File} is an existing directory, {@false}
243+
* otherwise.
244+
*/
245+
private static boolean isExistingDirectory(@Nullable File fileLocation) {
246+
return fileLocation != null && fileLocation.exists() && fileLocation.isDirectory();
247+
}
248+
}

src/test/java/pl/project13/core/AvailableGitTestRepo.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,41 @@ public enum AvailableGitTestRepo {
4848
*/
4949
WITH_COMMIT_THAT_HAS_TWO_TAGS("src/test/resources/_git_with_commit_that_has_two_tags"),
5050
ON_A_TAG_DIRTY("src/test/resources/_git_on_a_tag_dirty"),
51+
/**
52+
* <pre>
53+
* * 01ed93c - (11 years ago) any commit, just a readme - Konrad Malawski (HEAD -> master)
54+
* * 4ce26eb - (11 years ago) my submodules, yay - Konrad Malawski
55+
* </pre>
56+
* <pre>
57+
* $ git submodule status
58+
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module1
59+
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module2
60+
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module3
61+
* -9fd4b69a5ca09b60884d4f8f49ce16ea071077be module4
62+
*
63+
* $ git config --file .gitmodules --get-regexp '\.url$'
64+
* submodule.module1.url /tmp/module1
65+
* submodule.module2.url /tmp/module1
66+
* submodule.module3.url /tmp/module1
67+
* submodule.module4.url /tmp/module1
68+
* </pre>
69+
*/
5170
WITH_SUBMODULES("src/test/resources/_git_with_submodules"),
71+
72+
/**
73+
* <pre>
74+
* 6455ccd - (3 minutes ago) init (HEAD -> master)
75+
* </pre>
76+
* <pre>
77+
* $ git submodule status
78+
* 945bfe60e8a3eff168e915c7ba5bac37c9d0165b remote-module (heads/empty-branch)
79+
*
80+
* $ git submodule foreach --recursive git remote get-url origin
81+
* Entering 'remote-module'
82+
* git@github.com:git-commit-id/git-test-resources.git
83+
* </pre>
84+
*/
85+
WITH_REMOTE_SUBMODULES("src/test/resources/_git_with_remote_submodules"),
5286
/**
5387
* <pre>
5488
* b6a73ed - (HEAD, master) third addition (4 minutes ago) <p>Konrad Malawski</p>

0 commit comments

Comments
 (0)