-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add path repository handling in Installer with corresponding tests #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,8 @@ class Installer extends LibraryInstaller | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const DEFAULT_UPDATE_STRATEGY = 'merge'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| protected bool $skipUpdateCode = false; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const UPDATE_STRATEGY_MERGE = 'merge'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const UPDATE_STRATEGY_OVERWRITE = 'overwrite'; | ||||||||||||||||||||||||||||||||
|
|
@@ -273,27 +275,109 @@ protected function mergeStash(string $stash, string $base, string $install): voi | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Returns true when the package is served by a local path repository. | ||||||||||||||||||||||||||||||||
| * Path repos have source == install path, so the normal download/delete cycle would | ||||||||||||||||||||||||||||||||
| * wipe the user's files before trying to copy from the now-missing source. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| protected function isPathRepository(PackageInterface $package): bool | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return $package->getDistType() === 'path'; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Override install to remove excluded directories after installation. | ||||||||||||||||||||||||||||||||
| * Skips entirely for path repositories — their files are already in place. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * {@inheritDoc} | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| public function install(InstalledRepositoryInterface $repo, PackageInterface $package): ?PromiseInterface | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| if ($this->isPathRepository($package)) { | ||||||||||||||||||||||||||||||||
| $this->io->write(" - <info>Skipping install for path repository:</info> {$package->getPrettyName()}"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return \React\Promise\resolve(null); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $promise = parent::install($repo, $package); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return $promise->then(function () use ($package) { | ||||||||||||||||||||||||||||||||
| $this->removeExcludedDirectories($package); | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Call parent::update() solely for its installed.json tracking side-effects. | ||||||||||||||||||||||||||||||||
| * Sets the skip flag so updateCode() is a no-op, then resets it regardless of outcome. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| protected function delegateRepoTracking(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): PromiseInterface | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| $this->skipUpdateCode = true; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $resetFlag = function (): void { | ||||||||||||||||||||||||||||||||
| $this->skipUpdateCode = false; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return parent::update($repo, $initial, $target)->then( | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
| 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
AI
Apr 26, 2026
There was a problem hiding this comment.
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.
| 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
AI
Apr 26, 2026
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 returnsnullfor synchronous installers, this will throw. Consider handling thenullcase (performingremoveExcludedDirectories()immediately and returningnull, or wrapping withReact\Promise\resolve($promise)before chaining).