composer/composer · error · RuntimeException
Source directory ' . $path . ' has unpushed changes on the c
Error message
Source directory ' . $path . ' has unpushed changes on the current branch: "."\n".$unpushed
What it means
Thrown in cleanChanges (src/Composer/Downloader/GitDownloader.php:334) when getUnpushedChanges() returns a non-empty string of unpushed commits and either the IO is interactive or the 'discard-changes' config is not strictly true. Composer treats unpushed local commits as data that would be destroyed by an update, so it refuses unless the user has explicitly opted into discarding.
Source
Thrown at src/Composer/Downloader/GitDownloader.php:335
if (!$unpushedChanges) {
break;
}
}
return $unpushedChanges;
}
/**
* @inheritDoc
*/
protected function cleanChanges(PackageInterface $package, string $path, bool $update): PromiseInterface
{
GitUtil::cleanEnv($this->process);
$path = $this->normalizePath($path);
$unpushed = $this->getUnpushedChanges($package, $path);
if ($unpushed && ($this->io->isInteractive() || $this->config->get('discard-changes') !== true)) {
throw new \RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch: '."\n".$unpushed);
}
if (null === ($changes = $this->getLocalChanges($package, $path))) {
return \React\Promise\resolve(null);
}
if (!$this->io->isInteractive()) {
$discardChanges = $this->config->get('discard-changes');
if (true === $discardChanges) {
return $this->discardChanges($path);
}
if ('stash' === $discardChanges) {
if (!$update) {
return parent::cleanChanges($package, $path, $update);
}
return $this->stashChanges($path);
}View on GitHub (pinned to c435d285c9)
Solutions
- Inspect the listed unpushed changes and push them to a remote (or back them up) before updating.
- If the changes are disposable, set '"discard-changes": true' in composer.json config and re-run.
- In an interactive shell, let Composer prompt you and choose to discard when prompted.
- Reset the package branch to its remote counterpart ('git -C <path> reset --hard origin/<branch>') then retry.
Example fix
// composer.json before
{}
// composer.json after (explicitly discard)
{
"config": { "discard-changes": true }
} Defensive patterns
Strategy: validation
Validate before calling
// Detect unpushed commits before Composer's update does.
exec('git -C ' . escapeshellarg($path) . ' log --branches --not --remotes --oneline', $out, $rc);
$unpushed = $rc === 0 ? implode("\n", $out) : '';
if (trim($unpushed) !== '') {
throw new \LogicException("Unpushed commits in $path; push or back them up first.\n$unpushed");
} Try / catch
try {
$downloader->update($initial, $target, $path, $url);
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), 'has unpushed changes')) {
// push/backup the commits, or set discard-changes, then retry
}
throw $e;
} Prevention
- Never commit directly into vendor/ --prefer-source packages; use a fork repo.
- Set 'discard-changes' only when you truly intend to lose local work.
- Push package branches to a remote before running updates.
When it happens
Trigger: cleanChanges() is called during update/uninstall; getUnpushedChanges($package,$path) returns a truthy string; and ($this->io->isInteractive() || $this->config->get('discard-changes') !== true) holds.
Common situations: A developer committed hotfixes directly into a vendor package checked out by --prefer-source; CI configured non-interactively without 'discard-changes'; switching branches/refs where local commits exist on the package's branch.
Related errors
- Update aborted
- Failed to read package information from ${url} as the path d
- GitDriver requires a usable cache directory, and it looks li
- Can not clone ${url} to access package information. The "${d
- The source URL ${url} is invalid, ssh URLs should have a por
AI-assisted analysis of composer/composer@c435d285c9 (2026-08-07).
Data as JSON: /api/errors/13c87101eeae2ff7.
Report an issue: GitHub.