diff --git a/src/Plugins/Tia/Fingerprint.php b/src/Plugins/Tia/Fingerprint.php
index 0f4823a23..c3db69c19 100644
--- a/src/Plugins/Tia/Fingerprint.php
+++ b/src/Plugins/Tia/Fingerprint.php
@@ -5,7 +5,8 @@
namespace Pest\Plugins\Tia;
use Pest\Plugins\Tia\Contracts\Lockfile;
-use Symfony\Component\Finder\Finder;
+use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
+use Symfony\Component\Process\Process;
/**
* @internal
@@ -273,6 +274,9 @@ private static function trackedHash(string $projectRoot, string $relativePath):
* Gitignored lockfiles (e.g. `package-lock.json` excluded from the repo)
* regenerate per-machine with OS-specific optional deps, which would
* otherwise force a fingerprint mismatch on every fetched baseline.
+ *
+ * Git itself answers the ignore question so that linked worktrees, whose
+ * `.git` is a pointer file, resolve against the right repository.
*/
private static function isTrackedByGit(string $projectRoot, string $relativePath): bool
{
@@ -292,13 +296,18 @@ private static function isTrackedByGit(string $projectRoot, string $relativePath
return $cache[$key] = true;
}
- $finder = (new Finder)
- ->in($projectRoot)
- ->depth('== 0')
- ->name($relativePath)
- ->ignoreVCSIgnored(true);
+ $process = new Process(['git', 'check-ignore', '-q', '--', $relativePath], $projectRoot);
+ $process->setTimeout(5.0);
+
+ try {
+ $process->run();
+ } catch (ProcessException) {
+ return $cache[$key] = true;
+ }
- return $cache[$key] = $finder->hasResults();
+ // 0: ignored. 1: not ignored. Anything else: git unavailable or not
+ // a repository, in which case the file is kept like the no-git path.
+ return $cache[$key] = $process->getExitCode() !== 0;
}
private static function contentHashOrNull(string $path): ?string
diff --git a/tests/Unit/Plugins/Tia/Fingerprint.php b/tests/Unit/Plugins/Tia/Fingerprint.php
new file mode 100644
index 000000000..734d7c44b
--- /dev/null
+++ b/tests/Unit/Plugins/Tia/Fingerprint.php
@@ -0,0 +1,109 @@
+setTimeout(10.0);
+ $process->mustRun();
+}
+
+function fingerprintRepository(): string
+{
+ $root = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pest_fingerprint_'.uniqid();
+ mkdir($root, 0777, true);
+
+ fingerprintGit($root, 'init', '-q');
+ fingerprintGit($root, 'config', 'user.email', 'pest@example.com');
+ fingerprintGit($root, 'config', 'user.name', 'Pest');
+
+ return $root;
+}
+
+function fingerprintRemoveDirectory(string $path): void
+{
+ if (! is_dir($path)) {
+ return;
+ }
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::CHILD_FIRST,
+ );
+
+ foreach ($iterator as $file) {
+ @chmod($file->getPathname(), 0777);
+ $file->isDir() ? @rmdir($file->getPathname()) : @unlink($file->getPathname());
+ }
+
+ @rmdir($path);
+}
+
+describe('compute()', function (): void {
+ it('fingerprints tracked structural files in a regular clone', function (): void {
+ $clone = fingerprintRepository();
+
+ try {
+ file_put_contents($clone.'/composer.lock', '{"packages": []}');
+ file_put_contents($clone.'/phpunit.xml', '');
+ fingerprintGit($clone, 'add', '-A');
+ fingerprintGit($clone, 'commit', '-q', '-m', 'init');
+
+ $fingerprint = Fingerprint::compute($clone);
+
+ expect($fingerprint['structural']['composer_lock'])->not->toBeNull()
+ ->and($fingerprint['structural']['phpunit_xml'])->not->toBeNull();
+ } finally {
+ fingerprintRemoveDirectory($clone);
+ }
+ });
+
+ it('fingerprints tracked structural files inside a linked git worktree', function (): void {
+ $clone = fingerprintRepository();
+
+ try {
+ file_put_contents($clone.'/composer.lock', '{"packages": []}');
+ file_put_contents($clone.'/phpunit.xml', '');
+ file_put_contents($clone.'/.gitignore', ".worktrees/\n");
+ fingerprintGit($clone, 'add', '-A');
+ fingerprintGit($clone, 'commit', '-q', '-m', 'init');
+ fingerprintGit($clone, 'worktree', 'add', '-q', '.worktrees/feature');
+
+ $worktree = $clone.'/.worktrees/feature';
+
+ $cloneFingerprint = Fingerprint::compute($clone);
+ $worktreeFingerprint = Fingerprint::compute($worktree);
+
+ expect($worktreeFingerprint['structural']['composer_lock'])->not->toBeNull()
+ ->and($worktreeFingerprint['structural']['phpunit_xml'])->not->toBeNull()
+ ->and($worktreeFingerprint['structural']['composer_lock'])->toBe($cloneFingerprint['structural']['composer_lock'])
+ ->and($worktreeFingerprint['structural']['phpunit_xml'])->toBe($cloneFingerprint['structural']['phpunit_xml']);
+ } finally {
+ fingerprintRemoveDirectory($clone);
+ }
+ });
+
+ it('excludes gitignored untracked files from the fingerprint', function (): void {
+ $clone = fingerprintRepository();
+
+ try {
+ file_put_contents($clone.'/composer.lock', '{"packages": []}');
+ file_put_contents($clone.'/.gitignore', "phpunit.xml\n");
+ fingerprintGit($clone, 'add', '-A');
+ fingerprintGit($clone, 'commit', '-q', '-m', 'init');
+
+ file_put_contents($clone.'/phpunit.xml', '');
+
+ $fingerprint = Fingerprint::compute($clone);
+
+ expect($fingerprint['structural']['phpunit_xml'])->toBeNull()
+ ->and($fingerprint['structural']['composer_lock'])->not->toBeNull();
+ } finally {
+ fingerprintRemoveDirectory($clone);
+ }
+ });
+});