{"record":{"id":"5b5366729660fafc","repo":"gitbutlerapp/gitbutler","slug":"project-at-is-already-opened-for-writing-by-a","errorCode":null,"errorMessage":"Project at '{}' is already opened for writing by another GitButler instance","messagePattern":"Project at '(.+?)' is already opened for writing by another GitButler instance","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/but-core/src/sync.rs","lineNumber":68,"sourceCode":"    let got_lock = lock\n        .try_lock()\n        .context(\"Failed to check if lock is taken\")?;\n    if !got_lock {\n        let error_message = match scope {\n            LockScope::AllOperations => {\n                format!(\n                    \"Project at '{}' is already opened for writing by another GitButler instance\",\n                    project_data.display()\n                )\n            }\n            LockScope::BackgroundRefreshOperations => {\n                format!(\n                    \"Project at '{}' is already being refreshed in the background by another GitButler instance\",\n                    project_data.display()\n                )\n            }\n        };\n        bail!(error_message);\n    }\n    Ok(lock)\n}\n\n/// Return a guard for exclusive (read+write) *in-process* repository access for the project at\n/// `git_dir`, blocking while waiting for someone else in this process to release it, or for all\n/// readers to disappear. Locking is fair.\n/// Also use `project_data_dir` if `Some` to create an *inter-process* exclusive lock.\n/// Creating, opening, or locking that file is best-effort. Failures are logged and ignored, so\n/// the hard guarantee provided by this function remains in-process exclusivity only.\n///\n/// If the current process inherits Git's commit-hook environment (`GIT_EDITOR=:` together with\n/// `GIT_INDEX_FILE`), acquiring the inter-process lock becomes non-blocking: if another process\n/// already holds it, we continue without that file lock instead of waiting. This avoids\n/// deadlocking hook re-entry when the parent GitButler command is already holding the same\n/// inter-process lock while waiting for the hook to finish.\n/// If `project_data_dir` is `None`, no inter-process lock is obtained.\n///","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-core/src/sync.rs#L50-L86","documentation":"`try_exclusive_inter_process_access(project_data, LockScope::AllOperations)` takes an advisory lock file (`project.lock`) in the project's data directory. If `try_lock()` reports it's already held, another live GitButler process owns the project for writing, and this bail fires. The lock is released on process exit for any reason, so it cannot go stale — the competing process is genuinely alive.","triggerScenarios":"Opening the same project in a second GitButler desktop instance, or running a GitButler CLI/TUI command against a project the desktop app currently holds; two `but` invocations racing on the same project data directory.","commonSituations":"Desktop app running in the tray while the user runs `but` in a terminal; a second app window/installation pointed at the same project directory; a hung previous instance that is still alive but invisible.","solutions":["Close the other GitButler instance (quit the desktop app fully, including tray) and retry.","Find lingering holders: `ps aux | grep -i gitbutler` (or check the process holding the lock with `lsof <project_data>/project.lock`) and exit it normally.","If the holder is a stuck process, kill it — the OS then releases the advisory lock since it cannot go stale on its own.","For CLI tools meant to run alongside the app, use scopes/APIs designed for coexistence instead of the full AllOperations lock."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Check whether the lock is currently held before attempting a full open\nuse std::fs::File;\nuse fs4::FileExt; // same family but-core uses for the advisory lock\nfn project_lock_free(project_data: &std::path::Path) -> bool {\n    File::open(project_data.join(\"project.lock\"))\n        .and_then(|f| f.try_lock_exclusive().map(|_| true))\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// Treat as contention: prompt to close the other instance, then retry\nfor attempt in 0..3 {\n    match try_exclusive_inter_process_access(&dir, LockScope::AllOperations) {\n        Ok(lock) => return Ok(lock),\n        Err(err) if err.to_string().contains(\"another GitButler instance\") && attempt < 2 => {\n            notify_user(\"Close the other GitButler window/app, then retrying…\");\n            std::thread::sleep(std::time::Duration::from_secs(2));\n        }\n        Err(err) => return Err(err),\n    }\n}","preventionTips":["Run exactly one GitButler instance per project data directory; quit the desktop app (tray included) before heavy CLI work.","Since the lock auto-releases on process death, never delete lock files manually — find the live holder instead (`lsof <dir>/project.lock`).","In automation, retry with backoff rather than failing on first contention."],"tags":["locking","concurrency","single-instance","gitbutler"],"backgroundTag":"file-lock-contention","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}