{"record":{"id":"13c87101eeae2ff7","repo":"composer/composer","slug":"source-directory-path-has-unpushed-change","errorCode":null,"errorMessage":"Source directory ' . $path . ' has unpushed changes on the current branch: \".\"\\n\".$unpushed","messagePattern":"Source directory ' \\. \\$path \\. ' has unpushed changes on the current branch: \"\\.\"\\\\n\"\\.\\$unpushed","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Composer/Downloader/GitDownloader.php","lineNumber":335,"sourceCode":"            if (!$unpushedChanges) {\n                break;\n            }\n        }\n\n        return $unpushedChanges;\n    }\n\n    /**\n     * @inheritDoc\n     */\n    protected function cleanChanges(PackageInterface $package, string $path, bool $update): PromiseInterface\n    {\n        GitUtil::cleanEnv($this->process);\n        $path = $this->normalizePath($path);\n\n        $unpushed = $this->getUnpushedChanges($package, $path);\n        if ($unpushed && ($this->io->isInteractive() || $this->config->get('discard-changes') !== true)) {\n            throw new \\RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch: '.\"\\n\".$unpushed);\n        }\n\n        if (null === ($changes = $this->getLocalChanges($package, $path))) {\n            return \\React\\Promise\\resolve(null);\n        }\n\n        if (!$this->io->isInteractive()) {\n            $discardChanges = $this->config->get('discard-changes');\n            if (true === $discardChanges) {\n                return $this->discardChanges($path);\n            }\n            if ('stash' === $discardChanges) {\n                if (!$update) {\n                    return parent::cleanChanges($package, $path, $update);\n                }\n\n                return $this->stashChanges($path);\n            }","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/composer/composer/blob/c435d285c9120efdca35696769c72ea9fdcc0466/src/Composer/Downloader/GitDownloader.php#L317-L353","documentation":"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.","triggerScenarios":"cleanChanges() is called during update/uninstall; getUnpushedChanges($package,$path) returns a truthy string; and ($this->io->isInteractive() || $this->config->get('discard-changes') !== true) holds.","commonSituations":"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.","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."],"exampleFix":"// composer.json before\n{}\n// composer.json after (explicitly discard)\n{\n  \"config\": { \"discard-changes\": true }\n}","handlingStrategy":"validation","validationCode":"// Detect unpushed commits before Composer's update does.\nexec('git -C ' . escapeshellarg($path) . ' log --branches --not --remotes --oneline', $out, $rc);\n$unpushed = $rc === 0 ? implode(\"\\n\", $out) : '';\nif (trim($unpushed) !== '') {\n    throw new \\LogicException(\"Unpushed commits in $path; push or back them up first.\\n$unpushed\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    $downloader->update($initial, $target, $path, $url);\n} catch (\\RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'has unpushed changes')) {\n        // push/backup the commits, or set discard-changes, then retry\n    }\n    throw $e;\n}","preventionTips":["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."],"tags":["git","unpushed-changes","safety","config"],"backgroundTag":null,"analyzedSha":"c435d285c9120efdca35696769c72ea9fdcc0466","analyzedAt":"2026-08-07T18:58:23.525Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}