@@ -79,6 +79,115 @@ public function testOpensALinkedWorktree(): void
7979 $ this ->assertSame ($ repository ->getHeadCommit ()->getHash (), $ worktree ->getHeadCommit ()->getHash ());
8080 }
8181
82+ /**
83+ * The ".git" file of a submodule points to its git directory, in the one
84+ * of the superproject, with a relative path.
85+ */
86+ public function testOpensASubmodule (): void
87+ {
88+ $ superproject = self ::createFoobarRepository (false );
89+ $ source = self ::createFoobarRepository ();
90+ // git refuses to clone a local submodule since 2.38.1, unless protocol.file.allow allows it.
91+ new Process ([self ::gitCommand (), '-c ' , 'protocol.file.allow=always ' , 'submodule ' , 'add ' , '-q ' , $ source ->getGitDir (), 'sub ' ], $ superproject ->getWorkingDir (), self ::gitEnvs ())->mustRun ();
92+ $ dir = $ superproject ->getWorkingDir ().'/sub ' ;
93+ $ this ->assertFileMatchesFormat ('gitdir: ../.git/modules/sub ' , $ dir .'/.git ' );
94+
95+ $ submodule = new Repository ($ dir , driver: self ::getDriver ());
96+
97+ $ this ->assertSame (realpath ($ dir ), $ submodule ->getWorkingDir ());
98+ $ this ->assertSame (realpath ($ superproject ->getGitDir ().'/modules/sub ' ), $ submodule ->getGitDir ());
99+ $ this ->assertFalse ($ submodule ->isBare ());
100+ $ this ->assertSame ($ source ->getHeadCommit ()->getHash (), $ submodule ->getHeadCommit ()->getHash ());
101+ }
102+
103+ /**
104+ * "git clone --separate-git-dir" writes a ".git" file with the absolute
105+ * path of the git directory.
106+ */
107+ public function testOpensACloneWithASeparateGitDir (): void
108+ {
109+ $ source = self ::createFoobarRepository ();
110+ [$ dir , $ gitDir ] = self ::cloneWithASeparateGitDir ($ source );
111+
112+ $ repository = new Repository ($ dir , driver: self ::getDriver ());
113+
114+ $ this ->assertSame (realpath ($ dir ), $ repository ->getWorkingDir ());
115+ $ this ->assertSame (realpath ($ gitDir ), $ repository ->getGitDir ());
116+ $ this ->assertFalse ($ repository ->isBare ());
117+ $ this ->assertSame ($ source ->getHeadCommit ()->getHash (), $ repository ->getHeadCommit ()->getHash ());
118+ }
119+
120+ /**
121+ * Like git, which leaves out the line ending of a ".git" file only: a
122+ * path may end with a space.
123+ */
124+ #[DataProvider('provideLineEndings ' )]
125+ public function testOpensAGitFileWhateverItsLineEnding (string $ lineEnding ): void
126+ {
127+ [$ dir , $ gitDir ] = self ::cloneWithASeparateGitDir (self ::createFoobarRepository ());
128+ file_put_contents ($ dir .'/.git ' , 'gitdir: ' .$ gitDir .$ lineEnding );
129+
130+ $ this ->assertSame (realpath ($ gitDir ), new Repository ($ dir , driver: self ::getDriver ())->getGitDir ());
131+ }
132+
133+ /**
134+ * @return iterable<string, array{string}>
135+ */
136+ public static function provideLineEndings (): iterable
137+ {
138+ yield 'none ' => ['' ];
139+ yield 'LF ' => ["\n" ];
140+ yield 'CRLF, written on Windows ' => ["\r\n" ];
141+ }
142+
143+ #[DataProvider('provideGitFilesPointingToNoGitDirectory ' )]
144+ public function testAGitFilePointingToNoGitDirectoryThrows (string $ content , string $ message ): void
145+ {
146+ $ dir = self ::createTempDir ();
147+ file_put_contents ($ dir .'/.git ' , $ content );
148+
149+ $ this ->expectException (DriverException::class);
150+ $ this ->expectExceptionMessage (\sprintf ($ message , $ dir ));
151+
152+ new Repository ($ dir , driver: self ::getDriver ());
153+ }
154+
155+ /**
156+ * @return iterable<string, array{string, string}>
157+ */
158+ public static function provideGitFilesPointingToNoGitDirectory (): iterable
159+ {
160+ $ invalid = 'The "%s/.git" file doesn \'t point to a git directory. ' ;
161+
162+ yield 'another format ' => ["ref: refs/heads/master \n" , $ invalid ];
163+ // Like git, which reads "gitdir: " at the start of the file only.
164+ yield 'gitdir on the second line ' => ["\ngitdir: missing \n" , $ invalid ];
165+ yield 'no path ' => ["gitdir: \n" , $ invalid ];
166+ yield 'NUL byte ' => ["gitdir: a \0b \n" , $ invalid ];
167+ yield 'missing directory ' => ["gitdir: missing \n" , 'The git directory "missing" of "%s" does not exist. ' ];
168+ }
169+
170+ public function testAnUnreadableGitFileThrows (): void
171+ {
172+ $ dir = self ::createTempDir ();
173+ file_put_contents ($ dir .'/.git ' , "gitdir: missing \n" );
174+ chmod ($ dir .'/.git ' , 0 );
175+
176+ try {
177+ if (is_readable ($ dir .'/.git ' )) {
178+ $ this ->markTestSkipped ('The user of the tests reads any file, like root. ' );
179+ }
180+
181+ $ this ->expectException (DriverException::class);
182+ $ this ->expectExceptionMessage (\sprintf ('Unable to read the "%s/.git" file. ' , $ dir ));
183+
184+ new Repository ($ dir , driver: self ::getDriver ());
185+ } finally {
186+ // Or the file can't be deleted on Windows.
187+ chmod ($ dir .'/.git ' , 0o600 );
188+ }
189+ }
190+
82191 public function testOpeningADirectoryThatIsNotARepositoryThrows (): void
83192 {
84193 $ dir = self ::createTempDir ();
@@ -820,4 +929,16 @@ public function testDebugOptionSetToFalseIsRefused(): void
820929
821930 new Repository (self ::createTempDir (), ['debug ' => false ]);
822931 }
932+
933+ /**
934+ * @return array{string, string} The working tree of the clone, and its git directory
935+ */
936+ private static function cloneWithASeparateGitDir (Repository $ source ): array
937+ {
938+ $ dir = self ::createTempDir ().'/clone ' ;
939+ $ gitDir = self ::createTempDir ().'/git ' ;
940+ new Process ([self ::gitCommand (), 'clone ' , '-q ' , '--separate-git-dir= ' .$ gitDir , $ source ->getGitDir (), $ dir ], null , self ::gitEnvs ())->mustRun ();
941+
942+ return [$ dir , $ gitDir ];
943+ }
823944}
0 commit comments