@@ -25,6 +25,9 @@ class PackageDiff
2525 /** @var UrlGenerator */
2626 protected $ urlGenerator ;
2727
28+ /** @var bool */
29+ protected $ skipIoErrors = false ;
30+
2831 public function __construct ()
2932 {
3033 $ this ->urlGenerator = new GeneratorContainer ();
@@ -38,6 +41,16 @@ public function setUrlGenerator(UrlGenerator $urlGenerator)
3841 $ this ->urlGenerator = $ urlGenerator ;
3942 }
4043
44+ /**
45+ * @param bool $skipIoErrors
46+ *
47+ * @return void
48+ */
49+ public function setSkipIoErrors ($ skipIoErrors )
50+ {
51+ $ this ->skipIoErrors = (bool ) $ skipIoErrors ;
52+ }
53+
4154 /**
4255 * @param string[] $directPackages
4356 * @param bool $onlyDirect
@@ -213,79 +226,71 @@ private function getFileContents($path, $lockFile = true)
213226 return file_get_contents ($ localPath );
214227 }
215228
216- if ($ lockFile && !$ this ->containsGitSeparator ($ originalPath ) && $ this ->looksLikeComposerLockFile ($ localPath )) {
217- return '{} ' ;
218- }
229+ $ attemptedSpecs = array ();
230+ $ lastError = '' ;
219231
220- if (!$ this ->containsGitSeparator ($ originalPath )) {
221- $ path .= self ::GIT_SEPARATOR .self ::COMPOSER .($ lockFile ? self ::EXTENSION_LOCK : self ::EXTENSION_JSON );
222- }
223-
224- if (!$ lockFile ) {
225- $ path = $ this ->getJsonPath ($ path );
226- }
232+ foreach ($ this ->getGitCandidates ($ path , $ lockFile ) as $ gitPath ) {
233+ $ attemptedSpecs [] = $ gitPath ;
234+ $ output = array ();
235+ @exec (sprintf ('git show %s 2>&1 ' , escapeshellarg ($ gitPath )), $ output , $ exit );
236+ $ outputString = implode ("\n" , $ output );
227237
228- $ output = array ();
229- @exec (sprintf ('git show %s 2>&1 ' , escapeshellarg ($ path )), $ output , $ exit );
230- $ outputString = implode ("\n" , $ output );
238+ if (0 !== $ exit ) {
239+ $ lastError = $ outputString ;
231240
232- if (0 !== $ exit ) {
233- if ($ lockFile && $ this ->isMissingFileError ($ outputString )) {
234- return '{} ' ;
241+ continue ;
235242 }
236243
237- if ($ lockFile ) {
238- throw new \RuntimeException (sprintf ('Could not open file %s or find it in git as %s: %s ' , $ originalPath , $ path , $ outputString ));
244+ if (!$ this ->looksLikeJsonDocument ($ outputString )) {
245+ $ lastError = sprintf ('Unexpected non-JSON output for git spec %s ' , $ gitPath );
246+
247+ continue ;
239248 }
240249
241- /* @infection-ignore-all False-positive */
242- return '{} ' ; // Do not throw exception for composer.json as it might not exist and that's fine
250+ return $ outputString ;
251+ }
252+
253+ if ($ lockFile && !$ this ->skipIoErrors ) {
254+ throw new \RuntimeException (sprintf ('Could not open file %s or find it in git as %s: %s ' , $ originalPath , implode (', ' , $ attemptedSpecs ), $ lastError ));
243255 }
244256
245- return $ outputString ;
257+ /* @infection-ignore-all False-positive */
258+ // Do not throw exception for composer.json as it might not exist and that's fine.
259+ // For composer.lock this fallback is optional and controlled by setSkipIoErrors().
260+ return '{} ' ;
246261 }
247262
248263 /**
249264 * @param string $path
265+ * @param bool $lockFile
250266 *
251- * @return bool
267+ * @return string[]
252268 */
253- private function looksLikeComposerLockFile ($ path )
269+ private function getGitCandidates ($ path, $ lockFile = true )
254270 {
255- return self ::EXTENSION_LOCK === substr ($ path , -strlen (self ::EXTENSION_LOCK ));
256- }
271+ $ candidates = array ();
272+
273+ if ($ lockFile ) {
274+ $ candidates [] = $ path ;
275+ $ candidates [] = $ path .self ::GIT_SEPARATOR .self ::COMPOSER .self ::EXTENSION_LOCK ;
276+ } else {
277+ $ candidates [] = $ this ->getJsonPath ($ path );
278+ $ candidates [] = $ this ->getJsonPath ($ path .self ::GIT_SEPARATOR .self ::COMPOSER .self ::EXTENSION_LOCK );
279+ }
257280
258- /**
259- * @param string $gitOutput
260- *
261- * @return bool
262- */
263- private function isMissingFileError ($ gitOutput )
264- {
265- return false !== stripos ($ gitOutput , 'does not exist in ' ) || false !== stripos ($ gitOutput , 'exists on disk, but not in ' );
281+ return array_values (array_unique ($ candidates ));
266282 }
267283
268284 /**
269- * @param string $path
285+ * @param string $contents
270286 *
271287 * @return bool
272288 */
273- private function containsGitSeparator ( $ path )
289+ private function looksLikeJsonDocument ( $ contents )
274290 {
275- $ pos = strpos ($ path , self ::GIT_SEPARATOR );
276-
277- if (false === $ pos ) {
278- return false ;
279- }
280-
281- // Ignore Windows absolute drive paths (e.g. "C:\path" or "C:/path").
282- if (1 === $ pos && ctype_alpha ($ path [0 ])) {
283- if (isset ($ path [2 ]) && ('\\' === $ path [2 ] || '/ ' === $ path [2 ])) {
284- return false ;
285- }
286- }
291+ $ data = json_decode ($ contents , true );
287292
288- return true ;
293+ return JSON_ERROR_NONE === json_last_error () && is_array ( $ data ) ;
289294 }
290295
291296 /**
0 commit comments