Skip to content

feat: add path repository handling in Installer with corresponding tests#11

Merged
roble merged 2 commits into
mainfrom
dev
Apr 26, 2026
Merged

feat: add path repository handling in Installer with corresponding tests#11
roble merged 2 commits into
mainfrom
dev

Conversation

@roble

@roble roble commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces robust handling for Composer path repositories in the Installer class, ensuring that local package files are never overwritten or deleted during install, update, or uninstall operations. It also adds comprehensive tests to verify these behaviors. The main improvements include path repository detection, guarded install/update/uninstall logic, and a skip mechanism for unnecessary code updates.

Path repository handling

  • Added the isPathRepository() method to detect packages served from local path repositories, preventing destructive operations on user files. (src/Installer.php, src/Installer.phpR278-R375)
  • Modified the install(), update(), and uninstall() methods to skip file operations for path repositories, logging informative messages and only updating Composer's internal state. (src/Installer.php, [1] [2]

Update logic improvements

  • Introduced a skipUpdateCode flag and related logic to allow parent update methods to run only for their side effects (such as updating installed.json), while skipping actual file operations when appropriate. (src/Installer.php, [1] [2]
  • Refactored the update flow to download fresh copies of the target package and handle stashing/merging or overwriting as needed, further decoupling file operations from Composer's default update mechanism. (src/Installer.php, src/Installer.phpR384-R410)

Testing enhancements

  • Added extensive unit tests for path repository handling, including install, update, uninstall guards, and the skip flag behavior in updateCode(). (tests/ModuleInstallerTest.php, tests/ModuleInstallerTest.phpR498-R644)
  • Extended the test harness (TestableInstaller) to expose and verify new internal behaviors. (tests/ModuleInstallerTest.php, tests/ModuleInstallerTest.phpR62-R85)

These changes make the installer safer and more predictable when working with local development packages, and the new tests ensure these behaviors are well-covered.

Copilot AI review requested due to automatic review settings April 26, 2026 09:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds safeguards in the custom Composer Installer to avoid destructive file operations when installing/updating/uninstalling packages coming from Composer path repositories, and expands the PHPUnit suite to cover the new behaviors.

Changes:

  • Add path-repository detection and guarded install/update/uninstall behavior in Installer.
  • Refactor update flow to download the target package directly and delegate only repo-tracking to the parent updater via a skip mechanism.
  • Add PHPUnit coverage for path-repo guard behavior and the updateCode() skip flag.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/Installer.php Adds isPathRepository(), guarded install/update/uninstall, introduces $skipUpdateCode + delegation for repo tracking, and refactors update to download fresh code directly.
tests/ModuleInstallerTest.php Extends the TestableInstaller shim and adds tests for path-repo skipping and updateCode() skip behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Installer.php
Comment on lines 302 to 306
$promise = parent::install($repo, $package);

return $promise->then(function () use ($package) {
$this->removeExcludedDirectories($package);
});

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent::install() is declared to return ?PromiseInterface, but the code unconditionally calls ->then() on its return value. If Composer returns null for synchronous installers, this will throw. Consider handling the null case (performing removeExcludedDirectories() immediately and returning null, or wrapping with React\Promise\resolve($promise) before chaining).

Copilot uses AI. Check for mistakes.
Comment thread src/Installer.php
$this->skipUpdateCode = false;
};

return parent::update($repo, $initial, $target)->then(

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delegateRepoTracking() assumes parent::update() always returns a promise and immediately calls ->then(). Since Composer’s installer API allows update() to return null, this can fatal and also leave $skipUpdateCode stuck true. Handle the null return (reset the flag and return React\Promise\resolve(null) or similar) so the flag is always cleared.

Suggested change
return parent::update($repo, $initial, $target)->then(
$promise = parent::update($repo, $initial, $target);
if ($promise === null) {
$resetFlag();
return \React\Promise\resolve(null);
}
return $promise->then(

Copilot uses AI. Check for mistakes.
Comment thread src/Installer.php
Comment on lines 369 to +378
*
* {@inheritDoc}
*/
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): ?PromiseInterface
{
if ($this->isPathRepository($target)) {
$this->io->write(" - <info>Skipping update for path repository:</info> {$target->getPrettyName()}");

return \React\Promise\resolve(null);
}

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says path-repo install/update/uninstall should skip destructive file ops while still updating Composer’s internal tracking state. In update() the path-repo branch returns a resolved promise without calling the repo-tracking delegation logic, so installed.json/repo state may not be updated. Please clarify intended behavior and, if tracking should still occur, delegate to the parent tracking path here too (while keeping code updates skipped).

Copilot uses AI. Check for mistakes.
Comment thread src/Installer.php
Comment on lines +321 to +322
return parent::update($repo, $initial, $target)->then(
$resetFlag,

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior in delegateRepoTracking() (skip-flag lifecycle + parent update delegation) isn’t covered by tests. Adding tests for (1) flag reset on success, and (2) flag reset when parent::update() rejects/throws would help prevent regressions, especially since leaving the flag enabled would break subsequent updates.

Suggested change
return parent::update($repo, $initial, $target)->then(
$resetFlag,
try {
$promise = parent::update($repo, $initial, $target);
} catch (\Throwable $e) {
$resetFlag();
throw $e;
}
return $promise->then(
function ($result) use ($resetFlag) {
$resetFlag();
return $result;
},

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants