{"record":{"id":"5c454f01f3b3d62a","repo":"composer/composer","slug":"source-directory-path-has-unpushed-changes-on-th","errorCode":null,"errorMessage":"Source directory {path} has unpushed changes on the current branch:\n{unpushed}","messagePattern":"Source directory (.+?) has unpushed changes on the current branch:\n(.+?)","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/6ffc1177404d0c50119c22dde6564a380f4a82c9/src/Composer/Downloader/GitDownloader.php#L317-L353","documentation":"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.","triggerScenarios":"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.","commonSituations":"Developing patches inside vendored git packages, hotfixes committed directly to a --prefer-source dependency, or forgetting to push local work before updating.","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."],"exampleFix":"// before: unpushed commits block update\ncomposer update my/pkg\n\n# push them first\ncd vendor/my/pkg && git push origin my-branch && cd -\ncomposer update my/pkg\n\n# or opt-in to discard in composer.json:\n{\n  \"config\": { \"discard-changes\": true }\n}","handlingStrategy":"validation","validationCode":"// Before update, check for unpushed commits yourself and act deliberately\nif (is_dir($path . '/.git')) {\n    $proc->execute(['git', 'show-ref', '--head', '-d'], $refs, $path);\n    $proc->execute(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], $branch, $path);\n    $branch = trim($branch);\n    $proc->execute(['git', 'rev-parse', $branch], $local, $path);\n    $proc->execute(['git', 'rev-parse', 'origin/' . $branch], $remote, $path);\n    if (trim($local) !== trim($remote) && trim($remote) !== '') {\n        // push or discard before invoking the downloader\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["git","unpushed-changes","data-loss-prevention","config"],"analyzedSha":"6ffc1177404d0c50119c22dde6564a380f4a82c9","analyzedAt":"2026-08-07T00:01:08.491Z","schemaVersion":2},"datasetVersion":"2026-08-07T02:17:10.218Z"}