From daee86d8f0cb8ad05735417b3bc4f1699ccf9ab0 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 17 Mar 2026 20:09:09 +0000 Subject: [PATCH 1/6] fix: update README to correct package references from sauce-base to saucebase-dev --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2269594..3adad80 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![PHP Version](https://img.shields.io/badge/PHP-8.4%2B-777BB4?logo=php&logoColor=white)](#requirements) [![Composer](https://img.shields.io/badge/Composer-2.x-885630?logo=composer&logoColor=white)](#requirements) -[![Tests](https://github.com/sauce-base/module-installer/actions/workflows/php.yml/badge.svg)](https://github.com/sauce-base/module-installer/actions/workflows/php.yml) +[![Tests](https://github.com/saucebase-dev/module-installer/actions/workflows/php.yml/badge.svg)](https://github.com/saucebase-dev/module-installer/actions/workflows/php.yml) [![License](https://img.shields.io/badge/License-MIT-0A7EA4)](#license) -This Composer plugin installs Sauce Base modules into the correct directory. It ships with `sauce-base/core`, so every module that your project requires is placed where Sauce Base can find and load it. The installer stays compatible with [nWidart/laravel-modules](https://github.com/nWidart/laravel-modules) and offers a Sauce Base-focused alternative to [joshbrw/laravel-module-installer](https://github.com/joshbrw/laravel-module-installer). +This Composer plugin installs Sauce Base modules into the correct directory. It ships with `saucebase-dev/saucebase`, so every module that your project requires is placed where Sauce Base can find and load it. The installer stays compatible with [nWidart/laravel-modules](https://github.com/nWidart/laravel-modules) and offers a Sauce Base-focused alternative to [joshbrw/laravel-module-installer](https://github.com/joshbrw/laravel-module-installer). ## How It Works @@ -18,11 +18,11 @@ This Composer plugin installs Sauce Base modules into the correct directory. It - PHP 8.4 or newer - Composer 2.x -- A project based on `sauce-base/core` (the core already requires this plugin) +- A project based on `saucebase-dev/saucebase` (the core already requires this plugin) ## Installation -`sauce-base/core` already requires this package. When you install the core, Composer pulls in the plugin and activates it through the `Saucebase\\ModuleInstaller\\Plugin` class, so a typical Sauce Base project needs no extra configuration. +`saucebase-dev/saucebase` already requires this package. When you install the core, Composer pulls in the plugin and activates it through the `Saucebase\\ModuleInstaller\\Plugin` class, so a typical Sauce Base project needs no extra configuration. Need the installer for a different Composer project? Require it directly: From 421f231663e94f64bead91c56b266021578c04a5 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 17 Mar 2026 21:05:10 +0000 Subject: [PATCH 2/6] feat: implement mergeStash method for handling user edits and upstream changes --- composer.json | 4 +- src/Installer.php | 87 ++++++++++++++--- tests/ModuleInstallerTest.php | 169 ++++++++++++++++++++++++++++------ 3 files changed, 218 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index 1fd6135..faf6e55 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,9 @@ ], "require": { "php": "^8.4", - "composer-plugin-api": "^2.0" + "composer-plugin-api": "^2.0", + "symfony/finder": "^7.0", + "symfony/process": "^7.0" }, "require-dev": { "composer/composer": "^2.0", diff --git a/src/Installer.php b/src/Installer.php index 1687bcb..01d6397 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -11,6 +11,8 @@ use React\Promise\PromiseInterface; use Saucebase\ModuleInstaller\Exceptions\ModuleInstallerException; use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Process\Process; /** * @property PartialComposer $composer @@ -198,16 +200,64 @@ protected function stashModuleDir(string $path): ?string } /** - * Mirror the stash directory back into the install path so user edits win. + * Download the given package version into $basePath using Composer's DownloadManager + * so the dist cache is reused. Returns a promise that resolves when ready. */ - protected function restoreStash(string $stashPath, string $installPath): void + protected function downloadBase(PackageInterface $initial, string $basePath): PromiseInterface { - (new SymfonyFilesystem)->mirror( - $stashPath, - $installPath, - null, - ['override' => true, 'delete' => false] - ); + $dm = $this->composer->getDownloadManager(); + + return $dm->download($initial, $basePath) + ->then(fn () => $dm->install($initial, $basePath)); + } + + /** + * 3-way merge stash (user's copy) against base (original version) into install (new version). + * + * Decision table: + * stash + base + install → git merge-file (3-way merge) + * stash + base, no install → upstream deleted the file — leave it gone + * stash only (no base) → user-added file — copy to install + * install only → upstream-added file — already there, no action needed + */ + protected function mergeStash(string $stash, string $base, string $install): void + { + $finder = (new Finder)->files()->in($stash); + + foreach ($finder as $file) { + $rel = $file->getRelativePathname(); + $stashFile = $stash . '/' . $rel; + $baseFile = $base . '/' . $rel; + $newFile = $install . '/' . $rel; + + if (! file_exists($baseFile)) { + // User-added file (not in original dist) — always keep + (new SymfonyFilesystem)->copy($stashFile, $newFile, true); + continue; + } + + if (! file_exists($newFile)) { + // Upstream deleted this file — respect the deletion, do not restore + continue; + } + + // All three versions exist — 3-way merge + $merged = $stashFile . '.merge-work'; + copy($stashFile, $merged); + + $process = new Process( + ['git', 'merge-file', '-L', 'yours', '-L', 'original', '-L', 'upstream', + $merged, $baseFile, $newFile] + ); + $process->run(); + + // exit code >0 = conflict markers inserted, but result is still usable + if ($process->getExitCode() > 0) { + $this->io->writeError(" Merge conflict in $rel — conflict markers inserted"); + } + + rename($merged, $newFile); + } } /** @@ -232,25 +282,36 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): ?PromiseInterface { $stashPath = null; + $basePath = null; if ($this->getUpdateStrategy() === self::UPDATE_STRATEGY_MERGE) { $stashPath = $this->stashModuleDir($this->getInstallPath($initial)); + if ($stashPath !== null) { + $basePath = sys_get_temp_dir() . '/module-base-' . uniqid('', true); + } } - $promise = parent::update($repo, $initial, $target); + $prepareBase = ($basePath !== null) + ? $this->downloadBase($initial, $basePath) + : \React\Promise\resolve(null); - return $promise->then( - function () use ($target, $stashPath) { + return $prepareBase->then(fn () => parent::update($repo, $initial, $target))->then( + function () use ($target, $stashPath, $basePath) { $this->removeExcludedDirectories($target); if ($stashPath !== null) { - $this->restoreStash($stashPath, $this->getInstallPath($target)); + $installPath = $this->getInstallPath($target); + $this->mergeStash($stashPath, $basePath, $installPath); (new Filesystem)->removeDirectory($stashPath); + (new Filesystem)->removeDirectory($basePath); } }, - function (\Throwable $e) use ($stashPath) { + function (\Throwable $e) use ($stashPath, $basePath) { if ($stashPath !== null) { (new Filesystem)->removeDirectory($stashPath); } + if ($basePath !== null) { + (new Filesystem)->removeDirectory($basePath); + } throw $e; } ); diff --git a/tests/ModuleInstallerTest.php b/tests/ModuleInstallerTest.php index 53c0177..a79985b 100644 --- a/tests/ModuleInstallerTest.php +++ b/tests/ModuleInstallerTest.php @@ -49,9 +49,9 @@ public function callStashModuleDir(string $path): ?string return parent::stashModuleDir($path); } - public function callRestoreStash(string $from, string $to): void + public function callMergeStash(string $stash, string $base, string $install): void { - parent::restoreStash($from, $to); + parent::mergeStash($stash, $base, $install); } } @@ -214,56 +214,169 @@ public function test_stash_returns_null_when_dir_does_not_exist(): void } // ------------------------------------------------------------------------- - // restoreStash + // mergeStash // ------------------------------------------------------------------------- - public function test_restore_stash_copies_user_files_into_install_path(): void + private function makeTempDir(): string { - $io = $this->createStub(IOInterface::class); + $path = sys_get_temp_dir() . '/merge-test-' . uniqid('', true); + mkdir($path, 0755, true); + + return $path; + } + + public function test_merge_stash_applies_upstream_changes_to_unedited_file(): void + { + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = sys_get_temp_dir().'/module-stash-test-restore-'.uniqid('', true); - $install = sys_get_temp_dir().'/module-install-test-'.uniqid('', true); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); - mkdir($stash); - mkdir($install); - file_put_contents($stash.'/custom.txt', 'user edit'); + // base == stash (user made no edits), upstream changed the file + file_put_contents($stash . '/api.php', "line1\nline2\n"); + file_put_contents($base . '/api.php', "line1\nline2\n"); + file_put_contents($install . '/api.php', "line1\nline2-upstream\n"); - $installer->callRestoreStash($stash, $install); + $installer->callMergeStash($stash, $base, $install); - $this->assertFileExists($install.'/custom.txt'); - $this->assertSame('user edit', file_get_contents($install.'/custom.txt')); + $this->assertSame("line1\nline2-upstream\n", file_get_contents($install . '/api.php')); - // Cleanup $fs = new Filesystem; $fs->remove($stash); + $fs->remove($base); $fs->remove($install); } - public function test_restore_stash_leaves_new_upstream_files_intact(): void + public function test_merge_stash_preserves_user_edits_when_upstream_unchanged(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = sys_get_temp_dir().'/module-stash-test-upstream-'.uniqid('', true); - $install = sys_get_temp_dir().'/module-install-test-upstream-'.uniqid('', true); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); - mkdir($stash); - mkdir($install); + // User edited line 2; upstream kept the file as-is + file_put_contents($stash . '/api.php', "line1\nline2-user\n"); + file_put_contents($base . '/api.php', "line1\nline2\n"); + file_put_contents($install . '/api.php', "line1\nline2\n"); - // Upstream added a new file during update - file_put_contents($install.'/new-upstream.txt', 'new from upstream'); - // User had a customised file in the stash - file_put_contents($stash.'/custom.txt', 'user edit'); + $installer->callMergeStash($stash, $base, $install); - $installer->callRestoreStash($stash, $install); + $this->assertSame("line1\nline2-user\n", file_get_contents($install . '/api.php')); - $this->assertFileExists($install.'/new-upstream.txt'); - $this->assertFileExists($install.'/custom.txt'); + $fs = new Filesystem; + $fs->remove($stash); + $fs->remove($base); + $fs->remove($install); + } + + public function test_merge_stash_merges_non_overlapping_edits(): void + { + $io = $this->createStub(IOInterface::class); + $installer = new TestableInstaller($io, null); + + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); + + $baseContent = "line1\nline2\nline3\nline4\nline5\n"; + $userContent = "line1-user\nline2\nline3\nline4\nline5\n"; // user changed line 1 + $upstreamContent = "line1\nline2\nline3\nline4\nline5-up\n"; // upstream changed line 5 + + file_put_contents($stash . '/api.php', $userContent); + file_put_contents($base . '/api.php', $baseContent); + file_put_contents($install . '/api.php', $upstreamContent); + + $installer->callMergeStash($stash, $base, $install); + + $result = file_get_contents($install . '/api.php'); + $this->assertStringContainsString('line1-user', $result); + $this->assertStringContainsString('line5-up', $result); + $this->assertStringNotContainsString('<<<<<<<', $result); + + $fs = new Filesystem; + $fs->remove($stash); + $fs->remove($base); + $fs->remove($install); + } + + public function test_merge_stash_inserts_conflict_markers_on_overlapping_edits(): void + { + $io = $this->createMock(IOInterface::class); + $io->expects($this->once())->method('writeError'); + + $installer = new TestableInstaller($io, null); + + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); + + // Both sides changed the same line + file_put_contents($stash . '/api.php', "original\n"); + file_put_contents($base . '/api.php', "original\n"); + file_put_contents($install . '/api.php', "upstream-change\n"); + + // Make the stash differ from base on the same line + file_put_contents($stash . '/api.php', "user-change\n"); + + $installer->callMergeStash($stash, $base, $install); + + $result = file_get_contents($install . '/api.php'); + $this->assertStringContainsString('<<<<<<<', $result); + + $fs = new Filesystem; + $fs->remove($stash); + $fs->remove($base); + $fs->remove($install); + } + + public function test_merge_stash_keeps_user_added_files(): void + { + $io = $this->createStub(IOInterface::class); + $installer = new TestableInstaller($io, null); + + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); + + // File only in stash (user added it, never in original dist) + file_put_contents($stash . '/user-added.php', 'callMergeStash($stash, $base, $install); + + $this->assertFileExists($install . '/user-added.php'); + $this->assertSame('remove($stash); + $fs->remove($base); + $fs->remove($install); + } + + public function test_merge_stash_respects_upstream_deletions(): void + { + $io = $this->createStub(IOInterface::class); + $installer = new TestableInstaller($io, null); + + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); + $install = $this->makeTempDir(); + + // File exists in stash and base but upstream removed it (not in install) + file_put_contents($stash . '/deleted-upstream.php', 'old content'); + file_put_contents($base . '/deleted-upstream.php', 'old content'); + // Intentionally NOT present in $install + + $installer->callMergeStash($stash, $base, $install); + + $this->assertFileDoesNotExist($install . '/deleted-upstream.php'); - // Cleanup $fs = new Filesystem; $fs->remove($stash); + $fs->remove($base); $fs->remove($install); } } From 1c40e9926dfa10623554a3303994a270d3cd9bd0 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 17 Mar 2026 21:08:32 +0000 Subject: [PATCH 3/6] fix: update Symfony dependencies to version 8.0 in composer files --- composer.json | 4 +- composer.lock | 271 +++++++++++++++++++++++++------------------------- 2 files changed, 138 insertions(+), 137 deletions(-) diff --git a/composer.json b/composer.json index faf6e55..d314735 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,8 @@ "require": { "php": "^8.4", "composer-plugin-api": "^2.0", - "symfony/finder": "^7.0", - "symfony/process": "^7.0" + "symfony/finder": "^8.0", + "symfony/process": "^8.0" }, "require-dev": { "composer/composer": "^2.0", diff --git a/composer.lock b/composer.lock index f089680..ffd4e74 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,142 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bfb13925727acd3cb3da13b9924095f5", - "packages": [], + "content-hash": "3b9bd3ebf86ea0e34b28472b9de4c0f3", + "packages": [ + { + "name": "symfony/finder", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:41:02+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + } + ], "packages-dev": [ { "name": "composer/ca-bundle", @@ -2993,74 +3127,6 @@ ], "time": "2026-02-25T16:59:43+00:00" }, - { - "name": "symfony/finder", - "version": "v8.0.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "shasum": "" - }, - "require": { - "php": ">=8.4" - }, - "require-dev": { - "symfony/filesystem": "^7.4|^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-29T09:41:02+00:00" - }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -3720,71 +3786,6 @@ ], "time": "2025-06-24T13:30:11+00:00" }, - { - "name": "symfony/process", - "version": "v8.0.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", - "shasum": "" - }, - "require": { - "php": ">=8.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v8.0.5" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-01-26T15:08:38+00:00" - }, { "name": "symfony/service-contracts", "version": "v3.6.1", From 49efacb8e9ba38fa501c4900c260c2f02cafc725 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 17 Mar 2026 21:10:01 +0000 Subject: [PATCH 4/6] fix: update actions/checkout version to v6 in PHP workflow --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index a18d144..7441963 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -38,7 +38,7 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: "laravel-pint" uses: aglipanci/laravel-pint-action@latest with: From 9ff267429380f25e680528a6ec76e4075db83667 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 17 Mar 2026 21:13:09 +0000 Subject: [PATCH 5/6] fix: update actions/checkout and git-auto-commit-action versions in PHP workflow --- .github/workflows/php.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 7441963..9c32e4e 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -17,7 +17,7 @@ jobs: name: PHP ${{ matrix.php-versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup PHP uses: shivammathur/setup-php@master @@ -39,13 +39,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} - name: "laravel-pint" uses: aglipanci/laravel-pint-action@latest with: configPath: './pint.json' - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: PHP Linting (Pint) - skip_fetch: true From 092fd9594be913df6462fe7ddc25698af067e2c2 Mon Sep 17 00:00:00 2001 From: roble <3231587+roble@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:13:34 +0000 Subject: [PATCH 6/6] PHP Linting (Pint) --- src/Installer.php | 19 ++++---- tests/ModuleInstallerTest.php | 86 +++++++++++++++++------------------ 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/src/Installer.php b/src/Installer.php index 01d6397..0885f73 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -208,7 +208,7 @@ protected function downloadBase(PackageInterface $initial, string $basePath): Pr $dm = $this->composer->getDownloadManager(); return $dm->download($initial, $basePath) - ->then(fn () => $dm->install($initial, $basePath)); + ->then(fn () => $dm->install($initial, $basePath)); } /** @@ -225,14 +225,15 @@ protected function mergeStash(string $stash, string $base, string $install): voi $finder = (new Finder)->files()->in($stash); foreach ($finder as $file) { - $rel = $file->getRelativePathname(); - $stashFile = $stash . '/' . $rel; - $baseFile = $base . '/' . $rel; - $newFile = $install . '/' . $rel; + $rel = $file->getRelativePathname(); + $stashFile = $stash.'/'.$rel; + $baseFile = $base.'/'.$rel; + $newFile = $install.'/'.$rel; if (! file_exists($baseFile)) { // User-added file (not in original dist) — always keep (new SymfonyFilesystem)->copy($stashFile, $newFile, true); + continue; } @@ -242,12 +243,12 @@ protected function mergeStash(string $stash, string $base, string $install): voi } // All three versions exist — 3-way merge - $merged = $stashFile . '.merge-work'; + $merged = $stashFile.'.merge-work'; copy($stashFile, $merged); $process = new Process( ['git', 'merge-file', '-L', 'yours', '-L', 'original', '-L', 'upstream', - $merged, $baseFile, $newFile] + $merged, $baseFile, $newFile] ); $process->run(); @@ -282,12 +283,12 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): ?PromiseInterface { $stashPath = null; - $basePath = null; + $basePath = null; if ($this->getUpdateStrategy() === self::UPDATE_STRATEGY_MERGE) { $stashPath = $this->stashModuleDir($this->getInstallPath($initial)); if ($stashPath !== null) { - $basePath = sys_get_temp_dir() . '/module-base-' . uniqid('', true); + $basePath = sys_get_temp_dir().'/module-base-'.uniqid('', true); } } diff --git a/tests/ModuleInstallerTest.php b/tests/ModuleInstallerTest.php index a79985b..30d13f6 100644 --- a/tests/ModuleInstallerTest.php +++ b/tests/ModuleInstallerTest.php @@ -219,7 +219,7 @@ public function test_stash_returns_null_when_dir_does_not_exist(): void private function makeTempDir(): string { - $path = sys_get_temp_dir() . '/merge-test-' . uniqid('', true); + $path = sys_get_temp_dir().'/merge-test-'.uniqid('', true); mkdir($path, 0755, true); return $path; @@ -227,21 +227,21 @@ private function makeTempDir(): string public function test_merge_stash_applies_upstream_changes_to_unedited_file(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); // base == stash (user made no edits), upstream changed the file - file_put_contents($stash . '/api.php', "line1\nline2\n"); - file_put_contents($base . '/api.php', "line1\nline2\n"); - file_put_contents($install . '/api.php', "line1\nline2-upstream\n"); + file_put_contents($stash.'/api.php', "line1\nline2\n"); + file_put_contents($base.'/api.php', "line1\nline2\n"); + file_put_contents($install.'/api.php', "line1\nline2-upstream\n"); $installer->callMergeStash($stash, $base, $install); - $this->assertSame("line1\nline2-upstream\n", file_get_contents($install . '/api.php')); + $this->assertSame("line1\nline2-upstream\n", file_get_contents($install.'/api.php')); $fs = new Filesystem; $fs->remove($stash); @@ -251,21 +251,21 @@ public function test_merge_stash_applies_upstream_changes_to_unedited_file(): vo public function test_merge_stash_preserves_user_edits_when_upstream_unchanged(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); // User edited line 2; upstream kept the file as-is - file_put_contents($stash . '/api.php', "line1\nline2-user\n"); - file_put_contents($base . '/api.php', "line1\nline2\n"); - file_put_contents($install . '/api.php', "line1\nline2\n"); + file_put_contents($stash.'/api.php', "line1\nline2-user\n"); + file_put_contents($base.'/api.php', "line1\nline2\n"); + file_put_contents($install.'/api.php', "line1\nline2\n"); $installer->callMergeStash($stash, $base, $install); - $this->assertSame("line1\nline2-user\n", file_get_contents($install . '/api.php')); + $this->assertSame("line1\nline2-user\n", file_get_contents($install.'/api.php')); $fs = new Filesystem; $fs->remove($stash); @@ -275,24 +275,24 @@ public function test_merge_stash_preserves_user_edits_when_upstream_unchanged(): public function test_merge_stash_merges_non_overlapping_edits(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); - $baseContent = "line1\nline2\nline3\nline4\nline5\n"; - $userContent = "line1-user\nline2\nline3\nline4\nline5\n"; // user changed line 1 + $baseContent = "line1\nline2\nline3\nline4\nline5\n"; + $userContent = "line1-user\nline2\nline3\nline4\nline5\n"; // user changed line 1 $upstreamContent = "line1\nline2\nline3\nline4\nline5-up\n"; // upstream changed line 5 - file_put_contents($stash . '/api.php', $userContent); - file_put_contents($base . '/api.php', $baseContent); - file_put_contents($install . '/api.php', $upstreamContent); + file_put_contents($stash.'/api.php', $userContent); + file_put_contents($base.'/api.php', $baseContent); + file_put_contents($install.'/api.php', $upstreamContent); $installer->callMergeStash($stash, $base, $install); - $result = file_get_contents($install . '/api.php'); + $result = file_get_contents($install.'/api.php'); $this->assertStringContainsString('line1-user', $result); $this->assertStringContainsString('line5-up', $result); $this->assertStringNotContainsString('<<<<<<<', $result); @@ -310,21 +310,21 @@ public function test_merge_stash_inserts_conflict_markers_on_overlapping_edits() $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); // Both sides changed the same line - file_put_contents($stash . '/api.php', "original\n"); - file_put_contents($base . '/api.php', "original\n"); - file_put_contents($install . '/api.php', "upstream-change\n"); + file_put_contents($stash.'/api.php', "original\n"); + file_put_contents($base.'/api.php', "original\n"); + file_put_contents($install.'/api.php', "upstream-change\n"); // Make the stash differ from base on the same line - file_put_contents($stash . '/api.php', "user-change\n"); + file_put_contents($stash.'/api.php', "user-change\n"); $installer->callMergeStash($stash, $base, $install); - $result = file_get_contents($install . '/api.php'); + $result = file_get_contents($install.'/api.php'); $this->assertStringContainsString('<<<<<<<', $result); $fs = new Filesystem; @@ -335,20 +335,20 @@ public function test_merge_stash_inserts_conflict_markers_on_overlapping_edits() public function test_merge_stash_keeps_user_added_files(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); // File only in stash (user added it, never in original dist) - file_put_contents($stash . '/user-added.php', 'callMergeStash($stash, $base, $install); - $this->assertFileExists($install . '/user-added.php'); - $this->assertSame('assertFileExists($install.'/user-added.php'); + $this->assertSame('remove($stash); @@ -358,21 +358,21 @@ public function test_merge_stash_keeps_user_added_files(): void public function test_merge_stash_respects_upstream_deletions(): void { - $io = $this->createStub(IOInterface::class); + $io = $this->createStub(IOInterface::class); $installer = new TestableInstaller($io, null); - $stash = $this->makeTempDir(); - $base = $this->makeTempDir(); + $stash = $this->makeTempDir(); + $base = $this->makeTempDir(); $install = $this->makeTempDir(); // File exists in stash and base but upstream removed it (not in install) - file_put_contents($stash . '/deleted-upstream.php', 'old content'); - file_put_contents($base . '/deleted-upstream.php', 'old content'); + file_put_contents($stash.'/deleted-upstream.php', 'old content'); + file_put_contents($base.'/deleted-upstream.php', 'old content'); // Intentionally NOT present in $install $installer->callMergeStash($stash, $base, $install); - $this->assertFileDoesNotExist($install . '/deleted-upstream.php'); + $this->assertFileDoesNotExist($install.'/deleted-upstream.php'); $fs = new Filesystem; $fs->remove($stash);