2121import java .io .File ;
2222import java .io .FileReader ;
2323import java .io .IOException ;
24- import java .nio .file .Path ;
2524import org .eclipse .jgit .lib .Constants ;
2625import org .jspecify .annotations .NonNull ;
2726import org .jspecify .annotations .Nullable ;
@@ -75,43 +74,45 @@ public File lookupGitDirectory(@NonNull File manuallyConfiguredDir) throws GitCo
7574 + " project" );
7675 }
7776 // dotGitDirectory can be null here, when shouldFailOnNoGitDirectory == true
78- if (useNativeGit ) {
77+ if (useNativeGit && dotGitDirectory != null ) {
7978 // 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- }
79+ // path like `your-project/.git/modules/remote-module`, or like the administrative
80+ // directory of a linked worktree like `your-project/.git/worktrees/remote-worktree`.
81+ // First what we need to find is the unresolvedGitDir.
82+ File unresolvedGitDir = runSearch (manuallyConfiguredDir , false );
83+ // Now to be extra sure, check if the unresolved ".git" we have found is actually a file,
84+ // which is the case for both submodules and linked worktrees.
85+ if (unresolvedGitDir != null
86+ && unresolvedGitDir .isFile ()
87+ && (isSubmoduleGitDir (dotGitDirectory )
88+ || isWorktreeAdministrativeDir (readGitDirFile (unresolvedGitDir )))) {
89+ // Yes, it's a submodule or a linked worktree, so this becomes a bit more tricky!
90+ //
91+ // For a submodule we can not use the resolved dotGitDirectory which looks like
92+ // `your-project/.git/modules/remote-module`.
93+ // The main reason seems that some git commands like `git config`
94+ // consume the relative worktree configuration like
95+ // `worktree = ../../../remote-module` from that location.
96+ // When running `git config` in `your-project/.git/modules/remote-module`
97+ // it would fail with an error since the relative worktree location is
98+ // only valid from the original location (`your-project/remote-module/.git`).
99+ //
100+ // For a linked worktree we can not use the resolved dotGitDirectory either, since
101+ // that is the git directory shared by all worktrees. It has no working tree of its
102+ // own -- and when the worktrees are hosted by a bare repository there is no working
103+ // tree next to it at all -- so git commands run there would fail with
104+ // `fatal: this operation must be run in a work tree`. It also belongs to no worktree
105+ // in particular, so any branch or commit reported from there would not be the one of
106+ // the worktree that is currently being built.
107+ //
108+ // Hence instead of using the resolved git dir location we need to use the
109+ // unresolvedGitDir, but we need to keep in mind that we initially have pointed to
110+ // a `git`-File like `your-project/remote-module/.git`
111+ dotGitDirectory = unresolvedGitDir ;
109112 }
110113 // The directory is likely an actual .dot-dir like `your-project/.git`.
111114 // 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+ dotGitDirectory = dotGitDirectory .getParentFile ();
115116 }
116117 return dotGitDirectory ;
117118 }
@@ -179,11 +180,28 @@ private File findProjectGitDirectory(boolean resolveGitReferenceFile) {
179180 }
180181
181182 /**
182- * Load a ".git" git submodule file and read the gitdir path from it.
183+ * Load a ".git" git submodule or worktree file and read the gitdir path from it.
183184 *
184185 * @return File object with path loaded or null
185186 */
187+ @ Nullable
186188 private File processGitDirFile (@ NonNull File file ) {
189+ File gitDir = readGitDirFile (file );
190+ if (gitDir == null ) {
191+ return null ;
192+ }
193+ return resolveWorktree (gitDir );
194+ }
195+
196+ /**
197+ * Load a ".git" git submodule or worktree file and read the gitdir path from it, without
198+ * resolving that path any further. For a linked worktree the returned location therefore is
199+ * the administrative directory of that worktree, like {@code a/.git/worktrees/X}.
200+ *
201+ * @return File object with path loaded or null
202+ */
203+ @ Nullable
204+ private File readGitDirFile (@ NonNull File file ) {
187205 try (BufferedReader reader = new BufferedReader (new FileReader (file ))) {
188206 // There should be just one line in the file, e.g.
189207 // "gitdir: /usr/local/src/parentproject/.git/modules/submodule"
@@ -200,15 +218,8 @@ private File processGitDirFile(@NonNull File file) {
200218 }
201219
202220 // 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- }
221+ // A relative gitdir value is relative to the directory that contains the ".git" file.
222+ return resolveAgainst (file .getParentFile (), parts [1 ]);
212223 } catch (IOException e ) {
213224 return null ;
214225 }
@@ -220,18 +231,82 @@ private File processGitDirFile(@NonNull File file) {
220231 * For example for a worktree like {@code a/.git/worktrees/X} structure would
221232 * return {@code a/.git}.
222233 *
234+ * <p>The location is not derived from the name of the directories involved, since a repository
235+ * that hosts worktrees is not required to be named ".git" -- worktrees of a bare repository
236+ * live in {@code your-repository.git/worktrees/X}. Instead the "commondir" file that git
237+ * writes inside the administrative directory of every linked worktree is read, which points to
238+ * the git directory that is shared by all worktrees of the repository.
239+ *
223240 * If the conditions for a git worktree like file structure are met simply return the provided
224241 * argument as is.
225242 */
226243 static File resolveWorktree (File fileLocation ) {
227- Path parent = fileLocation .toPath ().getParent ();
228- if (parent == null ) {
244+ if (!isWorktreeAdministrativeDir (fileLocation )) {
229245 return fileLocation ;
230246 }
231- if (parent .endsWith (Path .of (".git" , "worktrees" ))) {
232- return parent .getParent ().toFile ();
247+ File commonDir = readPathFromFile (fileLocation , "commondir" );
248+ return commonDir != null ? commonDir : fileLocation ;
249+ }
250+
251+ /**
252+ * Checks if the given resolved git directory looks like the git directory of a submodule,
253+ * which is a path like {@code your-project/.git/modules/remote-module}.
254+ */
255+ private static boolean isSubmoduleGitDir (@ NonNull File dotGitDirectory ) {
256+ File parent = dotGitDirectory .getParentFile ();
257+ if (parent == null ) {
258+ return false ;
259+ }
260+ File parentParent = parent .getParentFile ();
261+ return parentParent != null
262+ && parentParent .getName ().equals (".git" )
263+ && parent .getName ().equals ("modules" );
264+ }
265+
266+ /**
267+ * Checks if the given location is the administrative directory git maintains for a linked
268+ * worktree, like {@code a/.git/worktrees/X}. Git writes a "gitdir" file (pointing back to the
269+ * ".git" file inside that worktree) and a "commondir" file (pointing to the git directory
270+ * shared by all worktrees) in there. Requiring both files also tells such a directory apart
271+ * from the git directory of a submodule.
272+ */
273+ private static boolean isWorktreeAdministrativeDir (@ Nullable File fileLocation ) {
274+ return fileLocation != null
275+ && new File (fileLocation , "gitdir" ).isFile ()
276+ && new File (fileLocation , "commondir" ).isFile ();
277+ }
278+
279+ /**
280+ * Reads a file that contains a single path, like the "gitdir" and "commondir" files git writes
281+ * inside the administrative directory of a linked worktree.
282+ *
283+ * @return the path the file points to, resolved against the directory that contains it when it
284+ * is relative, or null when the file can not be read.
285+ */
286+ @ Nullable
287+ private static File readPathFromFile (@ NonNull File directory , @ NonNull String filename ) {
288+ File file = new File (directory , filename );
289+ try (BufferedReader reader = new BufferedReader (new FileReader (file ))) {
290+ String line = reader .readLine ();
291+ if (line == null || line .trim ().isEmpty ()) {
292+ return null ;
293+ }
294+ return resolveAgainst (directory , line .trim ());
295+ } catch (IOException e ) {
296+ return null ;
297+ }
298+ }
299+
300+ /**
301+ * Resolves a path that git has written into one of its metadata files. Git may store those
302+ * either absolute or relative to the directory that holds the file it was read from.
303+ */
304+ private static File resolveAgainst (@ Nullable File directory , @ NonNull String path ) {
305+ File file = new File (path );
306+ if (file .isAbsolute () || directory == null ) {
307+ return file ;
233308 }
234- return fileLocation ;
309+ return new File ( directory , path ) ;
235310 }
236311
237312 /**
0 commit comments