Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/installation/exotic.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ The path to the root directory of your git project.
The path to the directory in which git stores it objects etc.
Most of the time this is the .git folder.
When using GIT submodules, this is the location of the submodule .git folder.
Inside a linked git worktree, this points at the shared repository root, since worktrees run the hooks
of the main repository. File listing and diffs still use the worktree's own branch.

**GRUMPHP_COMPOSER_DIR**

Expand Down
14 changes: 13 additions & 1 deletion src/Configuration/GuessedPaths.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,20 @@ class GuessedPaths
*/
private $configFile;

/**
* @var string
*/
private $gitWorktreeDir;

public function __construct(
string $gitWorkingDir,
string $gitRepositoryDir,
string $workingDir,
string $projectDir,
string $binDir,
ComposerFile $composerFile,
string $configFile
string $configFile,
?string $gitWorktreeDir = null
) {
$this->gitWorkingDir = $gitWorkingDir;
$this->gitRepositoryDir = $gitRepositoryDir;
Expand All @@ -59,6 +65,7 @@ public function __construct(
$this->binDir = $binDir;
$this->composerFile = $composerFile;
$this->configFile = $configFile;
$this->gitWorktreeDir = $gitWorktreeDir ?? $gitRepositoryDir;
}

public function getGitWorkingDir(): string
Expand All @@ -71,6 +78,11 @@ public function getGitRepositoryDir(): string
return $this->gitRepositoryDir;
}

public function getGitWorktreeDir(): string
{
return $this->gitWorktreeDir;
}

public function getWorkingDir(): string
{
return $this->workingDir;
Expand Down
7 changes: 6 additions & 1 deletion src/Locator/EnrichedGuessedPathsFromDotEnvLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public function locate(GuessedPaths $guessedPaths): GuessedPaths
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $guessedPaths->getGitRepositoryDir()),
$workingDir
);
$gitWorktreeDir = $this->filesystem->makePathAbsolute(
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $guessedPaths->getGitWorktreeDir()),
$workingDir
);
$binDir = $this->filesystem->makePathAbsolute(
(string) ($_SERVER['GRUMPHP_BIN_DIR'] ?? $guessedPaths->getBinDir()),
$workingDir
Expand All @@ -49,7 +53,8 @@ public function locate(GuessedPaths $guessedPaths): GuessedPaths
$projectDir,
$binDir,
$guessedPaths->getComposerFile(),
$guessedPaths->getConfigFile()
$guessedPaths->getConfigFile(),
$gitWorktreeDir
);
}
}
24 changes: 22 additions & 2 deletions src/Locator/GitRepositoryDirLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ public function __construct(Filesystem $filesystem)

/**
* Resolves the path to the git repository directory (aka as .git).
* For submodules, it parses the .git file and resolves to the .git/modules/[submodules] directory
* For submodules, it parses the .git file and resolves to the .git/modules/[submodules] directory.
* For worktrees, it resolves to the common repository root, since worktrees share the main
* repository's hooks. Use locateWorktreeGitDir() when the worktree's own git dir is required.
*/
public function locate(string $gitDir): string
{
return $this->resolveGitDir($gitDir, true);
}

/**
* Resolves the git directory used to read files and diffs.
* For worktrees this returns the worktree's own git dir (.git/worktrees/[id]) instead of collapsing
* to the common repository root, so file listing and diffs reflect the worktree's branch.
* For submodules and normal checkouts this is identical to locate().
*/
public function locateWorktreeGitDir(string $gitDir): string
{
return $this->resolveGitDir($gitDir, false);
}

private function resolveGitDir(string $gitDir, bool $collapseWorktreeToRoot): string
{
if (!$this->filesystem->isFile($gitDir)) {
return $gitDir;
Expand All @@ -36,7 +54,9 @@ public function locate(string $gitDir): string
$gitRepositoryDir = $matches[1];

if ($this->isWorktree($gitRepositoryDir)) {
return $this->locateWorktreeRoot($gitRepositoryDir);
return $collapseWorktreeToRoot
? $this->locateWorktreeRoot($gitRepositoryDir)
: $gitRepositoryDir;
}

return $this->filesystem->buildPath(
Expand Down
2 changes: 1 addition & 1 deletion src/Locator/GitRepositoryLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct(Paths $paths)
public function locate(array $options): Repository
{
return new Repository(
$this->paths->getGitRepositoryDir(),
$this->paths->getGitWorktreeDir(),
array_merge(
[
'working_dir' => $this->paths->getGitWorkingDir(),
Expand Down
9 changes: 8 additions & 1 deletion src/Locator/GuessedPathsLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ public function locate(?string $cliConfigFile): GuessedPaths
)),
$workingDir
);
$gitWorktreeDir = $this->filesystem->makePathAbsolute(
(string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $this->gitRepositoryDirLocator->locateWorktreeGitDir(
$this->filesystem->buildPath($gitWorkingDir, '.git')
)),
$workingDir
);

$composerFilePathname = $this->filesystem->guessFile(
[
Expand Down Expand Up @@ -125,7 +131,8 @@ public function locate(?string $cliConfigFile): GuessedPaths
$projectDir,
$binDir,
$composerFile,
$defaultConfigFile
$defaultConfigFile,
$gitWorktreeDir
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/Util/Paths.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ public function getGitRepositoryDir(): string
return $this->guessedPaths->getGitRepositoryDir();
}

/**
* The git dir to read files and diffs from. Inside a linked worktree this is the worktree's own
* git dir; for normal checkouts and submodules it equals getGitRepositoryDir().
*/
public function getGitWorktreeDir(): string
{
return $this->guessedPaths->getGitWorktreeDir();
}

public function getBinDir(): string
{
return $this->guessedPaths->getBinDir();
Expand Down
19 changes: 19 additions & 0 deletions test/E2E/AbstractE2ETestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ protected function initializeGitSubModule(string $gitPath, string $submodulePath
return $this->filesystem->buildPath($gitPath, basename($submodulePath));
}

protected function addGitWorktree(string $fromGitPath, string $worktreeName, string $branch): string
{
$this->changeGitPermissions();
$worktreePath = $this->relativeRootPath($worktreeName);
$this->runCommand('add git worktree', new Process(
[$this->executableFinder->find('git'), 'worktree', 'add', '-b', $branch, $worktreePath],
$fromGitPath
));

return $worktreePath;
}

protected function commitAllWithoutHook(string $gitPath)
{
$git = $this->executableFinder->find('git');
$this->gitAddPath($gitPath);
$this->runCommand('commit without hook', new Process([$git, 'commit', '--no-verify', '-mtest'], $gitPath));
}

protected function appendToGitignore(string $gitPath, array $paths = ['vendor'])
{
$gitignore = $this->filesystem->buildPath($gitPath, '.gitignore');
Expand Down
30 changes: 30 additions & 0 deletions test/E2E/SpecialGitStructuresTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@ function it_runs_inside_a_submodule()
$this->runGrumphp($submoduleInMain);
}

#[Test]
function it_runs_inside_a_worktree()
{
$main = $this->mkdir('main');

$this->initializeGit($main);
$this->appendToGitignore($main);
$this->initializeComposer($main);
$grumphpFile = $this->initializeGrumphpConfig($main);
$this->installComposer($main);
$this->ensureHooksExist($main);

// A file that lives on the main branch but not on the worktree's branch:
$this->dumpFile($this->filesystem->buildPath($main, 'only-on-main.txt'), 'main');
$this->enableValidatePathsTask($grumphpFile, $main);
$this->commitAll($main);

// A linked worktree on a diverging branch that drops the main-only file:
$worktree = $this->addGitWorktree($main, 'worktree', 'feature');
$this->filesystem->remove($this->filesystem->buildPath($worktree, 'only-on-main.txt'));
$this->commitAllWithoutHook($worktree);

$this->installComposer($worktree);
$worktreeGrumphpFile = $this->initializeGrumphpConfig($worktree);
$this->enableValidatePathsTask($worktreeGrumphpFile, $worktree);

// GrumPHP must list the worktree's branch, so only-on-main.txt stays out of the file list:
$this->runGrumphp($worktree);
}

#[Test]
function it_handles_partial_commits()
{
Expand Down
20 changes: 20 additions & 0 deletions test/Unit/Locator/EnrichedGuessedPathsFromDotEnvLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@ function (Filesystem $filesystem, string $workspace) use ($configure) {
];


yield 'keep-distinct-worktree-dir' => [
function (Filesystem $filesystem, string $workspace) use ($configure) {
\Closure::bind($configure, $this)($workspace);
},
$inputWithWorktree = function (string $workspace) {
return new GuessedPaths(
$workspace,
$this->path('.git'),
$workspace,
$workspace,
$this->path('vendor/bin'),
new ComposerFile($this->path('composer.json'), []),
$this->path('grumphp.yml'),
$this->path('.git/worktrees/wt1')
);
},
$inputWithWorktree
];


yield 'overwritten-config' => [
function (Filesystem $filesystem, string $workspace) use ($configure) {
\Closure::bind($configure, $this)($workspace);
Expand Down
50 changes: 50 additions & 0 deletions test/Unit/Locator/GitRepositoryDirLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,54 @@ public function it_can_locate_git_dir_in_workspaces(): void
$this->filesystem->dumpFile($ourWorktreeProject.'/.git', 'gitdir: '.$this->gitDir.'/worktrees/worktree1');
$this->assertEquals($this->gitDir, $this->locator->locate($this->gitDir));
}

#[Test]
public function it_collapses_a_worktree_to_the_common_root_for_hooks(): void
{
[$worktreeGitFile, ] = $this->setupWorktree();

$this->assertEquals($this->gitDir, $this->locator->locate($worktreeGitFile));
}

#[Test]
public function it_locates_the_worktree_own_git_dir_for_file_listing(): void
{
[$worktreeGitFile, $worktreeGitDir] = $this->setupWorktree();

$this->assertEquals($worktreeGitDir, $this->locator->locateWorktreeGitDir($worktreeGitFile));
}

#[Test]
public function it_locates_worktree_git_dir_identically_to_locate_for_normal_checkout(): void
{
$this->filesystem->mkdir($this->gitDir);
$this->assertEquals($this->gitDir, $this->locator->locateWorktreeGitDir($this->gitDir));
}

#[Test]
public function it_locates_worktree_git_dir_identically_to_locate_for_submodule(): void
{
$this->filesystem->dumpFile($this->gitDir, 'gitdir: ../dev/null');
$this->assertEquals(
$this->workspace . DIRECTORY_SEPARATOR . '../dev/null',
$this->locator->locateWorktreeGitDir($this->gitDir)
);
}

/**
* @return array{0: string, 1: string} [worktree .git file, worktree own git dir]
*/
private function setupWorktree(): array
{
$worktreeGitDir = $this->gitDir.DIRECTORY_SEPARATOR.'worktrees'.DIRECTORY_SEPARATOR.'worktree1';
$this->filesystem->mkdir($worktreeGitDir);
$this->filesystem->dumpFile($worktreeGitDir.DIRECTORY_SEPARATOR.'commondir', '../..');

$worktreeProject = $this->workspace.DIRECTORY_SEPARATOR.'worktree-project';
$this->filesystem->mkdir($worktreeProject);
$worktreeGitFile = $worktreeProject.DIRECTORY_SEPARATOR.'.git';
$this->filesystem->dumpFile($worktreeGitFile, 'gitdir: '.$worktreeGitDir);

return [$worktreeGitFile, $worktreeGitDir];
}
}
3 changes: 3 additions & 0 deletions test/Unit/Locator/GuessedPathsLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ protected function setUp(): void
$this->gitRepositoryDirLocator->locate(Argument::any())->will(function (array $arguments) {
return $arguments[0];
});
$this->gitRepositoryDirLocator->locateWorktreeGitDir(Argument::any())->will(function (array $arguments) {
return $arguments[0];
});

$this->guesser = new GuessedPathsLocator(
$this->filesystem,
Expand Down
29 changes: 29 additions & 0 deletions test/Unit/Util/PathsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ public function it_knows_the_git_hooks_directory(): void
);
}

#[Test]
public function it_knows_the_git_worktree_directory(): void
{
$this->assertSame(
$this->guessedPaths->getGitWorktreeDir(),
$this->paths->getGitWorktreeDir()
);
}

#[Test]
public function it_separates_the_worktree_dir_from_the_hooks_dir_inside_a_worktree(): void
{
$guessedPaths = new GuessedPaths(
$this->workspace,
$common = $this->buildPath($this->workspace, '.git'),
$this->workspace,
$this->workspace,
$this->buildPath($this->workspace, 'vendor/bin'),
new ComposerFile($this->buildPath($this->workspace, 'composer.json'), []),
$this->buildPath($this->workspace, 'grumphp.json'),
$worktree = $this->buildPath($this->workspace, '.git/worktrees/wt1')
);
$paths = new Paths($this->filesystem, $guessedPaths);

$this->assertSame($worktree, $paths->getGitWorktreeDir());
$this->assertSame($common, $paths->getGitRepositoryDir());
$this->assertSame($this->buildPath($common, 'hooks'), $paths->getGitHooksDir());
}

#[Test]
public function it_knows_the_project_directory(): void
{
Expand Down
Loading