Skip to content

Commit 15afba6

Browse files
committed
Read files from the worktree git dir instead of the shared repository
Inside a linked git worktree, GrumPHP listed and diffed files against the main repository's branch instead of the worktree's own. A task could then fail on a file that exists on main but not in the worktree: yamllint: SplFileInfo::openFile(config/services/commands.yaml): Failed to open stream: No such file or directory A worktree's .git is a file pointing at <main>/.git/worktrees/<id>, and that directory's commondir points back to the shared <main>/.git. GrumPHP collapsed the worktree straight to that shared directory and used it for everything, including the git client that lists files. So "git --git-dir=<main>/.git ls-files" returned main's index. The gitonomy client passes --git-dir on the command line, which overrides GIT_DIR, so correcting the environment around GrumPHP could not reach it. Collapsing to the shared directory is correct for hooks: a worktree runs the main repository's hooks. So the two concerns are now separate. Hooks still resolve to the shared .git; the git client that lists files and reads diffs points at the worktree's own git dir. A normal checkout and a submodule resolve as before. GRUMPHP_GIT_REPOSITORY_DIR still overrides the resolved directory and now drives both the hooks dir and the file-listing dir, which matches how it behaved before the split.
1 parent 738616a commit 15afba6

13 files changed

Lines changed: 212 additions & 6 deletions

doc/installation/exotic.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ The path to the root directory of your git project.
9494
The path to the directory in which git stores it objects etc.
9595
Most of the time this is the .git folder.
9696
When using GIT submodules, this is the location of the submodule .git folder.
97+
Inside a linked git worktree, this points at the shared repository root, since worktrees run the hooks
98+
of the main repository. File listing and diffs still use the worktree's own branch.
9799

98100
**GRUMPHP_COMPOSER_DIR**
99101

src/Configuration/GuessedPaths.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ class GuessedPaths
4343
*/
4444
private $configFile;
4545

46+
/**
47+
* @var string
48+
*/
49+
private $gitWorktreeDir;
50+
4651
public function __construct(
4752
string $gitWorkingDir,
4853
string $gitRepositoryDir,
4954
string $workingDir,
5055
string $projectDir,
5156
string $binDir,
5257
ComposerFile $composerFile,
53-
string $configFile
58+
string $configFile,
59+
?string $gitWorktreeDir = null
5460
) {
5561
$this->gitWorkingDir = $gitWorkingDir;
5662
$this->gitRepositoryDir = $gitRepositoryDir;
@@ -59,6 +65,7 @@ public function __construct(
5965
$this->binDir = $binDir;
6066
$this->composerFile = $composerFile;
6167
$this->configFile = $configFile;
68+
$this->gitWorktreeDir = $gitWorktreeDir ?? $gitRepositoryDir;
6269
}
6370

6471
public function getGitWorkingDir(): string
@@ -71,6 +78,11 @@ public function getGitRepositoryDir(): string
7178
return $this->gitRepositoryDir;
7279
}
7380

81+
public function getGitWorktreeDir(): string
82+
{
83+
return $this->gitWorktreeDir;
84+
}
85+
7486
public function getWorkingDir(): string
7587
{
7688
return $this->workingDir;

src/Locator/EnrichedGuessedPathsFromDotEnvLocator.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public function locate(GuessedPaths $guessedPaths): GuessedPaths
3737
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $guessedPaths->getGitRepositoryDir()),
3838
$workingDir
3939
);
40+
$gitWorktreeDir = $this->filesystem->makePathAbsolute(
41+
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $guessedPaths->getGitWorktreeDir()),
42+
$workingDir
43+
);
4044
$binDir = $this->filesystem->makePathAbsolute(
4145
(string) ($_SERVER['GRUMPHP_BIN_DIR'] ?? $guessedPaths->getBinDir()),
4246
$workingDir
@@ -49,7 +53,8 @@ public function locate(GuessedPaths $guessedPaths): GuessedPaths
4953
$projectDir,
5054
$binDir,
5155
$guessedPaths->getComposerFile(),
52-
$guessedPaths->getConfigFile()
56+
$guessedPaths->getConfigFile(),
57+
$gitWorktreeDir
5358
);
5459
}
5560
}

src/Locator/GitRepositoryDirLocator.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,27 @@ public function __construct(Filesystem $filesystem)
2020

2121
/**
2222
* Resolves the path to the git repository directory (aka as .git).
23-
* For submodules, it parses the .git file and resolves to the .git/modules/[submodules] directory
23+
* For submodules, it parses the .git file and resolves to the .git/modules/[submodules] directory.
24+
* For worktrees, it resolves to the common repository root, since worktrees share the main
25+
* repository's hooks. Use locateWorktreeGitDir() when the worktree's own git dir is required.
2426
*/
2527
public function locate(string $gitDir): string
28+
{
29+
return $this->resolveGitDir($gitDir, true);
30+
}
31+
32+
/**
33+
* Resolves the git directory used to read files and diffs.
34+
* For worktrees this returns the worktree's own git dir (.git/worktrees/[id]) instead of collapsing
35+
* to the common repository root, so file listing and diffs reflect the worktree's branch.
36+
* For submodules and normal checkouts this is identical to locate().
37+
*/
38+
public function locateWorktreeGitDir(string $gitDir): string
39+
{
40+
return $this->resolveGitDir($gitDir, false);
41+
}
42+
43+
private function resolveGitDir(string $gitDir, bool $collapseWorktreeToRoot): string
2644
{
2745
if (!$this->filesystem->isFile($gitDir)) {
2846
return $gitDir;
@@ -36,7 +54,9 @@ public function locate(string $gitDir): string
3654
$gitRepositoryDir = $matches[1];
3755

3856
if ($this->isWorktree($gitRepositoryDir)) {
39-
return $this->locateWorktreeRoot($gitRepositoryDir);
57+
return $collapseWorktreeToRoot
58+
? $this->locateWorktreeRoot($gitRepositoryDir)
59+
: $gitRepositoryDir;
4060
}
4161

4262
return $this->filesystem->buildPath(

src/Locator/GitRepositoryLocator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(Paths $paths)
2222
public function locate(array $options): Repository
2323
{
2424
return new Repository(
25-
$this->paths->getGitRepositoryDir(),
25+
$this->paths->getGitWorktreeDir(),
2626
array_merge(
2727
[
2828
'working_dir' => $this->paths->getGitWorkingDir(),

src/Locator/GuessedPathsLocator.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public function locate(?string $cliConfigFile): GuessedPaths
5656
)),
5757
$workingDir
5858
);
59+
$gitWorktreeDir = $this->filesystem->makePathAbsolute(
60+
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $this->gitRepositoryDirLocator->locateWorktreeGitDir(
61+
$this->filesystem->buildPath($gitWorkingDir, '.git')
62+
)),
63+
$workingDir
64+
);
5965

6066
$composerFilePathname = $this->filesystem->guessFile(
6167
[
@@ -125,7 +131,8 @@ public function locate(?string $cliConfigFile): GuessedPaths
125131
$projectDir,
126132
$binDir,
127133
$composerFile,
128-
$defaultConfigFile
134+
$defaultConfigFile,
135+
$gitWorktreeDir
129136
);
130137
}
131138

src/Util/Paths.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ public function getGitRepositoryDir(): string
6969
return $this->guessedPaths->getGitRepositoryDir();
7070
}
7171

72+
/**
73+
* The git dir to read files and diffs from. Inside a linked worktree this is the worktree's own
74+
* git dir; for normal checkouts and submodules it equals getGitRepositoryDir().
75+
*/
76+
public function getGitWorktreeDir(): string
77+
{
78+
return $this->guessedPaths->getGitWorktreeDir();
79+
}
80+
7281
public function getBinDir(): string
7382
{
7483
return $this->guessedPaths->getBinDir();

test/E2E/AbstractE2ETestCase.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ protected function initializeGitSubModule(string $gitPath, string $submodulePath
8787
return $this->filesystem->buildPath($gitPath, basename($submodulePath));
8888
}
8989

90+
protected function addGitWorktree(string $fromGitPath, string $worktreeName, string $branch): string
91+
{
92+
$this->changeGitPermissions();
93+
$worktreePath = $this->relativeRootPath($worktreeName);
94+
$this->runCommand('add git worktree', new Process(
95+
[$this->executableFinder->find('git'), 'worktree', 'add', '-b', $branch, $worktreePath],
96+
$fromGitPath
97+
));
98+
99+
return $worktreePath;
100+
}
101+
102+
protected function commitAllWithoutHook(string $gitPath)
103+
{
104+
$git = $this->executableFinder->find('git');
105+
$this->gitAddPath($gitPath);
106+
$this->runCommand('commit without hook', new Process([$git, 'commit', '--no-verify', '-mtest'], $gitPath));
107+
}
108+
90109
protected function appendToGitignore(string $gitPath, array $paths = ['vendor'])
91110
{
92111
$gitignore = $this->filesystem->buildPath($gitPath, '.gitignore');

test/E2E/SpecialGitStructuresTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,36 @@ function it_runs_inside_a_submodule()
2929
$this->runGrumphp($submoduleInMain);
3030
}
3131

32+
#[Test]
33+
function it_runs_inside_a_worktree()
34+
{
35+
$main = $this->mkdir('main');
36+
37+
$this->initializeGit($main);
38+
$this->appendToGitignore($main);
39+
$this->initializeComposer($main);
40+
$grumphpFile = $this->initializeGrumphpConfig($main);
41+
$this->installComposer($main);
42+
$this->ensureHooksExist($main);
43+
44+
// A file that lives on the main branch but not on the worktree's branch:
45+
$this->dumpFile($this->filesystem->buildPath($main, 'only-on-main.txt'), 'main');
46+
$this->enableValidatePathsTask($grumphpFile, $main);
47+
$this->commitAll($main);
48+
49+
// A linked worktree on a diverging branch that drops the main-only file:
50+
$worktree = $this->addGitWorktree($main, 'worktree', 'feature');
51+
$this->filesystem->remove($this->filesystem->buildPath($worktree, 'only-on-main.txt'));
52+
$this->commitAllWithoutHook($worktree);
53+
54+
$this->installComposer($worktree);
55+
$worktreeGrumphpFile = $this->initializeGrumphpConfig($worktree);
56+
$this->enableValidatePathsTask($worktreeGrumphpFile, $worktree);
57+
58+
// GrumPHP must list the worktree's branch, so only-on-main.txt stays out of the file list:
59+
$this->runGrumphp($worktree);
60+
}
61+
3262
#[Test]
3363
function it_handles_partial_commits()
3464
{

test/Unit/Locator/EnrichedGuessedPathsFromDotEnvLocatorTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@ function (Filesystem $filesystem, string $workspace) use ($configure) {
8383
];
8484

8585

86+
yield 'keep-distinct-worktree-dir' => [
87+
function (Filesystem $filesystem, string $workspace) use ($configure) {
88+
\Closure::bind($configure, $this)($workspace);
89+
},
90+
$inputWithWorktree = function (string $workspace) {
91+
return new GuessedPaths(
92+
$workspace,
93+
$this->path('.git'),
94+
$workspace,
95+
$workspace,
96+
$this->path('vendor/bin'),
97+
new ComposerFile($this->path('composer.json'), []),
98+
$this->path('grumphp.yml'),
99+
$this->path('.git/worktrees/wt1')
100+
);
101+
},
102+
$inputWithWorktree
103+
];
104+
105+
86106
yield 'overwritten-config' => [
87107
function (Filesystem $filesystem, string $workspace) use ($configure) {
88108
\Closure::bind($configure, $this)($workspace);

0 commit comments

Comments
 (0)