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

  1. Inspect the listed unpushed changes and push them to a remote (or back them up) before updating.
  2. If the changes are disposable, set '"discard-changes": true' in composer.json config and re-run.
  3. In an interactive shell, let Composer prompt you and choose to discard when prompted.
  4. 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

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


AI-assisted analysis of composer/composer@c435d285c9 (2026-08-07). Data as JSON: /api/errors/13c87101eeae2ff7. Report an issue: GitHub.