{"id":50539716,"url":"https://github.com/bitranox/pycharm-vcs-deadlock-fix","last_synced_at":"2026-06-03T19:30:40.790Z","repository":{"id":359561131,"uuid":"1246633374","full_name":"bitranox/pycharm-vcs-deadlock-fix","owner":"bitranox","description":"Fixes PyCharm/IntelliJ freezing on \"Analyzing project to enable smart features\" when a project has many nested git repositories.","archived":false,"fork":false,"pushed_at":"2026-05-22T12:58:35.000Z","size":174,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T17:44:42.942Z","etag":null,"topics":["asm","bytecode-manipulation","deadlock","git4idea","intellij-idea","intellij-platform","java-agent","pycharm","runblocking","vcs"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bitranox.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T11:46:24.000Z","updated_at":"2026-05-22T12:58:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bitranox/pycharm-vcs-deadlock-fix","commit_stats":null,"previous_names":["bitranox/pycharm-vcs-deadlock-fix"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bitranox/pycharm-vcs-deadlock-fix","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitranox%2Fpycharm-vcs-deadlock-fix","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitranox%2Fpycharm-vcs-deadlock-fix/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitranox%2Fpycharm-vcs-deadlock-fix/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitranox%2Fpycharm-vcs-deadlock-fix/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bitranox","download_url":"https://codeload.github.com/bitranox/pycharm-vcs-deadlock-fix/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitranox%2Fpycharm-vcs-deadlock-fix/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33876893,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asm","bytecode-manipulation","deadlock","git4idea","intellij-idea","intellij-platform","java-agent","pycharm","runblocking","vcs"],"created_at":"2026-06-03T19:30:39.968Z","updated_at":"2026-06-03T19:30:40.785Z","avatar_url":"https://github.com/bitranox.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pycharm_vcs_patch\n\nA small Java agent that fixes the JetBrains IntelliJ Platform VCS\ndeadlock during PyCharm's initial project scan on projects containing\nmany nested git repositories. The agent is small (≈80 lines of source,\n~130 KB packaged), surgical (modifies exactly one INVOKESTATIC inside\none method of one class), and self-contained (ASM bundled in the JAR).\n\nTested live against **PyCharm Community 2026.1** (build PY-261.23567.174)\nopening an umbrella project with 167 nested git repositories.\n\nFull diagnostic write-up: [`docs/DIAGNOSTIC.md`](docs/DIAGNOSTIC.md).\nHow to submit upstream: [`jetbrains_submit.md`](jetbrains_submit.md).\n\n---\n\n## What this is\n\nA `-javaagent` JAR that, before any IntelliJ code runs:\n\n- **Locates exactly one `INVOKESTATIC` instruction** in the bytecode of\n  `git4idea.repo.GitRepositoryImpl.\u003cinit\u003e` — the call to\n  `com.intellij.openapi.progress.CoroutinesKt.runBlockingMaybeCancellable`\n  that wraps `workingTreeHolder.updateState()`.\n- **Replaces** that two-instruction sequence with `POP; ACONST_NULL;`,\n  i.e. discard the Kotlin suspend lambda without running it and push\n  null for the subsequent stack pop. The constructor returns in\n  microseconds instead of waiting for a `git` subprocess + message-bus\n  publish + IDE read-write-mutex acquisition under\n  `VcsRepositoryManager.MODIFY_LOCK`.\n- No other call site is touched. The same helper is still used from\n  `GitRepository.update()` and elsewhere — those paths are not called\n  under the global VCS lock and the synchronous behaviour there is the\n  intended behaviour.\n\n## Why this is the right surgical target\n\nStack trace of the deadlock (captured live with `jcmd Thread.print` —\nsee [`docs/DIAGNOSTIC.md`](docs/DIAGNOSTIC.md) §3):\n\n```\nVcsRepositoryManager.ensureUpToDate$lambda$0          (VcsRepositoryManager.kt:165)\n  → checkAndUpdateRepositoryCollection                (VcsRepositoryManager.kt:368)   ← MODIFY_LOCK held from here\n    → findNewRoots                                    (VcsRepositoryManager.kt:408)\n      → tryCreateRepository                           (VcsRepositoryManager.kt:106)\n        → GitRepositoryCreator.createRepositoryIfValid(GitRepositoryCreator.java:19)\n          → GitRepositoryImpl.createInstance          (GitRepositoryImpl.kt:268)\n            → GitRepositoryImpl.\u003cinit\u003e                (GitRepositoryImpl.kt:81)\n              → runBlockingMaybeCancellable           (coroutines.kt:215)    ← *** THE BLOCKING CALL ***\n                → joinBlocking → LockSupport.parkNanos\n              ↑\n              ↑ suspended on:\n              └── GitWorkingTreeHolderImpl.updateState (GitWorkingTreeHolderImpl.kt:37)\n                   ↳ withLock(updateLock)\n                   ↳ messageBus.syncPublisher().on*()\n                   ↳ Git.listWorktrees(repository)    ← `git` subprocess\n```\n\nThe deadlock is *not* in one specific method's logic. It is in the\nfact that `checkAndUpdateRepositoryCollection` holds `MODIFY_LOCK`\nwhile calling `findNewRoots`, which constructs `GitRepositoryImpl`s,\nwhose constructors do `runBlockingMaybeCancellable { updateState() }`.\n\nFor N nested git repos, the constructor is called N times under one\nlock. Each call adds 100-500 ms of lock-hold time (its\n`runBlockingMaybeCancellable` waits for a coroutine that runs\n`messageBus.syncPublisher`, IDE read-mutex acquisition, and a `git\nlistWorktrees` subprocess). For N=167 the total lock-hold is\n1-2 minutes; while held, ~70 other VCS-aware features queue and the\nIDE is effectively frozen.\n\n**The patch removes that 100-500 ms per construction.** The\n`workingTreeHolder` is left in its initial empty state, exactly as it\nwould be after a stock IntelliJ refresh that fails. Real state is\npopulated the next time anything triggers `update()` (the same suspend\nfunction, called from outside any global lock) — gutter colours,\nChanges view, log refresher all subscribe to the state-flow and\nupdate reactively when it becomes non-empty.\n\n## Lifecycle\n\n```\nt=0     JVM launches with -javaagent:.../pycharm-vcs-patch.jar\n        ─ premain logs: \"premain: target=git4idea/repo/GitRepositoryImpl.\u003cinit\u003e\n                         neutralizing com/intellij/openapi/progress/CoroutinesKt.runBlockingMaybeCancellable\"\n        ─ registers a ClassFileTransformer\n\nt=class-load\n        When the JVM goes to define git4idea.repo.GitRepositoryImpl, our\n        transformer is invoked. It walks the bytecode of \u003cinit\u003e; for the\n        single INVOKESTATIC of runBlockingMaybeCancellable, it emits\n        POP + ACONST_NULL instead.\n        ─ logs: \"neutralizing com/intellij/openapi/progress/CoroutinesKt.runBlockingMaybeCancellable\n                 in git4idea/repo/GitRepositoryImpl.\u003cinit\u003e (call site #1)\"\n\nt=∞     Patch is permanent for the lifetime of the JVM. No restore\n        logic, no DumbService watching, no class redefinition gymnastics.\n        Every constructor call from here on runs the patched body.\n        The auto-discovery loop in findNewRoots still works — it just\n        completes in milliseconds instead of minutes, because each\n        GitRepositoryImpl.\u003cinit\u003e no longer blocks.\n```\n\nThe agent JAR on disk is byte-for-byte unchanged after install.\nPyCharm's own JARs are byte-for-byte unchanged on disk — the patch\nexists only in JVM memory. Removing the `-javaagent:` line from\n`pycharm64.vmoptions` fully reverts to stock behaviour on the next\nlaunch.\n\n## What this does NOT change\n\n- Existing mappings in `.idea/vcs.xml` work exactly as before —\n  gutter, blame, Changes view, commit UI, push, log, etc.\n- Auto-discovery still happens. `VcsRepositoryManager.findNewRoots`\n  still runs every alarm cycle and finds every nested `.git` it\n  should find. The difference is only that it doesn't block while\n  doing so.\n- The patched constructor's other dependencies (the staging area\n  holder, untracked files holder, merge conflicts holder, etc.) are\n  all initialized normally. We strip exactly the `runBlocking\n  updateState()` part.\n- `GitRepository.update()` and the user-initiated refresh path are\n  untouched — their `runBlockingMaybeCancellable` calls are at\n  different bytecode locations in different methods.\n\n## What this might subtly change\n\n- Immediately after a fresh `GitRepositoryImpl` is constructed, its\n  `workingTreeHolder` reports the default empty state instead of\n  whatever `updateState()` would have populated. If any IDE feature\n  reads the state synchronously between construction and the first\n  background `update()`, it sees: no current branch, no current HEAD,\n  empty staging info. In practice the IDE handles this transient\n  state gracefully — features either subscribe to the state-flow\n  (and update when populated) or tolerate empty state. The same\n  transient state exists after any normal VCS refresh fails or\n  retries.\n- The user-visible *first paint* of VCS-aware UI elements (e.g. the\n  branch name in the status bar) may take a few extra milliseconds\n  because it can only fill in after the asynchronous first\n  `update()` completes. This is invisible in practice — the IDE was\n  going to be busy with indexing during that window anyway.\n\n## Files in this directory\n\n```\npycharm_vcs_patch/\n├── README.md               this file\n├── CLAUDE.md               context for AI assistants\n├── jetbrains_submit.md     how/where to submit this upstream\n├── build.sh                rebuild the JAR from source\n├── install.sh              install JAR + wire vmoptions\n├── src/\n│   └── VcsPatchAgent.java  the agent source (single file, ~80 lines)\n└── dist/\n    └── pycharm-vcs-patch.jar  compiled, packaged agent JAR\n```\n\n## Quick start (already installed on this machine)\n\nThe agent is wired in at:\n\n```\n~/.config/JetBrains/PyCharm2026.1/agents/pycharm-vcs-patch.jar\n~/.config/JetBrains/PyCharm2026.1/pycharm64.vmoptions    ← contains:\n    -javaagent:/home/srvadmin/.config/JetBrains/PyCharm2026.1/agents/pycharm-vcs-patch.jar\n```\n\nRestart PyCharm. Watch the markers in PyCharm stdout or `idea.log`:\n\n```\n[VcsPatchAgent] premain: target=git4idea/repo/GitRepositoryImpl.\u003cinit\u003e neutralizing …\n[VcsPatchAgent] neutralizing com/intellij/openapi/progress/CoroutinesKt.runBlockingMaybeCancellable(...) in git4idea/repo/GitRepositoryImpl.\u003cinit\u003e (call site #1)\n```\n\nThe second line appears only when the JVM first goes to load\n`GitRepositoryImpl` — typically within seconds of project open.\n\n## Build from source\n\n```\n./build.sh    # uses PyCharm's bundled JBR javac and ASM from the Junie plugin\n```\n\n## Install on a fresh machine\n\n```\n./build.sh \u0026\u0026 ./install.sh\n```\n\nBoth scripts are idempotent. Restart PyCharm to activate.\n\n## Uninstall\n\nRemove the `-javaagent:` line from `pycharm64.vmoptions`, restart\nPyCharm. The on-disk install is byte-for-byte unchanged; the patch\nexists only as an in-memory transform of one method during a session.\n\n## Verification\n\nLive measurements on `rotek-apps` (167 nested git repos):\n\n| Metric | Stock PyCharm | Agent installed |\n|---|---|---|\n| `UnindexedFilesIndexer - Finished` | never (or many minutes) | **4.6 s** |\n| `exit dumb mode [rotek-apps]` | never | **3.0 s after launch** |\n| Perfwatcher freezes in first 80 s | many | **0** |\n| `checkAndUpdateRepositoryCollection` waiters in steady state | 70+ | **0** |\n| `GitRepositoryImpl.\u003cinit\u003e` in-flight | several | **0** |\n| Process RSS | 5+ GiB | 2.0 GiB |\n| UI responsive | no | yes |\n\n## Compatibility\n\n- Built against IntelliJ Platform 261.x. The targeted call\n  (`runBlockingMaybeCancellable` inside `GitRepositoryImpl.\u003cinit\u003e`)\n  has been stable in this form across recent IDEA / git4idea\n  versions, but if JetBrains rewrites the constructor body, the\n  agent's transformer simply finds zero call sites and logs a\n  WARNING (`patch had no effect`). No risk of breakage — it just\n  becomes a no-op and PyCharm runs stock.\n- Works on any JBR ≥ 17 (uses standard `Instrumentation` and ASM 9).\n\n## See also\n\n- [`docs/DIAGNOSTIC.md`](docs/DIAGNOSTIC.md) — full diagnostic\n  write-up including `jcmd` evidence, code-level root cause traced\n  into IntelliJ Community source with line numbers, before/after\n  measurements, and the upstream-fix recommendation.\n- [`jetbrains_submit.md`](jetbrains_submit.md) — how to submit this\n  issue / fix to JetBrains via YouTrack.\n- [`ai-stance.md`](ai-stance.md) — our general position on AI in\n  software: what we use it for, what we don't, and what we think\n  the better questions are.\n- [`ai-transparency.md`](ai-transparency.md) — specific accounting\n  for *this* repository: what AI did, what the human did, how\n  verification was performed, and how to audit / rebuild from\n  source if you don't want to trust the pre-built JAR.\n- [`CLAUDE.md`](CLAUDE.md) — context for AI assistants (Claude Code\n  and similar) entering this repo.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitranox%2Fpycharm-vcs-deadlock-fix","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitranox%2Fpycharm-vcs-deadlock-fix","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitranox%2Fpycharm-vcs-deadlock-fix/lists"}