composer/composer · error · RuntimeException
Source directory {path} has unpushed changes on the current
Error message
Source directory {path} has unpushed changes on the current branch:
{unpushed} What it means
Thrown by GitDownloader::cleanChanges() when getUnpushedChanges() returns a non-null result (commits exist on the local branch that are not on any remote) and either the IO is interactive or the 'discard-changes' config is not true. Composer aborts to avoid destroying unpushed work.
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 6ffc117740)
Solutions
- Push the local commits to a remote so getUnpushedChanges() returns clean, then re-run the update.
- In non-interactive mode, add "config": {"discard-changes": true} to composer.json to force-discarding (destroys unpushed commits).
- In interactive mode, choose 's' to stash changes (reapplied after) or 'y' to discard.
- Move your changes out of vendor/ into a proper fork/patch before updating.
Example fix
// before: unpushed commits block update
composer update my/pkg
# push them first
cd vendor/my/pkg && git push origin my-branch && cd -
composer update my/pkg
# or opt-in to discard in composer.json:
{
"config": { "discard-changes": true }
} Defensive patterns
Strategy: validation
Validate before calling
// Before update, check for unpushed commits yourself and act deliberately
if (is_dir($path . '/.git')) {
$proc->execute(['git', 'show-ref', '--head', '-d'], $refs, $path);
$proc->execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], $branch, $path);
$branch = trim($branch);
$proc->execute(['git', 'rev-parse', $branch], $local, $path);
$proc->execute(['git', 'rev-parse', 'origin/' . $branch], $remote, $path);
if (trim($local) !== trim($remote) && trim($remote) !== '') {
// push or discard before invoking the downloader
}
} Prevention
- Never commit directly into vendored source packages; maintain a fork and push there.
- If you must edit in place, set "config": {"discard-changes": true} deliberately and understand it discards unpushed commits.
- Push local commits to a remote branch before running composer update on that package.
When it happens
Trigger: A developer made commits inside a source-installed package (vendor/<pkg>) on a branch, then ran `composer update` on that package. Composer detects the commits aren't pushed anywhere and refuses to overwrite them.
Common situations: Developing patches inside vendored git packages, hotfixes committed directly to a --prefer-source dependency, or forgetting to push local work before updating.
Related errors
- Failed to read package information from ${url} as the path d
- GitDriver requires a usable cache directory, and it looks li
- The source URL ${url} is invalid, ssh URLs should have a por
- Your configuration does not allow connections to {url}. See
- Your configuration does not allow connections to {url}. See
AI-assisted analysis of composer/composer@6ffc117740 (2026-08-07).
Data as JSON: /api/errors/5c454f01f3b3d62a.
Report an issue: GitHub.