Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Changelog

All notable changes to this project will be documented in this file.
Expand All @@ -11,6 +11,7 @@

### Fixed

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

## [1.24.3] - 2026-04-30
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ following steps:
``.editorconfig``, and ``.github/dependabot.yml``, and refreshes
``.gitignore``, ``.gitattributes``, the project license, and packaged Git
hooks that prefer a project-local ``grumphp.yml`` override and otherwise
use the active packaged DevTools ``grumphp.yml`` path.
use a project-relative reference to the active packaged DevTools
``grumphp.yml`` path.
6. If ``.github/wiki`` is missing, ``dev-tools:sync`` adds it as a Git
submodule that points to the repository wiki.
7. ``dev-tools:sync`` runs ``gitignore`` to merge canonical ignore rules into
Expand Down
4 changes: 2 additions & 2 deletions docs/running/specialized-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ Important details:
.gitignore;
- it calls ``gitattributes`` to manage export-ignore rules in .gitattributes;
- it refreshes packaged Git hooks that prefer a local ``grumphp.yml``
override and otherwise use the active packaged DevTools ``grumphp.yml``
path resolved when sync installs them;
override and otherwise use a project-relative reference to the active
packaged DevTools ``grumphp.yml`` path resolved when sync installs them;
- it calls ``skills`` so ``.agents/skills`` contains links to the packaged
skill set;
- it calls ``agents`` so ``.agents/agents`` contains links to the packaged
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/syncing-consumer-projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ What the Command Changes
- Only when missing.
* - ``.git/hooks/*``
- Copies packaged hooks that prefer a local ``grumphp.yml`` override and
otherwise use the active packaged DevTools ``grumphp.yml`` path
resolved when sync installs them.
otherwise use a project-relative reference to the active packaged
DevTools ``grumphp.yml`` path resolved when sync installs them.
- Replaced when drift is detected.

When to Run It
Expand Down
3 changes: 2 additions & 1 deletion src/Console/Command/GitHooksCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$sourcePath = $this->fileLocator->locate((string) $input->getOption('source'));
$targetPath = (string) $this->filesystem->getAbsolutePath((string) $input->getOption('target'));
$projectPath = Path::canonicalize(Path::join($targetPath, '..', '..'));
Comment thread
coisa marked this conversation as resolved.
Outdated
$overwrite = ! $input->getOption('no-overwrite');
$dryRun = (bool) $input->getOption('dry-run');
$check = (bool) $input->getOption('check');
Expand All @@ -147,7 +148,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
foreach ($files as $file) {
$sourcePath = $file->getRealPath();
$sourceContents = $this->filesystem->readFile($sourcePath);
$renderedSourceContents = $this->hookContentRenderer->render($sourceContents);
$renderedSourceContents = $this->hookContentRenderer->render($sourceContents, $projectPath);
$hookPath = Path::join($targetPath, $file->getRelativePathname());

if (! $overwrite && ! $dryRun && ! $check && ! $interactive && $this->filesystem->exists($hookPath)) {
Expand Down
7 changes: 4 additions & 3 deletions src/GitHooks/HookContentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,23 @@
final class HookContentRenderer
{
/**
* Placeholder replaced with the active packaged GrumPHP config path.
* Placeholder replaced with the project-relative packaged GrumPHP config path.
*/
public const string MANAGED_GRUMPHP_CONFIG_PLACEHOLDER = '__DEV_TOOLS_GRUMPHP_CONFIG__';

/**
* Renders the hook contents for the active DevTools runtime.
*
* @param string $contents the packaged hook contents
* @param string $projectPath the consumer project root that will own the synchronized hook
*
* @return string the rendered hook contents
*/
public function render(string $contents): string
public function render(string $contents, string $projectPath = ''): string
{
return str_replace(
self::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER,
escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml')),
escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', $projectPath)),
$contents,
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/Path/DevToolsPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,28 @@ public static function getResourcesPath(string $path = ''): string
return self::getPackagePath(Path::join(self::RESOURCES, $path));
}

/**
* Returns a package-relative path rendered relative to the active project root.
*
* @param string $path the relative path under the package root
* @param string $projectPath an optional project root path; defaults to the working project root
* @param string $packagePath an optional package root path; defaults to the current package root
*/
public static function getPackagePathRelativeToProject(
string $path,
string $projectPath = '',
string $packagePath = '',
): string {
if (Path::isAbsolute($path)) {
throw new InvalidArgumentException('The DevTools package path MUST be relative to the package root.');
}

$projectPath = Path::canonicalize(WorkingProjectPathResolver::getProjectPath($projectPath));
$packagePath = Path::canonicalize('' === $packagePath ? self::getPackagePath() : $packagePath);

return Path::makeRelative(Path::join($packagePath, $path), $projectPath);
Comment thread
coisa marked this conversation as resolved.
Outdated
}

/**
* Returns the active Composer autoload file for the current DevTools installation mode.
*
Expand Down
4 changes: 3 additions & 1 deletion tests/Console/Command/GitHooksCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use FastForward\DevTools\Filesystem\FilesystemInterface;
use FastForward\DevTools\GitHooks\HookContentRenderer;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Path\WorkingProjectPathResolver;
use FastForward\DevTools\Resource\FileDiff;
use FastForward\DevTools\Resource\FileDiffer;
use PHPUnit\Framework\Attributes\CoversClass;
Expand Down Expand Up @@ -54,6 +55,7 @@

#[CoversClass(GitHooksCommand::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(WorkingProjectPathResolver::class)]
#[UsesClass(FileDiff::class)]
#[UsesClass(HookContentRenderer::class)]
#[UsesTrait(LogsCommandResults::class)]
Expand Down Expand Up @@ -230,7 +232,7 @@ public function executeWillRenderManagedGrumPhpConfigIntoPlaceholderHooks(): voi
Argument::that(
static fn(string $contents): bool => str_contains(
$contents,
escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml'))
escapeshellarg(DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/app'))
)
),
)->shouldBeCalledOnce();
Expand Down
11 changes: 9 additions & 2 deletions tests/GitHooks/HookContentRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@

use FastForward\DevTools\GitHooks\HookContentRenderer;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Path\WorkingProjectPathResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(HookContentRenderer::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(WorkingProjectPathResolver::class)]
final class HookContentRendererTest extends TestCase
{
/**
Expand All @@ -39,8 +41,13 @@ public function renderWillReplaceTheManagedGrumPhpConfigPlaceholder(): void
$renderer = new HookContentRenderer();

self::assertSame(
'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg(DevToolsPathResolver::getPackagePath('grumphp.yml')),
$renderer->render('DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER),
'DEVTOOLS_GRUMPHP_CONFIG=' . escapeshellarg(
DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', '/workspaces/project')
),
$renderer->render(
'DEVTOOLS_GRUMPHP_CONFIG=' . HookContentRenderer::MANAGED_GRUMPHP_CONFIG_PLACEHOLDER,
'/workspaces/project'
),
);
}
}
20 changes: 20 additions & 0 deletions tests/Path/DevToolsPathResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function itWillExposeCanonicalPackagePaths(): void
self::assertSame(\dirname(__DIR__, 2), DevToolsPathResolver::getPackagePath());
self::assertSame(\dirname(__DIR__, 2) . '/bin/dev-tools', DevToolsPathResolver::getBinaryPath());
self::assertSame(\dirname(__DIR__, 2) . '/resources', DevToolsPathResolver::getResourcesPath());
self::assertSame(
'grumphp.yml',
DevToolsPathResolver::getPackagePathRelativeToProject('grumphp.yml', \dirname(__DIR__, 2))
);
self::assertSame(\dirname(__DIR__, 2) . '/vendor/autoload.php', DevToolsPathResolver::getRuntimeAutoloadPath());
self::assertSame(
\dirname(__DIR__, 2) . '/vendor/bin/ecs',
Expand Down Expand Up @@ -114,6 +118,22 @@ public function itWillResolveRuntimeAutoloadAndVendorPathsForRepositoryAndDepend
'/workspaces/project/vendor/fast-forward/dev-tools'
)
);
self::assertSame(
'vendor/fast-forward/dev-tools/grumphp.yml',
DevToolsPathResolver::getPackagePathRelativeToProject(
'grumphp.yml',
'/workspaces/project',
'/workspaces/project/vendor/fast-forward/dev-tools'
)
);
self::assertSame(
'../../Users/example/.composer/vendor/fast-forward/dev-tools/grumphp.yml',
DevToolsPathResolver::getPackagePathRelativeToProject(
'grumphp.yml',
'/workspaces/project',
'/Users/example/.composer/vendor/fast-forward/dev-tools'
)
);
}

/**
Expand Down
Loading