Skip to content

Commit 7365228

Browse files
committed
Fix global GrumPHP hook fallback rendering
1 parent 2d1d2ed commit 7365228

10 files changed

Lines changed: 67 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Fixed
1313

14+
- Render managed GrumPHP hook fallback paths relative to the consumer project so global `dev-tools:sync` installs keep local GrumPHP hook execution working (#305)
1415
- Restore global DevTools new-version notifications by using a supported Symfony Process success check during release lookups (#300)
1516

1617
## [1.24.3] - 2026-04-30

docs/getting-started/installation.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ following steps:
3838
``.editorconfig``, and ``.github/dependabot.yml``, and refreshes
3939
``.gitignore``, ``.gitattributes``, the project license, and packaged Git
4040
hooks that prefer a project-local ``grumphp.yml`` override and otherwise
41-
use the active packaged DevTools ``grumphp.yml`` path.
41+
use a project-relative reference to the active packaged DevTools
42+
``grumphp.yml`` path.
4243
6. If ``.github/wiki`` is missing, ``dev-tools:sync`` adds it as a Git
4344
submodule that points to the repository wiki.
4445
7. ``dev-tools:sync`` runs ``gitignore`` to merge canonical ignore rules into

docs/running/specialized-commands.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,8 @@ Important details:
390390
.gitignore;
391391
- it calls ``gitattributes`` to manage export-ignore rules in .gitattributes;
392392
- it refreshes packaged Git hooks that prefer a local ``grumphp.yml``
393-
override and otherwise use the active packaged DevTools ``grumphp.yml``
394-
path resolved when sync installs them;
393+
override and otherwise use a project-relative reference to the active
394+
packaged DevTools ``grumphp.yml`` path resolved when sync installs them;
395395
- it calls ``skills`` so ``.agents/skills`` contains links to the packaged
396396
skill set;
397397
- it calls ``agents`` so ``.agents/agents`` contains links to the packaged

docs/usage/syncing-consumer-projects.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ What the Command Changes
5353
- Only when missing.
5454
* - ``.git/hooks/*``
5555
- Copies packaged hooks that prefer a local ``grumphp.yml`` override and
56-
otherwise use the active packaged DevTools ``grumphp.yml`` path
57-
resolved when sync installs them.
56+
otherwise use a project-relative reference to the active packaged
57+
DevTools ``grumphp.yml`` path resolved when sync installs them.
5858
- Replaced when drift is detected.
5959

6060
When to Run It

src/Console/Command/GitHooksCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
131131
{
132132
$sourcePath = $this->fileLocator->locate((string) $input->getOption('source'));
133133
$targetPath = (string) $this->filesystem->getAbsolutePath((string) $input->getOption('target'));
134+
$projectPath = Path::canonicalize(Path::join($targetPath, '..', '..'));
134135
$overwrite = ! $input->getOption('no-overwrite');
135136
$dryRun = (bool) $input->getOption('dry-run');
136137
$check = (bool) $input->getOption('check');
@@ -147,7 +148,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
147148
foreach ($files as $file) {
148149
$sourcePath = $file->getRealPath();
149150
$sourceContents = $this->filesystem->readFile($sourcePath);
150-
$renderedSourceContents = $this->hookContentRenderer->render($sourceContents);
151+
$renderedSourceContents = $this->hookContentRenderer->render($sourceContents, $projectPath);
151152
$hookPath = Path::join($targetPath, $file->getRelativePathname());
152153

153154
if (! $overwrite && ! $dryRun && ! $check && ! $interactive && $this->filesystem->exists($hookPath)) {

src/GitHooks/HookContentRenderer.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,23 @@
2727
final class HookContentRenderer
2828
{
2929
/**
30-
* Placeholder replaced with the active packaged GrumPHP config path.
30+
* Placeholder replaced with the project-relative packaged GrumPHP config path.
3131
*/
3232
public const string MANAGED_GRUMPHP_CONFIG_PLACEHOLDER = '__DEV_TOOLS_GRUMPHP_CONFIG__';
3333

3434
/**
3535
* Renders the hook contents for the active DevTools runtime.
3636
*
3737
* @param string $contents the packaged hook contents
38+
* @param string $projectPath the consumer project root that will own the synchronized hook
3839
*
3940
* @return string the rendered hook contents
4041
*/
41-
public function render(string $contents): string
42+
public function render(string $contents, string $projectPath = ''): string
4243
{
4344
return str_replace(
4445
self::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER,
45-
escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml')),
46+
escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', $projectPath)),
4647
$contents,
4748
);
4849
}

src/Path/DevToolsPathResolver.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,28 @@ public static function getResourcesPath(string $path = ''): string
8888
return self::getPackagePath(Path::join(self::RESOURCES, $path));
8989
}
9090

91+
/**
92+
* Returns a package-relative path rendered relative to the active project root.
93+
*
94+
* @param string $path the relative path under the package root
95+
* @param string $projectPath an optional project root path; defaults to the working project root
96+
* @param string $packagePath an optional package root path; defaults to the current package root
97+
*/
98+
public static function getPackagePathRelativeToProject(
99+
string $path,
100+
string $projectPath = '',
101+
string $packagePath = '',
102+
): string {
103+
if (Path::isAbsolute($path)) {
104+
throw new InvalidArgumentException('The DevTools package path MUST be relative to the package root.');
105+
}
106+
107+
$projectPath = Path::canonicalize(WorkingProjectPathResolver::getProjectPath($projectPath));
108+
$packagePath = Path::canonicalize('' === $packagePath ? self::getPackagePath() : $packagePath);
109+
110+
return Path::makeRelative(Path::join($packagePath, $path), $projectPath);
111+
}
112+
91113
/**
92114
* Returns the active Composer autoload file for the current DevTools installation mode.
93115
*

tests/Console/Command/GitHooksCommandTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use FastForward\DevTools\Filesystem\FilesystemInterface;
2626
use FastForward\DevTools\GitHooks\HookContentRenderer;
2727
use FastForward\DevTools\Path\DevToolsPathResolver;
28+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2829
use FastForward\DevTools\Resource\FileDiff;
2930
use FastForward\DevTools\Resource\FileDiffer;
3031
use PHPUnit\Framework\Attributes\CoversClass;
@@ -54,6 +55,7 @@
5455

5556
#[CoversClass(GitHooksCommand::class)]
5657
#[UsesClass(DevToolsPathResolver::class)]
58+
#[UsesClass(WorkingProjectPathResolver::class)]
5759
#[UsesClass(FileDiff::class)]
5860
#[UsesClass(HookContentRenderer::class)]
5961
#[UsesTrait(LogsCommandResults::class)]
@@ -230,7 +232,7 @@ public function executeWillRenderManagedGrumPhpConfigIntoPlaceholderHooks(): voi
230232
Argument::that(
231233
static fn(string $contents): bool => str_contains(
232234
$contents,
233-
escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml'))
235+
escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/app'))
234236
)
235237
),
236238
)->shouldBeCalledOnce();

tests/GitHooks/HookContentRendererTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121

2222
use FastForward\DevTools\GitHooks\HookContentRenderer;
2323
use FastForward\DevTools\Path\DevToolsPathResolver;
24+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2425
use PHPUnit\Framework\Attributes\CoversClass;
2526
use PHPUnit\Framework\Attributes\Test;
2627
use PHPUnit\Framework\Attributes\UsesClass;
2728
use PHPUnit\Framework\TestCase;
2829

2930
#[CoversClass(HookContentRenderer::class)]
3031
#[UsesClass(DevToolsPathResolver::class)]
32+
#[UsesClass(WorkingProjectPathResolver::class)]
3133
final class HookContentRendererTest extends TestCase
3234
{
3335
/**
@@ -39,8 +41,13 @@ public function renderWillReplaceTheManagedGrumPhpConfigPlaceholder(): void
3941
$renderer = new HookContentRenderer();
4042

4143
self::assertSame(
42-
'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml')),
43-
$renderer->render('DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER),
44+
'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg(
45+
DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/workspaces/project')
46+
),
47+
$renderer->render(
48+
'DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER,
49+
'/workspaces/project'
50+
),
4451
);
4552
}
4653
}

tests/Path/DevToolsPathResolverTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function itWillExposeCanonicalPackagePaths(): void
4646
self::assertSame(\dirname(__DIR__, 2), DevToolsPathResolver::getPackagePath());
4747
self::assertSame(\dirname(__DIR__, 2) . '/bin/dev-tools', DevToolsPathResolver::getBinaryPath());
4848
self::assertSame(\dirname(__DIR__, 2) . '/resources', DevToolsPathResolver::getResourcesPath());
49+
self::assertSame(
50+
'grumphp.yml',
51+
DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', \dirname(__DIR__, 2))
52+
);
4953
self::assertSame(\dirname(__DIR__, 2) . '/vendor/autoload.php', DevToolsPathResolver::getRuntimeAutoloadPath());
5054
self::assertSame(
5155
\dirname(__DIR__, 2) . '/vendor/bin/ecs',
@@ -114,6 +118,22 @@ public function itWillResolveRuntimeAutoloadAndVendorPathsForRepositoryAndDepend
114118
'/workspaces/project/vendor/fast-forward/dev-tools'
115119
)
116120
);
121+
self::assertSame(
122+
'vendor/fast-forward/dev-tools/grumphp.yml',
123+
DevToolsPathResolver::getPackagePathRelativeToProject(
124+
'grumphp.yml',
125+
'/workspaces/project',
126+
'/workspaces/project/vendor/fast-forward/dev-tools'
127+
)
128+
);
129+
self::assertSame(
130+
'../../Users/example/.composer/vendor/fast-forward/dev-tools/grumphp.yml',
131+
DevToolsPathResolver::getPackagePathRelativeToProject(
132+
'grumphp.yml',
133+
'/workspaces/project',
134+
'/Users/example/.composer/vendor/fast-forward/dev-tools'
135+
)
136+
);
117137
}
118138

119139
/**

0 commit comments

Comments
 (0)